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.js
CHANGED
|
@@ -96,14 +96,112 @@ var import_react2 = require("@testing-library/react");
|
|
|
96
96
|
var import_user_event = __toESM(require("@testing-library/user-event"));
|
|
97
97
|
var import_vitest = require("vitest");
|
|
98
98
|
|
|
99
|
+
// src/react/react-error-diagnostics.ts
|
|
100
|
+
var MAX_DIAGNOSTIC_LENGTH = 1e3;
|
|
101
|
+
function truncate(value) {
|
|
102
|
+
const trimmed = value.trim();
|
|
103
|
+
if (trimmed.length <= MAX_DIAGNOSTIC_LENGTH) return trimmed;
|
|
104
|
+
return `${trimmed.slice(0, MAX_DIAGNOSTIC_LENGTH - 1)}\u2026`;
|
|
105
|
+
}
|
|
106
|
+
function safeJson(value) {
|
|
107
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
108
|
+
try {
|
|
109
|
+
return JSON.stringify(value, (_key, nested) => {
|
|
110
|
+
if (typeof nested === "bigint") return `${nested}n`;
|
|
111
|
+
if (typeof nested === "function") {
|
|
112
|
+
return `Function<${nested.name || "anonymous"}>`;
|
|
113
|
+
}
|
|
114
|
+
if (typeof nested === "symbol") return nested.toString();
|
|
115
|
+
if (nested && typeof nested === "object") {
|
|
116
|
+
if (seen.has(nested)) return "[Circular]";
|
|
117
|
+
seen.add(nested);
|
|
118
|
+
}
|
|
119
|
+
return nested;
|
|
120
|
+
});
|
|
121
|
+
} catch {
|
|
122
|
+
return void 0;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function collectEntries(value, fallbackCode, seen) {
|
|
126
|
+
if (typeof value === "string") {
|
|
127
|
+
return value.trim() ? [{ code: fallbackCode, message: truncate(value) }] : [];
|
|
128
|
+
}
|
|
129
|
+
if (value === null || value === void 0 || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "symbol") {
|
|
130
|
+
return [{ code: fallbackCode, message: String(value) }];
|
|
131
|
+
}
|
|
132
|
+
if (typeof value === "function") {
|
|
133
|
+
return [
|
|
134
|
+
{ code: fallbackCode, message: `Function<${value.name || "anonymous"}>` }
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
if (seen.has(value)) return [];
|
|
138
|
+
seen.add(value);
|
|
139
|
+
if (Array.isArray(value)) {
|
|
140
|
+
return value.flatMap((item) => collectEntries(item, fallbackCode, seen));
|
|
141
|
+
}
|
|
142
|
+
const record = value;
|
|
143
|
+
const code = typeof record.code === "string" && record.code.trim() ? record.code.trim() : fallbackCode;
|
|
144
|
+
const entries = [];
|
|
145
|
+
if (typeof record.message === "string" && record.message.trim()) {
|
|
146
|
+
entries.push({ code, message: truncate(record.message) });
|
|
147
|
+
}
|
|
148
|
+
if (record.cause !== void 0) {
|
|
149
|
+
entries.push(...collectEntries(record.cause, fallbackCode, seen));
|
|
150
|
+
}
|
|
151
|
+
if (Array.isArray(record.errors)) {
|
|
152
|
+
entries.push(...collectEntries(record.errors, fallbackCode, seen));
|
|
153
|
+
}
|
|
154
|
+
if (entries.length > 0) return entries;
|
|
155
|
+
if (typeof record.stack === "string" && record.stack.trim()) {
|
|
156
|
+
return [{ code, message: truncate(record.stack) }];
|
|
157
|
+
}
|
|
158
|
+
const json = safeJson(value);
|
|
159
|
+
return json && json !== "{}" ? [{ code, message: truncate(json) }] : [];
|
|
160
|
+
}
|
|
161
|
+
function extractFailureEntries(value, fallbackCode = "TEST_FAILURE") {
|
|
162
|
+
const entries = collectEntries(value, fallbackCode, /* @__PURE__ */ new WeakSet());
|
|
163
|
+
const keys = /* @__PURE__ */ new Set();
|
|
164
|
+
return entries.filter((entry) => {
|
|
165
|
+
const key = `${entry.code}\0${entry.message}`;
|
|
166
|
+
if (keys.has(key)) return false;
|
|
167
|
+
keys.add(key);
|
|
168
|
+
return true;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
function createFailureDiagnostic(source, value) {
|
|
172
|
+
return { source, entries: extractFailureEntries(value) };
|
|
173
|
+
}
|
|
174
|
+
function appendCurrentAttemptFailures(current, runnerValue) {
|
|
175
|
+
const runner = createFailureDiagnostic("test-runtime", runnerValue);
|
|
176
|
+
if (!current || current.entries.length === 0) return runner;
|
|
177
|
+
const primary = current.entries[0];
|
|
178
|
+
const currentStart = runner.entries.findIndex(
|
|
179
|
+
(entry) => entry.code === primary.code && entry.message === primary.message
|
|
180
|
+
);
|
|
181
|
+
if (currentStart < 0) return current;
|
|
182
|
+
return {
|
|
183
|
+
source: current.source,
|
|
184
|
+
entries: extractFailureEntries([
|
|
185
|
+
...current.entries,
|
|
186
|
+
...runner.entries.slice(currentStart + 1)
|
|
187
|
+
])
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function codedError(code, message) {
|
|
191
|
+
const error = new Error(message);
|
|
192
|
+
error.code = code;
|
|
193
|
+
return error;
|
|
194
|
+
}
|
|
195
|
+
|
|
99
196
|
// src/react/react-playthrough-core.ts
|
|
100
197
|
var import_react = require("@testing-library/react");
|
|
101
198
|
function throwIfAborted(signal) {
|
|
102
199
|
if (!signal?.aborted) return;
|
|
103
200
|
if (signal.reason instanceof Error) throw signal.reason;
|
|
104
|
-
throw
|
|
105
|
-
|
|
106
|
-
|
|
201
|
+
throw codedError(
|
|
202
|
+
"PLAYTHROUGH_CANCELLED",
|
|
203
|
+
`Playthrough advancement was cancelled. Cause: ${String(signal.reason)}`
|
|
204
|
+
);
|
|
107
205
|
}
|
|
108
206
|
function formatDiagnostics(read) {
|
|
109
207
|
if (!read) return void 0;
|
|
@@ -119,7 +217,8 @@ function normalizePlaythroughWaiverReason(waiverReason) {
|
|
|
119
217
|
if (waiverReason === void 0) return void 0;
|
|
120
218
|
const reason = waiverReason.trim();
|
|
121
219
|
if (reason.length < 20) {
|
|
122
|
-
throw
|
|
220
|
+
throw codedError(
|
|
221
|
+
"INVALID_PLAYTHROUGH_WAIVER",
|
|
123
222
|
"playthroughTest.skip reason must contain at least 20 characters."
|
|
124
223
|
);
|
|
125
224
|
}
|
|
@@ -128,7 +227,8 @@ function normalizePlaythroughWaiverReason(waiverReason) {
|
|
|
128
227
|
async function runBoundedUntil(condition, options = {}) {
|
|
129
228
|
const maxSteps = options.maxSteps ?? 120;
|
|
130
229
|
if (!Number.isSafeInteger(maxSteps) || maxSteps < 0 || maxSteps > 1e4) {
|
|
131
|
-
throw
|
|
230
|
+
throw codedError(
|
|
231
|
+
"INVALID_STEP_BOUND",
|
|
132
232
|
"stepUntil maxSteps must be a safe integer between 0 and 10000."
|
|
133
233
|
);
|
|
134
234
|
}
|
|
@@ -145,7 +245,8 @@ async function runBoundedUntil(condition, options = {}) {
|
|
|
145
245
|
const diagnostics = formatDiagnostics(options.diagnostics);
|
|
146
246
|
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().";
|
|
147
247
|
const suffix = diagnostics ? ` Last diagnostics: ${diagnostics}` : "";
|
|
148
|
-
throw
|
|
248
|
+
throw codedError(
|
|
249
|
+
options.step ? "PLAYTHROUGH_BOUND_EXHAUSTED" : "PLAYTHROUGH_CLOCK_NOT_ADVANCED",
|
|
149
250
|
`Playthrough outcome was not reached within ${maxSteps} steps. ${guidance}${suffix}`
|
|
150
251
|
);
|
|
151
252
|
}
|
|
@@ -164,6 +265,11 @@ var MIN_STAGES = 5;
|
|
|
164
265
|
var MIN_MILESTONES = 3;
|
|
165
266
|
var MAX_TRACE_VALUE_LENGTH = 140;
|
|
166
267
|
var MAX_TRACE_LENGTH = 720;
|
|
268
|
+
function eventTargetsCanvas(event) {
|
|
269
|
+
const path = typeof event.composedPath === "function" ? event.composedPath() : [];
|
|
270
|
+
if (path.some((target) => target instanceof HTMLCanvasElement)) return true;
|
|
271
|
+
return event.target instanceof HTMLCanvasElement;
|
|
272
|
+
}
|
|
167
273
|
var REACT_PLAYTHROUGH_TRACE_ANNOTATION = "miaoda:react-playthrough-trace";
|
|
168
274
|
function truncateTraceValue(value, limit) {
|
|
169
275
|
const compact = value.replace(/\s+/g, " ").trim();
|
|
@@ -193,14 +299,18 @@ function sampleObservedState(observe, stage) {
|
|
|
193
299
|
try {
|
|
194
300
|
value = observe();
|
|
195
301
|
} catch (error) {
|
|
196
|
-
throw
|
|
302
|
+
throw codedError(
|
|
303
|
+
"OBSERVE_FAILED",
|
|
304
|
+
`observe() threw at ${stage}: ${String(error)}`
|
|
305
|
+
);
|
|
197
306
|
}
|
|
198
307
|
try {
|
|
199
308
|
const fingerprint = JSON.stringify(value);
|
|
200
309
|
if (fingerprint === void 0) throw new Error("unsupported value");
|
|
201
310
|
return { fingerprint, formatted: formatState(fingerprint) };
|
|
202
311
|
} catch {
|
|
203
|
-
throw
|
|
312
|
+
throw codedError(
|
|
313
|
+
"OBSERVE_NOT_SERIALIZABLE",
|
|
204
314
|
`observe() must return JSON-serializable read-only state; sampling failed at ${stage}.`
|
|
205
315
|
);
|
|
206
316
|
}
|
|
@@ -213,7 +323,7 @@ function createEvidence() {
|
|
|
213
323
|
return { domInputEvents: 0, stages: [], verified: false };
|
|
214
324
|
}
|
|
215
325
|
function createMetadata(waiverReason) {
|
|
216
|
-
return { version:
|
|
326
|
+
return { version: 5, waiverReason, evidence: createEvidence() };
|
|
217
327
|
}
|
|
218
328
|
function stageLabel(kind, name) {
|
|
219
329
|
return kind === "entered" ? "entered" : `${kind}(${JSON.stringify(name)})`;
|
|
@@ -249,6 +359,7 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
249
359
|
async ({ annotate, expect, onTestFailed, signal }) => {
|
|
250
360
|
metadata.evidence = createEvidence();
|
|
251
361
|
metadata.trace = void 0;
|
|
362
|
+
metadata.failure = void 0;
|
|
252
363
|
const evidence = metadata.evidence;
|
|
253
364
|
let entered = false;
|
|
254
365
|
let finished = false;
|
|
@@ -257,9 +368,12 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
257
368
|
let stepTrace;
|
|
258
369
|
let failureTraceFactory;
|
|
259
370
|
let acceptingStageInput = false;
|
|
371
|
+
let activeStageTargetedCanvas = false;
|
|
260
372
|
let inputCaptureAttached = false;
|
|
261
|
-
const recordInput = () => {
|
|
262
|
-
if (acceptingStageInput)
|
|
373
|
+
const recordInput = (event) => {
|
|
374
|
+
if (!acceptingStageInput) return;
|
|
375
|
+
evidence.domInputEvents += 1;
|
|
376
|
+
activeStageTargetedCanvas ||= eventTargetsCanvas(event);
|
|
263
377
|
};
|
|
264
378
|
const stopInputCapture = () => {
|
|
265
379
|
acceptingStageInput = false;
|
|
@@ -276,8 +390,12 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
276
390
|
return "stages=none";
|
|
277
391
|
}
|
|
278
392
|
};
|
|
279
|
-
onTestFailed(() => {
|
|
393
|
+
onTestFailed(({ task }) => {
|
|
280
394
|
metadata.trace ??= captureFailureTrace();
|
|
395
|
+
metadata.failure = appendCurrentAttemptFailures(
|
|
396
|
+
metadata.failure,
|
|
397
|
+
task.result?.errors ?? []
|
|
398
|
+
);
|
|
281
399
|
});
|
|
282
400
|
for (const event of INPUT_EVENTS) {
|
|
283
401
|
document.addEventListener(event, recordInput, true);
|
|
@@ -287,7 +405,8 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
287
405
|
try {
|
|
288
406
|
const view = (0, import_react2.render)(element);
|
|
289
407
|
if (view.container.childNodes.length === 0) {
|
|
290
|
-
throw
|
|
408
|
+
throw codedError(
|
|
409
|
+
"PRODUCTION_ENTRY_NOT_RENDERED",
|
|
291
410
|
"playthroughTest must render the production game entry."
|
|
292
411
|
);
|
|
293
412
|
}
|
|
@@ -316,29 +435,42 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
316
435
|
const executeStage = async (name, kind, stage) => {
|
|
317
436
|
const normalizedName = name.trim();
|
|
318
437
|
if (normalizedName.length === 0) {
|
|
319
|
-
throw
|
|
438
|
+
throw codedError(
|
|
439
|
+
"INVALID_STAGE_NAME",
|
|
440
|
+
"playthrough stage names must be non-empty strings."
|
|
441
|
+
);
|
|
320
442
|
}
|
|
321
443
|
if (evidence.stages.some(
|
|
322
444
|
(completed) => completed.name === normalizedName
|
|
323
445
|
)) {
|
|
324
|
-
throw
|
|
446
|
+
throw codedError(
|
|
447
|
+
"DUPLICATE_STAGE_NAME",
|
|
325
448
|
`playthrough stage ${JSON.stringify(normalizedName)} may only be recorded once.`
|
|
326
449
|
);
|
|
327
450
|
}
|
|
328
451
|
if (kind === "milestone" && ["entered", "progress", "terminal"].includes(normalizedName)) {
|
|
329
|
-
throw
|
|
452
|
+
throw codedError(
|
|
453
|
+
"RESERVED_STAGE_NAME",
|
|
330
454
|
`milestone name ${JSON.stringify(normalizedName)} is reserved; use a game-domain name such as "first-point" or "boss-entered".`
|
|
331
455
|
);
|
|
332
456
|
}
|
|
333
457
|
const before = lastSample ?? sampleState(`before ${normalizedName}`);
|
|
334
458
|
activeStage = { name: normalizedName, kind, before };
|
|
335
459
|
stepTrace = { bound: stage.maxSteps ?? 120 };
|
|
460
|
+
if (stage.step && !playthroughOptions?.observe) {
|
|
461
|
+
throw codedError(
|
|
462
|
+
"AUTHORITATIVE_OBSERVE_REQUIRED_FOR_STEP",
|
|
463
|
+
`${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.`
|
|
464
|
+
);
|
|
465
|
+
}
|
|
336
466
|
if (stage.until()) {
|
|
337
|
-
throw
|
|
467
|
+
throw codedError(
|
|
468
|
+
"STAGE_OUTCOME_ALREADY_REACHED",
|
|
338
469
|
`${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.`
|
|
339
470
|
);
|
|
340
471
|
}
|
|
341
472
|
const inputsBefore = evidence.domInputEvents;
|
|
473
|
+
activeStageTargetedCanvas = false;
|
|
342
474
|
if (stage.act) {
|
|
343
475
|
acceptingStageInput = true;
|
|
344
476
|
try {
|
|
@@ -347,10 +479,17 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
347
479
|
acceptingStageInput = false;
|
|
348
480
|
}
|
|
349
481
|
if (evidence.domInputEvents === inputsBefore) {
|
|
350
|
-
throw
|
|
482
|
+
throw codedError(
|
|
483
|
+
"PRODUCTION_INPUT_NOT_DISPATCHED",
|
|
351
484
|
`${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.`
|
|
352
485
|
);
|
|
353
486
|
}
|
|
487
|
+
if (activeStageTargetedCanvas && !playthroughOptions?.observe) {
|
|
488
|
+
throw codedError(
|
|
489
|
+
"AUTHORITATIVE_OBSERVE_REQUIRED_FOR_CANVAS",
|
|
490
|
+
`${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.`
|
|
491
|
+
);
|
|
492
|
+
}
|
|
354
493
|
}
|
|
355
494
|
let advancedSteps = 0;
|
|
356
495
|
const stepBound = stage.maxSteps ?? 120;
|
|
@@ -366,7 +505,8 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
366
505
|
});
|
|
367
506
|
stepTrace = { bound: stepBound, completed: steps };
|
|
368
507
|
if (!stage.act && advancedSteps === 0) {
|
|
369
|
-
throw
|
|
508
|
+
throw codedError(
|
|
509
|
+
"AUTONOMOUS_STAGE_NOT_ADVANCED",
|
|
370
510
|
`${stageLabel(kind, normalizedName)} did not execute its deterministic step. Autonomous stages must advance production time or frames at least once.`
|
|
371
511
|
);
|
|
372
512
|
}
|
|
@@ -374,14 +514,16 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
374
514
|
await stage.assert({ expect, user, view });
|
|
375
515
|
const assertions = expect.getState().assertionCalls - assertionsBefore;
|
|
376
516
|
if (assertions === 0) {
|
|
377
|
-
throw
|
|
517
|
+
throw codedError(
|
|
518
|
+
"STAGE_ASSERTION_MISSING",
|
|
378
519
|
`${stageLabel(kind, normalizedName)} assert must call the expect provided by playthroughTest at least once.`
|
|
379
520
|
);
|
|
380
521
|
}
|
|
381
522
|
const after = sampleState(`after ${normalizedName}`);
|
|
382
523
|
if (after.fingerprint === before.fingerprint) {
|
|
383
524
|
const source = playthroughOptions?.observe ? "authoritative observe() state" : "production DOM";
|
|
384
|
-
throw
|
|
525
|
+
throw codedError(
|
|
526
|
+
"STAGE_STATE_UNCHANGED",
|
|
385
527
|
`${stageLabel(kind, normalizedName)} did not change the ${source} from the previous stage. Each milestone must prove a new gameplay result.`
|
|
386
528
|
);
|
|
387
529
|
}
|
|
@@ -403,27 +545,27 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
403
545
|
user,
|
|
404
546
|
async enter(stage) {
|
|
405
547
|
if (entered) {
|
|
406
|
-
throw
|
|
548
|
+
throw codedError("INVALID_STAGE_ORDER", "enter may only be called once.");
|
|
407
549
|
}
|
|
408
550
|
if (evidence.stages.length > 0) {
|
|
409
|
-
throw
|
|
551
|
+
throw codedError("INVALID_STAGE_ORDER", "enter must be the first playthrough stage.");
|
|
410
552
|
}
|
|
411
553
|
await executeStage("entered", "entered", stage);
|
|
412
554
|
entered = true;
|
|
413
555
|
},
|
|
414
556
|
async milestone(name, stage) {
|
|
415
557
|
if (!entered) {
|
|
416
|
-
throw
|
|
558
|
+
throw codedError("INVALID_STAGE_ORDER", "milestone must follow enter.");
|
|
417
559
|
}
|
|
418
560
|
if (finished) {
|
|
419
|
-
throw
|
|
561
|
+
throw codedError("INVALID_STAGE_ORDER", "milestone cannot run after finish.");
|
|
420
562
|
}
|
|
421
563
|
await executeStage(name, "milestone", stage);
|
|
422
564
|
},
|
|
423
565
|
async finish(name, stage) {
|
|
424
|
-
if (!entered) throw
|
|
566
|
+
if (!entered) throw codedError("INVALID_STAGE_ORDER", "finish must follow enter.");
|
|
425
567
|
if (finished) {
|
|
426
|
-
throw
|
|
568
|
+
throw codedError("INVALID_STAGE_ORDER", "finish may only be called once.");
|
|
427
569
|
}
|
|
428
570
|
await executeStage(name, stage.kind, stage);
|
|
429
571
|
finished = true;
|
|
@@ -432,19 +574,22 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
432
574
|
const milestones = evidence.stages.filter(
|
|
433
575
|
(stage) => stage.kind === "milestone"
|
|
434
576
|
);
|
|
435
|
-
if (!entered) throw
|
|
577
|
+
if (!entered) throw codedError("INCOMPLETE_PLAYTHROUGH_EVIDENCE", "playthroughTest must call enter once.");
|
|
436
578
|
if (milestones.length < MIN_MILESTONES) {
|
|
437
|
-
throw
|
|
579
|
+
throw codedError(
|
|
580
|
+
"INCOMPLETE_PLAYTHROUGH_EVIDENCE",
|
|
438
581
|
`playthroughTest requires at least ${MIN_MILESTONES} named gameplay milestones between enter and finish; received ${milestones.length}.`
|
|
439
582
|
);
|
|
440
583
|
}
|
|
441
584
|
if (!finished) {
|
|
442
|
-
throw
|
|
585
|
+
throw codedError(
|
|
586
|
+
"INCOMPLETE_PLAYTHROUGH_EVIDENCE",
|
|
443
587
|
'playthroughTest must call finish with kind "progress" or "terminal".'
|
|
444
588
|
);
|
|
445
589
|
}
|
|
446
590
|
if (evidence.stages.length < MIN_STAGES) {
|
|
447
|
-
throw
|
|
591
|
+
throw codedError(
|
|
592
|
+
"INCOMPLETE_PLAYTHROUGH_EVIDENCE",
|
|
448
593
|
`playthroughTest requires at least ${MIN_STAGES} evidenced stages: enter, ${MIN_MILESTONES} named milestones, and finish.`
|
|
449
594
|
);
|
|
450
595
|
}
|
|
@@ -452,6 +597,7 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
452
597
|
} catch (error) {
|
|
453
598
|
const trace = captureFailureTrace();
|
|
454
599
|
metadata.trace = trace;
|
|
600
|
+
metadata.failure = createFailureDiagnostic("playthrough", error);
|
|
455
601
|
try {
|
|
456
602
|
await annotate(trace, REACT_PLAYTHROUGH_TRACE_ANNOTATION);
|
|
457
603
|
} catch {
|