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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion lib/common.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,22 @@
import {queries} from '@testing-library/dom'
import {Config as TestingLibraryConfig, queries} from '@testing-library/dom'

export type Config = Pick<TestingLibraryConfig, 'testIdAttribute' | 'asyncUtilTimeout'>

export const configureTestingLibraryScript = (
script: string,
{testIdAttribute, asyncUtilTimeout}: Partial<Config>,
) => {
const withTestId = testIdAttribute
? script.replace(
/testIdAttribute: (['|"])data-testid(['|"])/g,
`testIdAttribute: $1${testIdAttribute}$2`,
)
: script

return asyncUtilTimeout
? withTestId.replace(/asyncUtilTimeout: \d+/g, `asyncUtilTimeout: ${asyncUtilTimeout}`)
: withTestId
}

export const queryNames: Array<keyof typeof queries> = [
'queryByPlaceholderText',
Expand Down
11 changes: 7 additions & 4 deletions lib/fixture/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import {Fixtures} from '@playwright/test'

import {Config} from '../common'

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,
options,
registerSelectorsFixture,
within,
} from './locator'
Expand All @@ -15,24 +18,24 @@ const locatorFixtures: Fixtures = {
queries: locatorQueriesFixture,
registerSelectors: registerSelectorsFixture,
installTestingLibrary: installTestingLibraryFixture,
...options,
}

interface ElementHandleFixtures {
queries: ElementHandleQueries
}

interface LocatorFixtures {
interface LocatorFixtures extends Partial<Config> {
queries: LocatorQueries
registerSelectors: void
installTestingLibrary: void
}

export {configure} from '..'

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

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

export {configure} from '..'
66 changes: 20 additions & 46 deletions lib/fixture/locator.ts → lib/fixture/locator/fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,33 @@
import {promises as fs} from 'fs'

import type {Locator, PlaywrightTestArgs, TestFixture} from '@playwright/test'
import type {Locator, Page, 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'
import {queryNames as allQueryNames} from '../../common'
import {replacer} from '../helpers'
import type {Config, LocatorQueries as Queries, 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')
import {buildTestingLibraryScript, isAllQuery, isNotFindQuery, queryToSelector} from './helpers'

const queryNames = allQueryNames.filter(isNotFindQuery)
const defaultConfig: Config = {testIdAttribute: 'data-testid', asyncUtilTimeout: 1000}

const queryToSelector = (query: SupportedQuery) =>
query.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() as Selector
const options = Object.fromEntries(
Object.entries(defaultConfig).map(([key, value]) => [key, [value, {option: true}] as const]),
)

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

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

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,
)
const within = (locator: Locator): Queries => queriesFor(locator)

declare const queryName: SupportedQuery

Expand Down Expand Up @@ -108,25 +89,18 @@ const registerSelectorsFixture: [
]

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

await context.addInitScript(`
${testingLibraryDomUmdScript}

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

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

export {queriesFixture, registerSelectorsFixture, installTestingLibraryFixture, within}
export {installTestingLibraryFixture, options, queriesFixture, registerSelectorsFixture, within}
export type {Queries}
29 changes: 29 additions & 0 deletions lib/fixture/locator/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {promises as fs} from 'fs'

import {configureTestingLibraryScript} from '../../common'
import {reviver} from '../helpers'
import type {AllQuery, Config, FindQuery, Query, Selector, 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 queryToSelector = (query: SupportedQuery) =>
query.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() as Selector

const buildTestingLibraryScript = async ({config}: {config: Config}) => {
const testingLibraryDom = await fs.readFile(
require.resolve('@testing-library/dom/dist/@testing-library/dom.umd.js'),
'utf8',
)

const configuredTestingLibraryDom = configureTestingLibraryScript(testingLibraryDom, config)

return `
${configuredTestingLibraryDom}

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

export {isAllQuery, isNotFindQuery, queryToSelector, buildTestingLibraryScript}
8 changes: 8 additions & 0 deletions lib/fixture/locator/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export {
installTestingLibraryFixture,
options,
queriesFixture,
registerSelectorsFixture,
within,
} from './fixtures'
export type {Queries} from './fixtures'
12 changes: 12 additions & 0 deletions lib/fixture/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {Locator} from '@playwright/test'
import type * as TestingLibraryDom from '@testing-library/dom'
import {queries} from '@testing-library/dom'

import {Config} from '../common'

import {reviver} from './helpers'

/**
Expand Down Expand Up @@ -37,6 +39,7 @@ type KebabCase<S> = S extends `${infer C}${infer T}`
: S

export type LocatorQueries = StripNever<{[K in keyof Queries]: ConvertQuery<Queries[K]>}>
export type Within = (locator: Locator) => LocatorQueries

export type Query = keyof Queries

Expand All @@ -46,6 +49,15 @@ export type SupportedQuery = Exclude<Query, FindQuery>

export type Selector = KebabCase<SupportedQuery>

export type {Config}
export interface ConfigFn {
(existingConfig: Config): Partial<Config>
}

export type ConfigDelta = ConfigFn | Partial<Config>
export type Configure = (configDelta: ConfigDelta) => void
export type ConfigureLocator = (configDelta: ConfigDelta) => Config

declare global {
interface Window {
TestingLibraryDom: typeof TestingLibraryDom
Expand Down
27 changes: 8 additions & 19 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import * as path from 'path'
import {JSHandle, Page} from 'playwright'
import waitForExpect from 'wait-for-expect'

import {queryNames} from './common'
import {ConfigurationOptions, ElementHandle, Queries, ScopedQueries} from './typedefs'
import {Config, configureTestingLibraryScript, queryNames} from './common'
import {ElementHandle, Queries, ScopedQueries} from './typedefs'

const domLibraryAsString = readFileSync(
path.join(__dirname, '../dom-testing-library.js'),
Expand Down Expand Up @@ -176,26 +176,15 @@ export function wait(

export const waitFor = wait

export function configure(options: Partial<ConfigurationOptions>): void {
if (!options) {
export function configure(config: Partial<Config>): void {
if (!config) {
return
}

const {testIdAttribute, asyncUtilTimeout} = options

if (testIdAttribute) {
delegateFnBodyToExecuteInPage = delegateFnBodyToExecuteInPageInitial.replace(
/testIdAttribute: (['|"])data-testid(['|"])/g,
`testIdAttribute: $1${testIdAttribute}$2`,
)
}

if (asyncUtilTimeout) {
delegateFnBodyToExecuteInPage = delegateFnBodyToExecuteInPageInitial.replace(
/asyncUtilTimeout: \d+/g,
`asyncUtilTimeout: ${asyncUtilTimeout}`,
)
}
delegateFnBodyToExecuteInPage = configureTestingLibraryScript(
delegateFnBodyToExecuteInPageInitial,
config,
)
}

export function getQueriesForElement<T>(
Expand Down
6 changes: 0 additions & 6 deletions lib/typedefs.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
Matcher,
ByRoleOptions as TestingLibraryByRoleOptions,
Config as TestingLibraryConfig,
MatcherOptions as TestingLibraryMatcherOptions,
SelectorMatcherOptions as TestingLibrarySelectorMatcherOptions,
waitForOptions,
Expand Down Expand Up @@ -189,8 +188,3 @@ export interface Queries extends QueryMethods {
getQueriesForElement(): ScopedQueries
getNodeText(el: Element): Promise<string>
}

export type ConfigurationOptions = Pick<
TestingLibraryConfig,
'testIdAttribute' | 'asyncUtilTimeout'
>
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"prepare:playwright-test:package": "jscodeshift -t ./playwright-test/rename-imports.ts --extensions=ts --parser=ts ./lib",
"prepublishOnly": "npm run build",
"start:standalone": "hover-scripts test",
"test": "run-s build:testing-library test:*",
"test": "run-s build:testing-library test:standalone test:fixture",
"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",
Expand Down
48 changes: 48 additions & 0 deletions test/fixture/configure.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import * as path from 'path'

import * as playwright from '@playwright/test'

import {
LocatorFixtures as TestingLibraryFixtures,
locatorFixtures as fixtures,
} from '../../lib/fixture'

const test = playwright.test.extend<TestingLibraryFixtures>(fixtures)

const {expect} = test

test.use({testIdAttribute: 'data-new-id'})

test.describe('global configuration', () => {
test.beforeEach(async ({page}) => {
await page.goto(`file://${path.join(__dirname, '../fixtures/page.html')}`)
})

test('queries with test ID configured in module scope', async ({queries}) => {
const defaultTestIdLocator = queries.queryByTestId('testid-text-input')
const customTestIdLocator = queries.queryByTestId('first-level-header')

await expect(defaultTestIdLocator).not.toBeVisible()
await expect(customTestIdLocator).toBeVisible()
})

test.describe('overridding global configuration', () => {
test.use({testIdAttribute: 'data-id'})

test('overrides test ID configured in module scope', async ({queries}) => {
const globalTestIdLocator = queries.queryByTestId('first-level-header')
const overriddenTestIdLocator = queries.queryByTestId('second-level-header')

await expect(globalTestIdLocator).not.toBeVisible()
await expect(overriddenTestIdLocator).toBeVisible()
})
})

test("page override doesn't modify global configuration", async ({queries}) => {
const defaultTestIdLocator = queries.queryByTestId('testid-text-input')
const customTestIdLocator = queries.queryByTestId('first-level-header')

await expect(defaultTestIdLocator).not.toBeVisible()
await expect(customTestIdLocator).toBeVisible()
})
})
2 changes: 0 additions & 2 deletions test/fixture/element-handles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,6 @@ test.describe('lib/fixture.ts', () => {
})

test('does not throw when querying for a specific element', async ({queries: {getByRole}}) => {
expect.assertions(1)

await expect(getByRole('heading', {level: 3})).resolves.not.toThrow()
})
})
Expand Down
Loading