diff options
Diffstat (limited to 'doc/development')
-rw-r--r-- | doc/development/README.md | 1 | ||||
-rw-r--r-- | doc/development/ci_setup.md | 47 | ||||
-rw-r--r-- | doc/development/doc_styleguide.md | 7 | ||||
-rw-r--r-- | doc/development/fe_guide/droplab/droplab.md | 256 | ||||
-rw-r--r-- | doc/development/fe_guide/droplab/plugins/ajax.md | 37 | ||||
-rw-r--r-- | doc/development/fe_guide/droplab/plugins/filter.md | 45 | ||||
-rw-r--r-- | doc/development/fe_guide/droplab/plugins/input_setter.md | 60 | ||||
-rw-r--r-- | doc/development/fe_guide/img/boards_diagram.png | bin | 0 -> 30538 bytes | |||
-rw-r--r-- | doc/development/fe_guide/img/vue_arch.png | bin | 0 -> 9848 bytes | |||
-rw-r--r-- | doc/development/fe_guide/index.md | 63 | ||||
-rw-r--r-- | doc/development/fe_guide/style_guide_js.md | 50 | ||||
-rw-r--r-- | doc/development/fe_guide/testing.md | 11 | ||||
-rw-r--r-- | doc/development/fe_guide/vue.md | 355 | ||||
-rw-r--r-- | doc/development/testing.md | 15 | ||||
-rw-r--r-- | doc/development/writing_documentation.md | 4 |
15 files changed, 879 insertions, 72 deletions
diff --git a/doc/development/README.md b/doc/development/README.md index 3c797505aa9..77bb0263374 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -33,7 +33,6 @@ ## Backend howtos - [Architecture](architecture.md) of GitLab -- [CI setup](ci_setup.md) for testing GitLab - [Gotchas](gotchas.md) to avoid - [How to dump production data to staging](db_dump.md) - [Instrumentation](instrumentation.md) diff --git a/doc/development/ci_setup.md b/doc/development/ci_setup.md deleted file mode 100644 index 0810b32efd7..00000000000 --- a/doc/development/ci_setup.md +++ /dev/null @@ -1,47 +0,0 @@ -# CI setup - -This document describes what services we use for testing GitLab and GitLab CI. - -We currently use four CI services to test GitLab: - -1. GitLab CI on [GitHost.io](https://gitlab-ce.githost.io/projects/4/) for the [GitLab.com repo](https://gitlab.com/gitlab-org/gitlab-ce) -2. GitLab CI at ci.gitlab.org to test the private GitLab B.V. repo at dev.gitlab.org -3. [Semephore](https://semaphoreapp.com/gitlabhq/gitlabhq/) for [GitHub.com repo](https://github.com/gitlabhq/gitlabhq) -4. [Mock CI Service](../user/project/integrations/mock_ci.md) for local development - -| Software @ configuration being tested | GitLab CI (ci.gitlab.org) | GitLab CI (GitHost.io) | Semaphore | -|---------------------------------------|---------------------------|---------------------------------------------------------------------------|-----------| -| GitLab CE @ MySQL | ✓ | ✓ [Core team can trigger builds](https://gitlab-ce.githost.io/projects/4) | | -| GitLab CE @ PostgreSQL | | | ✓ [Core team can trigger builds](https://semaphoreapp.com/gitlabhq/gitlabhq/branches/master) | -| GitLab EE @ MySQL | ✓ | | | -| GitLab CI @ MySQL | ✓ | | | -| GitLab CI @ PostgreSQL | | | ✓ | -| GitLab CI Runner | ✓ | | ✓ | -| GitLab Shell | ✓ | | ✓ | -| GitLab Shell | ✓ | | ✓ | - -Core team has access to trigger builds if needed for GitLab CE. - -We use [these build scripts](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml) for testing with GitLab CI. - -# Build configuration on [Semaphore](https://semaphoreapp.com/gitlabhq/gitlabhq/) for testing the [GitHub.com repo](https://github.com/gitlabhq/gitlabhq) - -- Language: Ruby -- Ruby version: 2.1.8 -- database.yml: pg - -Build commands - -```bash -sudo apt-get install cmake libicu-dev -y (Setup) -bundle install --deployment --path vendor/bundle (Setup) -cp config/gitlab.yml.example config/gitlab.yml (Setup) -bundle exec rake db:create (Setup) -bundle exec rake spinach (Thread #1) -bundle exec rake spec (thread #2) -bundle exec rake rubocop (thread #3) -bundle exec rake brakeman (thread #4) -bundle exec rake jasmine:ci (thread #5) -``` - -Use rubygems mirror. diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md index bb78a0de0c5..1e81905c081 100644 --- a/doc/development/doc_styleguide.md +++ b/doc/development/doc_styleguide.md @@ -29,7 +29,8 @@ The table below shows what kind of documentation goes where. | `doc/legal/` | Legal documents about contributing to GitLab. | | `doc/install/`| Probably the most visited directory, since `installation.md` is there. Ideally this should go under `doc/administration/`, but it's best to leave it as-is in order to avoid confusion (still debated though). | | `doc/update/` | Same with `doc/install/`. Should be under `administration/`, but this is a well known location, better leave as-is, at least for now. | -| `doc/topics/` | Indexes per Topic (`doc/topics/topic-name/index.md`); Technical Articles: user guides, admin guides, technical overviews, tutorials (`doc/topics/topic-name/`). | +| `doc/topics/` | Indexes per Topic (`doc/topics/topic-name/index.md`): all resources for that topic (user and admin documentation, articles, and third-party docs) | +| `doc/articles/` | [Technical Articles](writing_documentation.md#technical-articles): user guides, admin guides, technical overviews, tutorials (`doc/articles/article-title/index.md`). | --- @@ -61,8 +62,8 @@ The table below shows what kind of documentation goes where. located at `doc/user/admin_area/settings/visibility_and_access_controls.md`. 1. The `doc/topics/` directory holds topic-related technical content. Create `doc/topics/topic-name/subtopic-name/index.md` when subtopics become necessary. - Note that `topics` holds the index page per topic, and technical articles. General - user- and admin- related documentation, should be placed accordingly. + General user- and admin- related documentation, should be placed accordingly. +1. For technical articles, place their images under `doc/articles/article-title/img/`. --- diff --git a/doc/development/fe_guide/droplab/droplab.md b/doc/development/fe_guide/droplab/droplab.md new file mode 100644 index 00000000000..8f0b6b21953 --- /dev/null +++ b/doc/development/fe_guide/droplab/droplab.md @@ -0,0 +1,256 @@ +# DropLab + +A generic dropdown for all of your custom dropdown needs. + +## Usage + +DropLab can be used by simply adding a `data-dropdown-trigger` HTML attribute. +This attribute allows us to find the "trigger" _(toggle)_ for the dropdown, +whether that is a button, link or input. + +The value of the `data-dropdown-trigger` should be a CSS selector that +DropLab can use to find the trigger's dropdown list. + +You should also add the `data-dropdown` attribute to declare the dropdown list. +The value is irrelevant. + +The DropLab class has no side effects, so you must always call `.init` when +the DOM is ready. `DropLab.prototype.init` takes the same arguments as `DropLab.prototype.addHook`. +If you do not provide any arguments, it will globally query and instantiate all droplab compatible dropdowns. + +```html +<a href="#" data-dropdown-trigger="#list">Toggle</a> + +<ul id="list" data-dropdown> + <!-- ... --> +<ul> +``` +```js +const droplab = new DropLab(); +droplab.init(); +``` + +As you can see, we have a "Toggle" link, that is declared as a trigger. +It provides a selector to find the dropdown list it should control. + +### Static data + +You can add static list items. + +```html +<a href="#" data-dropdown-trigger="#list">Toggle</a> + +<ul id="list" data-dropdown> + <li>Static value 1</li> + <li>Static value 2</li> +<ul> +``` +```js +const droplab = new DropLab(); +droplab.init(); +``` + +### Explicit instantiation + +You can pass the trigger and list elements as constructor arguments to return +a non-global instance of DropLab using the `DropLab.prototype.init` method. + +```html +<a href="#" id="trigger" data-dropdown-trigger="#list">Toggle</a> + +<ul id="list" data-dropdown> + <!-- ... --> +<ul> +``` +```js +const trigger = document.getElementById('trigger'); +const list = document.getElementById('list'); + +const droplab = new DropLab(); +droplab.init(trigger, list); +``` + +You can also add hooks to an existing DropLab instance using `DropLab.prototype.addHook`. + +```html +<a href="#" data-dropdown-trigger="#auto-dropdown">Toggle</a> +<ul id="auto-dropdown" data-dropdown><!-- ... --><ul> + +<a href="#" id="trigger" data-dropdown-trigger="#list">Toggle</a> +<ul id="list" data-dropdown><!-- ... --><ul> +``` +```js +const droplab = new DropLab(); + +droplab.init(); + +const trigger = document.getElementById('trigger'); +const list = document.getElementById('list'); + +droplab.addHook(trigger, list); +``` + + +### Dynamic data + +Adding `data-dynamic` to your dropdown element will enable dynamic list rendering. + +You can template a list item using the keys of the data object provided. +Use the handlebars syntax `{{ value }}` to HTML escape the value. +Use the `<%= value %>` syntax to simply interpolate the value. +Use the `<%= value %>` syntax to evaluate the value. + +Passing an array of objects to `DropLab.prototype.addData` will render that data +for all `data-dynamic` dropdown lists tracked by that DropLab instance. + +```html +<a href="#" data-dropdown-trigger="#list">Toggle</a> + +<ul id="list" data-dropdown data-dynamic> + <li><a href="#" data-id="{{id}}">{{text}}</a></li> +</ul> +``` +```js +const droplab = new DropLab(); + +droplab.init().addData([{ + id: 0, + text: 'Jacob', +}, { + id: 1, + text: 'Jeff', +}]); +``` + +Alternatively, you can specify a specific dropdown to add this data to but passing +the data as the second argument and and the `id` of the trigger element as the first argument. + +```html +<a href="#" data-dropdown-trigger="#list" id="trigger">Toggle</a> + +<ul id="list" data-dropdown data-dynamic> + <li><a href="#" data-id="{{id}}">{{text}}</a></li> +</ul> +``` +```js +const droplab = new DropLab(); + +droplab.init().addData('trigger', [{ + id: 0, + text: 'Jacob', +}, { + id: 1, + text: 'Jeff', +}]); +``` + +This allows you to mix static and dynamic content with ease, even with one trigger. + +Note the use of scoping regarding the `data-dropdown` attribute to capture both +dropdown lists, one of which is dynamic. + +```html +<input id="trigger" data-dropdown-trigger="#list"> +<div id="list" data-dropdown> + <ul> + <li><a href="#">Static item 1</a></li> + <li><a href="#">Static item 2</a></li> + </ul> + <ul data-dynamic> + <li><a href="#" data-id="{{id}}">{{text}}</a></li> + </ul> +</div> +``` +```js +const droplab = new DropLab(); + +droplab.init().addData('trigger', [{ + id: 0, + text: 'Jacob', +}, { + id: 1, + text: 'Jeff', +}]); +``` + +## Internal selectors + +DropLab adds some CSS classes to help lower the barrier to integration. + +For example, + +* The `droplab-item-selected` css class is added to items that have been selected +either by a mouse click or by enter key selection. +* The `droplab-item-active` css class is added to items that have been selected +using arrow key navigation. + +## Internal events + +DropLab uses some custom events to help lower the barrier to integration. + +For example, + +* The `click.dl` event is fired when an `li` list item has been clicked. It is also +fired when a list item has been selected with the keyboard. It is also fired when a +`HookButton` button is clicked (a registered `button` tag or `a` tag trigger). +* The `input.dl` event is fired when a `HookInput` (a registered `input` tag trigger) triggers an `input` event. +* The `mousedown.dl` event is fired when a `HookInput` triggers a `mousedown` event. +* The `keyup.dl` event is fired when a `HookInput` triggers a `keyup` event. +* The `keydown.dl` event is fired when a `HookInput` triggers a `keydown` event. + +These custom events add a `detail` object to the vanilla `Event` object that provides some potentially useful data. + +## Plugins + +Plugins are objects that are registered to be executed when a hook is added (when a droplab trigger and dropdown are instantiated). + +If no modules API is detected, the library will fall back as it does with `window.DropLab` and will add `window.DropLab.plugins.PluginName`. + +### Usage + +To use plugins, you can pass them in an array as the third argument of `DropLab.prototype.init` or `DropLab.prototype.addHook`. +Some plugins require configuration values, the config object can be passed as the fourth argument. + +```html +<a href="#" id="trigger" data-dropdown-trigger="#list">Toggle</a> +<ul id="list" data-dropdown><!-- ... --><ul> +``` +```js +const droplab = new DropLab(); + +const trigger = document.getElementById('trigger'); +const list = document.getElementById('list'); + +droplab.init(trigger, list, [droplabAjax], { + droplabAjax: { + endpoint: '/some-endpoint', + method: 'setData', + }, +}); +``` + +### Documentation + +* [Ajax plugin](plugins/ajax.md) +* [Filter plugin](plugins/filter.md) +* [InputSetter plugin](plugins/input_setter.md) + +### Development + +When plugins are initialised for a droplab trigger+dropdown, DropLab will +call the plugins `init` function, so this must be implemented in the plugin. + +```js +class MyPlugin { + static init() { + this.someProp = 'someProp'; + this.someMethod(); + } + + static someMethod() { + this.otherProp = 'otherProp'; + } +} + +export default MyPlugin; +``` diff --git a/doc/development/fe_guide/droplab/plugins/ajax.md b/doc/development/fe_guide/droplab/plugins/ajax.md new file mode 100644 index 00000000000..9c7e56fa448 --- /dev/null +++ b/doc/development/fe_guide/droplab/plugins/ajax.md @@ -0,0 +1,37 @@ +# Ajax + +`Ajax` is a droplab plugin that allows for retrieving and rendering list data from a server. + +## Usage + +Add the `Ajax` object to the plugins array of a `DropLab.prototype.init` or `DropLab.prototype.addHook` call. + +`Ajax` requires 2 config values, the `endpoint` and `method`. + +* `endpoint` should be a URL to the request endpoint. +* `method` should be `setData` or `addData`. +* `setData` completely replaces the dropdown with the response data. +* `addData` appends the response data to the current dropdown list. + +```html +<a href="#" id="trigger" data-dropdown-trigger="#list">Toggle</a> +<ul id="list" data-dropdown><!-- ... --><ul> +``` +```js + const droplab = new DropLab(); + + const trigger = document.getElementById('trigger'); + const list = document.getElementById('list'); + + droplab.addHook(trigger, list, [Ajax], { + Ajax: { + endpoint: '/some-endpoint', + method: 'setData', + }, + }); +``` + +Optionally you can set `loadingTemplate` to a HTML string. This HTML string will +replace the dropdown list whilst the request is pending. + +Additionally, you can set `onError` to a function to catch any XHR errors. diff --git a/doc/development/fe_guide/droplab/plugins/filter.md b/doc/development/fe_guide/droplab/plugins/filter.md new file mode 100644 index 00000000000..0853ea4d320 --- /dev/null +++ b/doc/development/fe_guide/droplab/plugins/filter.md @@ -0,0 +1,45 @@ +# Filter + +`Filter` is a plugin that allows for filtering data that has been added +to the dropdown using a simple fuzzy string search of an input value. + +## Usage + +Add the `Filter` object to the plugins array of a `DropLab.prototype.init` or `DropLab.prototype.addHook` call. + +* `Filter` requires a config value for `template`. +* `template` should be the key of the objects within your data array that you want to compare +to the user input string, for filtering. + +```html +<input href="#" id="trigger" data-dropdown-trigger="#list"> +<ul id="list" data-dropdown data-dynamic> + <li><a href="#" data-id="{{id}}">{{text}}</a></li> +<ul> +``` +```js + const droplab = new DropLab(); + + const trigger = document.getElementById('trigger'); + const list = document.getElementById('list'); + + droplab.init(trigger, list, [Filter], { + Filter: { + template: 'text', + }, + }); + + droplab.addData('trigger', [{ + id: 0, + text: 'Jacob', + }, { + id: 1, + text: 'Jeff', + }]); +``` + +Above, the input string will be compared against the `test` key of the passed data objects. + +Optionally you can set `filterFunction` to a function. This function will be used instead +of `Filter`'s built in string search. `filterFunction` is passed 2 arguments, the first +is one of the data objects, the second is the current input value. diff --git a/doc/development/fe_guide/droplab/plugins/input_setter.md b/doc/development/fe_guide/droplab/plugins/input_setter.md new file mode 100644 index 00000000000..a549603c20d --- /dev/null +++ b/doc/development/fe_guide/droplab/plugins/input_setter.md @@ -0,0 +1,60 @@ +# InputSetter + +`InputSetter` is a plugin that allows for udating DOM out of the scope of droplab when a list item is clicked. + +## Usage + +Add the `InputSetter` object to the plugins array of a `DropLab.prototype.init` or `DropLab.prototype.addHook` call. + +* `InputSetter` requires a config value for `input` and `valueAttribute`. +* `input` should be the DOM element that you want to manipulate. +* `valueAttribute` should be a string that is the name of an attribute on your list items that is used to get the value +to update the `input` element with. + +You can also set the `InputSetter` config to an array of objects, which will allow you to update multiple elements. + + +```html +<input id="input" value=""> +<div id="div" data-selected-id=""></div> + +<input href="#" id="trigger" data-dropdown-trigger="#list"> +<ul id="list" data-dropdown data-dynamic> + <li><a href="#" data-id="{{id}}">{{text}}</a></li> +<ul> +``` +```js + const droplab = new DropLab(); + + const trigger = document.getElementById('trigger'); + const list = document.getElementById('list'); + + const input = document.getElementById('input'); + const div = document.getElementById('div'); + + droplab.init(trigger, list, [InputSetter], { + InputSetter: [{ + input: input, + valueAttribute: 'data-id', + } { + input: div, + valueAttribute: 'data-id', + inputAttribute: 'data-selected-id', + }], + }); + + droplab.addData('trigger', [{ + id: 0, + text: 'Jacob', + }, { + id: 1, + text: 'Jeff', + }]); +``` + +Above, if the second list item was clicked, it would update the `#input` element +to have a `value` of `1`, it would also update the `#div` element's `data-selected-id` to `1`. + +Optionally you can set `inputAttribute` to a string that is the name of an attribute on your `input` element that you want to update. +If you do not provide an `inputAttribute`, `InputSetter` will update the `value` of the `input` element if it is an `INPUT` element, +or the `textContent` of the `input` element if it is not an `INPUT` element. diff --git a/doc/development/fe_guide/img/boards_diagram.png b/doc/development/fe_guide/img/boards_diagram.png Binary files differnew file mode 100644 index 00000000000..7a2cf972fd0 --- /dev/null +++ b/doc/development/fe_guide/img/boards_diagram.png diff --git a/doc/development/fe_guide/img/vue_arch.png b/doc/development/fe_guide/img/vue_arch.png Binary files differnew file mode 100644 index 00000000000..a67706c7c1e --- /dev/null +++ b/doc/development/fe_guide/img/vue_arch.png diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md index f963bffde37..a08694fb66a 100644 --- a/doc/development/fe_guide/index.md +++ b/doc/development/fe_guide/index.md @@ -27,6 +27,59 @@ For our currently-supported browsers, see our [requirements][requirements]. --- +## Development Process + +When you are assigned an issue please follow the next steps: + +### Divide a big feature into small Merge Requests +1. Big Merge Request are painful to review. In order to make this process easier we +must break a big feature into smaller ones and create a Merge Request for each step. +1. First step is to create a branch from `master`, let's call it `new-feature`. This branch +will be the recipient of all the smaller Merge Requests. Only this one will be merged to master. +1. Don't do any work on this one, let's keep it synced with master. +1. Create a new branch from `new-feature`, let's call it `new-feature-step-1`. We advise you +to clearly identify which step the branch represents. +1. Do the first part of the modifications in this branch. The target branch of this Merge Request +should be `new-feature`. +1. Once `new-feature-step-1` gets merged into `new-feature` we can continue our work. Create a new +branch from `new-feature`, let's call it `new-feature-step-2` and repeat the process done before. + +```shell +* master +|\ +| * new-feature +| |\ +| | * new-feature-step-1 +| |\ +| | * new-feature-step-2 +| |\ +| | * new-feature-step-3 +``` + +**Tips** +- Make sure `new-feature` branch is always synced with `master`: merge master frequently. +- Do the same for the feature branch you have opened. This can be accomplished by merging `master` into `new-feature` and `new-feature` into `new-feature-step-*` +- Avoid rewriting history. + +### Share your work early +1. Before writing code guarantee your vision of the architecture is aligned with +GitLab's architecture. +1. Add a diagram to the issue and ask a Frontend Architecture about it. + +  + +1. Don't take more than one week between starting work on a feature and +sharing a Merge Request with a reviewer or a maintainer. + +### Vue features +1. Follow the steps in [Vue.js Best Practices](vue.md) +1. Follow the style guide. +1. Only a handful of people are allowed to merge Vue related features. +Reach out to one of Vue experts early in this process. + + +--- + ## [Architecture](architecture.md) How we go about making fundamental design decisions in GitLab's frontend team or make changes to our frontend development guidelines. @@ -90,3 +143,13 @@ Our accessibility standards and resources. [scss-lint]: https://github.com/brigade/scss-lint [install]: ../../install/installation.md#4-node [requirements]: ../../install/requirements.md#supported-web-browsers + +--- + +## [DropLab](droplab/droplab.md) +Our internal `DropLab` dropdown library. + +* [DropLab](droplab/droplab.md) +* [Ajax plugin](droplab/plugins/ajax.md) +* [Filter plugin](droplab/plugins/filter.md) +* [InputSetter plugin](droplab/plugins/input_setter.md) diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md index abd241c0bc8..1d2b0558948 100644 --- a/doc/development/fe_guide/style_guide_js.md +++ b/doc/development/fe_guide/style_guide_js.md @@ -58,7 +58,7 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns. import Bar from './bar'; ``` -- **Never** disable the `no-undef` rule. Declare globals with `/* global Foo */` instead. +- **Never** disable the `no-undef` rule. Declare globals with `/* global Foo */` instead. - When declaring multiple globals, always use one `/* global [name] */` line per variable. @@ -71,6 +71,16 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns. /* global Cookies */ /* global jQuery */ ``` + +- Use up to 3 parameters for a function or class. If you need more accept an Object instead. + + ```javascript + // bad + fn(p1, p2, p3, p4) {} + + // good + fn(options) {} + ``` #### Modules, Imports, and Exports - Use ES module syntax to import modules @@ -168,6 +178,23 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns. - Avoid constructors with side-effects +- Prefer `.map`, `.reduce` or `.filter` over `.forEach` +A forEach will cause side effects, it will be mutating the array being iterated. Prefer using `.map`, +`.reduce` or `.filter` + + ```javascript + const users = [ { name: 'Foo' }, { name: 'Bar' } ]; + + // bad + users.forEach((user, index) => { + user.id = index; + }); + + // good + const usersWithId = users.map((user, index) => { + return Object.assign({}, user, { id: index }); + }); + ``` #### Parse Strings into Numbers - `parseInt()` is preferable over `Number()` or `+` @@ -183,6 +210,19 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns. parseInt('10', 10); ``` +#### CSS classes used for JavaScript +- If the class is being used in Javascript it needs to be prepend with `js-` + ```html + // bad + <button class="add-user"> + Add User + </button> + + // good + <button class="js-add-user"> + Add User + </button> + ``` ### Vue.js @@ -200,6 +240,7 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns. #### Naming - **Extensions**: Use `.vue` extension for Vue components. - **Reference Naming**: Use PascalCase for Vue components and camelCase for their instances: + ```javascript // bad import cardBoard from 'cardBoard'; @@ -217,6 +258,7 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns. cardBoard: CardBoard }; ``` + - **Props Naming:** - Avoid using DOM component prop names. - Use kebab-case instead of camelCase to provide props in templates. @@ -243,12 +285,18 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns. <component v-if="bar" param="baz" /> + <button class="btn">Click me</button> + // good <component v-if="bar" param="baz" /> + <button class="btn"> + Click me + </button> + // if props fit in one line then keep it on the same line <component bar="bar" /> ``` diff --git a/doc/development/fe_guide/testing.md b/doc/development/fe_guide/testing.md index a4631fd0073..157c13352ca 100644 --- a/doc/development/fe_guide/testing.md +++ b/doc/development/fe_guide/testing.md @@ -14,8 +14,10 @@ for more information on general testing practices at GitLab. GitLab uses the [Karma][karma] test runner with [Jasmine][jasmine] as its test framework for our JavaScript unit tests. For tests that rely on DOM -manipulation we use fixtures which are pre-compiled from HAML source files and -served during testing by the [jasmine-jquery][jasmine-jquery] plugin. +manipulation, we generate HTML files using RSpec suites (see `spec/javascripts/fixtures/*.rb` for examples). +Some fixtures are still HAML templates that are translated to HTML files using the same mechanism (see `static_fixtures.rb`). +Those will be migrated over time. +Fixtures are served during testing by the [jasmine-jquery][jasmine-jquery] plugin. JavaScript tests live in `spec/javascripts/`, matching the folder structure of `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js` @@ -26,6 +28,10 @@ browser and you will not have access to certain APIs, such as [`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification), which will have to be stubbed. +### Writing tests +### Vue.js unit tests +See this [section][vue-test]. + ### Running frontend tests `rake karma` runs the frontend-only (JavaScript) tests. @@ -134,3 +140,4 @@ Scenario: Developer can approve merge request [jasmine-focus]: https://jasmine.github.io/2.5/focused_specs.html [jasmine-jquery]: https://github.com/velesin/jasmine-jquery [karma]: http://karma-runner.github.io/ +[vue-test]:https://docs.gitlab.com/ce/development/fe_guide/vue.html#testing-vue-components diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md index 3e3406e7d6a..73d2ffc1bdc 100644 --- a/doc/development/fe_guide/vue.md +++ b/doc/development/fe_guide/vue.md @@ -19,13 +19,31 @@ We don't want to refactor all GitLab frontend code into Vue.js, here are some gu when not to use Vue.js: - Adding or changing static information; -- Features that highly depend on jQuery will be hard to work with Vue.js +- Features that highly depend on jQuery will be hard to work with Vue.js; +- Features without reactive data; As always, the Frontend Architectural Experts are available to help with any Vue or JavaScript questions. -## How to build a new feature with Vue.js +## Vue architecture -**Components, Stores and Services** +All new features built with Vue.js must follow a [Flux architecture][flux]. +The main goal we are trying to achieve is to have only one data flow and only one data entry. +In order to achieve this goal, each Vue bundle needs a Store - where we keep all the data -, +a Service - that we use to communicate with the server - and a main Vue component. + +Think of the Main Vue Component as the entry point of your application. This is the only smart +component that should exist in each Vue feature. +This component is responsible for: +1. Calling the Service to get data from the server +1. Calling the Store to store the data received +1. Mounting all the other components + +  + +You can also read about this architecture in vue docs about [state management][state-management] +and about [one way data flow][one-way-data-flow]. + +### Components, Stores and Services In some features implemented with Vue.js, like the [issue board][issue-boards] or [environments table][environments-table] @@ -46,16 +64,17 @@ _For consistency purposes, we recommend you to follow the same structure._ Let's look into each of them: -**A `*_bundle.js` file** +### A `*_bundle.js` file This is the index file of your new feature. This is where the root Vue instance of the new feature should be. -The Store and the Service should be imported and initialized in this file and provided as a prop to the main component. +The Store and the Service should be imported and initialized in this file and +provided as a prop to the main component. Don't forget to follow [these steps.][page_specific_javascript] -**A folder for Components** +### A folder for Components This folder holds all components that are specific of this new feature. If you need to use or create a component that will probably be used somewhere @@ -70,20 +89,219 @@ in one table would not be a good use of this pattern. You can read more about components in Vue.js site, [Component System][component-system] -**A folder for the Store** +### A folder for the Store The Store is a class that allows us to manage the state in a single -source of truth. +source of truth. It is not aware of the service or the components. The concept we are trying to follow is better explained by Vue documentation itself, please read this guide: [State Management][state-management] -**A folder for the Service** +### A folder for the Service + +The Service is a class used only to communicate with the server. +It does not store or manipulate any data. It is not aware of the store or the components. +We use [vue-resource][vue-resource-repo] to communicate with the server. + +Vue Resource should only be imported in the service file. + + ```javascript + import Vue from 'vue'; + import VueResource from 'vue-resource'; + + Vue.use(VueResource); + ``` + +### CSRF token +We use a Vue Resource interceptor to manage the CSRF token. +`app/assets/javascripts/vue_shared/vue_resource_interceptor.js` holds all our common interceptors. +Note: You don't need to load `app/assets/javascripts/vue_shared/vue_resource_interceptor.js` +since it's already being loaded by `common_vue.js`. + +### End Result + +The following example shows an application: + +```javascript +// store.js +export default class Store { + + /** + * This is where we will iniatialize the state of our data. + * Usually in a small SPA you don't need any options when starting the store. In the case you do + * need guarantee it's an Object and it's documented. + * + * @param {Object} options + */ + constructor(options) { + this.options = options; + + // Create a state object to handle all our data in the same place + this.todos = []: + } + + setTodos(todos = []) { + this.todos = todos; + } + + addTodo(todo) { + this.todos.push(todo); + } + + removeTodo(todoID) { + const state = this.todos; + + const newState = state.filter((element) => {element.id !== todoID}); + + this.todos = newState; + } +} + +// service.js +import Vue from 'vue'; +import VueResource from 'vue-resource'; +import 'vue_shared/vue_resource_interceptor'; + +Vue.use(VueResource); + +export default class Service { + constructor(options) { + this.todos = Vue.resource(endpoint.todosEndpoint); + } + + getTodos() { + return this.todos.get(); + } + + addTodo(todo) { + return this.todos.put(todo); + } +} +// todo_component.vue +<script> +export default { + props: { + data: { + type: Object, + required: true, + }, + } +} +</script> +<template> + <div> + <h1> + Title: {{data.title}} + </h1> + <p> + {{data.text}} + </p> + </div> +</template> + +// todos_main_component.vue +<script> +import Store from 'store'; +import Service from 'service'; +import TodoComponent from 'todoComponent'; +export default { + /** + * Although most data belongs in the store, each component it's own state. + * We want to show a loading spinner while we are fetching the todos, this state belong + * in the component. + * + * We need to access the store methods through all methods of our component. + * We need to access the state of our store. + */ + data() { + const store = new Store(); + + return { + isLoading: false, + store: store, + todos: store.todos, + }; + }, + + components: { + todo: TodoComponent, + }, + + created() { + this.service = new Service('todos'); + + this.getTodos(); + }, + + methods: { + getTodos() { + this.isLoading = true; + + this.service.getTodos() + .then(response => response.json()) + .then((response) => { + this.store.setTodos(response); + this.isLoading = false; + }) + .catch(() => { + this.isLoading = false; + // Show an error + }); + }, + + addTodo(todo) { + this.service.addTodo(todo) + then(response => response.json()) + .then((response) => { + this.store.addTodo(response); + }) + .catch(() => { + // Show an error + }); + } + } +} +</script> +<template> + <div class="container"> + <div v-if="isLoading"> + <i + class="fa fa-spin fa-spinner" + aria-hidden="true" /> + </div> + + <div + v-if="!isLoading" + class="js-todo-list"> + <template v-for='todo in todos'> + <todo :data="todo" /> + </template> + + <button + @click="addTodo" + class="js-add-todo"> + Add Todo + </button> + </div> + <div> +</template> + +// bundle.js +import todoComponent from 'todos_main_component.vue'; + +new Vue({ + el: '.js-todo-app', + components: { + todoComponent, + }, + render: createElement => createElement('todo-component' { + props: { + someProp: [], + } + }), +}); -The Service is used only to communicate with the server. -It does not store or manipulate any data. -We use [vue-resource][vue-resource-repo] to -communicate with the server. +``` The [issue boards service][issue-boards-service] is a good example of this pattern. @@ -93,6 +311,114 @@ is a good example of this pattern. Please refer to the Vue section of our [style guide](style_guide_js.md#vuejs) for best practices while writing your Vue components and templates. +## Testing Vue Components + +Each Vue component has a unique output. This output is always present in the render function. + +Although we can test each method of a Vue component individually, our goal must be to test the output +of the render/template function, which represents the state at all times. + +Make use of Vue Resource Interceptors to mock data returned by the service. + +Here's how we would test the Todo App above: + +```javascript +import component from 'todos_main_component'; + +describe('Todos App', () => { + it('should render the loading state while the request is being made', () => { + const Component = Vue.extend(component); + + const vm = new Component().$mount(); + + expect(vm.$el.querySelector('i.fa-spin')).toBeDefined(); + }); + + describe('with data', () => { + // Mock the service to return data + const interceptor = (request, next) => { + next(request.respondWith(JSON.stringify([{ + title: 'This is a todo', + body: 'This is the text' + }]), { + status: 200, + })); + }; + + let vm; + + beforeEach(() => { + Vue.http.interceptors.push(interceptor); + + const Component = Vue.extend(component); + + vm = new Component().$mount(); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + }); + + + it('should render todos', (done) => { + setTimeout(() => { + expect(vm.$el.querySelectorAll('.js-todo-list div').length).toBe(1); + done(); + }, 0); + }); + }); + + describe('add todo', () => { + let vm; + beforeEach(() => { + const Component = Vue.extend(component); + vm = new Component().$mount(); + }); + it('should add a todos', (done) => { + setTimeout(() => { + vm.$el.querySelector('.js-add-todo').click(); + + // Add a new interceptor to mock the add Todo request + Vue.nextTick(() => { + expect(vm.$el.querySelectorAll('.js-todo-list div').length).toBe(2); + }); + }, 0); + }); + }); +}); +``` + +### Stubbing API responses +[Vue Resource Interceptors][vue-resource-interceptor] allow us to add a interceptor with +the response we need: + +```javascript + // Mock the service to return data + const interceptor = (request, next) => { + next(request.respondWith(JSON.stringify([{ + title: 'This is a todo', + body: 'This is the text' + }]), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(interceptor); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + }); + + it('should do something', (done) => { + setTimeout(() => { + // Test received data + done(); + }, 0); + }); +``` + [vue-docs]: http://vuejs.org/guide/index.html [issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards @@ -100,5 +426,8 @@ for best practices while writing your Vue components and templates. [page_specific_javascript]: https://docs.gitlab.com/ce/development/frontend.html#page-specific-javascript [component-system]: https://vuejs.org/v2/guide/#Composing-with-Components [state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch +[one-way-data-flow]: https://vuejs.org/v2/guide/components.html#One-Way-Data-Flow [vue-resource-repo]: https://github.com/pagekit/vue-resource +[vue-resource-interceptor]: https://github.com/pagekit/vue-resource/blob/develop/docs/http.md#interceptors [issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6 +[flux]: https://facebook.github.io/flux diff --git a/doc/development/testing.md b/doc/development/testing.md index ad540ec13db..2c7154f1dea 100644 --- a/doc/development/testing.md +++ b/doc/development/testing.md @@ -448,13 +448,22 @@ is used for Spinach tests as well. ### Monitoring -The GitLab test suite is [monitored] and a [public dashboard] is available for -everyone to see. Feel free to look at the slowest test files and try to improve -them. +The GitLab test suite is [monitored] for the `master` branch, and any branch +that includes `rspec-profile` in their name. + +A [public dashboard] is available for everyone to see. Feel free to look at the +slowest test files and try to improve them. [monitored]: ./performance.md#rspec-profiling [public dashboard]: https://redash.gitlab.com/public/dashboards/l1WhHXaxrCWM5Ai9D7YDqHKehq6OU3bx5gssaiWe?org_slug=default +## CI setup + +- On CE, the test suite only runs against PostgreSQL by default. We additionally + run the suite against MySQL for tags, `master`, and any branch that includes + `mysql` in the name. +- On EE, the test suite always runs both PostgreSQL and MySQL. + ## Spinach (feature) tests GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426) diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md index 482ec54207b..166a10293c3 100644 --- a/doc/development/writing_documentation.md +++ b/doc/development/writing_documentation.md @@ -2,7 +2,7 @@ - **General Documentation**: written by the developers responsible by creating features. Should be submitted in the same merge request containing code. Feature proposals (by GitLab contributors) should also be accompanied by its respective documentation. They can be later improved by PMs and Technical Writers. - **Technical Articles**: written by any [GitLab Team](https://about.gitlab.com/team/) member, GitLab contributors, or [Community Writers](https://about.gitlab.com/handbook/product/technical-writing/community-writers/). - - **Indexes per topic**: initially prepared by the Technical Writing Team, and kept up-to-date by developers and PMs, in the same merge request containing code. + - **Indexes per topic**: initially prepared by the Technical Writing Team, and kept up-to-date by developers and PMs in the same merge request containing code. They gather all resources for that topic in a single page (user and admin documentation, articles, and third-party docs). ## Distinction between General Documentation and Technical Articles @@ -18,7 +18,7 @@ They are topic-related documentation, written with an user-friendly approach and A technical article guides users and/or admins to achieve certain objectives (within guides and tutorials), or provide an overview of that particular topic or feature (within technical overviews). It can also describe the use, implementation, or integration of third-party tools with GitLab. -They live under `doc/topics/topic-name/`, and can be searched per topic, within "Indexes per Topic" pages. The topics are listed on the main [Indexes per Topic](../topics/index.md) page. +They live under `doc/articles/article-title/index.md`, and their images should be placed under `doc/articles/article-title/img/`. Find a list of existing [technical articles](../articles/index.md) here. #### Types of Technical Articles |