Last active
June 21, 2022 12:49
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} | |
`); | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Looks like the next version of
@playwright-testing-library/test
will do something similar to this (and supersede this logic).