Skip to content

Commit

Permalink
feat(fixture): add support for find* queries in locator fixture
Browse files Browse the repository at this point in the history
  • Loading branch information
jrolfs committed Sep 18, 2022
1 parent f1e09ca commit 1b888cf
Show file tree
Hide file tree
Showing 7 changed files with 236 additions and 25 deletions.
10 changes: 6 additions & 4 deletions lib/fixture/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
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'
Expand All @@ -10,12 +8,15 @@ import {
queriesFixture as locatorQueriesFixture,
options,
registerSelectorsFixture,
within,
withinFixture,
} from './locator'
import type {Config} from './types'
import {Within} from './types'

const elementHandleFixtures: Fixtures = {queries: elementHandleQueriesFixture}
const locatorFixtures: Fixtures = {
queries: locatorQueriesFixture,
within: withinFixture,
registerSelectors: registerSelectorsFixture,
installTestingLibrary: installTestingLibraryFixture,
...options,
Expand All @@ -27,6 +28,7 @@ interface ElementHandleFixtures {

interface LocatorFixtures extends Partial<Config> {
queries: LocatorQueries
within: Within
registerSelectors: void
installTestingLibrary: void
}
Expand All @@ -38,4 +40,4 @@ export {elementHandleQueriesFixture as fixture}
export {elementHandleFixtures as fixtures}
export type {LocatorFixtures}
export {locatorQueriesFixture}
export {locatorFixtures, within}
export {locatorFixtures}
46 changes: 37 additions & 9 deletions lib/fixture/locator/fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import type {Locator, PlaywrightTestArgs, TestFixture} from '@playwright/test'
import {selectors} from '@playwright/test'

import type {Config, LocatorQueries as Queries, SelectorEngine, SynchronousQuery} from '../types'
import type {
Config,
LocatorQueries as Queries,
SelectorEngine,
SynchronousQuery,
Within,
} from '../types'

import {
buildTestingLibraryScript,
Expand All @@ -11,16 +17,30 @@ import {
synchronousQueryNames,
} from './helpers'

const defaultConfig: Config = {testIdAttribute: 'data-testid', asyncUtilTimeout: 1000}
type TestArguments = PlaywrightTestArgs & Config

const defaultConfig: Config = {
asyncUtilExpectedState: 'visible',
asyncUtilTimeout: 1000,
testIdAttribute: 'data-testid',
}

const options = Object.fromEntries(
Object.entries(defaultConfig).map(([key, value]) => [key, [value, {option: true}] as const]),
)

const queriesFixture: TestFixture<Queries, PlaywrightTestArgs> = async ({page}, use) =>
use(queriesFor(page))
const queriesFixture: TestFixture<Queries, TestArguments> = async (
{page, asyncUtilExpectedState, asyncUtilTimeout},
use,
) => use(queriesFor(page, {asyncUtilExpectedState, asyncUtilTimeout}))

const within = (locator: Locator): Queries => queriesFor(locator)
const withinFixture: TestFixture<Within, TestArguments> = async (
{asyncUtilExpectedState, asyncUtilTimeout},
use,
) =>
use(
(locator: Locator): Queries => queriesFor(locator, {asyncUtilExpectedState, asyncUtilTimeout}),
)

declare const queryName: SynchronousQuery

Expand Down Expand Up @@ -82,18 +102,26 @@ const registerSelectorsFixture: [
]

const installTestingLibraryFixture: [
TestFixture<void, PlaywrightTestArgs & Config>,
TestFixture<void, TestArguments>,
{scope: 'test'; auto?: boolean},
] = [
async ({context, asyncUtilTimeout, testIdAttribute}, use) => {
async ({context, asyncUtilExpectedState, asyncUtilTimeout, testIdAttribute}, use) => {
await context.addInitScript(
await buildTestingLibraryScript({config: {asyncUtilTimeout, testIdAttribute}}),
await buildTestingLibraryScript({
config: {asyncUtilExpectedState, asyncUtilTimeout, testIdAttribute},
}),
)

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

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

import type {Locator, Page} from '@playwright/test'
import {errors} from '@playwright/test'
import {queries} from '@testing-library/dom'

import {configureTestingLibraryScript} from '../../common'
Expand All @@ -9,18 +10,25 @@ import type {
AllQuery,
Config,
FindQuery,
GetQuery,
LocatorQueries as Queries,
Query,
QueryQuery,
Selector,
SynchronousQuery,
} from '../types'

const allQueryNames = Object.keys(queries) as Query[]

const isAllQuery = (query: Query): query is AllQuery => query.includes('All')

const isFindQuery = (query: Query): query is FindQuery => query.startsWith('find')
const isNotFindQuery = (query: Query): query is Exclude<Query, FindQuery> =>
!query.startsWith('find')

const findQueryToGetQuery = (query: FindQuery) => query.replace(/^find/, 'get') as GetQuery
const findQueryToQueryQuery = (query: FindQuery) => query.replace(/^find/, 'query') as QueryQuery

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

Expand All @@ -41,12 +49,57 @@ const buildTestingLibraryScript = async ({config}: {config: Config}) => {

const synchronousQueryNames = allQueryNames.filter(isNotFindQuery)

const queriesFor = (pageOrLocator: Page | Locator) =>
synchronousQueryNames.reduce(
const createFindQuery =
(
pageOrLocator: Page | Locator,
query: FindQuery,
{asyncUtilTimeout, asyncUtilExpectedState}: Partial<Config> = {},
) =>
async (...[id, options, waitForElementOptions]: Parameters<Queries[FindQuery]>) => {
const synchronousOptions = ([id, options] as const).filter(Boolean)

const locator = pageOrLocator.locator(
`${queryToSelector(findQueryToQueryQuery(query))}=${JSON.stringify(
synchronousOptions,
replacer,
)}`,
)

const {state = asyncUtilExpectedState, timeout = asyncUtilTimeout} = waitForElementOptions ?? {}

try {
await locator.first().waitFor({state, timeout})
} catch (error) {
// In the case of a `waitFor` timeout from Playwright, we want to
// surface the appropriate error from Testing Library, so run the
// query one more time as `get*` knowing that it will fail with the
// error that we want the user to see instead of the `TimeoutError`
if (error instanceof errors.TimeoutError) {
return pageOrLocator
.locator(
`${queryToSelector(findQueryToGetQuery(query))}=${JSON.stringify(
synchronousOptions,
replacer,
)}`,
)
.first()
.waitFor({state, timeout: 100})
}

throw error
}

return locator
}

const queriesFor = (pageOrLocator: Page | Locator, config?: Partial<Config>) =>
allQueryNames.reduce(
(rest, query) => ({
...rest,
[query]: (...args: Parameters<Queries[keyof Queries]>) =>
pageOrLocator.locator(`${queryToSelector(query)}=${JSON.stringify(args, replacer)}`),
[query]: isFindQuery(query)
? createFindQuery(pageOrLocator, query, config)
: (...args: Parameters<Queries[SynchronousQuery]>) =>
pageOrLocator.locator(`${queryToSelector(query)}=${JSON.stringify(args, replacer)}`),
}),
{} as Queries,
)
Expand Down
2 changes: 1 addition & 1 deletion lib/fixture/locator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ export {
options,
queriesFixture,
registerSelectorsFixture,
within,
withinFixture,
} from './fixtures'
export type {Queries} from './fixtures'
23 changes: 19 additions & 4 deletions lib/fixture/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {Locator} from '@playwright/test'
import type * as TestingLibraryDom from '@testing-library/dom'
import {queries} from '@testing-library/dom'

import {Config} from '../common'
import type {Config as CommonConfig} from '../common'

import {reviver} from './helpers'

Expand All @@ -23,13 +23,25 @@ export type SelectorEngine = {
}

type Queries = typeof queries
type WaitForState = Exclude<Parameters<Locator['waitFor']>[0], undefined>['state']
type AsyncUtilExpectedState = Extract<WaitForState, 'visible' | 'attached'>

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
: Query extends (
el: HTMLElement,
id: infer Id,
options: infer Options,
waitForOptions: infer WaitForOptions,
) => Promise<any>
? (
id: Id,
options?: Options,
waitForOptions?: WaitForOptions & {state?: AsyncUtilExpectedState},
) => Promise<Locator>
: never

type KebabCase<S> = S extends `${infer C}${infer T}`
Expand All @@ -38,19 +50,22 @@ type KebabCase<S> = S extends `${infer C}${infer T}`
: `${Uncapitalize<C>}-${KebabCase<T>}`
: S

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

export type Query = keyof Queries

export type AllQuery = Extract<Query, `${string}All${string}`>
export type FindQuery = Extract<Query, `find${string}`>
export type GetQuery = Extract<Query, `get${string}`>
export type QueryQuery = Extract<Query, `query${string}`>
export type SynchronousQuery = Exclude<Query, FindQuery>

export type Selector = KebabCase<SynchronousQuery>

export type {Config}
export interface Config extends CommonConfig {
asyncUtilExpectedState: AsyncUtilExpectedState
}
export interface ConfigFn {
(existingConfig: Config): Partial<Config>
}
Expand Down
Loading

0 comments on commit 1b888cf

Please sign in to comment.