Skip to content

Commit

Permalink
feat(fixture): add locatorFixtures that provide Locator-based que…
Browse files Browse the repository at this point in the history
…ries

This will likely replace the fixtures that provided `ElementHandle`-based
queries in a future major release, but for now the `Locator` queries are
exported as `locatorFixtures`:

```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;

test('my form', async ({queries: {getByTestId}}) => {
  // Queries now return `Locator`
  const formLocator = getByTestId('my-form');

  // Locator-based `within` support
  const {getByLabelText} = within(formLocator);

  const emailInputLocator = getByLabelText('Email');

  // Interact via `Locator` API 🥳
  await emailInputLocator.fill('email@playwright.dev');

  // Assert via `Locator` APIs 🎉
  await expect(emailInputLocator).toHaveValue('email@playwright.dev');
})
```
  • Loading branch information
jrolfs committed May 2, 2022
1 parent 817981e commit 1e75b4e
Show file tree
Hide file tree
Showing 9 changed files with 409 additions and 6 deletions.
7 changes: 7 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,12 @@ module.exports = {
'jest/no-done-callback': 'off',
},
},
{
files: ['lib/fixture/**/*.+(js|ts)'],
rules: {
'no-empty-pattern': 'off',
'no-underscore-dangle': ['error', {allow: ['__testingLibraryReviver']}],
},
},
],
}
10 changes: 9 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,18 @@ jobs:
npm install @playwright/test@${{ matrix.playwright }}
- name: Check types, run lint + tests
if: ${{ matrix.playwright == 'latest' }}
run: |
npm why playwright
npm why @playwright/test
npm run validate
npm run test
- name: Check types, run lint + tests
if: ${{ matrix.playwright != 'latest' }}
run: |
npm why playwright
npm why @playwright/test
npm run test:legacy
# Only release on Node 14

Expand Down
17 changes: 17 additions & 0 deletions lib/fixture/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const replacer = (_: string, value: unknown) => {
if (value instanceof RegExp) return `__REGEXP ${value.toString()}`

return value
}

const reviver = (_: string, value: string) => {
if (value.toString().includes('__REGEXP ')) {
const match = /\/(.*)\/(.*)?/.exec(value.split('__REGEXP ')[1])

return new RegExp(match![1], match![2] || '')
}

return value
}

export {replacer, reviver}
26 changes: 23 additions & 3 deletions lib/fixture/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,38 @@
import {Fixtures} from '@playwright/test'

import type {Queries as ElementHandleQueries} from './element-handle'
import {queriesFixture as elementHandleQueriesFixture} from './element-handle'
import type {Queries as LocatorQueries} from './locator'
import {
Queries as ElementHandleQueries,
queriesFixture as elementHandleQueriesFixture,
} from './element-handle'
installTestingLibraryFixture,
queriesFixture as locatorQueriesFixture,
registerSelectorsFixture,
within,
} from './locator'

const elementHandleFixtures: Fixtures = {queries: elementHandleQueriesFixture}
const locatorFixtures: Fixtures = {
queries: locatorQueriesFixture,
registerSelectors: registerSelectorsFixture,
installTestingLibrary: installTestingLibraryFixture,
}

interface ElementHandleFixtures {
queries: ElementHandleQueries
}

interface LocatorFixtures {
queries: LocatorQueries
registerSelectors: void
installTestingLibrary: void
}

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

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

export {configure} from '..'
132 changes: 132 additions & 0 deletions lib/fixture/locator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import {promises as fs} from 'fs'

import type {Locator, 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'

const isAllQuery = (query: Query): query is AllQuery => query.includes('All')
const isNotFindQuery = (query: Query): query is Exclude<Query, FindQuery> =>
!query.startsWith('find')

const queryNames = allQueryNames.filter(isNotFindQuery)

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

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

await use(queries)
}

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,
)

declare const queryName: SupportedQuery

const engine: () => SelectorEngine = () => ({
query(root, selector) {
const args = JSON.parse(selector, window.__testingLibraryReviver) as unknown as Parameters<
Queries[typeof queryName]
>

if (isAllQuery(queryName))
throw new Error(
`PlaywrightTestingLibrary: the plural '${queryName}' was used to create this Locator`,
)

// @ts-expect-error
const result = window.TestingLibraryDom[queryName](root, ...args)

return result
},
queryAll(root, selector) {
const testingLibrary = window.TestingLibraryDom
const args = JSON.parse(selector, window.__testingLibraryReviver) as unknown as Parameters<
Queries[typeof queryName]
>

// @ts-expect-error
const result = testingLibrary[queryName](root, ...args)

if (!result) return []

return Array.isArray(result) ? result : [result]
},
})

const registerSelectorsFixture: [
TestFixture<void, PlaywrightTestArgs>,
{scope: 'worker'; auto?: boolean},
] = [
async ({}, use) => {
try {
await Promise.all(
queryNames.map(async name =>
selectors.register(
queryToSelector(name),
`(${engine.toString().replace(/queryName/g, `"${name}"`)})()`,
),
),
)
} catch (error) {
// eslint-disable-next-line no-console
console.error(
'PlaywrightTestingLibrary: failed to register Testing Library functions\n',
error,
)
}
await use()
},
{scope: 'worker', auto: true},
]

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

await context.addInitScript(`
${testingLibraryDomUmdScript}
window.__testingLibraryReviver = ${reviver.toString()};
`)

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

export {queriesFixture, registerSelectorsFixture, installTestingLibraryFixture, within}
export type {Queries}
54 changes: 54 additions & 0 deletions lib/fixture/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {Locator} from '@playwright/test'
import type * as TestingLibraryDom from '@testing-library/dom'
import {queries} from '@testing-library/dom'

import {reviver} from './helpers'

/**
* This type was copied across from Playwright
*
* @see {@link https://github.com/microsoft/playwright/blob/82ff85b106e31ffd7b3702aef260c9c460cfb10c/packages/playwright-core/src/client/types.ts#L108-L117}
*/
export type SelectorEngine = {
/**
* Returns the first element matching given selector in the root's subtree.
*/
query(root: HTMLElement, selector: string): HTMLElement | null
/**
* Returns all elements matching given selector in the root's subtree.
*/
queryAll(root: HTMLElement, selector: string): HTMLElement[]
}

type Queries = typeof queries

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
: never

type KebabCase<S> = S extends `${infer C}${infer T}`
? T extends Uncapitalize<T>
? `${Uncapitalize<C>}${KebabCase<T>}`
: `${Uncapitalize<C>}-${KebabCase<T>}`
: S

export type LocatorQueries = StripNever<{[K in keyof Queries]: ConvertQuery<Queries[K]>}>

export type Query = keyof Queries

export type AllQuery = Extract<Query, `${string}All${string}`>
export type FindQuery = Extract<Query, `find${string}`>
export type SupportedQuery = Exclude<Query, FindQuery>

export type Selector = KebabCase<SupportedQuery>

declare global {
interface Window {
TestingLibraryDom: typeof TestingLibraryDom
__testingLibraryReviver: typeof reviver
}
}
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@
"prepublishOnly": "npm run build",
"start:standalone": "hover-scripts test",
"test": "run-s build:testing-library test:*",
"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",
"test:standalone": "hover-scripts test --no-watch",
"test:types": "tsc --noEmit",
"validate": "run-s test"
"test:types": "tsc --noEmit"
},
"repository": {
"type": "git",
Expand Down
Loading

0 comments on commit 1e75b4e

Please sign in to comment.