Skip to content
This repository was archived by the owner on Aug 6, 2025. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,12 @@ module.exports = {
'jest/no-done-callback': 'off',
},
},
{
files: ['lib/fixture/**/*.+(js|ts)'],
rules: {
'no-empty-pattern': 'off',
'no-underscore-dangle': ['error', {allow: ['__testingLibraryReviver']}],
},
},
],
}
10 changes: 9 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,18 @@ jobs:
npm install @playwright/test@${{ matrix.playwright }}
- name: Check types, run lint + tests
if: ${{ matrix.playwright == 'latest' }}
run: |
npm why playwright
npm why @playwright/test
npm run validate
npm run test
- name: Check types, run lint + tests
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old version of Playwright we still test against (1.12) does not have the Locator APIs. I'll add a comment here to explain that until we drop it.

if: ${{ matrix.playwright != 'latest' }}
run: |
npm why playwright
npm why @playwright/test
npm run test:legacy
# Only release on Node 14

Expand Down
4 changes: 2 additions & 2 deletions lib/common.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Queries} from './typedefs'
import {queries} from '@testing-library/dom'

export const queryNames: Array<keyof Queries> = [
export const queryNames: Array<keyof typeof queries> = [
'queryByPlaceholderText',
'queryAllByPlaceholderText',
'getByPlaceholderText',
Expand Down
20 changes: 6 additions & 14 deletions lib/fixture.ts → lib/fixture/element-handle.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import type {PlaywrightTestArgs, TestFixture} from '@playwright/test'

import {queryNames} from './common'
import type {FixtureQueries as Queries} from './typedefs'
import {getDocument, queries as unscopedQueries} from '..'
import {queryNames} from '../common'
import type {FixtureQueries as Queries} from '../typedefs'

import {getDocument, queries as unscopedQueries} from '.'

interface TestingLibraryFixtures {
queries: Queries
}

const fixture: TestFixture<Queries, PlaywrightTestArgs> = async ({page}, use) => {
const queriesFixture: TestFixture<Queries, PlaywrightTestArgs> = async ({page}, use) => {
const queries = {} as Queries

queryNames.forEach(name => {
Expand All @@ -27,8 +22,5 @@ const fixture: TestFixture<Queries, PlaywrightTestArgs> = async ({page}, use) =>
await use(queries)
}

const fixtures = {queries: fixture}

export {configure} from '.'
export {fixture, fixtures}
export type {Queries, TestingLibraryFixtures}
export {queriesFixture}
export type {Queries}
17 changes: 17 additions & 0 deletions lib/fixture/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const replacer = (_: string, value: unknown) => {
if (value instanceof RegExp) return `__REGEXP ${value.toString()}`

return value
}

const reviver = (_: string, value: string) => {
if (value.toString().includes('__REGEXP ')) {
const match = /\/(.*)\/(.*)?/.exec(value.split('__REGEXP ')[1])

return new RegExp(match![1], match![2] || '')
}

return value
}

export {replacer, reviver}
38 changes: 38 additions & 0 deletions lib/fixture/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {Fixtures} from '@playwright/test'

import type {Queries as ElementHandleQueries} from './element-handle'
import {queriesFixture as elementHandleQueriesFixture} from './element-handle'
import type {Queries as LocatorQueries} from './locator'
import {
installTestingLibraryFixture,
queriesFixture as locatorQueriesFixture,
registerSelectorsFixture,
within,
} from './locator'

const elementHandleFixtures: Fixtures = {queries: elementHandleQueriesFixture}
const locatorFixtures: Fixtures = {
queries: locatorQueriesFixture,
registerSelectors: registerSelectorsFixture,
installTestingLibrary: installTestingLibraryFixture,
}

interface ElementHandleFixtures {
queries: ElementHandleQueries
}

interface LocatorFixtures {
queries: LocatorQueries
registerSelectors: void
installTestingLibrary: void
}

export type {ElementHandleFixtures as TestingLibraryFixtures}
export {elementHandleQueriesFixture as fixture}
export {elementHandleFixtures as fixtures}

export type {LocatorFixtures}
export {locatorQueriesFixture}
export {locatorFixtures, within}

export {configure} from '..'
132 changes: 132 additions & 0 deletions lib/fixture/locator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import {promises as fs} from 'fs'

import type {Locator, PlaywrightTestArgs, TestFixture} from '@playwright/test'
import {selectors} from '@playwright/test'

import {queryNames as allQueryNames} from '../common'

import {replacer, reviver} from './helpers'
import type {
AllQuery,
FindQuery,
LocatorQueries as Queries,
Query,
Selector,
SelectorEngine,
SupportedQuery,
} from './types'

const isAllQuery = (query: Query): query is AllQuery => query.includes('All')
const isNotFindQuery = (query: Query): query is Exclude<Query, FindQuery> =>
!query.startsWith('find')

const queryNames = allQueryNames.filter(isNotFindQuery)

const queryToSelector = (query: SupportedQuery) =>
query.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() as Selector

const queriesFixture: TestFixture<Queries, PlaywrightTestArgs> = async ({page}, use) => {
const queries = queryNames.reduce(
(rest, query) => ({
...rest,
[query]: (...args: Parameters<Queries[keyof Queries]>) =>
page.locator(`${queryToSelector(query)}=${JSON.stringify(args, replacer)}`),
}),
{} as Queries,
)

await use(queries)
}

const within = (locator: Locator): Queries =>
queryNames.reduce(
(rest, query) => ({
...rest,
[query]: (...args: Parameters<Queries[keyof Queries]>) =>
locator.locator(`${queryToSelector(query)}=${JSON.stringify(args, replacer)}`),
}),
{} as Queries,
)

declare const queryName: SupportedQuery

const engine: () => SelectorEngine = () => ({
query(root, selector) {
const args = JSON.parse(selector, window.__testingLibraryReviver) as unknown as Parameters<
Queries[typeof queryName]
>

if (isAllQuery(queryName))
throw new Error(
`PlaywrightTestingLibrary: the plural '${queryName}' was used to create this Locator`,
)

// @ts-expect-error
const result = window.TestingLibraryDom[queryName](root, ...args)

return result
},
queryAll(root, selector) {
const testingLibrary = window.TestingLibraryDom
const args = JSON.parse(selector, window.__testingLibraryReviver) as unknown as Parameters<
Queries[typeof queryName]
>

// @ts-expect-error
const result = testingLibrary[queryName](root, ...args)

if (!result) return []

return Array.isArray(result) ? result : [result]
},
})

const registerSelectorsFixture: [
TestFixture<void, PlaywrightTestArgs>,
{scope: 'worker'; auto?: boolean},
] = [
async ({}, use) => {
try {
await Promise.all(
queryNames.map(async name =>
selectors.register(
queryToSelector(name),
`(${engine.toString().replace(/queryName/g, `"${name}"`)})()`,
),
),
)
} catch (error) {
// eslint-disable-next-line no-console
console.error(
'PlaywrightTestingLibrary: failed to register Testing Library functions\n',
error,
)
}
await use()
},
{scope: 'worker', auto: true},
]

const installTestingLibraryFixture: [
TestFixture<void, PlaywrightTestArgs>,
{scope: 'test'; auto?: boolean},
] = [
async ({context}, use) => {
const testingLibraryDomUmdScript = await fs.readFile(
require.resolve('@testing-library/dom/dist/@testing-library/dom.umd.js'),
'utf8',
)

await context.addInitScript(`
${testingLibraryDomUmdScript}

window.__testingLibraryReviver = ${reviver.toString()};
`)

await use()
},
{scope: 'test', auto: true},
]

export {queriesFixture, registerSelectorsFixture, installTestingLibraryFixture, within}
export type {Queries}
54 changes: 54 additions & 0 deletions lib/fixture/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {Locator} from '@playwright/test'
import type * as TestingLibraryDom from '@testing-library/dom'
import {queries} from '@testing-library/dom'

import {reviver} from './helpers'

/**
* This type was copied across from Playwright
*
* @see {@link https://github.com/microsoft/playwright/blob/82ff85b106e31ffd7b3702aef260c9c460cfb10c/packages/playwright-core/src/client/types.ts#L108-L117}
*/
export 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[]
}

type Queries = typeof queries

type StripNever<T> = {[P in keyof T as T[P] extends never ? never : P]: T[P]}
type ConvertQuery<Query extends Queries[keyof Queries]> = Query extends (
el: HTMLElement,
...rest: infer Rest
) => HTMLElement | (HTMLElement[] | null) | (HTMLElement | null)
? (...args: Rest) => Locator
: never

type KebabCase<S> = S extends `${infer C}${infer T}`
? T extends Uncapitalize<T>
? `${Uncapitalize<C>}${KebabCase<T>}`
: `${Uncapitalize<C>}-${KebabCase<T>}`
: S

export type LocatorQueries = StripNever<{[K in keyof Queries]: ConvertQuery<Queries[K]>}>

export type Query = keyof Queries

export type AllQuery = Extract<Query, `${string}All${string}`>
export type FindQuery = Extract<Query, `find${string}`>
export type SupportedQuery = Exclude<Query, FindQuery>

export type Selector = KebabCase<SupportedQuery>

declare global {
interface Window {
TestingLibraryDom: typeof TestingLibraryDom
__testingLibraryReviver: typeof reviver
}
}
5 changes: 2 additions & 3 deletions lib/typedefs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
Matcher,
ByRoleOptions as TestingLibraryByRoleOptions,
Config as TestingLibraryConfig,
MatcherOptions as TestingLibraryMatcherOptions,
SelectorMatcherOptions as TestingLibrarySelectorMatcherOptions,
waitForOptions,
Expand Down Expand Up @@ -189,6 +190,4 @@ export interface Queries extends QueryMethods {
getNodeText(el: Element): Promise<string>
}

export interface ConfigurationOptions {
testIdAttribute: string
}
export type ConfigurationOptions = Pick<TestingLibraryConfig, 'testIdAttribute'>
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@
"prepublishOnly": "npm run build",
"start:standalone": "hover-scripts test",
"test": "run-s build:testing-library test:*",
"test:legacy": "run-s build:testing-library test:standalone test:fixture:legacy",
"test:fixture": "playwright test",
"test:fixture:legacy": "playwright test test/fixture/element-handles.test.ts",
"test:standalone": "hover-scripts test --no-watch",
"test:types": "tsc --noEmit",
"validate": "run-s test"
"test:types": "tsc --noEmit"
},
"repository": {
"type": "git",
Expand Down
Loading