/
pptr-testing-library.ts
207 lines (188 loc) · 6.91 KB
/
pptr-testing-library.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
import { port } from './vite-server';
import type { queries, BoundFunctions } from '@testing-library/dom';
import { jsHandleToArray, removeFuncFromStackTrace } from './utils';
import type { JSHandle } from 'puppeteer';
type ElementToElementHandle<Input> = Input extends Element
? import('puppeteer').ElementHandle
: Input extends Element[]
? import('puppeteer').ElementHandle[]
: Input;
type Promisify<Input> = Input extends Promise<any> ? Input : Promise<Input>;
type UpdateReturnType<Fn> = Fn extends (...args: infer Args) => infer ReturnType
? (...args: Args) => Promisify<ElementToElementHandle<ReturnType>>
: never;
type AsyncDTLQueries = {
[K in keyof typeof queries]: UpdateReturnType<typeof queries[K]>;
};
const queryNames = [
'findAllByAltText',
'findAllByDisplayValue',
'findAllByLabelText',
'findAllByPlaceholderText',
'findAllByRole',
'findAllByTestId',
'findAllByText',
'findAllByTitle',
'findByAltText',
'findByDisplayValue',
'findByLabelText',
'findByPlaceholderText',
'findByRole',
'findByTestId',
'findByText',
'findByTitle',
'getAllByAltText',
'getAllByDisplayValue',
'getAllByLabelText',
'getAllByPlaceholderText',
'getAllByRole',
'getAllByTestId',
'getAllByText',
'getAllByTitle',
'getByAltText',
'getByDisplayValue',
'getByLabelText',
'getByPlaceholderText',
'getByRole',
'getByTestId',
'getByText',
'getByTitle',
'queryAllByAltText',
'queryAllByDisplayValue',
'queryAllByLabelText',
'queryAllByPlaceholderText',
'queryAllByRole',
'queryAllByTestId',
'queryAllByText',
'queryAllByTitle',
'queryByAltText',
'queryByDisplayValue',
'queryByLabelText',
'queryByPlaceholderText',
'queryByRole',
'queryByTestId',
'queryByText',
'queryByTitle',
] as const;
interface DTLError {
failed: true;
messageWithElementsRevived: unknown[];
messageWithElementsStringified: string;
}
export type BoundQueries = BoundFunctions<AsyncDTLQueries>;
export const getQueriesForElement = (
page: import('puppeteer').Page,
state: { isTestFinished: boolean },
element?: import('puppeteer').ElementHandle,
) => {
// @ts-expect-error TS doesn't understand the properties coming out of Object.fromEntries
const queries: BoundQueries = Object.fromEntries(
queryNames.map((queryName: typeof queryNames[number]) => {
const query = async (...args: any[]) => {
const serializedArgs = JSON.stringify(args, (_key, value) => {
if (value instanceof RegExp) {
return {
__serialized: 'RegExp',
source: value.source,
flags: value.flags,
};
}
return value;
});
const forgotAwait = removeFuncFromStackTrace(
new Error(
`Cannot execute query ${queryName} after test finishes. Did you forget to await?`,
),
query,
);
/** Handle error case for Target Closed error (forgot to await) */
const handleExecutionAfterTestFinished = (error: any) => {
if (/target closed/i.test(error.message) && state.isTestFinished) {
throw forgotAwait;
}
throw error;
};
const result: JSHandle<Element | Element[] | DTLError | null> =
await page
.evaluateHandle(
// Using new Function to avoid babel transpiling the import
// @ts-expect-error pptr's types don't like new Function
new Function(
'argsString',
'element',
`return import("http://localhost:${port}/@test-mule/dom-testing-library")
.then(async ({ reviveElementsInString, printElement, addToElementCache, ...dtl }) => {
const deserializedArgs = JSON.parse(argsString, (key, value) => {
if (value.__serialized === 'RegExp')
return new RegExp(value.source, value.flags)
return value
})
try {
return await dtl.${queryName}(element, ...deserializedArgs)
} catch (error) {
const message =
error.message +
(error.container
? '\\n\\nWithin: ' + addToElementCache(error.container)
: '')
const messageWithElementsRevived = reviveElementsInString(message)
const messageWithElementsStringified = messageWithElementsRevived
.map(el => {
if (el instanceof Element || el instanceof Document)
return printElement(el)
return el
})
.join('')
return { failed: true, messageWithElementsRevived, messageWithElementsStringified }
}
})`,
),
serializedArgs,
element?.asElement() ||
(await page
.evaluateHandle(() => document)
.catch(handleExecutionAfterTestFinished)),
)
.catch(handleExecutionAfterTestFinished);
const failed = await result.evaluate(
(r) => typeof r === 'object' && r !== null && (r as DTLError).failed,
);
if (failed) {
const resultProperties = Object.fromEntries(
await result.getProperties(),
);
const messageWithElementsStringified =
(await resultProperties.messageWithElementsStringified.jsonValue()) as any;
const messageWithElementsRevived = await jsHandleToArray(
resultProperties.messageWithElementsRevived,
);
const error = new Error(messageWithElementsStringified);
// @ts-expect-error messageForBrowser is a custom property that we add to Errors
error.messageForBrowser = messageWithElementsRevived;
// Manipulate the stack trace and remove this function
// That way Jest will show a code frame from the user's code, not ours
// https://kentcdodds.com/blog/improve-test-error-messages-of-your-abstractions
Error.captureStackTrace(error, query);
throw error;
}
// If it returns a JSHandle<Array>, make it into an array of JSHandles so that using [0] for getAllBy* queries works
if (await result.evaluate((r) => Array.isArray(r))) {
const array = Array.from({
length: await result.evaluate((r) => (r as Element[]).length),
});
const props = await result.getProperties();
for (const [key, value] of props.entries()) {
array[key as any as number] = value;
}
return array;
}
// If it is an element, return it
if (result.asElement() !== null) return result;
// Try to JSON-ify it (for example if it is null from queryBy*)
return result.jsonValue();
};
return [queryName, query];
}),
);
return queries;
};