Commit e42b82c5 authored by Mike Greiling's avatar Mike Greiling

Merge branch 'peterhegman/find-by-reference-implementation' into 'master'

Add `findBy*` and `findAllBy*`  to `extendWrapper` Jest helper

See merge request gitlab-org/gitlab!54398
parents 322a4e95 b98a889b
...@@ -212,8 +212,8 @@ When it comes to querying DOM elements in your tests, it is best to uniquely and ...@@ -212,8 +212,8 @@ When it comes to querying DOM elements in your tests, it is best to uniquely and
the element. 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/). 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/) When selecting by text it is best to use the [`byRole`](https://testing-library.com/docs/queries/byrole) query
as these enforce accessibility best practices as well. The examples below demonstrate the order of preference. 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 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. rather than dealing with the complexity of a child component's behavior.
...@@ -223,25 +223,27 @@ possible selectors include: ...@@ -223,25 +223,27 @@ possible selectors include:
- A semantic attribute like `name` (also verifies that `name` was setup properly) - 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)) - 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`) - a Vue `ref` (if using `@vue/test-utils`)
```javascript ```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`. // In this example, `wrapper` is a `@vue/test-utils` wrapper returned from `mount` or `shallowMount`.
it('exists', () => { it('exists', () => {
// Best (especially for integration tests) // Best (especially for integration tests)
getByRole(wrapper.element, 'link', { name: /Click Me/i }) wrapper.findByRole('link', { name: /Click Me/i })
getByRole(wrapper.element, 'link', { name: 'Click Me' }) wrapper.findByRole('link', { name: 'Click Me' })
getByText(wrapper.element, 'Click Me') wrapper.findByText('Click Me')
getByText(wrapper.element, /Click Me/i) wrapper.findByText(/Click Me/i)
// Good (especially for unit tests) // Good (especially for unit tests)
wrapper.find(FooComponent); wrapper.find(FooComponent);
wrapper.find('input[name=foo]'); wrapper.find('input[name=foo]');
wrapper.find('[data-testid="my-foo-id"]'); 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'}); wrapper.find({ ref: 'foo'});
// Bad // Bad
...@@ -1138,23 +1140,40 @@ These are very useful if you don't have a handle to the request's Promise, for e ...@@ -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. 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). The `shallowMountExtended` and `mountExtended` utilities provide you with the ability to perform
You can use the `extendedWrapper` utility on the `wrapper` returned by `shalowMount`/`mount`. any of the available [DOM Testing Library queries](https://testing-library.com/docs/queries/about)
By doing so, the `wrapper` provides you with the ability to perform a `findByTestId`, by prefixing them with `find` or `findAll`.
which is a shortcut to the more verbose `wrapper.find('[data-testid="my-test-id"]');`
```javascript ```javascript
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('FooComponent', () => { describe('FooComponent', () => {
const wrapper = extendedWrapper(shallowMount({ const wrapper = shallowMountExtended({
template: `<div data-testid="my-test-id"></div>`, 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', () => { it('finds elements with `findAllByRole`', () => {
expect(wrapper.findByTestId('my-test-id').exists()).toBe(true); 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) => const vNodeContainsText = (vnode, text) =>
(vnode.text && vnode.text.includes(text)) || (vnode.text && vnode.text.includes(text)) ||
...@@ -37,6 +39,17 @@ export const waitForMutation = (store, expectedMutationType) => ...@@ -37,6 +39,17 @@ export const waitForMutation = (store, expectedMutationType) =>
}); });
export const extendedWrapper = (wrapper) => { 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) { if (isArray(wrapper) || !wrapper?.find) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn( console.warn(
...@@ -56,5 +69,63 @@ export const extendedWrapper = (wrapper) => { ...@@ -56,5 +69,63 @@ export const extendedWrapper = (wrapper) => {
return this.findAll(`[data-testid="${id}"]`); 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 * as testingLibrary from '@testing-library/dom';
import { extendedWrapper, shallowWrapperContainsSlotText } from './vue_test_utils_helper'; 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', () => { describe('Vue test utils helpers', () => {
afterAll(() => {
jest.unmock('@testing-library/dom');
});
describe('shallowWrapperContainsSlotText', () => { describe('shallowWrapperContainsSlotText', () => {
const mockText = 'text'; const mockText = 'text';
const mockSlot = `<div>${mockText}</div>`; const mockSlot = `<div>${mockText}</div>`;
...@@ -84,7 +104,7 @@ describe('Vue test utils helpers', () => { ...@@ -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); expect(mockComponent.findByTestId(testId).exists()).toBe(true);
}); });
}); });
...@@ -105,5 +125,187 @@ describe('Vue test utils helpers', () => { ...@@ -105,5 +125,187 @@ describe('Vue test utils helpers', () => {
expect(mockComponent.findAllByTestId(testId)).toHaveLength(2); 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