-
-
Notifications
You must be signed in to change notification settings - Fork 135
Home
Welcome to the Neos.NeosUi wiki! This page hosts developer documentation and conventions.
Please follow the instructions in the README for getting started! This documentation is mainly for developers of this package.
The JS frontend of this code resides in Resources/Private/JavaScript.
As this package uses a Guest iFrame for rendering the actual website, we have two JS contexts which communicate with each other. The "outer frame" which contains the Neos UI is called host, while the iFrame containing the website is called the guest.
This structure is reflected in the sub-folder/package structure:
-
Host
: the main application of Neos.- This is the React.JS/Redux application.
-
Guest
: the connector running inside the guest frame. -
API
: the draft for the soon to be separated Neos JS API which handles the communication with the server instance.- The API is bootstrapped in the root of the Host application, since it relies on the
csrfToken
of the logged in user.
- The API is bootstrapped in the root of the Host application, since it relies on the
-
Login
: The login screen. Currently not used. -
Shared
: helper functions which are necessary both inside the guest and the host.- Should not contain state!
- We try to get rid of these functions!
TODO: explain how the guest and the host frame communicate.
We embrace the usage of stateless components as much as possible with the relatively new functional stateless components from React which where introduced in v0.14. In our containers we use the ES2015 class syntax, combined with the react-redux
@connect
decorator. The combination of both increases the separation of rendering and glue-code for the state.
We use redux to manage our state, and as described before we ban state from our react components. We also incorporate a bunch of packages like redux-actions
for reducing the code you need to write for creating actions, redux-saga
for managing complex and asynchronous actions and action collections, so called sagas
.
Sagas are our main pillar of complex, asynchronous, conditional action sequences. For example, in case of the page tree we need to cover sequences like the following example:
- UI toggle page tree item
- Check if the children are already loaded
- In case the items are already present in the state, just toggle the sub tree.
- If not, load them
- Display the loading icon in the UI
- As soon as the data got successfully loaded, append them to the store
- If added to the store, hide the loading icon
- In case the loading of the data failed, display the error icon
- Finish it all by toggling the sub tree.
You see, this is a lot of conditional logic, which is also asynchronous since we need to fetch data from the server. For this purpose we use Sagas, which rely on generators and include all of the above statements.
In our reducers we use Plow JS
which enforces immutable data and includes a bunch of helper functions for working with nested object structures. This library can optionally return, and we use it pretty much everywhere, curried functions.
If you don't know what a curry function is, you should read this nice interactive tutorial on functional programming. In the longer run we will also re-add immutable.js again.
Further documentation on Plow JS: https://grebaldi.gitbooks.io/plow-js/content/docs/getting-started.html
The application is basically split into two parts. The host frame contains the major parts of the UI, like the main navigation, page tree, inspector and so on. The guest frame handles the inline editing features and is considered to be one possible editing implementation.
For communication between those two frames, there's also a yet-to-be-finalized API part.
Both frames use Redux for state handling and React for UI rendering. Though the guest frames consists of logic with direct DOM access, called nodeComponent
.
Observer API this is a part of the public API, that exposes certain parts of the host store, whenever relevant changes occur
UI API A couple of public API methods that in most cases dispatch actions in the host frame
Node Component All logic, that is attached to dom elements in the guest frame, that represent CR nodes. This is the only place where we have to address nodes with their contextPath
and their typoScriptPath
. The component does nothing more than handling the hovered and focused state of nodes. And it certainly should not do anything more than that. This is the unavoidable leak in the implementation alongside with any inline editor, since both of these require direct DOM access and we can make almost no assumptions about the DOM that gets loaded.
Central store synchronization With Redux in the guest frame, we now have the opportunity to centralize all state and therefore also to centralize synchronization between host and guest. (All of which is happening in Guest/Process/Synchonization.js
)
We strive to a full coverage of the code base, not only by using unit, but also end to end tests before each release. Unit tests are executed by karma, results are asserted by chai and you can mock / spy using sinon.
Adding unit tests is fairly simple, just create a file ending with .spec.js
on the same folder level of your module you want to test, and it will automatically be executed by karma on the next run. Executing just one certain test is possible, just append an .only
at the beginning of your it
or describe
block. F.e. instead of describe('my component', ...)
you need to write describe.only('my component', ...)
and chai will only assert this block and all of it's children.
We also have a convention regarding naming the most outer describe
block. To make debugging easier in case of a failed test, we add the location of the test to the name of the outer describe
block. For example if your test is located in Host/Redux/UI/myModule/
you should begin the test with a wrapping describe block
import myModule from './index.js';
describe('"host.redux.ui.myModule" ', () => {
// Either your "it()" or nested "describe()" blocks.
});
What makes a unit test good? It should the main specification of your module, and thus be easy to understand. Your code and unit tests should also cover cases of misuse, f.e. if you expose a method addTodo
and the method requires the first argument to be an object, and a developer or maybe even the API passes an array by accident instead, you should add a type check against the argument and throw an error in case of a miss use.
Karma will generate unit test metrics after each run which are located in Coverage/
. If you are unsure about the completeness or quality of your test, you should open the generated reports index.html file and find your target module and watch for uncovered branches or conditions.
The tests are running on a selenium grid which is installed & started by the npm run selenium:init
command,
and executed by codecept which runs WebdriverIO in the background.
Like CucumberJS the test framework uses scenarios and steps. Unlike CucumberJS, common steps are predefined.
To run the acceptance tests, execute npm run selenium:init
first, and npm run selenium:run
in a separate session afterwards.
Adding user stories is as simple as creating unit tests, the only difference is that the file needs to be placed in the Tests/TestCodeceptIo
root directory and should end with *.story.js
instead of *.spec.js
.
Since acceptance testing can be relatively time-intensive, it’s important to plan how to organize your tests.
npm run selenium:run -- -f "should be able to type into a nodeType in the guest frame"
Tests are written in Javascript DSL instead of Gherkin.
Feature('CodeceptJS Demonstration');
Scenario('submit form successfully', (I) =>
I.amOnPage('/documentation')
I.fillField('Email', 'hello@world.com')
I.fillField('Password', '123456')
I.checkOption('Active')
I.checkOption('Male');
I.click('Create User')
I.see('User is valid')
I.dontSeeInCurrentUrl('/documentation')
});
We provide some neos specific step helpers which help us testing our interface.
The helpers *_helper.js
files and registered in the codecept.json.
I.focusAndEditInGuestFrame(): Focus the guest frame and edit the first text element.
I.focusAndEditInGuestFrame('Hello Worlds');
I.seeElementInViewport(<Class/ID/Xpath>): Check if element is visible in the current viewport.
I.seeElementInViewport('#neos__topBar__publishDropDown__contents');
I.dontSeeElementInViewport(<Class/ID/Xpath>):
Opposite of seeElementInViewport.
The state is divided into "categories" to differentiate and decouple it.
-
Changes
: Actions and reducers which are responsible of reflecting the changes the user has made locally. -
CR
: Actions and reducers for the content repository. -
Sagas
: All sagas for the application. You will find a reflection of the order structure in here as well. -
System
: Currently just some dummy actions, this will hold system or general app related logic and state. -
UI
: Actions and reducers for UI related logic and state. -
User
: Actions and reducers for user related logic and state.
TODO
For getting started, the "Mostly adequate guide to functional programming" gives you a fairly good overview with lots of examples in JS: https://drboolean.gitbooks.io/mostly-adequate-guide/content/
- Node
- StoredNode
- StoredNodeType / NodeType