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