Skip to content

Commit aa54dd4

Browse files
committed
fix(fixture): improve error message for invalid function TextMatch
1 parent 9995cc8 commit aa54dd4

File tree

4 files changed

+128
-51
lines changed

4 files changed

+128
-51
lines changed

lib/fixture/helpers.ts

+12-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
class TestingLibraryDeserializedFunction extends Function {
2+
original: string
3+
4+
constructor(fn: string) {
5+
super(`return (${fn}).apply(this, arguments)`)
6+
7+
this.original = fn
8+
}
9+
}
10+
111
const replacer = (_: string, value: unknown) => {
212
if (value instanceof RegExp) return `__REGEXP ${value.toString()}`
313
if (typeof value === 'function') return `__FUNCTION ${value.toString()}`
@@ -13,11 +23,10 @@ const reviver = (_: string, value: string) => {
1323
}
1424

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

2029
return value
2130
}
2231

23-
export {replacer, reviver}
32+
export {TestingLibraryDeserializedFunction, replacer, reviver}

lib/fixture/locator/fixtures.ts

+63-29
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type {Locator, PlaywrightTestArgs, TestFixture} from '@playwright/test'
22
import {Page, selectors} from '@playwright/test'
33

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

55-
declare const queryName: SynchronousQuery
56-
57-
const engine: () => SelectorEngine = () => ({
58-
query(root, selector) {
59-
const args = JSON.parse(selector, window.__testingLibraryReviver) as unknown as Parameters<
60-
Queries[typeof queryName]
61-
>
56+
type SynchronousQueryParameters = Parameters<Queries[SynchronousQuery]>
6257

63-
if (isAllQuery(queryName))
64-
throw new Error(
65-
`PlaywrightTestingLibrary: the plural '${queryName}' was used to create this Locator`,
58+
declare const queryName: SynchronousQuery
59+
declare class TestingLibraryDeserializedFunction extends DeserializedFunction {}
60+
61+
const engine: () => SelectorEngine = () => {
62+
const getError = (error: unknown, matcher: SynchronousQueryParameters[0]) => {
63+
if (typeof matcher === 'function' && error instanceof ReferenceError) {
64+
return new ReferenceError(
65+
[
66+
error.message,
67+
'\n⚠️ A ReferenceError was thrown when using a function TextMatch, did you reference external scope in your matcher function?',
68+
'\nProvided matcher function:',
69+
matcher instanceof TestingLibraryDeserializedFunction
70+
? matcher.original
71+
: matcher.toString(),
72+
'\n',
73+
].join('\n'),
6674
)
75+
}
6776

68-
// @ts-expect-error
69-
const result = window.TestingLibraryDom[queryName](root, ...args)
70-
71-
return result
72-
},
73-
queryAll(root, selector) {
74-
const testingLibrary = window.TestingLibraryDom
75-
const args = JSON.parse(selector, window.__testingLibraryReviver) as unknown as Parameters<
76-
Queries[typeof queryName]
77-
>
78-
79-
// @ts-expect-error
80-
const result = testingLibrary[queryName](root, ...args)
81-
82-
if (!result) return []
83-
84-
return Array.isArray(result) ? result : [result]
85-
},
86-
})
77+
return error
78+
}
79+
80+
return {
81+
query(root, selector) {
82+
const args = JSON.parse(
83+
selector,
84+
window.__testingLibraryReviver,
85+
) as unknown as SynchronousQueryParameters
86+
87+
if (isAllQuery(queryName))
88+
throw new Error(
89+
`PlaywrightTestingLibrary: the plural '${queryName}' was used to create this Locator`,
90+
)
91+
92+
try {
93+
// @ts-expect-error
94+
const result = window.TestingLibraryDom[queryName](root, ...args)
95+
96+
return result
97+
} catch (error) {
98+
throw getError(error, args[0])
99+
}
100+
},
101+
queryAll(root, selector) {
102+
const testingLibrary = window.TestingLibraryDom
103+
const args = JSON.parse(
104+
selector,
105+
window.__testingLibraryReviver,
106+
) as unknown as SynchronousQueryParameters
107+
108+
try {
109+
// @ts-expect-error
110+
const result = testingLibrary[queryName](root, ...args)
111+
112+
if (!result) return []
113+
114+
return Array.isArray(result) ? result : [result]
115+
} catch (error) {
116+
throw getError(error, args[0])
117+
}
118+
},
119+
}
120+
}
87121

88122
const registerSelectorsFixture: [
89123
TestFixture<void, PlaywrightTestArgs>,

lib/fixture/locator/helpers.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {promises as fs} from 'fs'
22

33
import {configureTestingLibraryScript} from '../../common'
4-
import {reviver} from '../helpers'
4+
import {TestingLibraryDeserializedFunction, reviver} from '../helpers'
55
import type {Config, Selector, SynchronousQuery} from '../types'
66

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

1818
return `
1919
${configuredTestingLibraryDom}
20-
20+
2121
window.__testingLibraryReviver = ${reviver.toString()};
22+
${TestingLibraryDeserializedFunction.toString()};
2223
`
2324
}
2425

test/fixture/locators.test.ts

+50-17
Original file line numberDiff line numberDiff line change
@@ -40,26 +40,59 @@ test.describe('lib/fixture.ts (locators)', () => {
4040
expect(await locator.textContent()).toEqual('Hello h1')
4141
})
4242

43-
test('supports function style `TextMatch`', async ({screen}) => {
44-
const locator = screen.getByText(
45-
// eslint-disable-next-line prefer-arrow-callback, func-names
46-
function (content, element) {
47-
return content.startsWith('Hello') && element?.tagName.toLowerCase() === 'h3'
48-
},
49-
)
43+
test.describe('function `TextMatch` argument', () => {
44+
test('supports function style `TextMatch`', async ({screen}) => {
45+
const locator = screen.getByText(
46+
// eslint-disable-next-line prefer-arrow-callback, func-names
47+
function (content, element) {
48+
return content.startsWith('Hello') && element?.tagName.toLowerCase() === 'h3'
49+
},
50+
)
51+
52+
expect(locator).toBeTruthy()
53+
expect(await locator.textContent()).toEqual('Hello h3')
54+
})
5055

51-
expect(locator).toBeTruthy()
52-
expect(await locator.textContent()).toEqual('Hello h3')
53-
})
56+
test('supports arrow function style `TextMatch`', async ({screen}) => {
57+
const locator = screen.getByText(
58+
(content, element) =>
59+
content.startsWith('Hello') && element?.tagName.toLowerCase() === 'h3',
60+
)
5461

55-
test('supports arrow function style `TextMatch`', async ({screen}) => {
56-
const locator = screen.getByText(
57-
(content, element) =>
58-
content.startsWith('Hello') && element?.tagName.toLowerCase() === 'h3',
59-
)
62+
expect(locator).toBeTruthy()
63+
expect(await locator.textContent()).toEqual('Hello h3')
64+
})
6065

61-
expect(locator).toBeTruthy()
62-
expect(await locator.textContent()).toEqual('Hello h3')
66+
test('allows local function references', async ({screen}) => {
67+
const locator = screen.getByText((content, element) => {
68+
const isMatch = (c: string, e: Element) =>
69+
c.startsWith('Hello') && e.tagName.toLowerCase() === 'h3'
70+
71+
return element ? isMatch(content, element) : false
72+
})
73+
74+
expect(locator).toBeTruthy()
75+
expect(await locator.textContent()).toEqual('Hello h3')
76+
})
77+
78+
test('fails with helpful warning when function references closure scope', async ({
79+
screen,
80+
}) => {
81+
const isMatch = (c: string, e: Element) =>
82+
c.startsWith('Hello') && e.tagName.toLowerCase() === 'h3'
83+
84+
const locator = screen.getByText((content, element) =>
85+
element ? isMatch(content, element) : false,
86+
)
87+
88+
await expect(async () => locator.textContent()).rejects.toThrowError(
89+
expect.objectContaining({
90+
message: expect.stringContaining(
91+
'A ReferenceError was thrown when using a function TextMatch',
92+
),
93+
}),
94+
)
95+
})
6396
})
6497

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

0 commit comments

Comments
 (0)