Skip to content

Commit

Permalink
feat: support configure API with locator fixture via test.use
Browse files Browse the repository at this point in the history
### 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}}) => {
    // ...
  });
});
```
  • Loading branch information
jrolfs committed Sep 18, 2022
1 parent b9c1937 commit ed66c3f
Show file tree
Hide file tree
Showing 11 changed files with 173 additions and 81 deletions.
20 changes: 19 additions & 1 deletion lib/common.ts
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
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
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
@@ -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'
>
48 changes: 48 additions & 0 deletions test/fixture/configure.test.ts
@@ -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
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
25 changes: 22 additions & 3 deletions test/fixture/locators.test.ts
Expand Up @@ -129,8 +129,6 @@ test.describe('lib/fixture.ts (locators)', () => {
})

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

await expect(getByRole('heading', {level: 3}).textContent()).resolves.not.toThrow()
})
})
Expand All @@ -147,6 +145,27 @@ test.describe('lib/fixture.ts (locators)', () => {
expect(await innerLocator.count()).toBe(1)
})

// TODO: configuration
test.describe('configuration', () => {
test.describe('custom data-testeid', () => {
test.use({testIdAttribute: 'data-id'})

test('supports custom data-testid attribute name', async ({queries}) => {
const locator = queries.getByTestId('second-level-header')

expect(await locator.textContent()).toEqual('Hello h2')
})
})

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

test('supports nested data-testid attribute names', async ({queries}) => {
const locator = queries.getByTestId('first-level-header')

expect(await locator.textContent()).toEqual('Hello h1')
})
})
})

// TDOO: deferred page (do we need some alternative to `findBy*`?)
})

0 comments on commit ed66c3f

Please sign in to comment.