Skip to content

Instantly share code, notes, and snippets.

@sebinsua
Last active June 21, 2022 12:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sebinsua/205682c7af6966fdadc103b4e15d0d8b to your computer and use it in GitHub Desktop.
Save sebinsua/205682c7af6966fdadc103b4e15d0d8b to your computer and use it in GitHub Desktop.
Playwright `@testing-library/dom` custom selectors. Contributed to discussion here: https://github.com/hoverinc/playwright-testing-library/pull/330#issuecomment-956348391
import { test, selectors } from '@playwright/test';
import { readFile } from 'fs-extra';
import type * as TestingLibraryDom from '@testing-library/dom';
// This type was copied across from Playwright's repo.
// See: https://github.com/microsoft/playwright/blob/82ff85b106e31ffd7b3702aef260c9c460cfb10c/packages/playwright-core/src/client/types.ts#L108-L117
type SelectorEngine = {
/**
* Returns the first element matching given selector in the root's subtree.
*/
query(root: HTMLElement, selector: string): HTMLElement | null;
/**
* Returns all elements matching given selector in the root's subtree.
*/
queryAll(root: HTMLElement, selector: string): HTMLElement[];
};
// Both `SelectorEngine#query` and `SelectorEngine#queryAll` receive a selector
// including any quote marks wrapped around it. The `@testing-library/dom` methods we're
// using internally expect to be passed strings that are not wrapped in quote marks
// therefore we strip them if they exist.
function stripWrappingQuotesFromSelector(selector: string): string {
const firstChar = selector.charAt(0);
const lastChar = selector.charAt(selector.length - 1);
if (
(firstChar === `'` && lastChar === `'`) ||
(firstChar === `"` && lastChar === `"`) ||
(firstChar === '`' && lastChar === '`')
) {
return selector.substring(1, selector.length - 1);
}
return selector;
}
// **The more your tests resemble the way your software is used, the more confidence they can give you.**
//
// With this in mind, we recommend this order of priority:
//
// 1. `role=button >> text="Text inside button"`
// 2. `label="Label name of form element displayed to user"`
// 3. `placeholder="Placeholder text of input element displayed to user"`
// 4. `text="Text currently shown on page"`
// 5. `value="current-value"`
// 6. `alt="Alternative text used to describe image"`
// 7. `title="Advisory information against element"`
// 8. `data-testid="some-test-id-you-attached-to-the-element"`
//
// Selectors defined as `engine=body` or in short-form can be combined with the `>>` token,
// e.g. `selector1 >> selector2 >> selector3`. When selectors are chained, the next one is
// queried relative to the previous one's result.
//
// See: https://testing-library.com/docs/queries/about#priority
// See: https://playwright.dev/docs/selectors#best-practices
// See: https://playwright.dev/docs/selectors/#chaining-selectors
export function installTestingLibrarySelectors() {
// We use `selectors#register` to register custom selectors in the browser.
//
// It calls `toString` on any function passed into it, before transpiling
// this and sending it on to the browser. Therefore, any reference here
// to imports in this file are unreliable due to renaming (e.g. `_dom`,
// `_dom2`, etc).
//
// That is why we grab `@testing-library/dom` from the `window` and need
// to inject any objects or functions into the page before the selectors
// can work.
//
// See: https://playwright.dev/docs/api/class-selectors/#selectors-register
test.beforeAll(async () => {
await selectors.register(
'role',
(): SelectorEngine => ({
query(root, selector) {
// @ts-ignore
const testingLibrary: typeof TestingLibraryDom = window['TestingLibraryDom'];
return testingLibrary.queryByRole(root, stripWrappingQuotesFromSelector(selector));
},
queryAll(root, selector) {
// @ts-ignore
const testingLibrary: typeof TestingLibraryDom = window['TestingLibraryDom'];
return testingLibrary.queryAllByRole(root, stripWrappingQuotesFromSelector(selector));
},
})
);
await selectors.register(
'label',
(): SelectorEngine => ({
query(root, selector) {
// @ts-ignore
const testingLibrary: typeof TestingLibraryDom = window['TestingLibraryDom'];
return testingLibrary.queryByLabelText(root, stripWrappingQuotesFromSelector(selector));
},
queryAll(root, selector) {
// @ts-ignore
const testingLibrary: typeof TestingLibraryDom = window['TestingLibraryDom'];
return testingLibrary.queryAllByLabelText(root, stripWrappingQuotesFromSelector(selector));
},
})
);
await selectors.register(
'placeholder',
(): SelectorEngine => ({
query(root, selector) {
// @ts-ignore
const testingLibrary: typeof TestingLibraryDom = window['TestingLibraryDom'];
return testingLibrary.queryByPlaceholderText(root, stripWrappingQuotesFromSelector(selector));
},
queryAll(root, selector) {
// @ts-ignore
const testingLibrary: typeof TestingLibraryDom = window['TestingLibraryDom'];
return testingLibrary.queryAllByPlaceholderText(root, stripWrappingQuotesFromSelector(selector));
},
})
);
await selectors.register(
'value',
(): SelectorEngine => ({
query(root, selector) {
// @ts-ignore
const testingLibrary: typeof TestingLibraryDom = window['TestingLibraryDom'];
return testingLibrary.queryByDisplayValue(root, stripWrappingQuotesFromSelector(selector));
},
queryAll(root, selector) {
// @ts-ignore
const testingLibrary: typeof TestingLibraryDom = window['TestingLibraryDom'];
return testingLibrary.queryAllByDisplayValue(root, stripWrappingQuotesFromSelector(selector));
},
})
);
await selectors.register(
'alt',
(): SelectorEngine => ({
query(root, selector) {
// @ts-ignore
const testingLibrary: typeof TestingLibraryDom = window['TestingLibraryDom'];
return testingLibrary.queryByAltText(root, stripWrappingQuotesFromSelector(selector));
},
queryAll(root, selector) {
// @ts-ignore
const testingLibrary: typeof TestingLibraryDom = window['TestingLibraryDom'];
return testingLibrary.queryAllByAltText(root, stripWrappingQuotesFromSelector(selector));
},
})
);
await selectors.register(
'title',
(): SelectorEngine => ({
query(root, selector) {
// @ts-ignore
const testingLibrary: typeof TestingLibraryDom = window['TestingLibraryDom'];
return testingLibrary.queryByTitle(root, stripWrappingQuotesFromSelector(selector));
},
queryAll(root, selector) {
// @ts-ignore
const testingLibrary: typeof TestingLibraryDom = window['TestingLibraryDom'];
return testingLibrary.queryAllByTitle(root, stripWrappingQuotesFromSelector(selector));
},
})
);
});
test.beforeEach(async ({ context }) => {
const testingLibraryDomUmdScript = await readFile(
// The location of the `@testing-library/dom` UMD script is found inside
// the `node_modules/` directory.
require.resolve('@testing-library/dom/dist/@testing-library/dom.umd.js'),
'utf8'
);
// On every page load, we must ensure that both `window['TestingLibraryDom']`
// and `stripWrappingQuotesFromSelector` are available to our custom selectors.
await context.addInitScript(`
${testingLibraryDomUmdScript}
${stripWrappingQuotesFromSelector}
`);
});
}
@sebinsua
Copy link
Author

sebinsua commented Jan 5, 2022

@petetnt Just seen this now. Thank you for posting some improvements to my Gist!

@sebinsua
Copy link
Author

Looks like the next version of @playwright-testing-library/test will do something similar to this (and supersede this logic).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment