miaoda-game-devkit 0.3.0 → 0.5.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 +13 -4
- package/dist/react/index.js +1 -1
- package/dist/react/index.mjs +1 -1
- package/dist/react/testing.d.mts +10 -11
- package/dist/react/testing.d.ts +10 -11
- package/dist/react/testing.js +174 -33
- package/dist/react/testing.mjs +174 -33
- package/dist/react/vitest-config.js +236 -52
- package/dist/react/vitest-config.mjs +236 -52
- package/dist/rules/react-test-boundary-plugin.js +97 -0
- package/oxlint-config.json +4 -2
- package/package.json +1 -1
package/dist/react/testing.mjs
CHANGED
|
@@ -114,47 +114,123 @@ var INPUT_EVENTS = [
|
|
|
114
114
|
"touchend"
|
|
115
115
|
];
|
|
116
116
|
var MIN_CHECKPOINTS = 2;
|
|
117
|
+
var REACT_PLAYTHROUGH_TRACE_ANNOTATION = "miaoda:react-playthrough-trace";
|
|
118
|
+
var MAX_TRACE_VALUE_LENGTH = 180;
|
|
119
|
+
var MAX_TRACE_LENGTH = 720;
|
|
120
|
+
function truncateTraceValue(value, limit) {
|
|
121
|
+
const compact = value.replace(/\s+/g, " ").trim();
|
|
122
|
+
if (compact.length <= limit) return compact;
|
|
123
|
+
return `${compact.slice(0, Math.max(0, limit - 1))}\u2026`;
|
|
124
|
+
}
|
|
125
|
+
function formatReactPlaythroughFailureTrace(trace) {
|
|
126
|
+
const stages = [
|
|
127
|
+
["entered", trace.entered],
|
|
128
|
+
["after-primary", trace.afterPrimary],
|
|
129
|
+
["last", trace.last]
|
|
130
|
+
].filter((stage) => stage[1] !== void 0).map(
|
|
131
|
+
([stage, value]) => `${stage}=${truncateTraceValue(value, MAX_TRACE_VALUE_LENGTH)}`
|
|
132
|
+
);
|
|
133
|
+
const details = [
|
|
134
|
+
trace.checkpoints.length > 0 ? `checkpoints=${trace.checkpoints.join(",")}` : "checkpoints=none",
|
|
135
|
+
trace.step ? `stepUntil=${trace.step.completed ?? "failed"}/${trace.step.bound}` : void 0
|
|
136
|
+
].filter((detail) => Boolean(detail));
|
|
137
|
+
const formatted = `${stages.join(" -> ")}${stages.length ? "; " : ""}${details.join("; ")}`;
|
|
138
|
+
return truncateTraceValue(formatted, MAX_TRACE_LENGTH);
|
|
139
|
+
}
|
|
140
|
+
var MAX_FORMATTED_OBSERVATION_LENGTH = 500;
|
|
141
|
+
function formatObservation(fingerprint) {
|
|
142
|
+
if (fingerprint.length <= MAX_FORMATTED_OBSERVATION_LENGTH) return fingerprint;
|
|
143
|
+
return `${fingerprint.slice(0, MAX_FORMATTED_OBSERVATION_LENGTH)}\u2026 (${fingerprint.length} chars)`;
|
|
144
|
+
}
|
|
145
|
+
function sampleObservation(observe, stage) {
|
|
146
|
+
let value;
|
|
147
|
+
try {
|
|
148
|
+
value = observe();
|
|
149
|
+
} catch (error) {
|
|
150
|
+
throw new Error(`observe() threw at ${stage}: ${String(error)}`);
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const fingerprint = JSON.stringify(value);
|
|
154
|
+
if (fingerprint === void 0) throw new Error("unsupported value");
|
|
155
|
+
return { fingerprint, formatted: formatObservation(fingerprint) };
|
|
156
|
+
} catch {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`observe() must return JSON-serializable read-only state; sampling failed at ${stage}.`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function formatObservationTimeline(entered, afterPrimary, outcome) {
|
|
163
|
+
return [
|
|
164
|
+
`entered=${entered.formatted}`,
|
|
165
|
+
`after-primary=${afterPrimary?.formatted ?? "<not sampled>"}`,
|
|
166
|
+
`outcome=${outcome.formatted}`
|
|
167
|
+
].join(", ");
|
|
168
|
+
}
|
|
117
169
|
function describeMissingEvidence(evidence) {
|
|
118
|
-
if (!evidence || evidence.entryInputs === 0) return "entry
|
|
119
|
-
if (evidence.primaryInputs === 0) return "primary
|
|
170
|
+
if (!evidence || evidence.entryInputs === 0) return "an entry input";
|
|
171
|
+
if (evidence.primaryInputs === 0) return "a primary gameplay input";
|
|
120
172
|
if (!evidence.checkpoints.includes("entered")) return "entered checkpoint";
|
|
121
|
-
if (evidence.boundedRuns === 0) return "
|
|
122
|
-
if (evidence.assertionsAfterOutcome === 0)
|
|
173
|
+
if (evidence.boundedRuns === 0) return "a bounded stepUntil call";
|
|
174
|
+
if (evidence.assertionsAfterOutcome === 0)
|
|
175
|
+
return "an outcome assertion after stepUntil";
|
|
123
176
|
if (evidence.checkpoints.length < MIN_CHECKPOINTS)
|
|
124
|
-
return
|
|
177
|
+
return `at least ${MIN_CHECKPOINTS} checkpoints`;
|
|
125
178
|
if (!evidence.checkpoints.some(
|
|
126
179
|
(checkpoint) => checkpoint === "progress" || checkpoint === "terminal"
|
|
127
180
|
)) {
|
|
128
181
|
return "progress/terminal checkpoint";
|
|
129
182
|
}
|
|
130
|
-
return "
|
|
183
|
+
return "a complete playthrough verification marker";
|
|
131
184
|
}
|
|
132
|
-
function
|
|
185
|
+
function createEvidence() {
|
|
133
186
|
return {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
assertionsAfterOutcome: 0,
|
|
142
|
-
checkpoints: [],
|
|
143
|
-
verified: false
|
|
144
|
-
}
|
|
187
|
+
domInputEvents: 0,
|
|
188
|
+
entryInputs: 0,
|
|
189
|
+
primaryInputs: 0,
|
|
190
|
+
boundedRuns: 0,
|
|
191
|
+
assertionsAfterOutcome: 0,
|
|
192
|
+
checkpoints: [],
|
|
193
|
+
verified: false
|
|
145
194
|
};
|
|
146
195
|
}
|
|
147
|
-
function
|
|
196
|
+
function createMetadata(waiverReason) {
|
|
197
|
+
return { version: 3, waiverReason, evidence: createEvidence() };
|
|
198
|
+
}
|
|
199
|
+
function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
148
200
|
const reason = normalizePlaythroughWaiverReason(waiverReason);
|
|
149
201
|
const metadata = createMetadata(reason);
|
|
150
202
|
test("production game completes a bounded playthrough", {
|
|
151
203
|
skip: Boolean(reason),
|
|
152
204
|
meta: { reactPlaythrough: metadata }
|
|
153
|
-
}, async ({ expect }) => {
|
|
205
|
+
}, async ({ annotate, expect }) => {
|
|
206
|
+
metadata.evidence = createEvidence();
|
|
207
|
+
metadata.trace = void 0;
|
|
154
208
|
const evidence = metadata.evidence;
|
|
155
209
|
let assertionsAtOutcome;
|
|
156
210
|
let enteredRecorded = false;
|
|
157
211
|
let domTextAtEntered;
|
|
212
|
+
let enteredObservation;
|
|
213
|
+
let afterPrimaryObservation;
|
|
214
|
+
let enteredTrace;
|
|
215
|
+
let afterPrimaryTrace;
|
|
216
|
+
let outcomeTrace;
|
|
217
|
+
let stepTrace;
|
|
218
|
+
const sampleDomTrace = () => JSON.stringify((document.body.textContent ?? "").replace(/\s+/g, " ").trim());
|
|
219
|
+
const sampleLastTrace = () => {
|
|
220
|
+
if (!playthroughOptions?.observe) return sampleDomTrace();
|
|
221
|
+
try {
|
|
222
|
+
return sampleObservation(playthroughOptions.observe, "outcome").formatted;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
return `<observe unavailable: ${String(error)}>`;
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
const createFailureTrace = () => formatReactPlaythroughFailureTrace({
|
|
228
|
+
entered: enteredTrace,
|
|
229
|
+
afterPrimary: afterPrimaryTrace,
|
|
230
|
+
last: outcomeTrace ?? sampleLastTrace(),
|
|
231
|
+
checkpoints: [...evidence.checkpoints],
|
|
232
|
+
step: stepTrace
|
|
233
|
+
});
|
|
158
234
|
const recordInput = () => {
|
|
159
235
|
evidence.domInputEvents += 1;
|
|
160
236
|
};
|
|
@@ -192,7 +268,18 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
192
268
|
);
|
|
193
269
|
}
|
|
194
270
|
if (kind === "entry") evidence.entryInputs += 1;
|
|
195
|
-
else
|
|
271
|
+
else {
|
|
272
|
+
evidence.primaryInputs += 1;
|
|
273
|
+
if (playthroughOptions?.observe) {
|
|
274
|
+
afterPrimaryObservation = sampleObservation(
|
|
275
|
+
playthroughOptions.observe,
|
|
276
|
+
"after-primary"
|
|
277
|
+
);
|
|
278
|
+
afterPrimaryTrace = afterPrimaryObservation.formatted;
|
|
279
|
+
} else {
|
|
280
|
+
afterPrimaryTrace = sampleDomTrace();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
196
283
|
},
|
|
197
284
|
checkpoint(kind) {
|
|
198
285
|
if (kind === "entered") {
|
|
@@ -207,7 +294,16 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
207
294
|
);
|
|
208
295
|
}
|
|
209
296
|
enteredRecorded = true;
|
|
210
|
-
|
|
297
|
+
if (playthroughOptions?.observe) {
|
|
298
|
+
enteredObservation = sampleObservation(
|
|
299
|
+
playthroughOptions.observe,
|
|
300
|
+
"entered"
|
|
301
|
+
);
|
|
302
|
+
enteredTrace = enteredObservation.formatted;
|
|
303
|
+
} else {
|
|
304
|
+
domTextAtEntered = document.body.textContent ?? "";
|
|
305
|
+
enteredTrace = sampleDomTrace();
|
|
306
|
+
}
|
|
211
307
|
evidence.checkpoints.push(kind);
|
|
212
308
|
return;
|
|
213
309
|
}
|
|
@@ -228,17 +324,43 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
228
324
|
}
|
|
229
325
|
evidence.checkpoints.push(kind);
|
|
230
326
|
},
|
|
231
|
-
async stepUntil(condition,
|
|
232
|
-
const
|
|
327
|
+
async stepUntil(condition, stepOptions = {}) {
|
|
328
|
+
const stepBound = stepOptions.maxSteps ?? 120;
|
|
329
|
+
stepTrace = { bound: stepBound };
|
|
330
|
+
const boundedOptions = stepOptions.diagnostics || !playthroughOptions?.observe ? stepOptions : {
|
|
331
|
+
...stepOptions,
|
|
332
|
+
diagnostics: playthroughOptions.observe
|
|
333
|
+
};
|
|
334
|
+
const steps = await runBoundedUntil(condition, boundedOptions);
|
|
335
|
+
stepTrace = { bound: stepBound, completed: steps };
|
|
233
336
|
if (evidence.primaryInputs === 0) {
|
|
234
337
|
throw new Error(
|
|
235
338
|
'stepUntil must follow performInput("primary", ...). A menu/help click is not gameplay evidence.'
|
|
236
339
|
);
|
|
237
340
|
}
|
|
238
|
-
if (
|
|
239
|
-
|
|
240
|
-
|
|
341
|
+
if (playthroughOptions?.observe) {
|
|
342
|
+
if (!enteredObservation) {
|
|
343
|
+
throw new Error(
|
|
344
|
+
'observe requires checkpoint("entered") before primary gameplay input.'
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
const outcomeObservation = sampleObservation(
|
|
348
|
+
playthroughOptions.observe,
|
|
349
|
+
"outcome"
|
|
241
350
|
);
|
|
351
|
+
outcomeTrace = outcomeObservation.formatted;
|
|
352
|
+
if (outcomeObservation.fingerprint === enteredObservation.fingerprint) {
|
|
353
|
+
throw new Error(
|
|
354
|
+
`The authoritative observation did not change from checkpoint("entered") to the outcome. Timeline: ${formatObservationTimeline(enteredObservation, afterPrimaryObservation, outcomeObservation)}`
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
} else {
|
|
358
|
+
outcomeTrace = sampleDomTrace();
|
|
359
|
+
if (steps === 0 && domTextAtEntered !== void 0 && (document.body.textContent ?? "") === domTextAtEntered) {
|
|
360
|
+
throw new Error(
|
|
361
|
+
'stepUntil found the outcome at step 0, and the DOM has not changed since checkpoint("entered"). The flow therefore provides no evidence that the primary gameplay input produced a result. For Canvas or Controller state outside the DOM, declare one playthrough observe callback.'
|
|
362
|
+
);
|
|
363
|
+
}
|
|
242
364
|
}
|
|
243
365
|
evidence.boundedRuns += 1;
|
|
244
366
|
assertionsAtOutcome = expect.getState().assertionCalls;
|
|
@@ -273,7 +395,15 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
273
395
|
);
|
|
274
396
|
}
|
|
275
397
|
evidence.verified = true;
|
|
398
|
+
} catch (error) {
|
|
399
|
+
metadata.trace = createFailureTrace();
|
|
400
|
+
try {
|
|
401
|
+
await annotate(metadata.trace, REACT_PLAYTHROUGH_TRACE_ANNOTATION);
|
|
402
|
+
} catch {
|
|
403
|
+
}
|
|
404
|
+
throw error;
|
|
276
405
|
} finally {
|
|
406
|
+
metadata.trace ??= createFailureTrace();
|
|
277
407
|
for (const event of INPUT_EVENTS) {
|
|
278
408
|
document.removeEventListener(event, recordInput, true);
|
|
279
409
|
}
|
|
@@ -281,9 +411,16 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
281
411
|
});
|
|
282
412
|
}
|
|
283
413
|
var playthroughTest = Object.assign(
|
|
284
|
-
(element,
|
|
414
|
+
(element, optionsOrRun, maybeRun) => {
|
|
415
|
+
if (typeof optionsOrRun === "function") {
|
|
416
|
+
definePlaythrough(element, optionsOrRun);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (!maybeRun) throw new TypeError("playthroughTest requires a run callback.");
|
|
420
|
+
definePlaythrough(element, maybeRun, optionsOrRun);
|
|
421
|
+
},
|
|
285
422
|
{
|
|
286
|
-
skip: (reason, element, run) => definePlaythrough(element, run, reason)
|
|
423
|
+
skip: (reason, element, run) => definePlaythrough(element, run, void 0, reason)
|
|
287
424
|
}
|
|
288
425
|
);
|
|
289
426
|
function auditReactPlaythroughRun(tests) {
|
|
@@ -300,7 +437,7 @@ function auditReactPlaythroughRun(tests) {
|
|
|
300
437
|
const issues = [];
|
|
301
438
|
if (declared.length === 0) {
|
|
302
439
|
issues.push(
|
|
303
|
-
'
|
|
440
|
+
'No production gameplay verification was declared. Use playthroughTest to render <App />, then run performInput("entry"), checkpoint("entered"), performInput("primary"), and a bounded stepUntil. Assert the authoritative outcome with the expect provided by playthroughTest, then record checkpoint("progress") or checkpoint("terminal").'
|
|
304
441
|
);
|
|
305
442
|
} else {
|
|
306
443
|
for (const candidate of declared) {
|
|
@@ -309,13 +446,17 @@ function auditReactPlaythroughRun(tests) {
|
|
|
309
446
|
if (isValid || isWaived) continue;
|
|
310
447
|
if (candidate.state === "skipped") {
|
|
311
448
|
issues.push(
|
|
312
|
-
|
|
449
|
+
`Playthrough ${JSON.stringify(candidate.name)} was skipped without an explicit reason of at least 20 characters.`
|
|
313
450
|
);
|
|
314
451
|
} else if (candidate.state !== "passed") {
|
|
315
|
-
issues.push(
|
|
452
|
+
issues.push(
|
|
453
|
+
`Playthrough ${JSON.stringify(candidate.name)} finished with state ${candidate.state}.`
|
|
454
|
+
);
|
|
316
455
|
} else {
|
|
317
456
|
const missing = describeMissingEvidence(candidate.metadata?.evidence);
|
|
318
|
-
issues.push(
|
|
457
|
+
issues.push(
|
|
458
|
+
`Playthrough ${JSON.stringify(candidate.name)} is missing ${missing}.`
|
|
459
|
+
);
|
|
319
460
|
}
|
|
320
461
|
}
|
|
321
462
|
}
|