Skip to content

Commit

Permalink
fix(fixture): improve error message for invalid function TextMatch
Browse files Browse the repository at this point in the history
  • Loading branch information
jrolfs committed Sep 24, 2022
1 parent 9995cc8 commit aa54dd4
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 51 deletions.
15 changes: 12 additions & 3 deletions lib/fixture/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
class TestingLibraryDeserializedFunction extends Function {
original: string

constructor(fn: string) {
super(`return (${fn}).apply(this, arguments)`)

this.original = fn
}
}

const replacer = (_: string, value: unknown) => {
if (value instanceof RegExp) return `__REGEXP ${value.toString()}`
if (typeof value === 'function') return `__FUNCTION ${value.toString()}`
Expand All @@ -13,11 +23,10 @@ const reviver = (_: string, value: string) => {
}

if (value.toString().includes('__FUNCTION ')) {
// eslint-disable-next-line @typescript-eslint/no-implied-eval
return new Function(`return (${value.split('__FUNCTION ')[1]}).apply(this, arguments)`)
return new TestingLibraryDeserializedFunction(value.split('__FUNCTION ')[1])
}

return value
}

export {replacer, reviver}
export {TestingLibraryDeserializedFunction, replacer, reviver}
92 changes: 63 additions & 29 deletions lib/fixture/locator/fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {Locator, PlaywrightTestArgs, TestFixture} from '@playwright/test'
import {Page, selectors} from '@playwright/test'

import type {TestingLibraryDeserializedFunction as DeserializedFunction} from '../helpers'
import type {
Config,
LocatorQueries as Queries,
Expand Down Expand Up @@ -52,38 +53,71 @@ const withinFixture: TestFixture<Within, TestArguments> = async (
: (queriesFor(root, {asyncUtilExpectedState, asyncUtilTimeout}) as WithinReturn<Root>),
)

declare const queryName: SynchronousQuery

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

if (isAllQuery(queryName))
throw new Error(
`PlaywrightTestingLibrary: the plural '${queryName}' was used to create this Locator`,
declare const queryName: SynchronousQuery
declare class TestingLibraryDeserializedFunction extends DeserializedFunction {}

const engine: () => SelectorEngine = () => {
const getError = (error: unknown, matcher: SynchronousQueryParameters[0]) => {
if (typeof matcher === 'function' && error instanceof ReferenceError) {
return new ReferenceError(
[
error.message,
'\n鈿狅笍 A ReferenceError was thrown when using a function TextMatch, did you reference external scope in your matcher function?',
'\nProvided matcher function:',
matcher instanceof TestingLibraryDeserializedFunction
? matcher.original
: matcher.toString(),
'\n',
].join('\n'),
)
}

// @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]
},
})
return error
}

return {
query(root, selector) {
const args = JSON.parse(
selector,
window.__testingLibraryReviver,
) as unknown as SynchronousQueryParameters

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

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

return result
} catch (error) {
throw getError(error, args[0])
}
},
queryAll(root, selector) {
const testingLibrary = window.TestingLibraryDom
const args = JSON.parse(
selector,
window.__testingLibraryReviver,
) as unknown as SynchronousQueryParameters

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

if (!result) return []

return Array.isArray(result) ? result : [result]
} catch (error) {
throw getError(error, args[0])
}
},
}
}

const registerSelectorsFixture: [
TestFixture<void, PlaywrightTestArgs>,
Expand Down
5 changes: 3 additions & 2 deletions lib/fixture/locator/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {promises as fs} from 'fs'

import {configureTestingLibraryScript} from '../../common'
import {reviver} from '../helpers'
import {TestingLibraryDeserializedFunction, reviver} from '../helpers'
import type {Config, Selector, SynchronousQuery} from '../types'

const queryToSelector = (query: SynchronousQuery) =>
Expand All @@ -17,8 +17,9 @@ const buildTestingLibraryScript = async ({config}: {config: Config}) => {

return `
${configuredTestingLibraryDom}
window.__testingLibraryReviver = ${reviver.toString()};
${TestingLibraryDeserializedFunction.toString()};
`
}

Expand Down
67 changes: 50 additions & 17 deletions test/fixture/locators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,26 +40,59 @@ test.describe('lib/fixture.ts (locators)', () => {
expect(await locator.textContent()).toEqual('Hello h1')
})

test('supports function style `TextMatch`', async ({screen}) => {
const locator = screen.getByText(
// eslint-disable-next-line prefer-arrow-callback, func-names
function (content, element) {
return content.startsWith('Hello') && element?.tagName.toLowerCase() === 'h3'
},
)
test.describe('function `TextMatch` argument', () => {
test('supports function style `TextMatch`', async ({screen}) => {
const locator = screen.getByText(
// eslint-disable-next-line prefer-arrow-callback, func-names
function (content, element) {
return content.startsWith('Hello') && element?.tagName.toLowerCase() === 'h3'
},
)

expect(locator).toBeTruthy()
expect(await locator.textContent()).toEqual('Hello h3')
})

expect(locator).toBeTruthy()
expect(await locator.textContent()).toEqual('Hello h3')
})
test('supports arrow function style `TextMatch`', async ({screen}) => {
const locator = screen.getByText(
(content, element) =>
content.startsWith('Hello') && element?.tagName.toLowerCase() === 'h3',
)

test('supports arrow function style `TextMatch`', async ({screen}) => {
const locator = screen.getByText(
(content, element) =>
content.startsWith('Hello') && element?.tagName.toLowerCase() === 'h3',
)
expect(locator).toBeTruthy()
expect(await locator.textContent()).toEqual('Hello h3')
})

expect(locator).toBeTruthy()
expect(await locator.textContent()).toEqual('Hello h3')
test('allows local function references', async ({screen}) => {
const locator = screen.getByText((content, element) => {
const isMatch = (c: string, e: Element) =>
c.startsWith('Hello') && e.tagName.toLowerCase() === 'h3'

return element ? isMatch(content, element) : false
})

expect(locator).toBeTruthy()
expect(await locator.textContent()).toEqual('Hello h3')
})

test('fails with helpful warning when function references closure scope', async ({
screen,
}) => {
const isMatch = (c: string, e: Element) =>
c.startsWith('Hello') && e.tagName.toLowerCase() === 'h3'

const locator = screen.getByText((content, element) =>
element ? isMatch(content, element) : false,
)

await expect(async () => locator.textContent()).rejects.toThrowError(
expect.objectContaining({
message: expect.stringContaining(
'A ReferenceError was thrown when using a function TextMatch',
),
}),
)
})
})

test('should handle the get* methods', async ({queries: {getByTestId}}) => {
Expand Down

0 comments on commit aa54dd4

Please sign in to comment.