This issue tracks a few ideas for improving the APIs we use for writing tests. This means reducing the amount of code needed for common uses, eliminating pitfalls etc.
This issue is a place to brainstorm. Separate issues can be created to flesh ideas out more fully. Some of these issues are also really about the underlying libraries, so the issues need to be created downstream.
waitFor method on wrappers
A common action is to trigger an async operation in a test, then wait for the UI to change in some way.
This is currently handled via a waitForElement function, but it would be useful if it was an operation on the wrapper.
Current:
import { mount, waitForElement } from '@hypothesis/frontend-testing';
...
const wrapper = mount(<Widget/>);
const items = waitForElement(wrapper, 'ul[data-testid="item-list"]');
Proposed:
import { mount } from '@hypothesis/frontend-testing';
...
const wrapper = mount(<Widget/>);
const items = await wrapper.waitFor('ul[data-testid="item-list"]');
Clarify or simplify use of act and wrapper.update()
It is often not intuitive when act and wrapper.update() are required. It would make writing tests easier if there was at least good documentation, or perhaps better yet changes that could be made to reduce how often this is needed.
- The
act function runs a callback and then repeatedly flushes effects and state updates, until a flush triggers no further updates
wrapper.update() synchronizes the Enzyme wrapper's snapshot of the rendered output to match the current output
A common pattern of usage is:
act(() => {
wrapper.find('SomeChild').prop('onClick')();
});
wrapper.update();
Here the act callback ensures that state updates and effects triggered by onClick are flushed, then wrapper.update ensures that subsequent queries on wrapper reflect the current UI.
Often it is possible to omit the use of act if the onClick handler triggers state updates but no effects. This is because the wrapper.update() call will implicitly flush any pending state updates. Internally wrapper.update() calls the Enzyme adapater's getNode method and the MountRenderer.getNode implementation in enzyme-adapter-preact-pure calls flushRenders first.
Inspecting updates to child components
Another common sequence looks like:
// Get child and trigger an update
let child = wrapper.find('SomeWidget');
assert.equal(child.prop('value'), 'Original value');
act(() => {
child.prop('onChange')('Updated value');
}); // nb. The `act` can often be omitted. See above.
// Refresh the parent component and re-find the child.
wrapper.update();
child = wrapper.find('SomeWidget');
assert.equal(child.prop('value'), 'Updated value');
Re-finding the child is necessary because the original child value was a snapshot from the initial render that doesn't change after refreshing the root wrapper. It would be more ergonomic if the above could be written as:
const child = wrapper.find('SomeWidget');
assert.equal(child.prop('value'), 'Original value');
child.prop('onChange')('Updated value');
assert.equal(child.prop('value'), 'Updated value');
Built-in data-testid support
We extensively use the pattern of adding a data-testid attribute to DOM elements and components, then use an attribute selector to find those in tests. This pattern is taken from Testing Library. eg.
wrapper.find('button[data-testid="foo-button"]')
These selectors are a bit awkward to write, especially if foo-button is partly dynamic. It would be useful to have some built-in support for them. A complication is that the data-testid value is often not enough on its own, because that attribute might be passed down multiple elements. eg. <Button data-testid="foo">...</Button> will render both a Button and button with data-testid props.
This issue tracks a few ideas for improving the APIs we use for writing tests. This means reducing the amount of code needed for common uses, eliminating pitfalls etc.
This issue is a place to brainstorm. Separate issues can be created to flesh ideas out more fully. Some of these issues are also really about the underlying libraries, so the issues need to be created downstream.
waitFormethod on wrappersA common action is to trigger an async operation in a test, then wait for the UI to change in some way.
This is currently handled via a
waitForElementfunction, but it would be useful if it was an operation on the wrapper.Current:
Proposed:
Clarify or simplify use of
actandwrapper.update()It is often not intuitive when
actandwrapper.update()are required. It would make writing tests easier if there was at least good documentation, or perhaps better yet changes that could be made to reduce how often this is needed.actfunction runs a callback and then repeatedly flushes effects and state updates, until a flush triggers no further updateswrapper.update()synchronizes the Enzyme wrapper's snapshot of the rendered output to match the current outputA common pattern of usage is:
Here the
actcallback ensures that state updates and effects triggered byonClickare flushed, thenwrapper.updateensures that subsequent queries onwrapperreflect the current UI.Often it is possible to omit the use of
actif theonClickhandler triggers state updates but no effects. This is because thewrapper.update()call will implicitly flush any pending state updates. Internallywrapper.update()calls the Enzyme adapater'sgetNodemethod and theMountRenderer.getNodeimplementation inenzyme-adapter-preact-purecallsflushRendersfirst.Inspecting updates to child components
Another common sequence looks like:
Re-finding the child is necessary because the original
childvalue was a snapshot from the initial render that doesn't change after refreshing the rootwrapper. It would be more ergonomic if the above could be written as:Built-in
data-testidsupportWe extensively use the pattern of adding a
data-testidattribute to DOM elements and components, then use an attribute selector to find those in tests. This pattern is taken from Testing Library. eg.These selectors are a bit awkward to write, especially if
foo-buttonis partly dynamic. It would be useful to have some built-in support for them. A complication is that thedata-testidvalue is often not enough on its own, because that attribute might be passed down multiple elements. eg.<Button data-testid="foo">...</Button>will render both aButtonandbuttonwithdata-testidprops.