miaoda-game-devkit 0.4.0 → 0.6.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/README.md +22 -10
- package/dist/react/testing.d.mts +50 -65
- package/dist/react/testing.d.ts +50 -65
- package/dist/react/testing.js +276 -188
- package/dist/react/testing.mjs +276 -188
- package/dist/react/vitest-config.js +328 -210
- package/dist/react/vitest-config.mjs +328 -210
- package/package.json +1 -1
package/dist/react/testing.mjs
CHANGED
|
@@ -60,6 +60,13 @@ import { test } from "vitest";
|
|
|
60
60
|
|
|
61
61
|
// src/react/react-playthrough-core.ts
|
|
62
62
|
import { act } from "@testing-library/react";
|
|
63
|
+
function throwIfAborted(signal) {
|
|
64
|
+
if (!signal?.aborted) return;
|
|
65
|
+
if (signal.reason instanceof Error) throw signal.reason;
|
|
66
|
+
throw new Error("Playthrough advancement was cancelled.", {
|
|
67
|
+
cause: signal.reason
|
|
68
|
+
});
|
|
69
|
+
}
|
|
63
70
|
function formatDiagnostics(read) {
|
|
64
71
|
if (!read) return void 0;
|
|
65
72
|
try {
|
|
@@ -88,11 +95,13 @@ async function runBoundedUntil(condition, options = {}) {
|
|
|
88
95
|
);
|
|
89
96
|
}
|
|
90
97
|
for (let step = 0; step <= maxSteps; step += 1) {
|
|
98
|
+
throwIfAborted(options.signal);
|
|
91
99
|
if (condition()) return step;
|
|
92
100
|
if (step < maxSteps) {
|
|
93
101
|
await act(async () => {
|
|
94
102
|
await options.step?.(step + 1);
|
|
95
103
|
});
|
|
104
|
+
throwIfAborted(options.signal);
|
|
96
105
|
}
|
|
97
106
|
}
|
|
98
107
|
const diagnostics = formatDiagnostics(options.diagnostics);
|
|
@@ -113,13 +122,35 @@ var INPUT_EVENTS = [
|
|
|
113
122
|
"touchstart",
|
|
114
123
|
"touchend"
|
|
115
124
|
];
|
|
116
|
-
var
|
|
117
|
-
var
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
125
|
+
var MIN_STAGES = 5;
|
|
126
|
+
var MIN_MILESTONES = 3;
|
|
127
|
+
var MAX_TRACE_VALUE_LENGTH = 140;
|
|
128
|
+
var MAX_TRACE_LENGTH = 720;
|
|
129
|
+
var REACT_PLAYTHROUGH_TRACE_ANNOTATION = "miaoda:react-playthrough-trace";
|
|
130
|
+
function truncateTraceValue(value, limit) {
|
|
131
|
+
const compact = value.replace(/\s+/g, " ").trim();
|
|
132
|
+
if (compact.length <= limit) return compact;
|
|
133
|
+
return `${compact.slice(0, Math.max(0, limit - 1))}\u2026`;
|
|
121
134
|
}
|
|
122
|
-
function
|
|
135
|
+
function formatReactPlaythroughFailureTrace(trace) {
|
|
136
|
+
const stages = trace.stages.map(
|
|
137
|
+
(stage) => `${stage.kind}:${stage.name}=${truncateTraceValue(stage.state, MAX_TRACE_VALUE_LENGTH)}`
|
|
138
|
+
);
|
|
139
|
+
if (trace.current) {
|
|
140
|
+
stages.push(
|
|
141
|
+
`current:${trace.current.name}=${truncateTraceValue(trace.current.before, 70)}\u2192${truncateTraceValue(trace.current.last, 70)}`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
const step = trace.step ? `stepUntil=${trace.step.completed ?? "failed"}/${trace.step.bound}` : void 0;
|
|
145
|
+
const formatted = `${stages.length > 0 ? stages.join(" -> ") : "stages=none"}${step ? `; ${step}` : ""}`;
|
|
146
|
+
return truncateTraceValue(formatted, MAX_TRACE_LENGTH);
|
|
147
|
+
}
|
|
148
|
+
var MAX_FORMATTED_STATE_LENGTH = 500;
|
|
149
|
+
function formatState(fingerprint) {
|
|
150
|
+
if (fingerprint.length <= MAX_FORMATTED_STATE_LENGTH) return fingerprint;
|
|
151
|
+
return `${fingerprint.slice(0, MAX_FORMATTED_STATE_LENGTH)}\u2026 (${fingerprint.length} chars)`;
|
|
152
|
+
}
|
|
153
|
+
function sampleObservedState(observe, stage) {
|
|
123
154
|
let value;
|
|
124
155
|
try {
|
|
125
156
|
value = observe();
|
|
@@ -129,222 +160,272 @@ function sampleObservation(observe, stage) {
|
|
|
129
160
|
try {
|
|
130
161
|
const fingerprint = JSON.stringify(value);
|
|
131
162
|
if (fingerprint === void 0) throw new Error("unsupported value");
|
|
132
|
-
return { fingerprint, formatted:
|
|
163
|
+
return { fingerprint, formatted: formatState(fingerprint) };
|
|
133
164
|
} catch {
|
|
134
165
|
throw new Error(
|
|
135
166
|
`observe() must return JSON-serializable read-only state; sampling failed at ${stage}.`
|
|
136
167
|
);
|
|
137
168
|
}
|
|
138
169
|
}
|
|
139
|
-
function
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
]
|
|
170
|
+
function sampleDomState(view) {
|
|
171
|
+
const fingerprint = view.container.innerHTML.replace(/\s+/g, " ").trim();
|
|
172
|
+
return { fingerprint, formatted: formatState(JSON.stringify(fingerprint)) };
|
|
173
|
+
}
|
|
174
|
+
function createEvidence() {
|
|
175
|
+
return { domInputEvents: 0, stages: [], verified: false };
|
|
176
|
+
}
|
|
177
|
+
function createMetadata(waiverReason) {
|
|
178
|
+
return { version: 4, waiverReason, evidence: createEvidence() };
|
|
179
|
+
}
|
|
180
|
+
function stageLabel(kind, name) {
|
|
181
|
+
return kind === "entered" ? "entered" : `${kind}(${JSON.stringify(name)})`;
|
|
145
182
|
}
|
|
146
183
|
function describeMissingEvidence(evidence) {
|
|
147
|
-
if (!evidence || evidence.
|
|
148
|
-
if (evidence.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
184
|
+
if (!evidence || evidence.stages.length === 0) return "an enter stage";
|
|
185
|
+
if (evidence.stages[0]?.kind !== "entered") return "the entered stage";
|
|
186
|
+
const milestones = evidence.stages.filter(
|
|
187
|
+
(stage) => stage.kind === "milestone"
|
|
188
|
+
);
|
|
189
|
+
if (milestones.length < MIN_MILESTONES) {
|
|
190
|
+
return `at least ${MIN_MILESTONES} gameplay milestones`;
|
|
191
|
+
}
|
|
192
|
+
const finalStage = evidence.stages.at(-1);
|
|
193
|
+
if (finalStage?.kind !== "progress" && finalStage?.kind !== "terminal") {
|
|
194
|
+
return "a progress or terminal finish stage";
|
|
195
|
+
}
|
|
196
|
+
if (evidence.stages.length < MIN_STAGES) {
|
|
197
|
+
return `at least ${MIN_STAGES} evidenced stages`;
|
|
159
198
|
}
|
|
160
199
|
return "a complete playthrough verification marker";
|
|
161
200
|
}
|
|
162
|
-
function createMetadata(waiverReason) {
|
|
163
|
-
return {
|
|
164
|
-
version: 3,
|
|
165
|
-
waiverReason,
|
|
166
|
-
evidence: {
|
|
167
|
-
domInputEvents: 0,
|
|
168
|
-
entryInputs: 0,
|
|
169
|
-
primaryInputs: 0,
|
|
170
|
-
boundedRuns: 0,
|
|
171
|
-
assertionsAfterOutcome: 0,
|
|
172
|
-
checkpoints: [],
|
|
173
|
-
verified: false
|
|
174
|
-
}
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
201
|
function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
178
202
|
const reason = normalizePlaythroughWaiverReason(waiverReason);
|
|
179
203
|
const metadata = createMetadata(reason);
|
|
180
|
-
test(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
204
|
+
test(
|
|
205
|
+
"production game completes a bounded playthrough",
|
|
206
|
+
{
|
|
207
|
+
concurrent: false,
|
|
208
|
+
skip: Boolean(reason),
|
|
209
|
+
meta: { reactPlaythrough: metadata }
|
|
210
|
+
},
|
|
211
|
+
async ({ annotate, expect, onTestFailed, signal }) => {
|
|
212
|
+
metadata.evidence = createEvidence();
|
|
213
|
+
metadata.trace = void 0;
|
|
214
|
+
const evidence = metadata.evidence;
|
|
215
|
+
let entered = false;
|
|
216
|
+
let finished = false;
|
|
217
|
+
let activeStage;
|
|
218
|
+
let lastSample;
|
|
219
|
+
let stepTrace;
|
|
220
|
+
let failureTraceFactory;
|
|
221
|
+
let acceptingStageInput = false;
|
|
222
|
+
let inputCaptureAttached = false;
|
|
223
|
+
const recordInput = () => {
|
|
224
|
+
if (acceptingStageInput) evidence.domInputEvents += 1;
|
|
225
|
+
};
|
|
226
|
+
const stopInputCapture = () => {
|
|
227
|
+
acceptingStageInput = false;
|
|
228
|
+
if (!inputCaptureAttached) return;
|
|
229
|
+
inputCaptureAttached = false;
|
|
230
|
+
for (const event of INPUT_EVENTS) {
|
|
231
|
+
document.removeEventListener(event, recordInput, true);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
const captureFailureTrace = () => {
|
|
235
|
+
try {
|
|
236
|
+
return failureTraceFactory?.() ?? "stages=none";
|
|
237
|
+
} catch {
|
|
238
|
+
return "stages=none";
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
onTestFailed(() => {
|
|
242
|
+
metadata.trace ??= captureFailureTrace();
|
|
243
|
+
});
|
|
244
|
+
for (const event of INPUT_EVENTS) {
|
|
245
|
+
document.addEventListener(event, recordInput, true);
|
|
202
246
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
247
|
+
inputCaptureAttached = true;
|
|
248
|
+
signal.addEventListener("abort", stopInputCapture, { once: true });
|
|
249
|
+
try {
|
|
250
|
+
const view = render(element);
|
|
251
|
+
if (view.container.childNodes.length === 0) {
|
|
252
|
+
throw new Error(
|
|
253
|
+
"playthroughTest must render the production game entry."
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
const user = userEvent.setup();
|
|
257
|
+
const sampleState = (label) => playthroughOptions?.observe ? sampleObservedState(playthroughOptions.observe, label) : sampleDomState(view);
|
|
258
|
+
lastSample = sampleState("initial render");
|
|
259
|
+
failureTraceFactory = () => formatReactPlaythroughFailureTrace({
|
|
260
|
+
stages: evidence.stages.map((stage) => ({
|
|
261
|
+
name: stage.name,
|
|
262
|
+
kind: stage.kind,
|
|
263
|
+
state: stage.after
|
|
264
|
+
})),
|
|
265
|
+
current: activeStage ? {
|
|
266
|
+
name: stageLabel(activeStage.kind, activeStage.name),
|
|
267
|
+
before: activeStage.before.formatted,
|
|
268
|
+
last: (() => {
|
|
269
|
+
try {
|
|
270
|
+
return sampleState("failure").formatted;
|
|
271
|
+
} catch (error) {
|
|
272
|
+
return `<state unavailable: ${String(error)}>`;
|
|
273
|
+
}
|
|
274
|
+
})()
|
|
275
|
+
} : void 0,
|
|
276
|
+
step: stepTrace
|
|
277
|
+
});
|
|
278
|
+
const executeStage = async (name, kind, stage) => {
|
|
279
|
+
const normalizedName = name.trim();
|
|
280
|
+
if (normalizedName.length === 0) {
|
|
281
|
+
throw new Error("playthrough stage names must be non-empty strings.");
|
|
282
|
+
}
|
|
283
|
+
if (evidence.stages.some(
|
|
284
|
+
(completed) => completed.name === normalizedName
|
|
285
|
+
)) {
|
|
210
286
|
throw new Error(
|
|
211
|
-
|
|
287
|
+
`playthrough stage ${JSON.stringify(normalizedName)} may only be recorded once.`
|
|
212
288
|
);
|
|
213
289
|
}
|
|
214
|
-
if (kind === "
|
|
290
|
+
if (kind === "milestone" && ["entered", "progress", "terminal"].includes(normalizedName)) {
|
|
215
291
|
throw new Error(
|
|
216
|
-
|
|
292
|
+
`milestone name ${JSON.stringify(normalizedName)} is reserved; use a game-domain name such as "first-point" or "boss-entered".`
|
|
217
293
|
);
|
|
218
294
|
}
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
295
|
+
const before = lastSample ?? sampleState(`before ${normalizedName}`);
|
|
296
|
+
activeStage = { name: normalizedName, kind, before };
|
|
297
|
+
stepTrace = { bound: stage.maxSteps ?? 120 };
|
|
298
|
+
if (stage.until()) {
|
|
222
299
|
throw new Error(
|
|
223
|
-
|
|
300
|
+
`${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.`
|
|
224
301
|
);
|
|
225
302
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
},
|
|
237
|
-
checkpoint(kind) {
|
|
238
|
-
if (kind === "entered") {
|
|
239
|
-
if (enteredRecorded) {
|
|
240
|
-
throw new Error(
|
|
241
|
-
'checkpoint("entered") may only be recorded once, before the primary input.'
|
|
242
|
-
);
|
|
303
|
+
const inputsBefore = evidence.domInputEvents;
|
|
304
|
+
if (stage.act) {
|
|
305
|
+
acceptingStageInput = true;
|
|
306
|
+
try {
|
|
307
|
+
await stage.act();
|
|
308
|
+
} finally {
|
|
309
|
+
acceptingStageInput = false;
|
|
243
310
|
}
|
|
244
|
-
if (evidence.
|
|
311
|
+
if (evidence.domInputEvents === inputsBefore) {
|
|
245
312
|
throw new Error(
|
|
246
|
-
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
enteredRecorded = true;
|
|
250
|
-
if (playthroughOptions?.observe) {
|
|
251
|
-
enteredObservation = sampleObservation(
|
|
252
|
-
playthroughOptions.observe,
|
|
253
|
-
"entered"
|
|
313
|
+
`${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.`
|
|
254
314
|
);
|
|
255
|
-
} else {
|
|
256
|
-
domTextAtEntered = document.body.textContent ?? "";
|
|
257
315
|
}
|
|
258
|
-
evidence.checkpoints.push(kind);
|
|
259
|
-
return;
|
|
260
316
|
}
|
|
261
|
-
|
|
317
|
+
let advancedSteps = 0;
|
|
318
|
+
const stepBound = stage.maxSteps ?? 120;
|
|
319
|
+
stepTrace = { bound: stepBound };
|
|
320
|
+
const steps = await runBoundedUntil(stage.until, {
|
|
321
|
+
maxSteps: stage.maxSteps,
|
|
322
|
+
signal,
|
|
323
|
+
diagnostics: stage.diagnostics ?? playthroughOptions?.observe,
|
|
324
|
+
step: stage.step ? async (step) => {
|
|
325
|
+
advancedSteps += 1;
|
|
326
|
+
await stage.step?.(step);
|
|
327
|
+
} : void 0
|
|
328
|
+
});
|
|
329
|
+
stepTrace = { bound: stepBound, completed: steps };
|
|
330
|
+
if (!stage.act && advancedSteps === 0) {
|
|
262
331
|
throw new Error(
|
|
263
|
-
|
|
332
|
+
`${stageLabel(kind, normalizedName)} did not execute its deterministic step. Autonomous stages must advance production time or frames at least once.`
|
|
264
333
|
);
|
|
265
334
|
}
|
|
266
|
-
|
|
335
|
+
const assertionsBefore = expect.getState().assertionCalls;
|
|
336
|
+
await stage.assert({ expect, user, view });
|
|
337
|
+
const assertions = expect.getState().assertionCalls - assertionsBefore;
|
|
338
|
+
if (assertions === 0) {
|
|
267
339
|
throw new Error(
|
|
268
|
-
|
|
340
|
+
`${stageLabel(kind, normalizedName)} assert must call the expect provided by playthroughTest at least once.`
|
|
269
341
|
);
|
|
270
342
|
}
|
|
271
|
-
|
|
343
|
+
const after = sampleState(`after ${normalizedName}`);
|
|
344
|
+
if (after.fingerprint === before.fingerprint) {
|
|
345
|
+
const source = playthroughOptions?.observe ? "authoritative observe() state" : "production DOM";
|
|
272
346
|
throw new Error(
|
|
273
|
-
|
|
347
|
+
`${stageLabel(kind, normalizedName)} did not change the ${source} from the previous stage. Each milestone must prove a new gameplay result.`
|
|
274
348
|
);
|
|
275
349
|
}
|
|
276
|
-
evidence.
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
350
|
+
evidence.stages.push({
|
|
351
|
+
name: normalizedName,
|
|
352
|
+
kind,
|
|
353
|
+
domInputEvents: evidence.domInputEvents - inputsBefore,
|
|
354
|
+
advancedSteps,
|
|
355
|
+
assertions,
|
|
356
|
+
stateChanged: true,
|
|
357
|
+
before: before.formatted,
|
|
358
|
+
after: after.formatted
|
|
359
|
+
});
|
|
360
|
+
lastSample = after;
|
|
361
|
+
activeStage = void 0;
|
|
362
|
+
};
|
|
363
|
+
await run({
|
|
364
|
+
view,
|
|
365
|
+
user,
|
|
366
|
+
async enter(stage) {
|
|
367
|
+
if (entered) {
|
|
368
|
+
throw new Error("enter may only be called once.");
|
|
294
369
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
"outcome"
|
|
298
|
-
);
|
|
299
|
-
if (outcomeObservation.fingerprint === enteredObservation.fingerprint) {
|
|
300
|
-
throw new Error(
|
|
301
|
-
`The authoritative observation did not change from checkpoint("entered") to the outcome. Timeline: ${formatObservationTimeline(enteredObservation, afterPrimaryObservation, outcomeObservation)}`
|
|
302
|
-
);
|
|
370
|
+
if (evidence.stages.length > 0) {
|
|
371
|
+
throw new Error("enter must be the first playthrough stage.");
|
|
303
372
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
373
|
+
await executeStage("entered", "entered", stage);
|
|
374
|
+
entered = true;
|
|
375
|
+
},
|
|
376
|
+
async milestone(name, stage) {
|
|
377
|
+
if (!entered) {
|
|
378
|
+
throw new Error("milestone must follow enter.");
|
|
379
|
+
}
|
|
380
|
+
if (finished) {
|
|
381
|
+
throw new Error("milestone cannot run after finish.");
|
|
382
|
+
}
|
|
383
|
+
await executeStage(name, "milestone", stage);
|
|
384
|
+
},
|
|
385
|
+
async finish(name, stage) {
|
|
386
|
+
if (!entered) throw new Error("finish must follow enter.");
|
|
387
|
+
if (finished) {
|
|
388
|
+
throw new Error("finish may only be called once.");
|
|
389
|
+
}
|
|
390
|
+
await executeStage(name, stage.kind, stage);
|
|
391
|
+
finished = true;
|
|
308
392
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}
|
|
313
|
-
});
|
|
314
|
-
const assertionCalls = expect.getState().assertionCalls;
|
|
315
|
-
evidence.assertionsAfterOutcome = assertionsAtOutcome === void 0 ? 0 : assertionCalls - assertionsAtOutcome;
|
|
316
|
-
if (evidence.entryInputs === 0) {
|
|
317
|
-
throw new Error(
|
|
318
|
-
'playthroughTest must perform an entry input with performInput("entry", ...).'
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
if (evidence.primaryInputs === 0) {
|
|
322
|
-
throw new Error(
|
|
323
|
-
'playthroughTest must perform a core game action with performInput("primary", ...).'
|
|
393
|
+
});
|
|
394
|
+
const milestones = evidence.stages.filter(
|
|
395
|
+
(stage) => stage.kind === "milestone"
|
|
324
396
|
);
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
397
|
+
if (!entered) throw new Error("playthroughTest must call enter once.");
|
|
398
|
+
if (milestones.length < MIN_MILESTONES) {
|
|
399
|
+
throw new Error(
|
|
400
|
+
`playthroughTest requires at least ${MIN_MILESTONES} named gameplay milestones between enter and finish; received ${milestones.length}.`
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
if (!finished) {
|
|
404
|
+
throw new Error(
|
|
405
|
+
'playthroughTest must call finish with kind "progress" or "terminal".'
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
if (evidence.stages.length < MIN_STAGES) {
|
|
409
|
+
throw new Error(
|
|
410
|
+
`playthroughTest requires at least ${MIN_STAGES} evidenced stages: enter, ${MIN_MILESTONES} named milestones, and finish.`
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
evidence.verified = true;
|
|
414
|
+
} catch (error) {
|
|
415
|
+
const trace = captureFailureTrace();
|
|
416
|
+
metadata.trace = trace;
|
|
417
|
+
try {
|
|
418
|
+
await annotate(trace, REACT_PLAYTHROUGH_TRACE_ANNOTATION);
|
|
419
|
+
} catch {
|
|
420
|
+
}
|
|
421
|
+
throw error;
|
|
422
|
+
} finally {
|
|
423
|
+
metadata.trace ??= failureTraceFactory?.();
|
|
424
|
+
signal.removeEventListener("abort", stopInputCapture);
|
|
425
|
+
stopInputCapture();
|
|
345
426
|
}
|
|
346
427
|
}
|
|
347
|
-
|
|
428
|
+
);
|
|
348
429
|
}
|
|
349
430
|
var playthroughTest = Object.assign(
|
|
350
431
|
(element, optionsOrRun, maybeRun) => {
|
|
@@ -352,19 +433,29 @@ var playthroughTest = Object.assign(
|
|
|
352
433
|
definePlaythrough(element, optionsOrRun);
|
|
353
434
|
return;
|
|
354
435
|
}
|
|
355
|
-
if (!maybeRun)
|
|
436
|
+
if (!maybeRun) {
|
|
437
|
+
throw new TypeError("playthroughTest requires a run callback.");
|
|
438
|
+
}
|
|
356
439
|
definePlaythrough(element, maybeRun, optionsOrRun);
|
|
357
440
|
},
|
|
358
441
|
{
|
|
359
|
-
skip: (reason, element
|
|
442
|
+
skip: (reason, element) => definePlaythrough(element, async () => {
|
|
443
|
+
}, void 0, reason)
|
|
360
444
|
}
|
|
361
445
|
);
|
|
362
446
|
function auditReactPlaythroughRun(tests) {
|
|
363
447
|
const declared = tests.filter((candidate) => candidate.metadata);
|
|
364
448
|
const valid = declared.filter(({ state, metadata }) => {
|
|
365
449
|
const evidence = metadata?.evidence;
|
|
366
|
-
|
|
367
|
-
|
|
450
|
+
if (!evidence || state !== "passed" || evidence.verified !== true) {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
const milestones = evidence.stages.filter(
|
|
454
|
+
(stage) => stage.kind === "milestone"
|
|
455
|
+
);
|
|
456
|
+
const finalStage = evidence.stages.at(-1);
|
|
457
|
+
return evidence.domInputEvents > 0 && evidence.stages.length >= MIN_STAGES && evidence.stages[0]?.kind === "entered" && milestones.length >= MIN_MILESTONES && (finalStage?.kind === "progress" || finalStage?.kind === "terminal") && evidence.stages.every(
|
|
458
|
+
(stage) => stage.assertions > 0 && (stage.domInputEvents > 0 || stage.advancedSteps > 0) && stage.stateChanged === true
|
|
368
459
|
);
|
|
369
460
|
});
|
|
370
461
|
const waivers = declared.filter(
|
|
@@ -373,13 +464,11 @@ function auditReactPlaythroughRun(tests) {
|
|
|
373
464
|
const issues = [];
|
|
374
465
|
if (declared.length === 0) {
|
|
375
466
|
issues.push(
|
|
376
|
-
|
|
467
|
+
"No production gameplay verification was declared. Use playthroughTest to render <App />, then compose one real-input enter stage, at least three named milestones, and finish. Later stages may drive production input or deterministic advancement; every stage must reach and assert a bounded new result."
|
|
377
468
|
);
|
|
378
469
|
} else {
|
|
379
470
|
for (const candidate of declared) {
|
|
380
|
-
|
|
381
|
-
const isWaived = waivers.includes(candidate);
|
|
382
|
-
if (isValid || isWaived) continue;
|
|
471
|
+
if (valid.includes(candidate) || waivers.includes(candidate)) continue;
|
|
383
472
|
if (candidate.state === "skipped") {
|
|
384
473
|
issues.push(
|
|
385
474
|
`Playthrough ${JSON.stringify(candidate.name)} was skipped without an explicit reason of at least 20 characters.`
|
|
@@ -389,9 +478,8 @@ function auditReactPlaythroughRun(tests) {
|
|
|
389
478
|
`Playthrough ${JSON.stringify(candidate.name)} finished with state ${candidate.state}.`
|
|
390
479
|
);
|
|
391
480
|
} else {
|
|
392
|
-
const missing = describeMissingEvidence(candidate.metadata?.evidence);
|
|
393
481
|
issues.push(
|
|
394
|
-
`Playthrough ${JSON.stringify(candidate.name)} is missing ${
|
|
482
|
+
`Playthrough ${JSON.stringify(candidate.name)} is missing ${describeMissingEvidence(candidate.metadata?.evidence)}.`
|
|
395
483
|
);
|
|
396
484
|
}
|
|
397
485
|
}
|