Skip to content

Commit 5d65239

Browse files
committed
feat: support configure API with locator fixture via test.use
### Global ```ts // playwright.config.ts import type { PlaywrightTestConfig } from '@playwright/test'; const config: PlaywrightTestConfig = { use: { testIdAttribute: 'data-custom-test-id', asyncUtilsTimeout: 5000, }, }; export default config; ``` ### Local ```ts import { test as baseTest } from '@playwright/test' import { locatorFixtures as fixtures, LocatorFixtures as TestingLibraryFixtures, within } from '@playwright-testing-library/test/fixture'; const test = baseTest.extend<TestingLibraryFixtures>(fixtures); const {expect} = test; // Entire test suite test.use({ testIdAttribute: 'data-custom-test-id' }); test.describe(() => { // Specific block test.use({ testIdAttribute: 'some-other-test-id', asyncUtilsTimeout: 5000, }); test('my form', async ({queries: {getByTestId}}) => { // ... }); }); ```
1 parent 38b01b7 commit 5d65239

File tree

11 files changed

+173
-81
lines changed

11 files changed

+173
-81
lines changed

lib/common.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,22 @@
1-
import {queries} from '@testing-library/dom'
1+
import {Config as TestingLibraryConfig, queries} from '@testing-library/dom'
2+
3+
export type Config = Pick<TestingLibraryConfig, 'testIdAttribute' | 'asyncUtilTimeout'>
4+
5+
export const configureTestingLibraryScript = (
6+
script: string,
7+
{testIdAttribute, asyncUtilTimeout}: Partial<Config>,
8+
) => {
9+
const withTestId = testIdAttribute
10+
? script.replace(
11+
/testIdAttribute: (['|"])data-testid(['|"])/g,
12+
`testIdAttribute: $1${testIdAttribute}$2`,
13+
)
14+
: script
15+
16+
return asyncUtilTimeout
17+
? withTestId.replace(/asyncUtilTimeout: \d+/g, `asyncUtilTimeout: ${asyncUtilTimeout}`)
18+
: withTestId
19+
}
220

321
export const queryNames: Array<keyof typeof queries> = [
422
'queryByPlaceholderText',

lib/fixture/index.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import {Fixtures} from '@playwright/test'
22

3+
import {Config} from '../common'
4+
35
import type {Queries as ElementHandleQueries} from './element-handle'
46
import {queriesFixture as elementHandleQueriesFixture} from './element-handle'
57
import type {Queries as LocatorQueries} from './locator'
68
import {
79
installTestingLibraryFixture,
810
queriesFixture as locatorQueriesFixture,
11+
options,
912
registerSelectorsFixture,
1013
within,
1114
} from './locator'
@@ -15,24 +18,24 @@ const locatorFixtures: Fixtures = {
1518
queries: locatorQueriesFixture,
1619
registerSelectors: registerSelectorsFixture,
1720
installTestingLibrary: installTestingLibraryFixture,
21+
...options,
1822
}
1923

2024
interface ElementHandleFixtures {
2125
queries: ElementHandleQueries
2226
}
2327

24-
interface LocatorFixtures {
28+
interface LocatorFixtures extends Partial<Config> {
2529
queries: LocatorQueries
2630
registerSelectors: void
2731
installTestingLibrary: void
2832
}
2933

34+
export {configure} from '..'
35+
3036
export type {ElementHandleFixtures as TestingLibraryFixtures}
3137
export {elementHandleQueriesFixture as fixture}
3238
export {elementHandleFixtures as fixtures}
33-
3439
export type {LocatorFixtures}
3540
export {locatorQueriesFixture}
3641
export {locatorFixtures, within}
37-
38-
export {configure} from '..'

lib/fixture/locator.ts renamed to lib/fixture/locator/fixtures.ts

Lines changed: 20 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,33 @@
1-
import {promises as fs} from 'fs'
2-
3-
import type {Locator, PlaywrightTestArgs, TestFixture} from '@playwright/test'
1+
import type {Locator, Page, PlaywrightTestArgs, TestFixture} from '@playwright/test'
42
import {selectors} from '@playwright/test'
53

6-
import {queryNames as allQueryNames} from '../common'
7-
8-
import {replacer, reviver} from './helpers'
9-
import type {
10-
AllQuery,
11-
FindQuery,
12-
LocatorQueries as Queries,
13-
Query,
14-
Selector,
15-
SelectorEngine,
16-
SupportedQuery,
17-
} from './types'
4+
import {queryNames as allQueryNames} from '../../common'
5+
import {replacer} from '../helpers'
6+
import type {Config, LocatorQueries as Queries, SelectorEngine, SupportedQuery} from '../types'
187

19-
const isAllQuery = (query: Query): query is AllQuery => query.includes('All')
20-
const isNotFindQuery = (query: Query): query is Exclude<Query, FindQuery> =>
21-
!query.startsWith('find')
8+
import {buildTestingLibraryScript, isAllQuery, isNotFindQuery, queryToSelector} from './helpers'
229

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

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

28-
const queriesFixture: TestFixture<Queries, PlaywrightTestArgs> = async ({page}, use) => {
29-
const queries = queryNames.reduce(
17+
const queriesFor = (pageOrLocator: Page | Locator) =>
18+
queryNames.reduce(
3019
(rest, query) => ({
3120
...rest,
3221
[query]: (...args: Parameters<Queries[keyof Queries]>) =>
33-
page.locator(`${queryToSelector(query)}=${JSON.stringify(args, replacer)}`),
22+
pageOrLocator.locator(`${queryToSelector(query)}=${JSON.stringify(args, replacer)}`),
3423
}),
3524
{} as Queries,
3625
)
3726

38-
await use(queries)
39-
}
27+
const queriesFixture: TestFixture<Queries, PlaywrightTestArgs> = async ({page}, use) =>
28+
use(queriesFor(page))
4029

41-
const within = (locator: Locator): Queries =>
42-
queryNames.reduce(
43-
(rest, query) => ({
44-
...rest,
45-
[query]: (...args: Parameters<Queries[keyof Queries]>) =>
46-
locator.locator(`${queryToSelector(query)}=${JSON.stringify(args, replacer)}`),
47-
}),
48-
{} as Queries,
49-
)
30+
const within = (locator: Locator): Queries => queriesFor(locator)
5031

5132
declare const queryName: SupportedQuery
5233

@@ -108,25 +89,18 @@ const registerSelectorsFixture: [
10889
]
10990

11091
const installTestingLibraryFixture: [
111-
TestFixture<void, PlaywrightTestArgs>,
92+
TestFixture<void, PlaywrightTestArgs & Config>,
11293
{scope: 'test'; auto?: boolean},
11394
] = [
114-
async ({context}, use) => {
115-
const testingLibraryDomUmdScript = await fs.readFile(
116-
require.resolve('@testing-library/dom/dist/@testing-library/dom.umd.js'),
117-
'utf8',
95+
async ({context, asyncUtilTimeout, testIdAttribute}, use) => {
96+
await context.addInitScript(
97+
await buildTestingLibraryScript({config: {asyncUtilTimeout, testIdAttribute}}),
11898
)
11999

120-
await context.addInitScript(`
121-
${testingLibraryDomUmdScript}
122-
123-
window.__testingLibraryReviver = ${reviver.toString()};
124-
`)
125-
126100
await use()
127101
},
128102
{scope: 'test', auto: true},
129103
]
130104

131-
export {queriesFixture, registerSelectorsFixture, installTestingLibraryFixture, within}
105+
export {installTestingLibraryFixture, options, queriesFixture, registerSelectorsFixture, within}
132106
export type {Queries}

lib/fixture/locator/helpers.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {promises as fs} from 'fs'
2+
3+
import {configureTestingLibraryScript} from '../../common'
4+
import {reviver} from '../helpers'
5+
import type {AllQuery, Config, FindQuery, Query, Selector, SupportedQuery} from '../types'
6+
7+
const isAllQuery = (query: Query): query is AllQuery => query.includes('All')
8+
const isNotFindQuery = (query: Query): query is Exclude<Query, FindQuery> =>
9+
!query.startsWith('find')
10+
11+
const queryToSelector = (query: SupportedQuery) =>
12+
query.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() as Selector
13+
14+
const buildTestingLibraryScript = async ({config}: {config: Config}) => {
15+
const testingLibraryDom = await fs.readFile(
16+
require.resolve('@testing-library/dom/dist/@testing-library/dom.umd.js'),
17+
'utf8',
18+
)
19+
20+
const configuredTestingLibraryDom = configureTestingLibraryScript(testingLibraryDom, config)
21+
22+
return `
23+
${configuredTestingLibraryDom}
24+
25+
window.__testingLibraryReviver = ${reviver.toString()};
26+
`
27+
}
28+
29+
export {isAllQuery, isNotFindQuery, queryToSelector, buildTestingLibraryScript}

lib/fixture/locator/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export {
2+
installTestingLibraryFixture,
3+
options,
4+
queriesFixture,
5+
registerSelectorsFixture,
6+
within,
7+
} from './fixtures'
8+
export type {Queries} from './fixtures'

lib/fixture/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import {Locator} from '@playwright/test'
22
import type * as TestingLibraryDom from '@testing-library/dom'
33
import {queries} from '@testing-library/dom'
44

5+
import {Config} from '../common'
6+
57
import {reviver} from './helpers'
68

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

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

4144
export type Query = keyof Queries
4245

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

4750
export type Selector = KebabCase<SupportedQuery>
4851

52+
export type {Config}
53+
export interface ConfigFn {
54+
(existingConfig: Config): Partial<Config>
55+
}
56+
57+
export type ConfigDelta = ConfigFn | Partial<Config>
58+
export type Configure = (configDelta: ConfigDelta) => void
59+
export type ConfigureLocator = (configDelta: ConfigDelta) => Config
60+
4961
declare global {
5062
interface Window {
5163
TestingLibraryDom: typeof TestingLibraryDom

lib/index.ts

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import * as path from 'path'
66
import {JSHandle, Page} from 'playwright'
77
import waitForExpect from 'wait-for-expect'
88

9-
import {queryNames} from './common'
10-
import {ConfigurationOptions, ElementHandle, Queries, ScopedQueries} from './typedefs'
9+
import {Config, configureTestingLibraryScript, queryNames} from './common'
10+
import {ElementHandle, Queries, ScopedQueries} from './typedefs'
1111

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

177177
export const waitFor = wait
178178

179-
export function configure(options: Partial<ConfigurationOptions>): void {
180-
if (!options) {
179+
export function configure(config: Partial<Config>): void {
180+
if (!config) {
181181
return
182182
}
183183

184-
const {testIdAttribute, asyncUtilTimeout} = options
185-
186-
if (testIdAttribute) {
187-
delegateFnBodyToExecuteInPage = delegateFnBodyToExecuteInPageInitial.replace(
188-
/testIdAttribute: (['|"])data-testid(['|"])/g,
189-
`testIdAttribute: $1${testIdAttribute}$2`,
190-
)
191-
}
192-
193-
if (asyncUtilTimeout) {
194-
delegateFnBodyToExecuteInPage = delegateFnBodyToExecuteInPageInitial.replace(
195-
/asyncUtilTimeout: \d+/g,
196-
`asyncUtilTimeout: ${asyncUtilTimeout}`,
197-
)
198-
}
184+
delegateFnBodyToExecuteInPage = configureTestingLibraryScript(
185+
delegateFnBodyToExecuteInPageInitial,
186+
config,
187+
)
199188
}
200189

201190
export function getQueriesForElement<T>(

lib/typedefs.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import {
22
Matcher,
33
ByRoleOptions as TestingLibraryByRoleOptions,
4-
Config as TestingLibraryConfig,
54
MatcherOptions as TestingLibraryMatcherOptions,
65
SelectorMatcherOptions as TestingLibrarySelectorMatcherOptions,
76
waitForOptions,
@@ -189,8 +188,3 @@ export interface Queries extends QueryMethods {
189188
getQueriesForElement(): ScopedQueries
190189
getNodeText(el: Element): Promise<string>
191190
}
192-
193-
export type ConfigurationOptions = Pick<
194-
TestingLibraryConfig,
195-
'testIdAttribute' | 'asyncUtilTimeout'
196-
>

test/fixture/configure.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import * as path from 'path'
2+
3+
import * as playwright from '@playwright/test'
4+
5+
import {
6+
LocatorFixtures as TestingLibraryFixtures,
7+
locatorFixtures as fixtures,
8+
} from '../../lib/fixture'
9+
10+
const test = playwright.test.extend<TestingLibraryFixtures>(fixtures)
11+
12+
const {expect} = test
13+
14+
test.use({testIdAttribute: 'data-new-id'})
15+
16+
test.describe('global configuration', () => {
17+
test.beforeEach(async ({page}) => {
18+
await page.goto(`file://${path.join(__dirname, '../fixtures/page.html')}`)
19+
})
20+
21+
test('queries with test ID configured in module scope', async ({queries}) => {
22+
const defaultTestIdLocator = queries.queryByTestId('testid-text-input')
23+
const customTestIdLocator = queries.queryByTestId('first-level-header')
24+
25+
await expect(defaultTestIdLocator).not.toBeVisible()
26+
await expect(customTestIdLocator).toBeVisible()
27+
})
28+
29+
test.describe('overridding global configuration', () => {
30+
test.use({testIdAttribute: 'data-id'})
31+
32+
test('overrides test ID configured in module scope', async ({queries}) => {
33+
const globalTestIdLocator = queries.queryByTestId('first-level-header')
34+
const overriddenTestIdLocator = queries.queryByTestId('second-level-header')
35+
36+
await expect(globalTestIdLocator).not.toBeVisible()
37+
await expect(overriddenTestIdLocator).toBeVisible()
38+
})
39+
})
40+
41+
test("page override doesn't modify global configuration", async ({queries}) => {
42+
const defaultTestIdLocator = queries.queryByTestId('testid-text-input')
43+
const customTestIdLocator = queries.queryByTestId('first-level-header')
44+
45+
await expect(defaultTestIdLocator).not.toBeVisible()
46+
await expect(customTestIdLocator).toBeVisible()
47+
})
48+
})

test/fixture/element-handles.test.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,6 @@ test.describe('lib/fixture.ts', () => {
135135
})
136136

137137
test('does not throw when querying for a specific element', async ({queries: {getByRole}}) => {
138-
expect.assertions(1)
139-
140138
await expect(getByRole('heading', {level: 3})).resolves.not.toThrow()
141139
})
142140
})

test/fixture/locators.test.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,6 @@ test.describe('lib/fixture.ts (locators)', () => {
129129
})
130130

131131
test('does not throw when querying for a specific element', async ({queries: {getByRole}}) => {
132-
expect.assertions(1)
133-
134132
await expect(getByRole('heading', {level: 3}).textContent()).resolves.not.toThrow()
135133
})
136134
})
@@ -147,6 +145,27 @@ test.describe('lib/fixture.ts (locators)', () => {
147145
expect(await innerLocator.count()).toBe(1)
148146
})
149147

150-
// TODO: configuration
148+
test.describe('configuration', () => {
149+
test.describe('custom data-testeid', () => {
150+
test.use({testIdAttribute: 'data-id'})
151+
152+
test('supports custom data-testid attribute name', async ({queries}) => {
153+
const locator = queries.getByTestId('second-level-header')
154+
155+
expect(await locator.textContent()).toEqual('Hello h2')
156+
})
157+
})
158+
159+
test.describe('nested configuration', () => {
160+
test.use({testIdAttribute: 'data-new-id'})
161+
162+
test('supports nested data-testid attribute names', async ({queries}) => {
163+
const locator = queries.getByTestId('first-level-header')
164+
165+
expect(await locator.textContent()).toEqual('Hello h1')
166+
})
167+
})
168+
})
169+
151170
// TDOO: deferred page (do we need some alternative to `findBy*`?)
152171
})

0 commit comments

Comments
 (0)