diff options
Diffstat (limited to 'doc/development/fe_guide/vue.md')
-rw-r--r-- | doc/development/fe_guide/vue.md | 107 |
1 files changed, 77 insertions, 30 deletions
diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md index 41fbd128631..b3fbb9556a9 100644 --- a/doc/development/fe_guide/vue.md +++ b/doc/development/fe_guide/vue.md @@ -22,7 +22,8 @@ All new features built with Vue.js must follow a [Flux architecture](https://fac 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](https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch) +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 @@ -62,14 +63,15 @@ Be sure to read about [page-specific JavaScript](performance.md#page-specific-ja While mounting a Vue application, you might need to provide data from Rails to JavaScript. To do that, you can use the `data` attributes in the HTML element and query them while mounting the application. -You should only do this while initializing the application, because the mounted element is replaced with a Vue-generated DOM. +You should only do this while initializing the application, because the mounted element is replaced +with a Vue-generated DOM. -The advantage of providing data from the DOM to the Vue instance through `props` in the `render` function -instead of querying the DOM inside the main Vue component is avoiding the need to create a fixture or an HTML element in the unit test, -which makes the tests easier. +The advantage of providing data from the DOM to the Vue instance through `props` in the `render` +function instead of querying the DOM inside the main Vue component is avoiding the need to create a +fixture or an HTML element in the unit test, which makes the tests easier. -See the following example, also, please refer to our [Vue style guide](style/vue.md#basic-rules) for additional -information on why we explicitly declare the data being passed into the Vue app; +See the following example, also, please refer to our [Vue style guide](style/vue.md#basic-rules) for +additional information on why we explicitly declare the data being passed into the Vue app; ```javascript // haml @@ -94,13 +96,15 @@ return new Vue({ }); ``` -> When adding an `id` attribute to mount a Vue application, please make sure this `id` is unique across the codebase +> When adding an `id` attribute to mount a Vue application, please make sure this `id` is unique +across the codebase. #### Accessing the `gl` object -When we need to query the `gl` object for data that doesn't change during the application's life cycle, we should do it in the same place where we query the DOM. -By following this practice, we can avoid the need to mock the `gl` object, which makes tests easier. -It should be done while initializing our Vue instance, and the data should be provided as `props` to the main component: +When we need to query the `gl` object for data that doesn't change during the application's life +cycle, we should do it in the same place where we query the DOM. By following this practice, we can +avoid the need to mock the `gl` object, which makes tests easier. It should be done while +initializing our Vue instance, and the data should be provided as `props` to the main component: ```javascript return new Vue({ @@ -192,13 +196,18 @@ Check this [page](vuex.md) for more details. In the [Vue documentation](https://vuejs.org/v2/api/#Options-Data) the Data function/object is defined as follows: -> The data object for the Vue instance. Vue recursively converts its properties into getter/setters to make it “reactive”. The object must be plain: native objects such as browser API objects and prototype properties are ignored. A rule of thumb is that data should just be data - it is not recommended to observe objects with their own stateful behavior. +> The data object for the Vue instance. Vue recursively converts its properties into getter/setters +to make it “reactive”. The object must be plain: native objects such as browser API objects and +prototype properties are ignored. A rule of thumb is that data should just be data - it is not +recommended to observe objects with their own stateful behavior. Based on the Vue guidance: -- **Do not** use or create a JavaScript class in your [data function](https://vuejs.org/v2/api/#data), such as `user: new User()`. +- **Do not** use or create a JavaScript class in your [data function](https://vuejs.org/v2/api/#data), +such as `user: new User()`. - **Do not** add new JavaScript class implementations. -- **Do** use [GraphQL](../api_graphql_styleguide.md), [Vuex](vuex.md) or a set of components if cannot use simple primitives or objects. +- **Do** use [GraphQL](../api_graphql_styleguide.md), [Vuex](vuex.md) or a set of components if +cannot use simple primitives or objects. - **Do** maintain existing implementations using such approaches. - **Do** Migrate components to a pure object model when there are substantial changes to it. - **Do** add business logic to helpers or utils, so you can test them separately from your component. @@ -209,7 +218,8 @@ There are additional reasons why having a JavaScript class presents maintainabil - Once a class is created, it is easy to extend it in a way that can infringe Vue reactivity and best practices. - A class adds a layer of abstraction, which makes the component API and its inner workings less clear. -- It makes it harder to test. Since the class is instantiated by the component data function, it is harder to 'manage' component and class separately. +- It makes it harder to test. Since the class is instantiated by the component data function, it is +harder to 'manage' component and class separately. - Adding OOP to a functional codebase adds yet another way of writing code, reducing consistency and clarity. ## Style guide @@ -231,6 +241,7 @@ Here's an example of a well structured unit test for [this Vue component](#appen ```javascript import { shallowMount } from '@vue/test-utils'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { GlLoadingIcon } from '@gitlab/ui'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; @@ -263,19 +274,21 @@ describe('~/todos/app.vue', () => { }); // It is very helpful to separate setting up the component from - // its collaborators (i.e. Vuex, axios, etc.) + // its collaborators (for example, Vuex and axios). const createWrapper = (props = {}) => { - wrapper = shallowMount(App, { - propsData: { - path: TEST_TODO_PATH, - ...props, - }, - }); + wrapper = extendedWrapper( + shallowMount(App, { + propsData: { + path: TEST_TODO_PATH, + ...props, + }, + }) + ); }; // 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 findAddButton = () => wrapper.findByTestId('add-button'); + const findTextInput = () => wrapper.findByTestId('text-input'); const findTodoData = () => wrapper.findAll('[data-testid="todo-item"]').wrappers.map(wrapper => ({ text: wrapper.text() })); describe('when mounted and loading', () => { @@ -323,11 +336,41 @@ describe('~/todos/app.vue', () => { 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. Visit the [Vue testing guide](https://vuejs.org/v2/guide/testing.html#Unit-Testing). +### Child components + +1. Test any directive that defines if/how child component is rendered (for example, `v-if` and `v-for`). +1. Test any props we are passing to child components (especially if the prop is calculated in the +component under test, with the `computed` property, for example). Remember to use `.props()` and not `.vm.someProp`. +1. Test we react correctly to any events emitted from child components: + + ```javascript + const checkbox = wrapper.findByTestId('checkboxTestId'); + + expect(checkbox.attributes('disabled')).not.toBeDefined(); + + findChildComponent().vm.$emit('primary'); + await nextTick(); + + expect(checkbox.attributes('disabled')).toBeDefined(); + ``` + +1. **Do not** test the internal implementation of the child components: + + ```javascript + // bad + expect(findChildComponent().find('.error-alert').exists()).toBe(false); + + // good + expect(findChildComponent().props('withAlertContainer')).toBe(false); + ``` + ### 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. +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. +For any DOM events we should use [`trigger`](https://vue-test-utils.vuejs.org/api/wrapper/#trigger) +to fire out event. ```javascript // Assuming SomeButton renders: <button>Some button</button> @@ -342,7 +385,8 @@ it('should fire the click event', () => { }) ``` -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. +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. ```javascript wrapper = shallowMount(DropdownItem); @@ -355,7 +399,8 @@ it('should fire the itemClicked event', () => { }) ``` -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 +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 @@ -371,7 +416,8 @@ You should only apply to be a Vue.js expert when your own merge requests and you > 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: +Currently, we recommend to minimize adding certain features to the codebase to prevent increasing +the tech debt for the eventual migration: - filters; - event buses; @@ -382,7 +428,8 @@ 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: +This is the template for the example component which is tested in the +[Testing Vue components](#testing-vue-components) section: ```html <template> |