Stencil, the Web Components library, ships with unit and E2E tests by default, like other JavaScript frameworks.
https://stenciljs.jp/docs/testing-overview
E2E tests for Web Components often trip you up on Shadow DOM access. I struggled with how to change a select and fire an event, so I am sharing what worked.
Component Under Test
Suppose the component has a select DOM inside the Web Component, like this. In the E2E test I want to run onChange there and fire the myChange event. Let the component name be my-select.
@Component({
tag: 'my-select',
styleUrl: 'my-select.scss',
shadow: true,
})
export class MySelect {
...
private onChange = event => {
this.myChange.emit({ value: event.target.value });
};
render() {
return (
<Host>
<select onChange={this.onChange}>
{options.map(option => (
<option value={option.value}>{option.label}</option>
))}
</select>
</Host>
);
}
}
Test Code
One thing to watch when writing the test: Puppeteer's select method does not work with Shadow DOM.
https://github.com/puppeteer/puppeteer/issues/4171
So use Puppeteer's evaluateHandle to reach the window and document objects and manipulate the DOM from there.
The test then looks like this.
describe('my-select', () => {
it('change', async () => {
const page = await newE2EPage();
await page.setContent(`<my-select></my-select>`,);
await page.waitForChanges();
const change = await page.spyOnEvent('myChange');
await page.evaluateHandle(() => {
return new Promise(resolve => {
// Get the my-select DOM element
const element = document.querySelector('my-select');
// Resolve the async operation when the myChange event fires
element.addEventListener('myChange', event => {
resolve();
});
// Get the select DOM element inside my-select
const select: HTMLSelectElement = element.shadowRoot.querySelector(
'select',
);
// Dispatch the event on the select DOM element
select.dispatchEvent(
new Event('change', { bubbles: true, composed: true }),
);
});
});
await page.waitForChanges();
expect(change).toHaveReceivedEvent();
});
});
page.spyOnEvent('myChange') creates a mock object that checks whether the myChange event fired, stored in change. After that, when the event fires, expect(change).toHaveReceivedEvent() passes. (If the event did not fire, toHaveReceivedEvent fails and the test fails.)
Because Puppeteer's select method is unavailable, I use dispatchEvent to force the onChange handler to run. That fires myChange, which the addEventListener was watching, so the Promise inside evaluateHandle resolves and the rest of the test runs.
Shadow DOM made it a bit fiddly, but the test was straightforward to write. Here I only check that the event fired; with toHaveReceivedEventDetail you can also assert the event payload.
See you next time.