Commit 0b63523b authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch 'fe-docs-on-vue-mount-testing' into 'master'

Update docs on vue component testing

See merge request gitlab-org/gitlab!31699
parents 50dce215 b6d9ae2e
......@@ -189,92 +189,97 @@ 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';
describe('Todos App', () => {
let vm;
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.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
There is a helper in `spec/javascripts/helpers/vue_mount_component_helper.js` that allows you to mount a component with the given props:
```javascript
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'
import component from 'component.vue'
const Component = Vue.extend(component);
const data = {prop: 'foo'};
const vm = mountComponent(Component, data);
```
### Test the component's output
The main return value of a Vue component is the rendered output. In order to test the component we
......@@ -336,3 +341,35 @@ Currently, we recommend to minimize adding certain features to the codebase to p
- `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>
```
......@@ -832,43 +832,6 @@ testAction(
Check an example in [spec/javascripts/ide/stores/actions_spec.jsspec/javascripts/ide/stores/actions_spec.js](https://gitlab.com/gitlab-org/gitlab/blob/master/spec/javascripts/ide/stores/actions_spec.js).
### Vue Helper: `mountComponent`
To make mounting a Vue component easier and more readable, we have a few helpers available in `spec/helpers/vue_mount_component_helper`:
- `createComponentWithStore`
- `mountComponentWithStore`
Examples of usage:
```javascript
beforeEach(() => {
vm = createComponentWithStore(Component, store);
vm.$store.state.currentBranchId = 'master';
vm.$mount();
});
```
```javascript
beforeEach(() => {
vm = mountComponentWithStore(Component, {
el: '#dummy-element',
store,
props: { badge },
});
});
```
Don't forget to clean up:
```javascript
afterEach(() => {
vm.$destroy();
});
```
### Wait until axios requests finish
The axios utils mock module located in `spec/frontend/mocks/ce/lib/utils/axios_utils.js` contains two helper methods for Jest tests that spawn HTTP requests.
......
......@@ -153,11 +153,16 @@ describe('my component', () => {
let trackingSpy;
beforeEach(() => {
const vm = mountComponent(MyComponent);
trackingSpy = mockTracking('_category_', vm.$el, spyOn);
});
const triggerEvent = () => {
// action which should trigger a event
};
it('tracks an event when toggled', () => {
expect(trackingSpy).not.toHaveBeenCalled();
triggerEvent('a.toggle');
expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_edit_button', {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment