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/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
|
}
|
|
@@ -160,14 +261,18 @@ function sampleObservedState(observe, stage) {
|
|
|
160
261
|
try {
|
|
161
262
|
value = observe();
|
|
162
263
|
} catch (error) {
|
|
163
|
-
throw
|
|
264
|
+
throw codedError(
|
|
265
|
+
"OBSERVE_FAILED",
|
|
266
|
+
`observe() threw at ${stage}: ${String(error)}`
|
|
267
|
+
);
|
|
164
268
|
}
|
|
165
269
|
try {
|
|
166
270
|
const fingerprint = JSON.stringify(value);
|
|
167
271
|
if (fingerprint === void 0) throw new Error("unsupported value");
|
|
168
272
|
return { fingerprint, formatted: formatState(fingerprint) };
|
|
169
273
|
} catch {
|
|
170
|
-
throw
|
|
274
|
+
throw codedError(
|
|
275
|
+
"OBSERVE_NOT_SERIALIZABLE",
|
|
171
276
|
`observe() must return JSON-serializable read-only state; sampling failed at ${stage}.`
|
|
172
277
|
);
|
|
173
278
|
}
|
|
@@ -180,7 +285,7 @@ function createEvidence() {
|
|
|
180
285
|
return { domInputEvents: 0, stages: [], verified: false };
|
|
181
286
|
}
|
|
182
287
|
function createMetadata(waiverReason) {
|
|
183
|
-
return { version:
|
|
288
|
+
return { version: 5, waiverReason, evidence: createEvidence() };
|
|
184
289
|
}
|
|
185
290
|
function stageLabel(kind, name) {
|
|
186
291
|
return kind === "entered" ? "entered" : `${kind}(${JSON.stringify(name)})`;
|
|
@@ -216,6 +321,7 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
216
321
|
async ({ annotate, expect, onTestFailed, signal }) => {
|
|
217
322
|
metadata.evidence = createEvidence();
|
|
218
323
|
metadata.trace = void 0;
|
|
324
|
+
metadata.failure = void 0;
|
|
219
325
|
const evidence = metadata.evidence;
|
|
220
326
|
let entered = false;
|
|
221
327
|
let finished = false;
|
|
@@ -246,8 +352,12 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
246
352
|
return "stages=none";
|
|
247
353
|
}
|
|
248
354
|
};
|
|
249
|
-
onTestFailed(() => {
|
|
355
|
+
onTestFailed(({ task }) => {
|
|
250
356
|
metadata.trace ??= captureFailureTrace();
|
|
357
|
+
metadata.failure = appendCurrentAttemptFailures(
|
|
358
|
+
metadata.failure,
|
|
359
|
+
task.result?.errors ?? []
|
|
360
|
+
);
|
|
251
361
|
});
|
|
252
362
|
for (const event of INPUT_EVENTS) {
|
|
253
363
|
document.addEventListener(event, recordInput, true);
|
|
@@ -257,7 +367,8 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
257
367
|
try {
|
|
258
368
|
const view = render(element);
|
|
259
369
|
if (view.container.childNodes.length === 0) {
|
|
260
|
-
throw
|
|
370
|
+
throw codedError(
|
|
371
|
+
"PRODUCTION_ENTRY_NOT_RENDERED",
|
|
261
372
|
"playthroughTest must render the production game entry."
|
|
262
373
|
);
|
|
263
374
|
}
|
|
@@ -286,17 +397,22 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
286
397
|
const executeStage = async (name, kind, stage) => {
|
|
287
398
|
const normalizedName = name.trim();
|
|
288
399
|
if (normalizedName.length === 0) {
|
|
289
|
-
throw
|
|
400
|
+
throw codedError(
|
|
401
|
+
"INVALID_STAGE_NAME",
|
|
402
|
+
"playthrough stage names must be non-empty strings."
|
|
403
|
+
);
|
|
290
404
|
}
|
|
291
405
|
if (evidence.stages.some(
|
|
292
406
|
(completed) => completed.name === normalizedName
|
|
293
407
|
)) {
|
|
294
|
-
throw
|
|
408
|
+
throw codedError(
|
|
409
|
+
"DUPLICATE_STAGE_NAME",
|
|
295
410
|
`playthrough stage ${JSON.stringify(normalizedName)} may only be recorded once.`
|
|
296
411
|
);
|
|
297
412
|
}
|
|
298
413
|
if (kind === "milestone" && ["entered", "progress", "terminal"].includes(normalizedName)) {
|
|
299
|
-
throw
|
|
414
|
+
throw codedError(
|
|
415
|
+
"RESERVED_STAGE_NAME",
|
|
300
416
|
`milestone name ${JSON.stringify(normalizedName)} is reserved; use a game-domain name such as "first-point" or "boss-entered".`
|
|
301
417
|
);
|
|
302
418
|
}
|
|
@@ -304,12 +420,14 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
304
420
|
activeStage = { name: normalizedName, kind, before };
|
|
305
421
|
stepTrace = { bound: stage.maxSteps ?? 120 };
|
|
306
422
|
if (stage.step && !playthroughOptions?.observe) {
|
|
307
|
-
throw
|
|
423
|
+
throw codedError(
|
|
424
|
+
"AUTHORITATIVE_OBSERVE_REQUIRED_FOR_STEP",
|
|
308
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.`
|
|
309
426
|
);
|
|
310
427
|
}
|
|
311
428
|
if (stage.until()) {
|
|
312
|
-
throw
|
|
429
|
+
throw codedError(
|
|
430
|
+
"STAGE_OUTCOME_ALREADY_REACHED",
|
|
313
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.`
|
|
314
432
|
);
|
|
315
433
|
}
|
|
@@ -323,12 +441,14 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
323
441
|
acceptingStageInput = false;
|
|
324
442
|
}
|
|
325
443
|
if (evidence.domInputEvents === inputsBefore) {
|
|
326
|
-
throw
|
|
444
|
+
throw codedError(
|
|
445
|
+
"PRODUCTION_INPUT_NOT_DISPATCHED",
|
|
327
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.`
|
|
328
447
|
);
|
|
329
448
|
}
|
|
330
449
|
if (activeStageTargetedCanvas && !playthroughOptions?.observe) {
|
|
331
|
-
throw
|
|
450
|
+
throw codedError(
|
|
451
|
+
"AUTHORITATIVE_OBSERVE_REQUIRED_FOR_CANVAS",
|
|
332
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.`
|
|
333
453
|
);
|
|
334
454
|
}
|
|
@@ -347,7 +467,8 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
347
467
|
});
|
|
348
468
|
stepTrace = { bound: stepBound, completed: steps };
|
|
349
469
|
if (!stage.act && advancedSteps === 0) {
|
|
350
|
-
throw
|
|
470
|
+
throw codedError(
|
|
471
|
+
"AUTONOMOUS_STAGE_NOT_ADVANCED",
|
|
351
472
|
`${stageLabel(kind, normalizedName)} did not execute its deterministic step. Autonomous stages must advance production time or frames at least once.`
|
|
352
473
|
);
|
|
353
474
|
}
|
|
@@ -355,14 +476,16 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
355
476
|
await stage.assert({ expect, user, view });
|
|
356
477
|
const assertions = expect.getState().assertionCalls - assertionsBefore;
|
|
357
478
|
if (assertions === 0) {
|
|
358
|
-
throw
|
|
479
|
+
throw codedError(
|
|
480
|
+
"STAGE_ASSERTION_MISSING",
|
|
359
481
|
`${stageLabel(kind, normalizedName)} assert must call the expect provided by playthroughTest at least once.`
|
|
360
482
|
);
|
|
361
483
|
}
|
|
362
484
|
const after = sampleState(`after ${normalizedName}`);
|
|
363
485
|
if (after.fingerprint === before.fingerprint) {
|
|
364
486
|
const source = playthroughOptions?.observe ? "authoritative observe() state" : "production DOM";
|
|
365
|
-
throw
|
|
487
|
+
throw codedError(
|
|
488
|
+
"STAGE_STATE_UNCHANGED",
|
|
366
489
|
`${stageLabel(kind, normalizedName)} did not change the ${source} from the previous stage. Each milestone must prove a new gameplay result.`
|
|
367
490
|
);
|
|
368
491
|
}
|
|
@@ -384,27 +507,27 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
384
507
|
user,
|
|
385
508
|
async enter(stage) {
|
|
386
509
|
if (entered) {
|
|
387
|
-
throw
|
|
510
|
+
throw codedError("INVALID_STAGE_ORDER", "enter may only be called once.");
|
|
388
511
|
}
|
|
389
512
|
if (evidence.stages.length > 0) {
|
|
390
|
-
throw
|
|
513
|
+
throw codedError("INVALID_STAGE_ORDER", "enter must be the first playthrough stage.");
|
|
391
514
|
}
|
|
392
515
|
await executeStage("entered", "entered", stage);
|
|
393
516
|
entered = true;
|
|
394
517
|
},
|
|
395
518
|
async milestone(name, stage) {
|
|
396
519
|
if (!entered) {
|
|
397
|
-
throw
|
|
520
|
+
throw codedError("INVALID_STAGE_ORDER", "milestone must follow enter.");
|
|
398
521
|
}
|
|
399
522
|
if (finished) {
|
|
400
|
-
throw
|
|
523
|
+
throw codedError("INVALID_STAGE_ORDER", "milestone cannot run after finish.");
|
|
401
524
|
}
|
|
402
525
|
await executeStage(name, "milestone", stage);
|
|
403
526
|
},
|
|
404
527
|
async finish(name, stage) {
|
|
405
|
-
if (!entered) throw
|
|
528
|
+
if (!entered) throw codedError("INVALID_STAGE_ORDER", "finish must follow enter.");
|
|
406
529
|
if (finished) {
|
|
407
|
-
throw
|
|
530
|
+
throw codedError("INVALID_STAGE_ORDER", "finish may only be called once.");
|
|
408
531
|
}
|
|
409
532
|
await executeStage(name, stage.kind, stage);
|
|
410
533
|
finished = true;
|
|
@@ -413,19 +536,22 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
413
536
|
const milestones = evidence.stages.filter(
|
|
414
537
|
(stage) => stage.kind === "milestone"
|
|
415
538
|
);
|
|
416
|
-
if (!entered) throw
|
|
539
|
+
if (!entered) throw codedError("INCOMPLETE_PLAYTHROUGH_EVIDENCE", "playthroughTest must call enter once.");
|
|
417
540
|
if (milestones.length < MIN_MILESTONES) {
|
|
418
|
-
throw
|
|
541
|
+
throw codedError(
|
|
542
|
+
"INCOMPLETE_PLAYTHROUGH_EVIDENCE",
|
|
419
543
|
`playthroughTest requires at least ${MIN_MILESTONES} named gameplay milestones between enter and finish; received ${milestones.length}.`
|
|
420
544
|
);
|
|
421
545
|
}
|
|
422
546
|
if (!finished) {
|
|
423
|
-
throw
|
|
547
|
+
throw codedError(
|
|
548
|
+
"INCOMPLETE_PLAYTHROUGH_EVIDENCE",
|
|
424
549
|
'playthroughTest must call finish with kind "progress" or "terminal".'
|
|
425
550
|
);
|
|
426
551
|
}
|
|
427
552
|
if (evidence.stages.length < MIN_STAGES) {
|
|
428
|
-
throw
|
|
553
|
+
throw codedError(
|
|
554
|
+
"INCOMPLETE_PLAYTHROUGH_EVIDENCE",
|
|
429
555
|
`playthroughTest requires at least ${MIN_STAGES} evidenced stages: enter, ${MIN_MILESTONES} named milestones, and finish.`
|
|
430
556
|
);
|
|
431
557
|
}
|
|
@@ -433,6 +559,7 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
433
559
|
} catch (error) {
|
|
434
560
|
const trace = captureFailureTrace();
|
|
435
561
|
metadata.trace = trace;
|
|
562
|
+
metadata.failure = createFailureDiagnostic("playthrough", error);
|
|
436
563
|
try {
|
|
437
564
|
await annotate(trace, REACT_PLAYTHROUGH_TRACE_ANNOTATION);
|
|
438
565
|
} catch {
|