miaoda-game-devkit 0.6.1 → 0.6.3
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/cli/phaser-lint.js +187 -19
- package/dist/cli/react-lint.js +187 -19
- package/dist/react/index.js +11 -1
- package/dist/react/index.mjs +11 -1
- package/dist/react/testing.d.mts +19 -2
- package/dist/react/testing.d.ts +19 -2
- package/dist/react/testing.js +177 -31
- package/dist/react/testing.mjs +177 -31
- package/dist/react/vitest-config.js +342 -65
- package/dist/react/vitest-config.mjs +342 -65
- package/dist/react/vitest-setup.js +87 -4
- package/dist/react/vitest-setup.mjs +87 -4
- package/dist/rules/react-test-boundary-plugin.js +14 -1
- package/package.json +1 -1
package/dist/react/testing.mjs
CHANGED
|
@@ -58,14 +58,112 @@ import { render } from "@testing-library/react";
|
|
|
58
58
|
import userEvent from "@testing-library/user-event";
|
|
59
59
|
import { test } from "vitest";
|
|
60
60
|
|
|
61
|
+
// src/react/react-error-diagnostics.ts
|
|
62
|
+
var MAX_DIAGNOSTIC_LENGTH = 1e3;
|
|
63
|
+
function truncate(value) {
|
|
64
|
+
const trimmed = value.trim();
|
|
65
|
+
if (trimmed.length <= MAX_DIAGNOSTIC_LENGTH) return trimmed;
|
|
66
|
+
return `${trimmed.slice(0, MAX_DIAGNOSTIC_LENGTH - 1)}\u2026`;
|
|
67
|
+
}
|
|
68
|
+
function safeJson(value) {
|
|
69
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
70
|
+
try {
|
|
71
|
+
return JSON.stringify(value, (_key, nested) => {
|
|
72
|
+
if (typeof nested === "bigint") return `${nested}n`;
|
|
73
|
+
if (typeof nested === "function") {
|
|
74
|
+
return `Function<${nested.name || "anonymous"}>`;
|
|
75
|
+
}
|
|
76
|
+
if (typeof nested === "symbol") return nested.toString();
|
|
77
|
+
if (nested && typeof nested === "object") {
|
|
78
|
+
if (seen.has(nested)) return "[Circular]";
|
|
79
|
+
seen.add(nested);
|
|
80
|
+
}
|
|
81
|
+
return nested;
|
|
82
|
+
});
|
|
83
|
+
} catch {
|
|
84
|
+
return void 0;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function collectEntries(value, fallbackCode, seen) {
|
|
88
|
+
if (typeof value === "string") {
|
|
89
|
+
return value.trim() ? [{ code: fallbackCode, message: truncate(value) }] : [];
|
|
90
|
+
}
|
|
91
|
+
if (value === null || value === void 0 || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "symbol") {
|
|
92
|
+
return [{ code: fallbackCode, message: String(value) }];
|
|
93
|
+
}
|
|
94
|
+
if (typeof value === "function") {
|
|
95
|
+
return [
|
|
96
|
+
{ code: fallbackCode, message: `Function<${value.name || "anonymous"}>` }
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
if (seen.has(value)) return [];
|
|
100
|
+
seen.add(value);
|
|
101
|
+
if (Array.isArray(value)) {
|
|
102
|
+
return value.flatMap((item) => collectEntries(item, fallbackCode, seen));
|
|
103
|
+
}
|
|
104
|
+
const record = value;
|
|
105
|
+
const code = typeof record.code === "string" && record.code.trim() ? record.code.trim() : fallbackCode;
|
|
106
|
+
const entries = [];
|
|
107
|
+
if (typeof record.message === "string" && record.message.trim()) {
|
|
108
|
+
entries.push({ code, message: truncate(record.message) });
|
|
109
|
+
}
|
|
110
|
+
if (record.cause !== void 0) {
|
|
111
|
+
entries.push(...collectEntries(record.cause, fallbackCode, seen));
|
|
112
|
+
}
|
|
113
|
+
if (Array.isArray(record.errors)) {
|
|
114
|
+
entries.push(...collectEntries(record.errors, fallbackCode, seen));
|
|
115
|
+
}
|
|
116
|
+
if (entries.length > 0) return entries;
|
|
117
|
+
if (typeof record.stack === "string" && record.stack.trim()) {
|
|
118
|
+
return [{ code, message: truncate(record.stack) }];
|
|
119
|
+
}
|
|
120
|
+
const json = safeJson(value);
|
|
121
|
+
return json && json !== "{}" ? [{ code, message: truncate(json) }] : [];
|
|
122
|
+
}
|
|
123
|
+
function extractFailureEntries(value, fallbackCode = "TEST_FAILURE") {
|
|
124
|
+
const entries = collectEntries(value, fallbackCode, /* @__PURE__ */ new WeakSet());
|
|
125
|
+
const keys = /* @__PURE__ */ new Set();
|
|
126
|
+
return entries.filter((entry) => {
|
|
127
|
+
const key = `${entry.code}\0${entry.message}`;
|
|
128
|
+
if (keys.has(key)) return false;
|
|
129
|
+
keys.add(key);
|
|
130
|
+
return true;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function createFailureDiagnostic(source, value) {
|
|
134
|
+
return { source, entries: extractFailureEntries(value) };
|
|
135
|
+
}
|
|
136
|
+
function appendCurrentAttemptFailures(current, runnerValue) {
|
|
137
|
+
const runner = createFailureDiagnostic("test-runtime", runnerValue);
|
|
138
|
+
if (!current || current.entries.length === 0) return runner;
|
|
139
|
+
const primary = current.entries[0];
|
|
140
|
+
const currentStart = runner.entries.findIndex(
|
|
141
|
+
(entry) => entry.code === primary.code && entry.message === primary.message
|
|
142
|
+
);
|
|
143
|
+
if (currentStart < 0) return current;
|
|
144
|
+
return {
|
|
145
|
+
source: current.source,
|
|
146
|
+
entries: extractFailureEntries([
|
|
147
|
+
...current.entries,
|
|
148
|
+
...runner.entries.slice(currentStart + 1)
|
|
149
|
+
])
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function codedError(code, message) {
|
|
153
|
+
const error = new Error(message);
|
|
154
|
+
error.code = code;
|
|
155
|
+
return error;
|
|
156
|
+
}
|
|
157
|
+
|
|
61
158
|
// src/react/react-playthrough-core.ts
|
|
62
159
|
import { act } from "@testing-library/react";
|
|
63
160
|
function throwIfAborted(signal) {
|
|
64
161
|
if (!signal?.aborted) return;
|
|
65
162
|
if (signal.reason instanceof Error) throw signal.reason;
|
|
66
|
-
throw
|
|
67
|
-
|
|
68
|
-
|
|
163
|
+
throw codedError(
|
|
164
|
+
"PLAYTHROUGH_CANCELLED",
|
|
165
|
+
`Playthrough advancement was cancelled. Cause: ${String(signal.reason)}`
|
|
166
|
+
);
|
|
69
167
|
}
|
|
70
168
|
function formatDiagnostics(read) {
|
|
71
169
|
if (!read) return void 0;
|
|
@@ -81,7 +179,8 @@ function normalizePlaythroughWaiverReason(waiverReason) {
|
|
|
81
179
|
if (waiverReason === void 0) return void 0;
|
|
82
180
|
const reason = waiverReason.trim();
|
|
83
181
|
if (reason.length < 20) {
|
|
84
|
-
throw
|
|
182
|
+
throw codedError(
|
|
183
|
+
"INVALID_PLAYTHROUGH_WAIVER",
|
|
85
184
|
"playthroughTest.skip reason must contain at least 20 characters."
|
|
86
185
|
);
|
|
87
186
|
}
|
|
@@ -90,7 +189,8 @@ function normalizePlaythroughWaiverReason(waiverReason) {
|
|
|
90
189
|
async function runBoundedUntil(condition, options = {}) {
|
|
91
190
|
const maxSteps = options.maxSteps ?? 120;
|
|
92
191
|
if (!Number.isSafeInteger(maxSteps) || maxSteps < 0 || maxSteps > 1e4) {
|
|
93
|
-
throw
|
|
192
|
+
throw codedError(
|
|
193
|
+
"INVALID_STEP_BOUND",
|
|
94
194
|
"stepUntil maxSteps must be a safe integer between 0 and 10000."
|
|
95
195
|
);
|
|
96
196
|
}
|
|
@@ -107,7 +207,8 @@ async function runBoundedUntil(condition, options = {}) {
|
|
|
107
207
|
const diagnostics = formatDiagnostics(options.diagnostics);
|
|
108
208
|
const guidance = options.step ? "The step callback ran, but the authoritative outcome did not change." : "No step callback was provided, so time-driven gameplay was not advanced. Inject a ManualGameClock for this test and pass step: () => clock.stepFrame().";
|
|
109
209
|
const suffix = diagnostics ? ` Last diagnostics: ${diagnostics}` : "";
|
|
110
|
-
throw
|
|
210
|
+
throw codedError(
|
|
211
|
+
options.step ? "PLAYTHROUGH_BOUND_EXHAUSTED" : "PLAYTHROUGH_CLOCK_NOT_ADVANCED",
|
|
111
212
|
`Playthrough outcome was not reached within ${maxSteps} steps. ${guidance}${suffix}`
|
|
112
213
|
);
|
|
113
214
|
}
|
|
@@ -126,6 +227,11 @@ var MIN_STAGES = 5;
|
|
|
126
227
|
var MIN_MILESTONES = 3;
|
|
127
228
|
var MAX_TRACE_VALUE_LENGTH = 140;
|
|
128
229
|
var MAX_TRACE_LENGTH = 720;
|
|
230
|
+
function eventTargetsCanvas(event) {
|
|
231
|
+
const path = typeof event.composedPath === "function" ? event.composedPath() : [];
|
|
232
|
+
if (path.some((target) => target instanceof HTMLCanvasElement)) return true;
|
|
233
|
+
return event.target instanceof HTMLCanvasElement;
|
|
234
|
+
}
|
|
129
235
|
var REACT_PLAYTHROUGH_TRACE_ANNOTATION = "miaoda:react-playthrough-trace";
|
|
130
236
|
function truncateTraceValue(value, limit) {
|
|
131
237
|
const compact = value.replace(/\s+/g, " ").trim();
|
|
@@ -155,14 +261,18 @@ function sampleObservedState(observe, stage) {
|
|
|
155
261
|
try {
|
|
156
262
|
value = observe();
|
|
157
263
|
} catch (error) {
|
|
158
|
-
throw
|
|
264
|
+
throw codedError(
|
|
265
|
+
"OBSERVE_FAILED",
|
|
266
|
+
`observe() threw at ${stage}: ${String(error)}`
|
|
267
|
+
);
|
|
159
268
|
}
|
|
160
269
|
try {
|
|
161
270
|
const fingerprint = JSON.stringify(value);
|
|
162
271
|
if (fingerprint === void 0) throw new Error("unsupported value");
|
|
163
272
|
return { fingerprint, formatted: formatState(fingerprint) };
|
|
164
273
|
} catch {
|
|
165
|
-
throw
|
|
274
|
+
throw codedError(
|
|
275
|
+
"OBSERVE_NOT_SERIALIZABLE",
|
|
166
276
|
`observe() must return JSON-serializable read-only state; sampling failed at ${stage}.`
|
|
167
277
|
);
|
|
168
278
|
}
|
|
@@ -175,7 +285,7 @@ function createEvidence() {
|
|
|
175
285
|
return { domInputEvents: 0, stages: [], verified: false };
|
|
176
286
|
}
|
|
177
287
|
function createMetadata(waiverReason) {
|
|
178
|
-
return { version:
|
|
288
|
+
return { version: 5, waiverReason, evidence: createEvidence() };
|
|
179
289
|
}
|
|
180
290
|
function stageLabel(kind, name) {
|
|
181
291
|
return kind === "entered" ? "entered" : `${kind}(${JSON.stringify(name)})`;
|
|
@@ -211,6 +321,7 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
211
321
|
async ({ annotate, expect, onTestFailed, signal }) => {
|
|
212
322
|
metadata.evidence = createEvidence();
|
|
213
323
|
metadata.trace = void 0;
|
|
324
|
+
metadata.failure = void 0;
|
|
214
325
|
const evidence = metadata.evidence;
|
|
215
326
|
let entered = false;
|
|
216
327
|
let finished = false;
|
|
@@ -219,9 +330,12 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
219
330
|
let stepTrace;
|
|
220
331
|
let failureTraceFactory;
|
|
221
332
|
let acceptingStageInput = false;
|
|
333
|
+
let activeStageTargetedCanvas = false;
|
|
222
334
|
let inputCaptureAttached = false;
|
|
223
|
-
const recordInput = () => {
|
|
224
|
-
if (acceptingStageInput)
|
|
335
|
+
const recordInput = (event) => {
|
|
336
|
+
if (!acceptingStageInput) return;
|
|
337
|
+
evidence.domInputEvents += 1;
|
|
338
|
+
activeStageTargetedCanvas ||= eventTargetsCanvas(event);
|
|
225
339
|
};
|
|
226
340
|
const stopInputCapture = () => {
|
|
227
341
|
acceptingStageInput = false;
|
|
@@ -238,8 +352,12 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
238
352
|
return "stages=none";
|
|
239
353
|
}
|
|
240
354
|
};
|
|
241
|
-
onTestFailed(() => {
|
|
355
|
+
onTestFailed(({ task }) => {
|
|
242
356
|
metadata.trace ??= captureFailureTrace();
|
|
357
|
+
metadata.failure = appendCurrentAttemptFailures(
|
|
358
|
+
metadata.failure,
|
|
359
|
+
task.result?.errors ?? []
|
|
360
|
+
);
|
|
243
361
|
});
|
|
244
362
|
for (const event of INPUT_EVENTS) {
|
|
245
363
|
document.addEventListener(event, recordInput, true);
|
|
@@ -249,7 +367,8 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
249
367
|
try {
|
|
250
368
|
const view = render(element);
|
|
251
369
|
if (view.container.childNodes.length === 0) {
|
|
252
|
-
throw
|
|
370
|
+
throw codedError(
|
|
371
|
+
"PRODUCTION_ENTRY_NOT_RENDERED",
|
|
253
372
|
"playthroughTest must render the production game entry."
|
|
254
373
|
);
|
|
255
374
|
}
|
|
@@ -278,29 +397,42 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
278
397
|
const executeStage = async (name, kind, stage) => {
|
|
279
398
|
const normalizedName = name.trim();
|
|
280
399
|
if (normalizedName.length === 0) {
|
|
281
|
-
throw
|
|
400
|
+
throw codedError(
|
|
401
|
+
"INVALID_STAGE_NAME",
|
|
402
|
+
"playthrough stage names must be non-empty strings."
|
|
403
|
+
);
|
|
282
404
|
}
|
|
283
405
|
if (evidence.stages.some(
|
|
284
406
|
(completed) => completed.name === normalizedName
|
|
285
407
|
)) {
|
|
286
|
-
throw
|
|
408
|
+
throw codedError(
|
|
409
|
+
"DUPLICATE_STAGE_NAME",
|
|
287
410
|
`playthrough stage ${JSON.stringify(normalizedName)} may only be recorded once.`
|
|
288
411
|
);
|
|
289
412
|
}
|
|
290
413
|
if (kind === "milestone" && ["entered", "progress", "terminal"].includes(normalizedName)) {
|
|
291
|
-
throw
|
|
414
|
+
throw codedError(
|
|
415
|
+
"RESERVED_STAGE_NAME",
|
|
292
416
|
`milestone name ${JSON.stringify(normalizedName)} is reserved; use a game-domain name such as "first-point" or "boss-entered".`
|
|
293
417
|
);
|
|
294
418
|
}
|
|
295
419
|
const before = lastSample ?? sampleState(`before ${normalizedName}`);
|
|
296
420
|
activeStage = { name: normalizedName, kind, before };
|
|
297
421
|
stepTrace = { bound: stage.maxSteps ?? 120 };
|
|
422
|
+
if (stage.step && !playthroughOptions?.observe) {
|
|
423
|
+
throw codedError(
|
|
424
|
+
"AUTHORITATIVE_OBSERVE_REQUIRED_FOR_STEP",
|
|
425
|
+
`${stageLabel(kind, normalizedName)} uses deterministic step advancement without an authoritative observe callback. Time- or frame-driven stages must observe the same production Controller that <App /> renders through Telemetry; DOM labels alone are not deterministic gameplay state.`
|
|
426
|
+
);
|
|
427
|
+
}
|
|
298
428
|
if (stage.until()) {
|
|
299
|
-
throw
|
|
429
|
+
throw codedError(
|
|
430
|
+
"STAGE_OUTCOME_ALREADY_REACHED",
|
|
300
431
|
`${stageLabel(kind, normalizedName)} until condition must be false before its driver runs. Wait for a result caused by this stage, not state left by an earlier stage.`
|
|
301
432
|
);
|
|
302
433
|
}
|
|
303
434
|
const inputsBefore = evidence.domInputEvents;
|
|
435
|
+
activeStageTargetedCanvas = false;
|
|
304
436
|
if (stage.act) {
|
|
305
437
|
acceptingStageInput = true;
|
|
306
438
|
try {
|
|
@@ -309,10 +441,17 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
309
441
|
acceptingStageInput = false;
|
|
310
442
|
}
|
|
311
443
|
if (evidence.domInputEvents === inputsBefore) {
|
|
312
|
-
throw
|
|
444
|
+
throw codedError(
|
|
445
|
+
"PRODUCTION_INPUT_NOT_DISPATCHED",
|
|
313
446
|
`${stageLabel(kind, normalizedName)} act did not dispatch a supported production DOM input. Use the provided user to click, type, press, point, or touch the production target; do not call Controller commands directly.`
|
|
314
447
|
);
|
|
315
448
|
}
|
|
449
|
+
if (activeStageTargetedCanvas && !playthroughOptions?.observe) {
|
|
450
|
+
throw codedError(
|
|
451
|
+
"AUTHORITATIVE_OBSERVE_REQUIRED_FOR_CANVAS",
|
|
452
|
+
`${stageLabel(kind, normalizedName)} dispatched production input to Canvas without an authoritative observe callback. Canvas pixels and control labels are not gameplay state; observe the same production Controller that <App /> renders through Telemetry.`
|
|
453
|
+
);
|
|
454
|
+
}
|
|
316
455
|
}
|
|
317
456
|
let advancedSteps = 0;
|
|
318
457
|
const stepBound = stage.maxSteps ?? 120;
|
|
@@ -328,7 +467,8 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
328
467
|
});
|
|
329
468
|
stepTrace = { bound: stepBound, completed: steps };
|
|
330
469
|
if (!stage.act && advancedSteps === 0) {
|
|
331
|
-
throw
|
|
470
|
+
throw codedError(
|
|
471
|
+
"AUTONOMOUS_STAGE_NOT_ADVANCED",
|
|
332
472
|
`${stageLabel(kind, normalizedName)} did not execute its deterministic step. Autonomous stages must advance production time or frames at least once.`
|
|
333
473
|
);
|
|
334
474
|
}
|
|
@@ -336,14 +476,16 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
336
476
|
await stage.assert({ expect, user, view });
|
|
337
477
|
const assertions = expect.getState().assertionCalls - assertionsBefore;
|
|
338
478
|
if (assertions === 0) {
|
|
339
|
-
throw
|
|
479
|
+
throw codedError(
|
|
480
|
+
"STAGE_ASSERTION_MISSING",
|
|
340
481
|
`${stageLabel(kind, normalizedName)} assert must call the expect provided by playthroughTest at least once.`
|
|
341
482
|
);
|
|
342
483
|
}
|
|
343
484
|
const after = sampleState(`after ${normalizedName}`);
|
|
344
485
|
if (after.fingerprint === before.fingerprint) {
|
|
345
486
|
const source = playthroughOptions?.observe ? "authoritative observe() state" : "production DOM";
|
|
346
|
-
throw
|
|
487
|
+
throw codedError(
|
|
488
|
+
"STAGE_STATE_UNCHANGED",
|
|
347
489
|
`${stageLabel(kind, normalizedName)} did not change the ${source} from the previous stage. Each milestone must prove a new gameplay result.`
|
|
348
490
|
);
|
|
349
491
|
}
|
|
@@ -365,27 +507,27 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
365
507
|
user,
|
|
366
508
|
async enter(stage) {
|
|
367
509
|
if (entered) {
|
|
368
|
-
throw
|
|
510
|
+
throw codedError("INVALID_STAGE_ORDER", "enter may only be called once.");
|
|
369
511
|
}
|
|
370
512
|
if (evidence.stages.length > 0) {
|
|
371
|
-
throw
|
|
513
|
+
throw codedError("INVALID_STAGE_ORDER", "enter must be the first playthrough stage.");
|
|
372
514
|
}
|
|
373
515
|
await executeStage("entered", "entered", stage);
|
|
374
516
|
entered = true;
|
|
375
517
|
},
|
|
376
518
|
async milestone(name, stage) {
|
|
377
519
|
if (!entered) {
|
|
378
|
-
throw
|
|
520
|
+
throw codedError("INVALID_STAGE_ORDER", "milestone must follow enter.");
|
|
379
521
|
}
|
|
380
522
|
if (finished) {
|
|
381
|
-
throw
|
|
523
|
+
throw codedError("INVALID_STAGE_ORDER", "milestone cannot run after finish.");
|
|
382
524
|
}
|
|
383
525
|
await executeStage(name, "milestone", stage);
|
|
384
526
|
},
|
|
385
527
|
async finish(name, stage) {
|
|
386
|
-
if (!entered) throw
|
|
528
|
+
if (!entered) throw codedError("INVALID_STAGE_ORDER", "finish must follow enter.");
|
|
387
529
|
if (finished) {
|
|
388
|
-
throw
|
|
530
|
+
throw codedError("INVALID_STAGE_ORDER", "finish may only be called once.");
|
|
389
531
|
}
|
|
390
532
|
await executeStage(name, stage.kind, stage);
|
|
391
533
|
finished = true;
|
|
@@ -394,19 +536,22 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
394
536
|
const milestones = evidence.stages.filter(
|
|
395
537
|
(stage) => stage.kind === "milestone"
|
|
396
538
|
);
|
|
397
|
-
if (!entered) throw
|
|
539
|
+
if (!entered) throw codedError("INCOMPLETE_PLAYTHROUGH_EVIDENCE", "playthroughTest must call enter once.");
|
|
398
540
|
if (milestones.length < MIN_MILESTONES) {
|
|
399
|
-
throw
|
|
541
|
+
throw codedError(
|
|
542
|
+
"INCOMPLETE_PLAYTHROUGH_EVIDENCE",
|
|
400
543
|
`playthroughTest requires at least ${MIN_MILESTONES} named gameplay milestones between enter and finish; received ${milestones.length}.`
|
|
401
544
|
);
|
|
402
545
|
}
|
|
403
546
|
if (!finished) {
|
|
404
|
-
throw
|
|
547
|
+
throw codedError(
|
|
548
|
+
"INCOMPLETE_PLAYTHROUGH_EVIDENCE",
|
|
405
549
|
'playthroughTest must call finish with kind "progress" or "terminal".'
|
|
406
550
|
);
|
|
407
551
|
}
|
|
408
552
|
if (evidence.stages.length < MIN_STAGES) {
|
|
409
|
-
throw
|
|
553
|
+
throw codedError(
|
|
554
|
+
"INCOMPLETE_PLAYTHROUGH_EVIDENCE",
|
|
410
555
|
`playthroughTest requires at least ${MIN_STAGES} evidenced stages: enter, ${MIN_MILESTONES} named milestones, and finish.`
|
|
411
556
|
);
|
|
412
557
|
}
|
|
@@ -414,6 +559,7 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
414
559
|
} catch (error) {
|
|
415
560
|
const trace = captureFailureTrace();
|
|
416
561
|
metadata.trace = trace;
|
|
562
|
+
metadata.failure = createFailureDiagnostic("playthrough", error);
|
|
417
563
|
try {
|
|
418
564
|
await annotate(trace, REACT_PLAYTHROUGH_TRACE_ANNOTATION);
|
|
419
565
|
} catch {
|