diff options
Diffstat (limited to 'doc/development/fe_guide/vue.md')
-rw-r--r-- | doc/development/fe_guide/vue.md | 216 |
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> +``` |