miaoda-game-devkit 0.6.2 → 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/react/index.js +11 -1
- package/dist/react/index.mjs +11 -1
- package/dist/react/testing.d.mts +11 -1
- package/dist/react/testing.d.ts +11 -1
- package/dist/react/testing.js +158 -31
- package/dist/react/testing.mjs +158 -31
- package/dist/react/vitest-config.js +319 -71
- package/dist/react/vitest-config.mjs +319 -71
- package/dist/react/vitest-setup.js +87 -4
- package/dist/react/vitest-setup.mjs +87 -4
- package/package.json +1 -1
package/dist/react/index.js
CHANGED
|
@@ -54,6 +54,15 @@ function useOwnedGameController(createController) {
|
|
|
54
54
|
|
|
55
55
|
// src/react/use-game-controller.ts
|
|
56
56
|
var import_react2 = require("react");
|
|
57
|
+
|
|
58
|
+
// src/react/react-error-diagnostics.ts
|
|
59
|
+
function codedError(code, message) {
|
|
60
|
+
const error = new Error(message);
|
|
61
|
+
error.code = code;
|
|
62
|
+
return error;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/react/use-game-controller.ts
|
|
57
66
|
function useGameController(createController) {
|
|
58
67
|
const game = useOwnedGameController(createController);
|
|
59
68
|
useSnapshotIntegrityCheck(game);
|
|
@@ -91,7 +100,8 @@ function useSnapshotIntegrityCheck(game) {
|
|
|
91
100
|
}
|
|
92
101
|
if (previousJson !== void 0 && nextJson !== previousJson) {
|
|
93
102
|
reported.current = true;
|
|
94
|
-
throw
|
|
103
|
+
throw codedError(
|
|
104
|
+
"GAME_SNAPSHOT_REFERENCE_REUSED",
|
|
95
105
|
"Game state changed while snapshot() returned the same reference. React compares snapshots by reference and skips the render when Object.is(previous, next) is true, so the UI freezes. Publish a new top-level snapshot before each notification, for example cachedSnapshot = { ...state }, and never expose a mutable internal object as the snapshot."
|
|
96
106
|
);
|
|
97
107
|
}
|
package/dist/react/index.mjs
CHANGED
|
@@ -26,6 +26,15 @@ function useOwnedGameController(createController) {
|
|
|
26
26
|
|
|
27
27
|
// src/react/use-game-controller.ts
|
|
28
28
|
import { useEffect as useEffect2, useRef as useRef2, useSyncExternalStore } from "react";
|
|
29
|
+
|
|
30
|
+
// src/react/react-error-diagnostics.ts
|
|
31
|
+
function codedError(code, message) {
|
|
32
|
+
const error = new Error(message);
|
|
33
|
+
error.code = code;
|
|
34
|
+
return error;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/react/use-game-controller.ts
|
|
29
38
|
function useGameController(createController) {
|
|
30
39
|
const game = useOwnedGameController(createController);
|
|
31
40
|
useSnapshotIntegrityCheck(game);
|
|
@@ -63,7 +72,8 @@ function useSnapshotIntegrityCheck(game) {
|
|
|
63
72
|
}
|
|
64
73
|
if (previousJson !== void 0 && nextJson !== previousJson) {
|
|
65
74
|
reported.current = true;
|
|
66
|
-
throw
|
|
75
|
+
throw codedError(
|
|
76
|
+
"GAME_SNAPSHOT_REFERENCE_REUSED",
|
|
67
77
|
"Game state changed while snapshot() returned the same reference. React compares snapshots by reference and skips the render when Object.is(previous, next) is true, so the UI freezes. Publish a new top-level snapshot before each notification, for example cachedSnapshot = { ...state }, and never expose a mutable internal object as the snapshot."
|
|
68
78
|
);
|
|
69
79
|
}
|
package/dist/react/testing.d.mts
CHANGED
|
@@ -22,6 +22,15 @@ declare class ManualGameClock implements GameClock {
|
|
|
22
22
|
pendingTimerCount(): number;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
interface ReactFailureEntry {
|
|
26
|
+
code: string;
|
|
27
|
+
message: string;
|
|
28
|
+
}
|
|
29
|
+
interface ReactFailureDiagnostic {
|
|
30
|
+
source: "playthrough" | "test-runtime";
|
|
31
|
+
entries: ReactFailureEntry[];
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
/** 有界推进的通用配置,不绑定任何特定 Core、phase 或时钟实现。 */
|
|
26
35
|
interface StepUntilOptions {
|
|
27
36
|
/** 条件仍不成立时允许执行的最大生产步数,默认 120。 */
|
|
@@ -57,9 +66,10 @@ interface ReactPlaythroughEvidence {
|
|
|
57
66
|
}
|
|
58
67
|
/** 通过 Vitest task metadata 从 worker 传递给主线程 reporter 的数据。 */
|
|
59
68
|
interface ReactPlaythroughMetadata {
|
|
60
|
-
version:
|
|
69
|
+
version: 5;
|
|
61
70
|
waiverReason?: string;
|
|
62
71
|
trace?: string;
|
|
72
|
+
failure?: ReactFailureDiagnostic;
|
|
63
73
|
evidence: ReactPlaythroughEvidence;
|
|
64
74
|
}
|
|
65
75
|
interface ReactPlaythroughOptions {
|
package/dist/react/testing.d.ts
CHANGED
|
@@ -22,6 +22,15 @@ declare class ManualGameClock implements GameClock {
|
|
|
22
22
|
pendingTimerCount(): number;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
interface ReactFailureEntry {
|
|
26
|
+
code: string;
|
|
27
|
+
message: string;
|
|
28
|
+
}
|
|
29
|
+
interface ReactFailureDiagnostic {
|
|
30
|
+
source: "playthrough" | "test-runtime";
|
|
31
|
+
entries: ReactFailureEntry[];
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
/** 有界推进的通用配置,不绑定任何特定 Core、phase 或时钟实现。 */
|
|
26
35
|
interface StepUntilOptions {
|
|
27
36
|
/** 条件仍不成立时允许执行的最大生产步数,默认 120。 */
|
|
@@ -57,9 +66,10 @@ interface ReactPlaythroughEvidence {
|
|
|
57
66
|
}
|
|
58
67
|
/** 通过 Vitest task metadata 从 worker 传递给主线程 reporter 的数据。 */
|
|
59
68
|
interface ReactPlaythroughMetadata {
|
|
60
|
-
version:
|
|
69
|
+
version: 5;
|
|
61
70
|
waiverReason?: string;
|
|
62
71
|
trace?: string;
|
|
72
|
+
failure?: ReactFailureDiagnostic;
|
|
63
73
|
evidence: ReactPlaythroughEvidence;
|
|
64
74
|
}
|
|
65
75
|
interface ReactPlaythroughOptions {
|
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
|
}
|
|
@@ -198,14 +299,18 @@ function sampleObservedState(observe, stage) {
|
|
|
198
299
|
try {
|
|
199
300
|
value = observe();
|
|
200
301
|
} catch (error) {
|
|
201
|
-
throw
|
|
302
|
+
throw codedError(
|
|
303
|
+
"OBSERVE_FAILED",
|
|
304
|
+
`observe() threw at ${stage}: ${String(error)}`
|
|
305
|
+
);
|
|
202
306
|
}
|
|
203
307
|
try {
|
|
204
308
|
const fingerprint = JSON.stringify(value);
|
|
205
309
|
if (fingerprint === void 0) throw new Error("unsupported value");
|
|
206
310
|
return { fingerprint, formatted: formatState(fingerprint) };
|
|
207
311
|
} catch {
|
|
208
|
-
throw
|
|
312
|
+
throw codedError(
|
|
313
|
+
"OBSERVE_NOT_SERIALIZABLE",
|
|
209
314
|
`observe() must return JSON-serializable read-only state; sampling failed at ${stage}.`
|
|
210
315
|
);
|
|
211
316
|
}
|
|
@@ -218,7 +323,7 @@ function createEvidence() {
|
|
|
218
323
|
return { domInputEvents: 0, stages: [], verified: false };
|
|
219
324
|
}
|
|
220
325
|
function createMetadata(waiverReason) {
|
|
221
|
-
return { version:
|
|
326
|
+
return { version: 5, waiverReason, evidence: createEvidence() };
|
|
222
327
|
}
|
|
223
328
|
function stageLabel(kind, name) {
|
|
224
329
|
return kind === "entered" ? "entered" : `${kind}(${JSON.stringify(name)})`;
|
|
@@ -254,6 +359,7 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
254
359
|
async ({ annotate, expect, onTestFailed, signal }) => {
|
|
255
360
|
metadata.evidence = createEvidence();
|
|
256
361
|
metadata.trace = void 0;
|
|
362
|
+
metadata.failure = void 0;
|
|
257
363
|
const evidence = metadata.evidence;
|
|
258
364
|
let entered = false;
|
|
259
365
|
let finished = false;
|
|
@@ -284,8 +390,12 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
284
390
|
return "stages=none";
|
|
285
391
|
}
|
|
286
392
|
};
|
|
287
|
-
onTestFailed(() => {
|
|
393
|
+
onTestFailed(({ task }) => {
|
|
288
394
|
metadata.trace ??= captureFailureTrace();
|
|
395
|
+
metadata.failure = appendCurrentAttemptFailures(
|
|
396
|
+
metadata.failure,
|
|
397
|
+
task.result?.errors ?? []
|
|
398
|
+
);
|
|
289
399
|
});
|
|
290
400
|
for (const event of INPUT_EVENTS) {
|
|
291
401
|
document.addEventListener(event, recordInput, true);
|
|
@@ -295,7 +405,8 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
295
405
|
try {
|
|
296
406
|
const view = (0, import_react2.render)(element);
|
|
297
407
|
if (view.container.childNodes.length === 0) {
|
|
298
|
-
throw
|
|
408
|
+
throw codedError(
|
|
409
|
+
"PRODUCTION_ENTRY_NOT_RENDERED",
|
|
299
410
|
"playthroughTest must render the production game entry."
|
|
300
411
|
);
|
|
301
412
|
}
|
|
@@ -324,17 +435,22 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
324
435
|
const executeStage = async (name, kind, stage) => {
|
|
325
436
|
const normalizedName = name.trim();
|
|
326
437
|
if (normalizedName.length === 0) {
|
|
327
|
-
throw
|
|
438
|
+
throw codedError(
|
|
439
|
+
"INVALID_STAGE_NAME",
|
|
440
|
+
"playthrough stage names must be non-empty strings."
|
|
441
|
+
);
|
|
328
442
|
}
|
|
329
443
|
if (evidence.stages.some(
|
|
330
444
|
(completed) => completed.name === normalizedName
|
|
331
445
|
)) {
|
|
332
|
-
throw
|
|
446
|
+
throw codedError(
|
|
447
|
+
"DUPLICATE_STAGE_NAME",
|
|
333
448
|
`playthrough stage ${JSON.stringify(normalizedName)} may only be recorded once.`
|
|
334
449
|
);
|
|
335
450
|
}
|
|
336
451
|
if (kind === "milestone" && ["entered", "progress", "terminal"].includes(normalizedName)) {
|
|
337
|
-
throw
|
|
452
|
+
throw codedError(
|
|
453
|
+
"RESERVED_STAGE_NAME",
|
|
338
454
|
`milestone name ${JSON.stringify(normalizedName)} is reserved; use a game-domain name such as "first-point" or "boss-entered".`
|
|
339
455
|
);
|
|
340
456
|
}
|
|
@@ -342,12 +458,14 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
342
458
|
activeStage = { name: normalizedName, kind, before };
|
|
343
459
|
stepTrace = { bound: stage.maxSteps ?? 120 };
|
|
344
460
|
if (stage.step && !playthroughOptions?.observe) {
|
|
345
|
-
throw
|
|
461
|
+
throw codedError(
|
|
462
|
+
"AUTHORITATIVE_OBSERVE_REQUIRED_FOR_STEP",
|
|
346
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.`
|
|
347
464
|
);
|
|
348
465
|
}
|
|
349
466
|
if (stage.until()) {
|
|
350
|
-
throw
|
|
467
|
+
throw codedError(
|
|
468
|
+
"STAGE_OUTCOME_ALREADY_REACHED",
|
|
351
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.`
|
|
352
470
|
);
|
|
353
471
|
}
|
|
@@ -361,12 +479,14 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
361
479
|
acceptingStageInput = false;
|
|
362
480
|
}
|
|
363
481
|
if (evidence.domInputEvents === inputsBefore) {
|
|
364
|
-
throw
|
|
482
|
+
throw codedError(
|
|
483
|
+
"PRODUCTION_INPUT_NOT_DISPATCHED",
|
|
365
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.`
|
|
366
485
|
);
|
|
367
486
|
}
|
|
368
487
|
if (activeStageTargetedCanvas && !playthroughOptions?.observe) {
|
|
369
|
-
throw
|
|
488
|
+
throw codedError(
|
|
489
|
+
"AUTHORITATIVE_OBSERVE_REQUIRED_FOR_CANVAS",
|
|
370
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.`
|
|
371
491
|
);
|
|
372
492
|
}
|
|
@@ -385,7 +505,8 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
385
505
|
});
|
|
386
506
|
stepTrace = { bound: stepBound, completed: steps };
|
|
387
507
|
if (!stage.act && advancedSteps === 0) {
|
|
388
|
-
throw
|
|
508
|
+
throw codedError(
|
|
509
|
+
"AUTONOMOUS_STAGE_NOT_ADVANCED",
|
|
389
510
|
`${stageLabel(kind, normalizedName)} did not execute its deterministic step. Autonomous stages must advance production time or frames at least once.`
|
|
390
511
|
);
|
|
391
512
|
}
|
|
@@ -393,14 +514,16 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
393
514
|
await stage.assert({ expect, user, view });
|
|
394
515
|
const assertions = expect.getState().assertionCalls - assertionsBefore;
|
|
395
516
|
if (assertions === 0) {
|
|
396
|
-
throw
|
|
517
|
+
throw codedError(
|
|
518
|
+
"STAGE_ASSERTION_MISSING",
|
|
397
519
|
`${stageLabel(kind, normalizedName)} assert must call the expect provided by playthroughTest at least once.`
|
|
398
520
|
);
|
|
399
521
|
}
|
|
400
522
|
const after = sampleState(`after ${normalizedName}`);
|
|
401
523
|
if (after.fingerprint === before.fingerprint) {
|
|
402
524
|
const source = playthroughOptions?.observe ? "authoritative observe() state" : "production DOM";
|
|
403
|
-
throw
|
|
525
|
+
throw codedError(
|
|
526
|
+
"STAGE_STATE_UNCHANGED",
|
|
404
527
|
`${stageLabel(kind, normalizedName)} did not change the ${source} from the previous stage. Each milestone must prove a new gameplay result.`
|
|
405
528
|
);
|
|
406
529
|
}
|
|
@@ -422,27 +545,27 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
422
545
|
user,
|
|
423
546
|
async enter(stage) {
|
|
424
547
|
if (entered) {
|
|
425
|
-
throw
|
|
548
|
+
throw codedError("INVALID_STAGE_ORDER", "enter may only be called once.");
|
|
426
549
|
}
|
|
427
550
|
if (evidence.stages.length > 0) {
|
|
428
|
-
throw
|
|
551
|
+
throw codedError("INVALID_STAGE_ORDER", "enter must be the first playthrough stage.");
|
|
429
552
|
}
|
|
430
553
|
await executeStage("entered", "entered", stage);
|
|
431
554
|
entered = true;
|
|
432
555
|
},
|
|
433
556
|
async milestone(name, stage) {
|
|
434
557
|
if (!entered) {
|
|
435
|
-
throw
|
|
558
|
+
throw codedError("INVALID_STAGE_ORDER", "milestone must follow enter.");
|
|
436
559
|
}
|
|
437
560
|
if (finished) {
|
|
438
|
-
throw
|
|
561
|
+
throw codedError("INVALID_STAGE_ORDER", "milestone cannot run after finish.");
|
|
439
562
|
}
|
|
440
563
|
await executeStage(name, "milestone", stage);
|
|
441
564
|
},
|
|
442
565
|
async finish(name, stage) {
|
|
443
|
-
if (!entered) throw
|
|
566
|
+
if (!entered) throw codedError("INVALID_STAGE_ORDER", "finish must follow enter.");
|
|
444
567
|
if (finished) {
|
|
445
|
-
throw
|
|
568
|
+
throw codedError("INVALID_STAGE_ORDER", "finish may only be called once.");
|
|
446
569
|
}
|
|
447
570
|
await executeStage(name, stage.kind, stage);
|
|
448
571
|
finished = true;
|
|
@@ -451,19 +574,22 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
451
574
|
const milestones = evidence.stages.filter(
|
|
452
575
|
(stage) => stage.kind === "milestone"
|
|
453
576
|
);
|
|
454
|
-
if (!entered) throw
|
|
577
|
+
if (!entered) throw codedError("INCOMPLETE_PLAYTHROUGH_EVIDENCE", "playthroughTest must call enter once.");
|
|
455
578
|
if (milestones.length < MIN_MILESTONES) {
|
|
456
|
-
throw
|
|
579
|
+
throw codedError(
|
|
580
|
+
"INCOMPLETE_PLAYTHROUGH_EVIDENCE",
|
|
457
581
|
`playthroughTest requires at least ${MIN_MILESTONES} named gameplay milestones between enter and finish; received ${milestones.length}.`
|
|
458
582
|
);
|
|
459
583
|
}
|
|
460
584
|
if (!finished) {
|
|
461
|
-
throw
|
|
585
|
+
throw codedError(
|
|
586
|
+
"INCOMPLETE_PLAYTHROUGH_EVIDENCE",
|
|
462
587
|
'playthroughTest must call finish with kind "progress" or "terminal".'
|
|
463
588
|
);
|
|
464
589
|
}
|
|
465
590
|
if (evidence.stages.length < MIN_STAGES) {
|
|
466
|
-
throw
|
|
591
|
+
throw codedError(
|
|
592
|
+
"INCOMPLETE_PLAYTHROUGH_EVIDENCE",
|
|
467
593
|
`playthroughTest requires at least ${MIN_STAGES} evidenced stages: enter, ${MIN_MILESTONES} named milestones, and finish.`
|
|
468
594
|
);
|
|
469
595
|
}
|
|
@@ -471,6 +597,7 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
471
597
|
} catch (error) {
|
|
472
598
|
const trace = captureFailureTrace();
|
|
473
599
|
metadata.trace = trace;
|
|
600
|
+
metadata.failure = createFailureDiagnostic("playthrough", error);
|
|
474
601
|
try {
|
|
475
602
|
await annotate(trace, REACT_PLAYTHROUGH_TRACE_ANNOTATION);
|
|
476
603
|
} catch {
|