diff --git a/lib/common.ts b/lib/common.ts index feec307..09bcf7c 100644 --- a/lib/common.ts +++ b/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 + +export const configureTestingLibraryScript = ( + script: string, + {testIdAttribute, asyncUtilTimeout}: Partial, +) => { + 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 = [ 'queryByPlaceholderText', diff --git a/lib/fixture/index.ts b/lib/fixture/index.ts index 6e2e9af..21c8846 100644 --- a/lib/fixture/index.ts +++ b/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' @@ -15,24 +18,24 @@ const locatorFixtures: Fixtures = { queries: locatorQueriesFixture, registerSelectors: registerSelectorsFixture, installTestingLibrary: installTestingLibraryFixture, + ...options, } interface ElementHandleFixtures { queries: ElementHandleQueries } -interface LocatorFixtures { +interface LocatorFixtures extends Partial { 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 '..' diff --git a/lib/fixture/locator.ts b/lib/fixture/locator/fixtures.ts similarity index 56% rename from lib/fixture/locator.ts rename to lib/fixture/locator/fixtures.ts index 7c6f596..10095f6 100644 --- a/lib/fixture/locator.ts +++ b/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.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 = async ({page}, use) => { - const queries = queryNames.reduce( +const queriesFor = (pageOrLocator: Page | Locator) => + queryNames.reduce( (rest, query) => ({ ...rest, [query]: (...args: Parameters) => - page.locator(`${queryToSelector(query)}=${JSON.stringify(args, replacer)}`), + pageOrLocator.locator(`${queryToSelector(query)}=${JSON.stringify(args, replacer)}`), }), {} as Queries, ) - await use(queries) -} +const queriesFixture: TestFixture = async ({page}, use) => + use(queriesFor(page)) -const within = (locator: Locator): Queries => - queryNames.reduce( - (rest, query) => ({ - ...rest, - [query]: (...args: Parameters) => - locator.locator(`${queryToSelector(query)}=${JSON.stringify(args, replacer)}`), - }), - {} as Queries, - ) +const within = (locator: Locator): Queries => queriesFor(locator) declare const queryName: SupportedQuery @@ -108,25 +89,18 @@ const registerSelectorsFixture: [ ] const installTestingLibraryFixture: [ - TestFixture, + TestFixture, {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} diff --git a/lib/fixture/locator/helpers.ts b/lib/fixture/locator/helpers.ts new file mode 100644 index 0000000..a4829c9 --- /dev/null +++ b/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.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} diff --git a/lib/fixture/locator/index.ts b/lib/fixture/locator/index.ts new file mode 100644 index 0000000..f2ad787 --- /dev/null +++ b/lib/fixture/locator/index.ts @@ -0,0 +1,8 @@ +export { + installTestingLibraryFixture, + options, + queriesFixture, + registerSelectorsFixture, + within, +} from './fixtures' +export type {Queries} from './fixtures' diff --git a/lib/fixture/types.ts b/lib/fixture/types.ts index 29e90ed..d8f9ef5 100644 --- a/lib/fixture/types.ts +++ b/lib/fixture/types.ts @@ -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' /** @@ -37,6 +39,7 @@ type KebabCase = S extends `${infer C}${infer T}` : S export type LocatorQueries = StripNever<{[K in keyof Queries]: ConvertQuery}> +export type Within = (locator: Locator) => LocatorQueries export type Query = keyof Queries @@ -46,6 +49,15 @@ export type SupportedQuery = Exclude export type Selector = KebabCase +export type {Config} +export interface ConfigFn { + (existingConfig: Config): Partial +} + +export type ConfigDelta = ConfigFn | Partial +export type Configure = (configDelta: ConfigDelta) => void +export type ConfigureLocator = (configDelta: ConfigDelta) => Config + declare global { interface Window { TestingLibraryDom: typeof TestingLibraryDom diff --git a/lib/index.ts b/lib/index.ts index bbd2725..7640d62 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -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'), @@ -176,26 +176,15 @@ export function wait( export const waitFor = wait -export function configure(options: Partial): void { - if (!options) { +export function configure(config: Partial): 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( diff --git a/lib/typedefs.ts b/lib/typedefs.ts index 1a9853c..89a29cd 100644 --- a/lib/typedefs.ts +++ b/lib/typedefs.ts @@ -1,7 +1,6 @@ import { Matcher, ByRoleOptions as TestingLibraryByRoleOptions, - Config as TestingLibraryConfig, MatcherOptions as TestingLibraryMatcherOptions, SelectorMatcherOptions as TestingLibrarySelectorMatcherOptions, waitForOptions, @@ -189,8 +188,3 @@ export interface Queries extends QueryMethods { getQueriesForElement(): ScopedQueries getNodeText(el: Element): Promise } - -export type ConfigurationOptions = Pick< - TestingLibraryConfig, - 'testIdAttribute' | 'asyncUtilTimeout' -> diff --git a/test/fixture/configure.test.ts b/test/fixture/configure.test.ts new file mode 100644 index 0000000..dba69eb --- /dev/null +++ b/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(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() + }) +}) diff --git a/test/fixture/element-handles.test.ts b/test/fixture/element-handles.test.ts index 0cee28f..f97261a 100644 --- a/test/fixture/element-handles.test.ts +++ b/test/fixture/element-handles.test.ts @@ -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() }) }) diff --git a/test/fixture/locators.test.ts b/test/fixture/locators.test.ts index 8669e33..7dc56ea 100644 --- a/test/fixture/locators.test.ts +++ b/test/fixture/locators.test.ts @@ -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() }) }) @@ -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*`?) })