Commit b98a889b authored by peterhegman's avatar peterhegman

Add testing library queries to `extendedWrapper`

Adds `findBy*` and `findAllBy*` testing library queries to
extendedWrapper jest helper. Also expose `shallowMountExtended` and
`mountExtended`
parent 20ecbca2
......@@ -212,8 +212,8 @@ When it comes to querying DOM elements in your tests, it is best to uniquely and
the element.
Preferentially, this is done by targeting what the user actually sees using [DOM Testing Library](https://testing-library.com/docs/dom-testing-library/intro/).
When selecting by text it is best to use [`getByRole` or `findByRole`](https://testing-library.com/docs/queries/byrole/)
as these enforce accessibility best practices as well. The examples below demonstrate the order of preference.
When selecting by text it is best to use the [`byRole`](https://testing-library.com/docs/queries/byrole) query
as it helps enforce accessibility best practices. `findByRole` and the other [DOM Testing Library queries](https://testing-library.com/docs/queries/about) are available when using [`shallowMountExtended` or `mountExtended`](#shallowmountextended-and-mountextended).
When writing Vue component unit tests, it can be wise to query children by component, so that the unit test can focus on comprehensive value coverage
rather than dealing with the complexity of a child component's behavior.
......@@ -223,25 +223,27 @@ possible selectors include:
- A semantic attribute like `name` (also verifies that `name` was setup properly)
- A `data-testid` attribute ([recommended by maintainers of `@vue/test-utils`](https://github.com/vuejs/vue-test-utils/issues/1498#issuecomment-610133465))
optionally combined with [`findByTestId`](#extendedwrapper-and-findbytestid)
optionally combined with [`shallowMountExtended` or `mountExtended`](#shallowmountextended-and-mountextended)
- a Vue `ref` (if using `@vue/test-utils`)
```javascript
import { getByRole, getByText } from '@testing-library/dom'
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'
const wrapper = shallowMountExtended(ExampleComponent);
// In this example, `wrapper` is a `@vue/test-utils` wrapper returned from `mount` or `shallowMount`.
it('exists', () => {
// Best (especially for integration tests)
getByRole(wrapper.element, 'link', { name: /Click Me/i })
getByRole(wrapper.element, 'link', { name: 'Click Me' })
getByText(wrapper.element, 'Click Me')
getByText(wrapper.element, /Click Me/i)
wrapper.findByRole('link', { name: /Click Me/i })
wrapper.findByRole('link', { name: 'Click Me' })
wrapper.findByText('Click Me')
wrapper.findByText(/Click Me/i)
// Good (especially for unit tests)
wrapper.find(FooComponent);
wrapper.find('input[name=foo]');
wrapper.find('[data-testid="my-foo-id"]');
wrapper.findByTestId('my-foo-id'); // with the extendedWrapper utility – check below
wrapper.findByTestId('my-foo-id'); // with shallowMountExtended or mountExtended – check below
wrapper.find({ ref: 'foo'});
// Bad
......@@ -1138,23 +1140,40 @@ These are very useful if you don't have a handle to the request's Promise, for e
Both functions run `callback` on the next tick after the requests finish (using `setImmediate()`), to allow any `.then()` or `.catch()` handlers to run.
### `extendedWrapper` and `findByTestId`
### `shallowMountExtended` and `mountExtended`
Using `data-testid` is one of the [recommended ways to query DOM elements](#how-to-query-dom-elements).
You can use the `extendedWrapper` utility on the `wrapper` returned by `shalowMount`/`mount`.
By doing so, the `wrapper` provides you with the ability to perform a `findByTestId`,
which is a shortcut to the more verbose `wrapper.find('[data-testid="my-test-id"]');`
The `shallowMountExtended` and `mountExtended` utilities provide you with the ability to perform
any of the available [DOM Testing Library queries](https://testing-library.com/docs/queries/about)
by prefixing them with `find` or `findAll`.
```javascript
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('FooComponent', () => {
const wrapper = extendedWrapper(shallowMount({
template: `<div data-testid="my-test-id"></div>`,
}));
const wrapper = shallowMountExtended({
template: `
<div data-testid="gitlab-frontend-stack">
<p>GitLab frontend stack</p>
<div role="tablist">
<button role="tab" aria-selected="true">Vue.js</button>
<button role="tab" aria-selected="false">GraphQL</button>
<button role="tab" aria-selected="false">SCSS</button>
</div>
</div>
`,
});
it('finds elements with `findByTestId`', () => {
expect(wrapper.findByTestId('gitlab-frontend-stack').exists()).toBe(true);
});
it('finds elements with `findByText`', () => {
expect(wrapper.findByText('GitLab frontend stack').exists()).toBe(true);
expect(wrapper.findByText('TypeScript').exists()).toBe(false);
});
it('exists', () => {
expect(wrapper.findByTestId('my-test-id').exists()).toBe(true);
it('finds elements with `findAllByRole`', () => {
expect(wrapper.findAllByRole('tab').length).toBe(3);
});
});
```
......
import { isArray } from 'lodash';
import * as testingLibrary from '@testing-library/dom';
import { createWrapper, WrapperArray, mount, shallowMount } from '@vue/test-utils';
import { isArray, upperFirst } from 'lodash';
const vNodeContainsText = (vnode, text) =>
(vnode.text && vnode.text.includes(text)) ||
......@@ -37,6 +39,17 @@ export const waitForMutation = (store, expectedMutationType) =>
});
export const extendedWrapper = (wrapper) => {
// https://testing-library.com/docs/queries/about
const AVAILABLE_QUERIES = [
'byRole',
'byLabelText',
'byPlaceholderText',
'byText',
'byDisplayValue',
'byAltText',
'byTitle',
];
if (isArray(wrapper) || !wrapper?.find) {
// eslint-disable-next-line no-console
console.warn(
......@@ -56,5 +69,63 @@ export const extendedWrapper = (wrapper) => {
return this.findAll(`[data-testid="${id}"]`);
},
},
// `findBy`
...AVAILABLE_QUERIES.reduce((accumulator, query) => {
return {
...accumulator,
[`find${upperFirst(query)}`]: {
value(text, options = {}) {
const elements = testingLibrary[`queryAll${upperFirst(query)}`](
wrapper.element,
text,
options,
);
// Return VTU `ErrorWrapper` if element is not found
// https://github.com/vuejs/vue-test-utils/blob/dev/packages/test-utils/src/error-wrapper.js
// VTU does not expose `ErrorWrapper` so, as of now, this is the best way to
// create an `ErrorWrapper`
if (!elements.length) {
const emptyElement = document.createElement('div');
return createWrapper(emptyElement).find('testing-library-element-not-found');
}
return createWrapper(elements[0], this.options || {});
},
},
};
}, {}),
// `findAllBy`
...AVAILABLE_QUERIES.reduce((accumulator, query) => {
return {
...accumulator,
[`findAll${upperFirst(query)}`]: {
value(text, options = {}) {
const elements = testingLibrary[`queryAll${upperFirst(query)}`](
wrapper.element,
text,
options,
);
const wrappers = elements.map((element) => {
const elementWrapper = createWrapper(element, this.options || {});
elementWrapper.selector = text;
return elementWrapper;
});
const wrapperArray = new WrapperArray(wrappers);
wrapperArray.selector = text;
return wrapperArray;
},
},
};
}, {}),
});
};
export const shallowMountExtended = (...args) => extendedWrapper(shallowMount(...args));
export const mountExtended = (...args) => extendedWrapper(mount(...args));
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper, shallowWrapperContainsSlotText } from './vue_test_utils_helper';
import * as testingLibrary from '@testing-library/dom';
import * as vtu from '@vue/test-utils';
import {
shallowMount,
Wrapper as VTUWrapper,
WrapperArray as VTUWrapperArray,
} from '@vue/test-utils';
import {
extendedWrapper,
shallowMountExtended,
mountExtended,
shallowWrapperContainsSlotText,
} from './vue_test_utils_helper';
jest.mock('@testing-library/dom', () => ({
__esModule: true,
...jest.requireActual('@testing-library/dom'),
}));
describe('Vue test utils helpers', () => {
afterAll(() => {
jest.unmock('@testing-library/dom');
});
describe('shallowWrapperContainsSlotText', () => {
const mockText = 'text';
const mockSlot = `<div>${mockText}</div>`;
......@@ -84,7 +104,7 @@ describe('Vue test utils helpers', () => {
);
});
it('should find the component by test id', () => {
it('should find the element by test id', () => {
expect(mockComponent.findByTestId(testId).exists()).toBe(true);
});
});
......@@ -105,5 +125,187 @@ describe('Vue test utils helpers', () => {
expect(mockComponent.findAllByTestId(testId)).toHaveLength(2);
});
});
describe.each`
findMethod | expectedQuery
${'findByRole'} | ${'queryAllByRole'}
${'findByLabelText'} | ${'queryAllByLabelText'}
${'findByPlaceholderText'} | ${'queryAllByPlaceholderText'}
${'findByText'} | ${'queryAllByText'}
${'findByDisplayValue'} | ${'queryAllByDisplayValue'}
${'findByAltText'} | ${'queryAllByAltText'}
`('$findMethod', ({ findMethod, expectedQuery }) => {
const text = 'foo bar';
const options = { selector: 'div' };
const mockDiv = document.createElement('div');
let wrapper;
beforeEach(() => {
wrapper = extendedWrapper(
shallowMount({
template: `<div>foo bar</div>`,
}),
);
});
it(`calls Testing Library \`${expectedQuery}\` function with correct parameters`, () => {
jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => [mockDiv]);
wrapper[findMethod](text, options);
expect(testingLibrary[expectedQuery]).toHaveBeenLastCalledWith(
wrapper.element,
text,
options,
);
});
describe('when element is found', () => {
beforeEach(() => {
jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => [mockDiv]);
jest.spyOn(vtu, 'createWrapper');
});
it('returns a VTU wrapper', () => {
const result = wrapper[findMethod](text, options);
expect(vtu.createWrapper).toHaveBeenCalledWith(mockDiv, wrapper.options);
expect(result).toBeInstanceOf(VTUWrapper);
});
});
describe('when multiple elements are found', () => {
beforeEach(() => {
const mockSpan = document.createElement('span');
jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => [mockDiv, mockSpan]);
jest.spyOn(vtu, 'createWrapper');
});
it('returns the first element as a VTU wrapper', () => {
const result = wrapper[findMethod](text, options);
expect(vtu.createWrapper).toHaveBeenCalledWith(mockDiv, wrapper.options);
expect(result).toBeInstanceOf(VTUWrapper);
});
});
describe('when element is not found', () => {
beforeEach(() => {
jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => []);
});
it('returns a VTU error wrapper', () => {
expect(wrapper[findMethod](text, options).exists()).toBe(false);
});
});
});
describe.each`
findMethod | expectedQuery
${'findAllByRole'} | ${'queryAllByRole'}
${'findAllByLabelText'} | ${'queryAllByLabelText'}
${'findAllByPlaceholderText'} | ${'queryAllByPlaceholderText'}
${'findAllByText'} | ${'queryAllByText'}
${'findAllByDisplayValue'} | ${'queryAllByDisplayValue'}
${'findAllByAltText'} | ${'queryAllByAltText'}
`('$findMethod', ({ findMethod, expectedQuery }) => {
const text = 'foo bar';
const options = { selector: 'div' };
const mockElements = [
document.createElement('li'),
document.createElement('li'),
document.createElement('li'),
];
let wrapper;
beforeEach(() => {
wrapper = extendedWrapper(
shallowMount({
template: `
<ul>
<li>foo</li>
<li>bar</li>
<li>baz</li>
</ul>
`,
}),
);
});
it(`calls Testing Library \`${expectedQuery}\` function with correct parameters`, () => {
jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => mockElements);
wrapper[findMethod](text, options);
expect(testingLibrary[expectedQuery]).toHaveBeenLastCalledWith(
wrapper.element,
text,
options,
);
});
describe('when elements are found', () => {
beforeEach(() => {
jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => mockElements);
});
it('returns a VTU wrapper array', () => {
const result = wrapper[findMethod](text, options);
expect(result).toBeInstanceOf(VTUWrapperArray);
expect(
result.wrappers.every(
(resultWrapper) =>
resultWrapper instanceof VTUWrapper && resultWrapper.options === wrapper.options,
),
).toBe(true);
expect(result.length).toBe(3);
});
});
describe('when elements are not found', () => {
beforeEach(() => {
jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => []);
});
it('returns an empty VTU wrapper array', () => {
const result = wrapper[findMethod](text, options);
expect(result).toBeInstanceOf(VTUWrapperArray);
expect(result.length).toBe(0);
});
});
});
});
describe.each`
mountExtendedFunction | expectedMountFunction
${shallowMountExtended} | ${'shallowMount'}
${mountExtended} | ${'mount'}
`('$mountExtendedFunction', ({ mountExtendedFunction, expectedMountFunction }) => {
const FakeComponent = jest.fn();
const options = {
propsData: {
foo: 'bar',
},
};
beforeEach(() => {
const mockWrapper = { find: jest.fn() };
jest.spyOn(vtu, expectedMountFunction).mockImplementation(() => mockWrapper);
});
it(`calls \`${expectedMountFunction}\` with passed arguments`, () => {
mountExtendedFunction(FakeComponent, options);
expect(vtu[expectedMountFunction]).toHaveBeenCalledWith(FakeComponent, options);
});
it('returns extended wrapper', () => {
const result = mountExtendedFunction(FakeComponent, options);
expect(result).toHaveProperty('find');
expect(result).toHaveProperty('findByTestId');
});
});
});
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