summaryrefslogtreecommitdiff
path: root/doc/development/fe_guide/vue.md
diff options
context:
space:
mode:
Diffstat (limited to 'doc/development/fe_guide/vue.md')
-rw-r--r--doc/development/fe_guide/vue.md216
1 files changed, 145 insertions, 71 deletions
diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md
index aeedd57fd83..972c2ded9c9 100644
--- a/doc/development/fe_guide/vue.md
+++ b/doc/development/fe_guide/vue.md
@@ -12,17 +12,17 @@ What is described in the following sections can be found in these examples:
## Vue architecture
-All new features built with Vue.js must follow a [Flux architecture][flux].
+All new features built with Vue.js must follow a [Flux architecture](https://facebook.github.io/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 we use [vuex](#vuex).
-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].
+You can also read about this architecture in vue docs about [state management](https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch)
+and about [one way data flow](https://vuejs.org/v2/guide/components.html#One-Way-Data-Flow).
### Components and Store
-In some features implemented with Vue.js, like the [issue board][issue-boards]
-or [environments table][environments-table]
+In some features implemented with Vue.js, like the [issue board](https://gitlab.com/gitlab-org/gitlab-foss/tree/master/app/assets/javascripts/boards)
+or [environments table](https://gitlab.com/gitlab-org/gitlab-foss/tree/master/app/assets/javascripts/environments)
you can find a clear separation of concerns:
```plaintext
@@ -47,7 +47,7 @@ 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.
-Be sure to read about [page-specific JavaScript][page_specific_javascript].
+Be sure to read about [page-specific JavaScript](./performance.md#page-specific-javascript).
### Bootstrapping Gotchas
@@ -162,7 +162,7 @@ For example, tables are used in a quite amount of places across GitLab, a table
would be a good fit for a component. On the other hand, a table cell used only
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]
+You can read more about components in Vue.js site, [Component System](https://vuejs.org/v2/guide/#Composing-with-Components).
### A folder for the Store
@@ -189,96 +189,135 @@ Each Vue component has a unique output. This output is always present in the ren
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 the [axios mock adapter](axios.md#mock-axios-response-in-tests) to mock data returned.
-
-Here's how we would test the Todo App above:
+Here's an example of a well structured unit test for [this Vue component](#appendix---vue-component-subject-under-test):
```javascript
-import Vue from 'vue';
-import axios from '~/lib/utils/axios_utils';
+import { shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import App from '~/todos/app.vue';
+
+const TEST_TODOS = [
+ { text: 'Lorem ipsum test text' },
+ { text: 'Lorem ipsum 2' },
+];
+const TEST_NEW_TODO = 'New todo title';
+const TEST_TODO_PATH = '/todos';
-describe('Todos App', () => {
- let vm;
+describe('~/todos/app.vue', () => {
+ let wrapper;
let mock;
beforeEach(() => {
- // Create a mock adapter for stubbing axios API requests
+ // IMPORTANT: Use axios-mock-adapter for stubbing axios API requests
mock = new MockAdapter(axios);
-
- const Component = Vue.extend(component);
-
- // Mount the Component
- vm = new Component().$mount();
+ mock.onGet(TEST_TODO_PATH).reply(200, TEST_TODOS);
+ mock.onPost(TEST_TODO_PATH).reply(200);
});
afterEach(() => {
- // Reset the mock adapter
- mock.restore();
- // Destroy the mounted component
- vm.$destroy();
- });
+ // IMPORTANT: Clean up the component instance and axios mock adapter
+ wrapper.destroy();
+ wrapper = null;
- it('should render the loading state while the request is being made', () => {
- expect(vm.$el.querySelector('i.fa-spin')).toBeDefined();
+ mock.restore();
});
- it('should render todos returned by the endpoint', done => {
- // Mock the get request on the API endpoint to return data
- mock.onGet('/todos').replyOnce(200, [
- {
- title: 'This is a todo',
- text: 'This is the text',
+ // NOTE: It is very helpful to separate setting up the component from
+ // its collaborators (i.e. Vuex, axios, etc.)
+ const createWrapper = (props = {}) => {
+ wrapper = shallowMount(App, {
+ propsData: {
+ path: TEST_TODO_PATH,
+ ...props,
},
- ]);
+ });
+ };
+ // NOTE: Helper methods greatly help test maintainability and readability.
+ const findLoader = () => wrapper.find(GlLoadingIcon);
+ const findAddButton = () => wrapper.find('[data-testid="add-button"]');
+ const findTextInput = () => wrapper.find('[data-testid="text-input"]');
+ const findTodoData = () => wrapper.findAll('[data-testid="todo-item"]').wrappers.map(wrapper => ({ text: wrapper.text() }));
+
+ describe('when mounted and loading', () => {
+ beforeEach(() => {
+ // Create request which will never resolve
+ mock.onGet(TEST_TODO_PATH).reply(() => new Promise(() => {}));
+ createWrapper();
+ });
- Vue.nextTick(() => {
- const items = vm.$el.querySelectorAll('.js-todo-list div')
- expect(items.length).toBe(1);
- expect(items[0].textContent).toContain('This is the text');
- done();
+ it('should render the loading state', () => {
+ expect(findLoader().exists()).toBe(true);
});
});
- it('should add a todos on button click', (done) => {
+ describe('when todos are loaded', () => {
+ beforeEach(() => {
+ createWrapper();
+ // IMPORTANT: This component fetches data asynchronously on mount, so let's wait for the Vue template to update
+ return wrapper.vm.$nextTick();
+ });
- // Mock the put request and check that the sent data object is correct
- mock.onPut('/todos').replyOnce((req) => {
- expect(req.data).toContain('text');
- expect(req.data).toContain('title');
+ it('should not show loading', () => {
+ expect(findLoader().exists()).toBe(false);
+ });
- return [201, {}];
+ it('should render todos', () => {
+ expect(findTodoData()).toEqual(TEST_TODOS);
});
- vm.$el.querySelector('.js-add-todo').click();
+ it('when todo is added, should post new todo', () => {
+ findTextInput().vm.$emit('update', TEST_NEW_TODO)
+ findAddButton().vm.$emit('click');
- // Add a new interceptor to mock the add Todo request
- Vue.nextTick(() => {
- expect(vm.$el.querySelectorAll('.js-todo-list div').length).toBe(2);
- done();
+ return wrapper.vm.$nextTick()
+ .then(() => {
+ expect(mock.history.post.map(x => JSON.parse(x.data))).toEqual([{ text: TEST_NEW_TODO }]);
+ });
});
});
});
```
-### `mountComponent` helper
+### Test the component's output
-There is a helper in `spec/javascripts/helpers/vue_mount_component_helper.js` that allows you to mount a component with the given props:
+The main return value of a Vue component is the rendered output. In order to test the component we
+need to test the rendered output. [Vue](https://vuejs.org/v2/guide/unit-testing.html) guide's to unit test show us exactly that:
+
+### Events
+
+We should test for events emitted in response to an action within our component, this is useful to verify the correct events are being fired with the correct arguments.
+
+For any DOM events we should use [`trigger`](https://vue-test-utils.vuejs.org/api/wrapper/#trigger) to fire out event.
```javascript
-import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper'
-import component from 'component.vue'
+// Assuming SomeButton renders: <button>Some button</button>
+wrapper = mount(SomeButton);
-const Component = Vue.extend(component);
-const data = {prop: 'foo'};
-const vm = mountComponent(Component, data);
+...
+it('should fire the click event', () => {
+ const btn = wrapper.find('button')
+
+ btn.trigger('click');
+ ...
+})
```
-### Test the component's output
+When we need to fire a Vue event, we should use [`emit`](https://vuejs.org/v2/guide/components-custom-events.html) to fire our event.
-The main return value of a Vue component is the rendered output. In order to test the component we
-need to test the rendered output. [Vue][vue-test] guide's to unit test show us exactly that:
+```javascript
+wrapper = shallowMount(DropdownItem);
+
+...
+
+it('should fire the itemClicked event', () => {
+ DropdownItem.vm.$emit('itemClicked');
+ ...
+})
+```
+
+We should verify an event has been fired by asserting against the result of the [`emitted()`](https://vue-test-utils.vuejs.org/api/wrapper/#emitted) method
## Vue.js Expert Role
@@ -287,15 +326,50 @@ One should apply to be a Vue.js expert by opening an MR when the Merge Request's
- Deep understanding of Vue and Vuex reactivity
- Vue and Vuex code are structured according to both official and our guidelines
- Full understanding of testing a Vue and Vuex application
-- Vuex code follows the [documented pattern](vuex.md#actions-pattern-request-and-receive-namespaces)
+- Vuex code follows the [documented pattern](vuex.md#naming-pattern-request-and-receive-namespaces)
- Knowledge about the existing Vue and Vuex applications and existing reusable components
-[issue-boards]: https://gitlab.com/gitlab-org/gitlab-foss/tree/master/app/assets/javascripts/boards
-[environments-table]: https://gitlab.com/gitlab-org/gitlab-foss/tree/master/app/assets/javascripts/environments
-[page_specific_javascript]: ./performance.md#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-test]: https://vuejs.org/v2/guide/unit-testing.html
-[flux]: https://facebook.github.io/flux/
-[axios]: https://github.com/axios/axios
+## Vue 2 -> Vue 3 Migration
+
+> This section is added temporarily to support the efforts to migrate the codebase from Vue 2.x to Vue 3.x
+
+Currently, we recommend to minimize adding certain features to the codebase to prevent increasing the tech debt for the eventual migration:
+
+- filters;
+- event buses;
+- functional templated
+- `slot` attributes
+
+You can find more details on [Migration to Vue 3](vue3_migration.md)
+
+## Appendix - Vue component subject under test
+
+This is the template for the example component which is tested in the [Testing Vue components](#testing-vue-components) section:
+
+```html
+<template>
+ <div class="content">
+ <gl-loading-icon v-if="isLoading" />
+ <template v-else>
+ <div
+ v-for="todo in todos"
+ :key="todo.id"
+ :class="{ 'gl-strike': todo.isDone }"
+ data-testid="todo-item"
+ >{{ toddo.text }}</div>
+ <footer class="gl-border-t-1 gl-mt-3 gl-pt-3">
+ <gl-form-input
+ type="text"
+ v-model="todoText"
+ data-testid="text-input"
+ >
+ <gl-button
+ variant="success"
+ data-testid="add-button"
+ @click="addTodo"
+ >Add</gl-button>
+ </footer>
+ </template>
+ </div>
+</template>
+```