End-to-end Testing
E2E tests verify your components in a real browser. For example, when my-component
has the X attribute, the child component then renders the text Y, and expects to receive the event Z. By using Puppeteer for rendering tests (rather than a Node environment simulating how a browser works), your end-to-end tests are able to run within an actual browser in order to give better results.
Rindo provides many utility functions to help test Jest and Puppeteer. For example, a component's shadow dom can be queried and tested with the Rindo utility functions built on top of Puppeteer. Tests can not only be provided mock HTML content, but they can also go to URLs of your app which Puppeteer is able to open up and test on Rindo's dev server.
End-to-end tests require a fresh build, dev-server, and puppeteer browser instance created before the tests can actually run. With the added build complexities, the rindo test
command is able to organize the build requirements beforehand.
To run E2E tests, run rindo test --e2e
. By default, files ending in .e2e.ts
will be executed.
Rindo's E2E test are provided with the following API, available via @rindo/core/testing
.
Most methods are async and return Promises. Use async
and await
to declutter your tests.
newE2EPage
: Should be invoked at the start of each test to instantiate a newE2EPage
object
E2EPage
is a wrapper utility to Puppeteer to simplify writing tests. Some helpful methods on E2EPage
include:
find(selector: string)
: Find an element that matches the selector. Similar todocument.querySelector
.setContent(html: string)
: Sets the content of a page. This is where you would include the markup of the component under test.setViewport(viewport: Viewport)
: Updates the page to emulate a device display. This is helpful for testing a component's behavior in different orientations and viewport sizes.waitForChanges()
: Both Rindo and Puppeteer have an asynchronous architecture, which is a good thing for performance. Since all calls are async, it's required thatawait page.waitForChanges()
is called when changes are made to components.
An example E2E test might have the following boilerplate:
import { newE2EPage } from '@rindo/core/testing';
describe('example', () => {
it('should render a foo-component', async () => {
const page = await newE2EPage();
await page.setContent(`<foo-component></foo-component>`);
const el = await page.find('foo-component');
expect(el).not.toBeNull();
});
});
Example End-to-end Test
import { newE2EPage } from '@rindo/core/testing';
it('should create toggle, unchecked by default', async () => {
const page = await newE2EPage();
await page.setContent(`
<fml-toggle class="pretty-toggle"></fml-toggle>
`);
const fmlChange = await page.spyOnEvent('fmlChange');
const toggle = await page.find('fml-toggle');
expect(toggle).toHaveClasses(['pretty-toggle', 'hydrated']);
expect(toggle).not.toHaveClass('toggle-checked');
toggle.setProperty('checked', true);
await page.waitForChanges();
expect(toggle).toHaveClass('toggle-checked');
expect(fmlChange).toHaveReceivedEventDetail({
checked: true,
value: 'on'
});
});
E2E Testing Recipes
Find an element in the Shadow DOM
Use the "piercing" selector >>>
to query for an object inside a component's shadow root:
const el = await page.find('foo-component >>> .close-button');
Make sure to only use >>>
once in your query. Puppeteer does not support nested deep selectors. Instead, separate the calls into two queries, e.g.:
// 👎 multiple nested deep selector aren't supported
const otherShadowElement = await page.find('my-component >>> div > my-other-component >>> h3');
// 👍 separated calls using `>>>` once
const divElement = await page.find('my-component >>> div');
const otherShadowElement = await divElement.find('my-other-component >>> h3');
Find more information about this selector type for Puppeteer in their project docs.
Set a @Prop() on a component
Use page.$eval
(part of the Puppeteer API) to set props or otherwise manipulate a component:
// create a new puppeteer page
// load the page with html content
await page.setContent(`
<prop-cmp></prop-cmp>
`);
// select the "prop-cmp" element
// and run the callback in the browser's context
await page.$eval('prop-cmp', (elm: any) => {
// within the browser's context
// let's set new property values on the component
elm.first = 'Marty';
elm.lastName = 'McFly';
});
// we just made a change and now the async queue need to process it
// make sure the queue does its work before we continue
await page.waitForChanges();