@lingxia/test 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/expect.d.ts +26 -0
- package/dist/expect.d.ts.map +1 -0
- package/dist/expect.js +220 -0
- package/dist/expect.js.map +1 -0
- package/dist/fixture.d.ts +96 -0
- package/dist/fixture.d.ts.map +1 -0
- package/dist/fixture.js +703 -0
- package/dist/fixture.js.map +1 -0
- package/dist/format.d.ts +14 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +179 -0
- package/dist/format.js.map +1 -0
- package/dist/host.d.ts +29 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +204 -0
- package/dist/host.js.map +1 -0
- package/dist/ids.d.ts +26 -0
- package/dist/ids.d.ts.map +1 -0
- package/dist/ids.js +92 -0
- package/dist/ids.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/inventory.d.ts +24 -0
- package/dist/inventory.d.ts.map +1 -0
- package/dist/inventory.js +38 -0
- package/dist/inventory.js.map +1 -0
- package/dist/junit.d.ts +8 -0
- package/dist/junit.d.ts.map +1 -0
- package/dist/junit.js +97 -0
- package/dist/junit.js.map +1 -0
- package/dist/locator.d.ts +62 -0
- package/dist/locator.d.ts.map +1 -0
- package/dist/locator.js +124 -0
- package/dist/locator.js.map +1 -0
- package/dist/report.d.ts +15 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +967 -0
- package/dist/report.js.map +1 -0
- package/dist/runtime.d.ts +33 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +487 -0
- package/dist/runtime.js.map +1 -0
- package/dist/spec-api.d.ts +15 -0
- package/dist/spec-api.d.ts.map +1 -0
- package/dist/spec-api.js +2 -0
- package/dist/spec-api.js.map +1 -0
- package/dist/types.d.ts +234 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +21 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +21 -0
- package/dist/version.js.map +1 -0
- package/package.json +56 -0
package/dist/fixture.js
ADDED
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
import { AssertionError, applyMatcher, expect as immediateExpect, popAssertionSilence, pushAssertionSilence, setAssertionSink, } from "./expect.js";
|
|
2
|
+
import { formatValue, truncate } from "./format.js";
|
|
3
|
+
import { encodeAttachPayload, remapStack } from "./host.js";
|
|
4
|
+
import { rememberInline } from "./report.js";
|
|
5
|
+
import { callerLocation, displayLocation, parseFrames } from "./ids.js";
|
|
6
|
+
import { PageLocator, sleep, testIdSelector, } from "./locator.js";
|
|
7
|
+
import { DEFAULT_ACTION_TIMEOUT_MS, DEFAULT_POLL_INTERVAL_MS, DEFAULT_SPEC_TIMEOUT_MS, MAX_ACTIONS, MAX_EVAL_BUDGET_MS, WEDGED_DEFER_BUDGET_MS, } from "./version.js";
|
|
8
|
+
export class TimeoutError extends Error {
|
|
9
|
+
constructor() {
|
|
10
|
+
super(...arguments);
|
|
11
|
+
this.name = "TimeoutError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export class LiveFixture {
|
|
15
|
+
constructor(specId, rawApp, host, args, automation, specBudgetMs = DEFAULT_SPEC_TIMEOUT_MS) {
|
|
16
|
+
this.specId = specId;
|
|
17
|
+
this.host = host;
|
|
18
|
+
this.automation = automation;
|
|
19
|
+
this.specBudgetMs = specBudgetMs;
|
|
20
|
+
this.steps = [];
|
|
21
|
+
this.assertions = [];
|
|
22
|
+
this.attachments = [];
|
|
23
|
+
this.defers = [];
|
|
24
|
+
this.aborted = false;
|
|
25
|
+
this.abortError = null;
|
|
26
|
+
this.actionSilence = 0;
|
|
27
|
+
this.actionCount = 0;
|
|
28
|
+
/** Actions still in flight, so an abort can mark them instead of leaving
|
|
29
|
+
* them at their optimistic default. */
|
|
30
|
+
this.openActions = new Set();
|
|
31
|
+
this.cleanupUntil = 0;
|
|
32
|
+
this.cleanupActive = false;
|
|
33
|
+
this.failurePhase = null;
|
|
34
|
+
this.stepStack = [];
|
|
35
|
+
this.rawApp = rawApp;
|
|
36
|
+
this.args = args;
|
|
37
|
+
setAssertionSink((entry) => this.noteAssertion(entry));
|
|
38
|
+
this.apps = {
|
|
39
|
+
lxapp: (appId) => this.wrapApp(this.automation.lxapp(appId)),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
get app() {
|
|
43
|
+
return this.wrapApp(this.rawApp);
|
|
44
|
+
}
|
|
45
|
+
get raw() {
|
|
46
|
+
return this.rawApp;
|
|
47
|
+
}
|
|
48
|
+
step(name, body) {
|
|
49
|
+
return this.guard(async () => {
|
|
50
|
+
const record = {
|
|
51
|
+
name,
|
|
52
|
+
path: [...this.stepStack.map((step) => step.name), name].join(" > "),
|
|
53
|
+
status: "passed",
|
|
54
|
+
duration_ms: 0,
|
|
55
|
+
steps: [],
|
|
56
|
+
attachments: [],
|
|
57
|
+
assertions: [],
|
|
58
|
+
};
|
|
59
|
+
const parent = this.stepStack[this.stepStack.length - 1];
|
|
60
|
+
(parent ? parent.steps : this.steps).push(record);
|
|
61
|
+
this.stepStack.push(record);
|
|
62
|
+
await this.host.emit({
|
|
63
|
+
type: "step_started",
|
|
64
|
+
name,
|
|
65
|
+
path: record.path,
|
|
66
|
+
});
|
|
67
|
+
const started = Date.now();
|
|
68
|
+
try {
|
|
69
|
+
const result = await body();
|
|
70
|
+
record.duration_ms = Date.now() - started;
|
|
71
|
+
await this.host.emit({
|
|
72
|
+
type: "step_finished",
|
|
73
|
+
name,
|
|
74
|
+
path: record.path,
|
|
75
|
+
status: record.status,
|
|
76
|
+
duration_ms: record.duration_ms,
|
|
77
|
+
});
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
record.duration_ms = Date.now() - started;
|
|
82
|
+
record.status = error instanceof TimeoutError ? "timeout" : "failed";
|
|
83
|
+
record.error = toReportError(error, record.path);
|
|
84
|
+
this.lastStepPath = record.path;
|
|
85
|
+
await this.host.emit({
|
|
86
|
+
type: "step_finished",
|
|
87
|
+
name,
|
|
88
|
+
path: record.path,
|
|
89
|
+
status: record.status,
|
|
90
|
+
duration_ms: record.duration_ms,
|
|
91
|
+
error: record.error,
|
|
92
|
+
});
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
this.stepStack.pop();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
get expect() {
|
|
101
|
+
const fn = ((locator) => this.locatorMatchers(locator, false));
|
|
102
|
+
fn.poll = (read, options) => this.pollMatchers(read, options, false);
|
|
103
|
+
return fn;
|
|
104
|
+
}
|
|
105
|
+
async reject(operation, expected = {}) {
|
|
106
|
+
return this.guard(async () => {
|
|
107
|
+
const location = callerLocation();
|
|
108
|
+
let received;
|
|
109
|
+
let didThrow = false;
|
|
110
|
+
try {
|
|
111
|
+
await operation();
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
didThrow = true;
|
|
115
|
+
received = error;
|
|
116
|
+
}
|
|
117
|
+
if (!didThrow) {
|
|
118
|
+
this.noteAssertion({
|
|
119
|
+
matcher: "reject",
|
|
120
|
+
expected: formatValue(expected),
|
|
121
|
+
actual: "resolved",
|
|
122
|
+
passed: false,
|
|
123
|
+
});
|
|
124
|
+
throw new AssertionError("reject", undefined, expected, [
|
|
125
|
+
"Expected the operation to reject.",
|
|
126
|
+
`Expected: ${formatValue(expected)}`,
|
|
127
|
+
"Received: resolved",
|
|
128
|
+
`at ${displayLocation(location.file, location.line, location.column)}`,
|
|
129
|
+
this.stepPathLine(),
|
|
130
|
+
].filter(Boolean).join("\n"));
|
|
131
|
+
}
|
|
132
|
+
const record = received;
|
|
133
|
+
if (expected.code !== undefined && record.code !== expected.code) {
|
|
134
|
+
this.noteAssertion({
|
|
135
|
+
matcher: "reject",
|
|
136
|
+
expected: formatValue(expected.code),
|
|
137
|
+
actual: formatValue(record.code),
|
|
138
|
+
passed: false,
|
|
139
|
+
});
|
|
140
|
+
throw new AssertionError("reject", record.code, expected.code, [
|
|
141
|
+
"Rejected with the wrong code.",
|
|
142
|
+
`Expected: ${formatValue(expected.code)}`,
|
|
143
|
+
`Received: ${formatValue(record.code)}`,
|
|
144
|
+
`at ${displayLocation(location.file, location.line, location.column)}`,
|
|
145
|
+
this.stepPathLine(),
|
|
146
|
+
].filter(Boolean).join("\n"));
|
|
147
|
+
}
|
|
148
|
+
if (typeof expected.message === "string") {
|
|
149
|
+
immediateExpect(String(record.message)).toContain(expected.message);
|
|
150
|
+
}
|
|
151
|
+
if (expected.message instanceof RegExp) {
|
|
152
|
+
immediateExpect(String(record.message)).toMatch(expected.message);
|
|
153
|
+
}
|
|
154
|
+
this.noteAssertion({
|
|
155
|
+
matcher: "reject",
|
|
156
|
+
expected: formatValue(expected),
|
|
157
|
+
actual: formatValue({ code: record.code, message: record.message }),
|
|
158
|
+
passed: true,
|
|
159
|
+
});
|
|
160
|
+
return received;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
defer(cleanup) {
|
|
164
|
+
this.defers.push(cleanup);
|
|
165
|
+
}
|
|
166
|
+
async attach(name, data) {
|
|
167
|
+
await this.guard(async () => {
|
|
168
|
+
await this.attachRaw(name, data);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
async attachRaw(name, data) {
|
|
172
|
+
const payload = encodeAttachPayload(data);
|
|
173
|
+
if (typeof data === "object" && data && "base64" in data && !("mimeType" in data)) {
|
|
174
|
+
if (name.endsWith(".png"))
|
|
175
|
+
payload.mimeType = "image/png";
|
|
176
|
+
}
|
|
177
|
+
const path = `attachments/${this.specId}/${name}`;
|
|
178
|
+
await this.host.attach(path, payload);
|
|
179
|
+
if (payload.mimeType.startsWith("image/")) {
|
|
180
|
+
rememberInline(this.specId, name, {
|
|
181
|
+
dataUrl: `data:${payload.mimeType};base64,${payload.base64}`,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
else if (isPreviewable(payload.mimeType)) {
|
|
185
|
+
rememberInline(this.specId, name, { text: previewText(data, payload) });
|
|
186
|
+
}
|
|
187
|
+
const ref = { name, path, mimeType: payload.mimeType };
|
|
188
|
+
const current = this.stepStack[this.stepStack.length - 1];
|
|
189
|
+
(current ? current.attachments : this.attachments).push(ref);
|
|
190
|
+
return ref;
|
|
191
|
+
}
|
|
192
|
+
abort(reason) {
|
|
193
|
+
this.aborted = true;
|
|
194
|
+
this.abortError = reason;
|
|
195
|
+
this.failurePhase = "timeout";
|
|
196
|
+
for (const record of this.openActions) {
|
|
197
|
+
record.status = "timeout";
|
|
198
|
+
record.error = toReportError(reason, record.path);
|
|
199
|
+
}
|
|
200
|
+
this.openActions.clear();
|
|
201
|
+
}
|
|
202
|
+
allowCleanup(budgetMs = WEDGED_DEFER_BUDGET_MS) {
|
|
203
|
+
this.cleanupUntil = Date.now() + budgetMs;
|
|
204
|
+
this.cleanupActive = true;
|
|
205
|
+
}
|
|
206
|
+
endCleanup() {
|
|
207
|
+
this.cleanupActive = false;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Record a driver call so a spec that never calls `t.step` still leaves a
|
|
211
|
+
* trace of what it did. Retry loops silence themselves: a five-second poll
|
|
212
|
+
* would otherwise bury the report in a hundred identical rows.
|
|
213
|
+
*/
|
|
214
|
+
async act(name, detail, op) {
|
|
215
|
+
if (this.actionSilence > 0)
|
|
216
|
+
return this.guard(op);
|
|
217
|
+
// Past the cap, keep recording failures: the action that finally breaks is
|
|
218
|
+
// the one row worth having, and dropping it leaves nothing pointing at it.
|
|
219
|
+
if (this.actionCount >= MAX_ACTIONS) {
|
|
220
|
+
try {
|
|
221
|
+
return await this.guard(op);
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
this.recordFailedAction(name, detail, error, 0);
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const parent = this.stepStack[this.stepStack.length - 1];
|
|
229
|
+
const siblings = parent ? parent.steps : this.steps;
|
|
230
|
+
// A hand-rolled poll calls the same driver over and over. Collapse the run
|
|
231
|
+
// into one row so the trace reads as what happened, not as a stutter.
|
|
232
|
+
const previous = siblings[siblings.length - 1];
|
|
233
|
+
if (previous?.kind === "action" &&
|
|
234
|
+
previous.status === "passed" &&
|
|
235
|
+
previous.name === name &&
|
|
236
|
+
previous.detail === detail) {
|
|
237
|
+
const started = Date.now();
|
|
238
|
+
try {
|
|
239
|
+
const result = await this.guard(op);
|
|
240
|
+
previous.repeat = (previous.repeat ?? 1) + 1;
|
|
241
|
+
previous.duration_ms += Date.now() - started;
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
// A failure is its own row: it is the one attempt worth reading.
|
|
246
|
+
const failed = {
|
|
247
|
+
name,
|
|
248
|
+
detail,
|
|
249
|
+
kind: "action",
|
|
250
|
+
path: previous.path,
|
|
251
|
+
status: error instanceof TimeoutError ? "timeout" : "failed",
|
|
252
|
+
duration_ms: Date.now() - started,
|
|
253
|
+
steps: [],
|
|
254
|
+
attachments: [],
|
|
255
|
+
assertions: [],
|
|
256
|
+
error: toReportError(error, previous.path),
|
|
257
|
+
};
|
|
258
|
+
siblings.push(failed);
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
this.actionCount += 1;
|
|
263
|
+
const record = {
|
|
264
|
+
name,
|
|
265
|
+
detail,
|
|
266
|
+
kind: "action",
|
|
267
|
+
path: [...this.stepStack.map((step) => step.name), name].join(" > "),
|
|
268
|
+
status: "passed",
|
|
269
|
+
duration_ms: 0,
|
|
270
|
+
steps: [],
|
|
271
|
+
attachments: [],
|
|
272
|
+
assertions: [],
|
|
273
|
+
};
|
|
274
|
+
siblings.push(record);
|
|
275
|
+
const started = Date.now();
|
|
276
|
+
// A spec timeout abandons the in-flight call: `run()` stops awaiting the
|
|
277
|
+
// body, so a record left at its optimistic default would serialise as an
|
|
278
|
+
// instant success — the hung call rendered as the fastest one in the trace.
|
|
279
|
+
this.openActions.add(record);
|
|
280
|
+
try {
|
|
281
|
+
const result = await this.guard(op);
|
|
282
|
+
record.duration_ms = Date.now() - started;
|
|
283
|
+
this.openActions.delete(record);
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
record.duration_ms = Date.now() - started;
|
|
288
|
+
record.status = error instanceof TimeoutError ? "timeout" : "failed";
|
|
289
|
+
record.error = toReportError(error, record.path);
|
|
290
|
+
this.openActions.delete(record);
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
recordFailedAction(name, detail, error, duration) {
|
|
295
|
+
const parent = this.stepStack[this.stepStack.length - 1];
|
|
296
|
+
const path = [...this.stepStack.map((step) => step.name), name].join(" > ");
|
|
297
|
+
(parent ? parent.steps : this.steps).push({
|
|
298
|
+
name,
|
|
299
|
+
detail,
|
|
300
|
+
kind: "action",
|
|
301
|
+
path,
|
|
302
|
+
status: error instanceof TimeoutError ? "timeout" : "failed",
|
|
303
|
+
duration_ms: duration,
|
|
304
|
+
steps: [],
|
|
305
|
+
attachments: [],
|
|
306
|
+
assertions: [],
|
|
307
|
+
error: toReportError(error, path),
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
silenceActions() {
|
|
311
|
+
this.actionSilence += 1;
|
|
312
|
+
}
|
|
313
|
+
resumeActions() {
|
|
314
|
+
this.actionSilence = Math.max(0, this.actionSilence - 1);
|
|
315
|
+
}
|
|
316
|
+
async guard(op) {
|
|
317
|
+
this.assertRunnable();
|
|
318
|
+
const result = await op();
|
|
319
|
+
this.assertRunnable();
|
|
320
|
+
return result;
|
|
321
|
+
}
|
|
322
|
+
currentStepPath() {
|
|
323
|
+
const current = this.stepStack[this.stepStack.length - 1];
|
|
324
|
+
return current?.path ?? this.lastStepPath;
|
|
325
|
+
}
|
|
326
|
+
noteAssertion(entry) {
|
|
327
|
+
const record = {
|
|
328
|
+
...entry,
|
|
329
|
+
step: this.currentStepPath(),
|
|
330
|
+
};
|
|
331
|
+
const current = this.stepStack[this.stepStack.length - 1];
|
|
332
|
+
(current ? current.assertions : this.assertions).push(record);
|
|
333
|
+
}
|
|
334
|
+
assertRunnable() {
|
|
335
|
+
if (this.cleanupActive) {
|
|
336
|
+
if (this.cleanupUntil > 0 && Date.now() > this.cleanupUntil) {
|
|
337
|
+
throw new TimeoutError("fixture cleanup budget exceeded");
|
|
338
|
+
}
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (this.aborted && this.abortError)
|
|
342
|
+
throw this.abortError;
|
|
343
|
+
}
|
|
344
|
+
stepPathLine() {
|
|
345
|
+
const path = this.currentStepPath();
|
|
346
|
+
return path ? `in step ${JSON.stringify(path)}` : "";
|
|
347
|
+
}
|
|
348
|
+
wrapApp(driver) {
|
|
349
|
+
const page = this.wrapPage(driver.page);
|
|
350
|
+
return {
|
|
351
|
+
page,
|
|
352
|
+
nav: guardObject(driver.nav, this, "nav."),
|
|
353
|
+
info: () => this.act("app.info", "", () => driver.info()),
|
|
354
|
+
pages: () => this.act("app.pages", "", () => driver.pages()),
|
|
355
|
+
surfaceLayout: () => this.act("app.surfaceLayout", "", () => driver.surfaceLayout()),
|
|
356
|
+
eval: (options) => this.act("app.eval", summarise(options), () => driver.eval(this.withEvalBudget(options))),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* The driver's own eval default is a flat few seconds, so a call that runs
|
|
361
|
+
* long under load fails a spec that still had most of its budget left. An
|
|
362
|
+
* eval gets a share of the spec's budget instead — a third, capped — never
|
|
363
|
+
* all of it: a call allowed to run the full budget leaves the spec no room
|
|
364
|
+
* to retry, so one stalled call takes the whole spec down with it.
|
|
365
|
+
*/
|
|
366
|
+
withEvalBudget(options) {
|
|
367
|
+
if (options && typeof options === "object" && options.timeoutMs === undefined) {
|
|
368
|
+
const share = Math.floor(this.specBudgetMs / 3);
|
|
369
|
+
return { ...options, timeoutMs: Math.max(1, Math.min(share, MAX_EVAL_BUDGET_MS)) };
|
|
370
|
+
}
|
|
371
|
+
return options;
|
|
372
|
+
}
|
|
373
|
+
wrapPage(page) {
|
|
374
|
+
const location = () => {
|
|
375
|
+
const frame = callerLocation();
|
|
376
|
+
return { source: frame.file, line: frame.line, column: frame.column };
|
|
377
|
+
};
|
|
378
|
+
// Every override lives in the proxy's `get` trap. Assigning onto the proxy
|
|
379
|
+
// would write straight through to the real driver — `page.eval` would then
|
|
380
|
+
// call itself forever.
|
|
381
|
+
const overrides = {
|
|
382
|
+
testId: (id) => this.locator(page, testIdSelector(id), location()),
|
|
383
|
+
css: (selector) => this.locator(page, selector, location()),
|
|
384
|
+
eval: (options) => this.act("page.eval", summarise(options), () => page.eval(this.withEvalBudget(options))),
|
|
385
|
+
};
|
|
386
|
+
const guarded = guardObject(page, this, "page.", Object.keys(overrides));
|
|
387
|
+
return new Proxy(guarded, {
|
|
388
|
+
get(target, prop, receiver) {
|
|
389
|
+
if (typeof prop === "string" && prop in overrides)
|
|
390
|
+
return overrides[prop];
|
|
391
|
+
return Reflect.get(target, prop, receiver);
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
locator(page, selector, location) {
|
|
396
|
+
return new PageLocator(page, (fn) => this.guard(fn), (verb, detail, op) => this.act(verb, detail, op), selector, location);
|
|
397
|
+
}
|
|
398
|
+
locatorMatchers(locator, inverted) {
|
|
399
|
+
const self = {
|
|
400
|
+
toBeVisible: (options) => this.retryLocator(locator, "toBeVisible", inverted, options, inverted ? "not visible" : "visible"),
|
|
401
|
+
toHaveText: (expected, options) => this.retryLocator(locator, "toHaveText", inverted, options, expected),
|
|
402
|
+
toHaveCount: (expected, options) => this.retryLocator(locator, "toHaveCount", inverted, options, expected),
|
|
403
|
+
toHaveValue: (expected, options) => this.retryLocator(locator, "toHaveValue", inverted, options, expected),
|
|
404
|
+
};
|
|
405
|
+
Object.defineProperty(self, "not", {
|
|
406
|
+
get: () => this.locatorMatchers(locator, !inverted),
|
|
407
|
+
});
|
|
408
|
+
return self;
|
|
409
|
+
}
|
|
410
|
+
pollMatchers(read, options, inverted) {
|
|
411
|
+
const run = (matcher, expected) => this.retryPoll(read, matcher, inverted, options, expected);
|
|
412
|
+
const fixture = this;
|
|
413
|
+
const self = {
|
|
414
|
+
toBe: (expected) => run("toBe", expected),
|
|
415
|
+
toEqual: (expected) => run("toEqual", expected),
|
|
416
|
+
toContain: (expected) => run("toContain", expected),
|
|
417
|
+
toMatch: (expected) => run("toMatch", expected),
|
|
418
|
+
toBeTruthy: () => run("toBeTruthy"),
|
|
419
|
+
toBeFalsy: () => run("toBeFalsy"),
|
|
420
|
+
toBeDefined: () => run("toBeDefined"),
|
|
421
|
+
toBeUndefined: () => run("toBeUndefined"),
|
|
422
|
+
toBeInstanceOf: (expected) => run("toBeInstanceOf", expected),
|
|
423
|
+
};
|
|
424
|
+
Object.defineProperty(self, "not", {
|
|
425
|
+
get: () => fixture.pollMatchers(read, options, !inverted),
|
|
426
|
+
});
|
|
427
|
+
return self;
|
|
428
|
+
}
|
|
429
|
+
async retryLocator(locator, matcher, inverted, options, expected) {
|
|
430
|
+
await this.guard(async () => {
|
|
431
|
+
const frame = callerLocation();
|
|
432
|
+
const location = { source: frame.file, line: frame.line, column: frame.column };
|
|
433
|
+
const timeout = options?.timeout ?? DEFAULT_ACTION_TIMEOUT_MS;
|
|
434
|
+
const interval = options?.interval ?? DEFAULT_POLL_INTERVAL_MS;
|
|
435
|
+
const started = Date.now();
|
|
436
|
+
let lastResolved;
|
|
437
|
+
let lastError;
|
|
438
|
+
pushAssertionSilence();
|
|
439
|
+
this.silenceActions();
|
|
440
|
+
try {
|
|
441
|
+
while (Date.now() - started < timeout) {
|
|
442
|
+
try {
|
|
443
|
+
lastResolved = await resolveLocator(locator);
|
|
444
|
+
matchLocator(locator, lastResolved, matcher, expected, inverted);
|
|
445
|
+
this.noteAssertion({
|
|
446
|
+
matcher: inverted ? `not.${matcher}` : matcher,
|
|
447
|
+
expected: formatValue(expected),
|
|
448
|
+
actual: formatValue(locatorActual(matcher, lastResolved)),
|
|
449
|
+
passed: true,
|
|
450
|
+
});
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
catch (error) {
|
|
454
|
+
if (error instanceof TimeoutError || this.aborted)
|
|
455
|
+
throw error;
|
|
456
|
+
lastError = error;
|
|
457
|
+
}
|
|
458
|
+
if (Date.now() - started >= timeout)
|
|
459
|
+
break;
|
|
460
|
+
await sleep(interval);
|
|
461
|
+
if (this.aborted && this.abortError)
|
|
462
|
+
throw this.abortError;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
finally {
|
|
466
|
+
popAssertionSilence();
|
|
467
|
+
this.resumeActions();
|
|
468
|
+
}
|
|
469
|
+
const duration = Date.now() - started;
|
|
470
|
+
throw this.retryFailure({
|
|
471
|
+
matcher: inverted ? `not.${matcher}` : matcher,
|
|
472
|
+
expected,
|
|
473
|
+
actual: locatorActual(matcher, lastResolved),
|
|
474
|
+
duration,
|
|
475
|
+
location,
|
|
476
|
+
lastError,
|
|
477
|
+
extra: lastResolved && locator instanceof PageLocator ? locator.missText(lastResolved) : undefined,
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
async retryPoll(read, matcher, inverted, options, expected) {
|
|
482
|
+
await this.guard(async () => {
|
|
483
|
+
const frame = callerLocation();
|
|
484
|
+
const location = { source: frame.file, line: frame.line, column: frame.column };
|
|
485
|
+
const timeout = options?.timeout ?? DEFAULT_ACTION_TIMEOUT_MS;
|
|
486
|
+
const interval = options?.interval ?? DEFAULT_POLL_INTERVAL_MS;
|
|
487
|
+
const started = Date.now();
|
|
488
|
+
let lastActual;
|
|
489
|
+
let lastError;
|
|
490
|
+
pushAssertionSilence();
|
|
491
|
+
this.silenceActions();
|
|
492
|
+
try {
|
|
493
|
+
while (Date.now() - started < timeout) {
|
|
494
|
+
try {
|
|
495
|
+
lastActual = await read();
|
|
496
|
+
applyMatcher(matcher, lastActual, expected, inverted);
|
|
497
|
+
this.noteAssertion({
|
|
498
|
+
matcher: inverted ? `not.${matcher}` : matcher,
|
|
499
|
+
expected: formatValue(expected),
|
|
500
|
+
actual: formatValue(lastActual),
|
|
501
|
+
passed: true,
|
|
502
|
+
});
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
catch (error) {
|
|
506
|
+
if (error instanceof TimeoutError || this.aborted)
|
|
507
|
+
throw error;
|
|
508
|
+
lastError = error;
|
|
509
|
+
}
|
|
510
|
+
if (Date.now() - started >= timeout)
|
|
511
|
+
break;
|
|
512
|
+
await sleep(interval);
|
|
513
|
+
if (this.aborted && this.abortError)
|
|
514
|
+
throw this.abortError;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
finally {
|
|
518
|
+
popAssertionSilence();
|
|
519
|
+
this.resumeActions();
|
|
520
|
+
}
|
|
521
|
+
throw this.retryFailure({
|
|
522
|
+
matcher: inverted ? `not.${matcher}` : matcher,
|
|
523
|
+
expected,
|
|
524
|
+
actual: lastActual,
|
|
525
|
+
duration: Date.now() - started,
|
|
526
|
+
location,
|
|
527
|
+
lastError,
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
retryFailure(input) {
|
|
532
|
+
const where = displayLocation(input.location.source, input.location.line, input.location.column);
|
|
533
|
+
const last = input.lastError instanceof AssertionError
|
|
534
|
+
? input.lastError.message
|
|
535
|
+
: input.lastError instanceof Error
|
|
536
|
+
? input.lastError.message
|
|
537
|
+
: undefined;
|
|
538
|
+
const lines = [
|
|
539
|
+
`Timed out after ${input.duration}ms retrying ${input.matcher}.`,
|
|
540
|
+
input.extra,
|
|
541
|
+
`Expected: ${formatValue(input.expected)}`,
|
|
542
|
+
`Received: ${formatValue(input.actual)}`,
|
|
543
|
+
`Retried for ${input.duration}ms`,
|
|
544
|
+
`at ${where}`,
|
|
545
|
+
this.stepPathLine(),
|
|
546
|
+
last && last !== `Expected: ${formatValue(input.expected)}` ? last : undefined,
|
|
547
|
+
].filter((line) => Boolean(line));
|
|
548
|
+
this.noteAssertion({
|
|
549
|
+
matcher: input.matcher,
|
|
550
|
+
expected: formatValue(input.expected),
|
|
551
|
+
actual: formatValue(input.actual),
|
|
552
|
+
passed: false,
|
|
553
|
+
});
|
|
554
|
+
return new AssertionError(input.matcher, input.actual, input.expected, lines.join("\n"));
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
function resolveLocator(locator) {
|
|
558
|
+
if (locator instanceof PageLocator)
|
|
559
|
+
return locator.resolve();
|
|
560
|
+
throw new Error("t.expect() requires a locator from page.testId() or page.css()");
|
|
561
|
+
}
|
|
562
|
+
function locatorActual(matcher, resolved) {
|
|
563
|
+
if (!resolved)
|
|
564
|
+
return undefined;
|
|
565
|
+
if (matcher === "toHaveCount")
|
|
566
|
+
return resolved.count;
|
|
567
|
+
if (matcher === "toHaveText")
|
|
568
|
+
return resolved.text;
|
|
569
|
+
if (matcher === "toHaveValue")
|
|
570
|
+
return resolved.value;
|
|
571
|
+
if (matcher === "toBeVisible")
|
|
572
|
+
return resolved.kind;
|
|
573
|
+
return resolved.kind;
|
|
574
|
+
}
|
|
575
|
+
function matchLocator(locator, resolved, matcher, expected, inverted) {
|
|
576
|
+
if (matcher === "toBeVisible") {
|
|
577
|
+
const pass = resolved.visible && resolved.kind === "unique";
|
|
578
|
+
if (pass === inverted) {
|
|
579
|
+
const detail = locator instanceof PageLocator ? locator.missText(resolved) : `resolved to ${resolved.kind}`;
|
|
580
|
+
throw new AssertionError(inverted ? "not.toBeVisible" : "toBeVisible", resolved.kind, inverted ? "not visible" : "visible", `${detail}\nExpected: ${inverted ? "not " : ""}visible\nReceived: ${formatValue(resolved.kind)}`);
|
|
581
|
+
}
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (matcher === "toHaveCount") {
|
|
585
|
+
applyMatcher("toBe", resolved.count, expected, inverted);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (matcher === "toHaveText") {
|
|
589
|
+
if (expected instanceof RegExp)
|
|
590
|
+
applyMatcher("toMatch", resolved.text, expected, inverted);
|
|
591
|
+
else
|
|
592
|
+
applyMatcher("toBe", resolved.text, expected, inverted);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (matcher === "toHaveValue") {
|
|
596
|
+
if (expected instanceof RegExp)
|
|
597
|
+
applyMatcher("toMatch", resolved.value ?? "", expected, inverted);
|
|
598
|
+
else
|
|
599
|
+
applyMatcher("toBe", resolved.value, expected, inverted);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
function guardObject(target, fixture, path, skip = []) {
|
|
603
|
+
const cache = new Map();
|
|
604
|
+
return new Proxy(target, {
|
|
605
|
+
get(obj, prop, receiver) {
|
|
606
|
+
if (skip.includes(prop))
|
|
607
|
+
return Reflect.get(obj, prop, receiver);
|
|
608
|
+
if (cache.has(prop))
|
|
609
|
+
return cache.get(prop);
|
|
610
|
+
const value = Reflect.get(obj, prop, receiver);
|
|
611
|
+
if (typeof value === "function") {
|
|
612
|
+
const name = `${path}${String(prop)}`;
|
|
613
|
+
const bound = (...args) => fixture.act(name, summarise(args[0]), () => value.apply(obj, args));
|
|
614
|
+
cache.set(prop, bound);
|
|
615
|
+
return bound;
|
|
616
|
+
}
|
|
617
|
+
if (value && typeof value === "object") {
|
|
618
|
+
const nested = guardObject(value, fixture, `${path}${String(prop)}.`);
|
|
619
|
+
cache.set(prop, nested);
|
|
620
|
+
return nested;
|
|
621
|
+
}
|
|
622
|
+
return value;
|
|
623
|
+
},
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
/** The one detail worth showing beside an action: what it acted on. */
|
|
627
|
+
function summarise(input) {
|
|
628
|
+
if (input === undefined || input === null)
|
|
629
|
+
return "";
|
|
630
|
+
if (typeof input === "string")
|
|
631
|
+
return truncate(input, 80);
|
|
632
|
+
if (typeof input !== "object")
|
|
633
|
+
return String(input);
|
|
634
|
+
const record = input;
|
|
635
|
+
for (const key of ["css", "page", "script", "url", "text", "id", "name"]) {
|
|
636
|
+
const value = record[key];
|
|
637
|
+
if (typeof value === "string" && value.length > 0) {
|
|
638
|
+
return truncate(key === "script" ? value.replace(/\s+/g, " ").trim() : value, 80);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return truncate(formatValue(input), 80);
|
|
642
|
+
}
|
|
643
|
+
/** Text and JSON preview in the report; anything else is named, not embedded. */
|
|
644
|
+
function isPreviewable(mimeType) {
|
|
645
|
+
return mimeType.startsWith("text/") || mimeType.startsWith("application/json");
|
|
646
|
+
}
|
|
647
|
+
const MAX_PREVIEW_CHARS = 20000;
|
|
648
|
+
function previewText(data, payload) {
|
|
649
|
+
const raw = typeof data === "string"
|
|
650
|
+
? data
|
|
651
|
+
: payload.mimeType.startsWith("application/json")
|
|
652
|
+
? safeJson(data)
|
|
653
|
+
: String(data);
|
|
654
|
+
return raw.length > MAX_PREVIEW_CHARS
|
|
655
|
+
? `${raw.slice(0, MAX_PREVIEW_CHARS)}\n… truncated, open the file for the rest`
|
|
656
|
+
: raw;
|
|
657
|
+
}
|
|
658
|
+
function safeJson(data) {
|
|
659
|
+
try {
|
|
660
|
+
return JSON.stringify(data, null, 2) ?? String(data);
|
|
661
|
+
}
|
|
662
|
+
catch {
|
|
663
|
+
return String(data);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
export function toReportError(error, step) {
|
|
667
|
+
if (error instanceof AssertionError) {
|
|
668
|
+
const stack = remapStack(error.stack);
|
|
669
|
+
return {
|
|
670
|
+
name: error.name,
|
|
671
|
+
message: error.message,
|
|
672
|
+
stack,
|
|
673
|
+
matcher: error.matcher,
|
|
674
|
+
expected: formatValue(error.expected),
|
|
675
|
+
actual: formatValue(error.actual),
|
|
676
|
+
location: firstLocation(stack),
|
|
677
|
+
step,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
if (error instanceof Error) {
|
|
681
|
+
const stack = remapStack(error.stack);
|
|
682
|
+
return {
|
|
683
|
+
name: error.name,
|
|
684
|
+
message: error.message,
|
|
685
|
+
stack,
|
|
686
|
+
location: firstLocation(stack),
|
|
687
|
+
step,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
return { name: "Error", message: String(error), step };
|
|
691
|
+
}
|
|
692
|
+
function firstLocation(stack) {
|
|
693
|
+
const frame = parseFrames(stack)[0];
|
|
694
|
+
return frame ? `${frame.file}:${frame.line}:${frame.column}` : undefined;
|
|
695
|
+
}
|
|
696
|
+
export function protocolStatus(status) {
|
|
697
|
+
if (status === "skipped" || status === "xfail")
|
|
698
|
+
return status === "skipped" ? "skipped" : "passed";
|
|
699
|
+
if (status === "passed")
|
|
700
|
+
return "passed";
|
|
701
|
+
return "failed";
|
|
702
|
+
}
|
|
703
|
+
//# sourceMappingURL=fixture.js.map
|