miaoda-game-devkit 0.2.21 → 0.4.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 +5 -4
- package/dist/react/index.d.mts +19 -1
- package/dist/react/index.d.ts +19 -1
- package/dist/react/index.js +50 -0
- package/dist/react/index.mjs +49 -0
- package/dist/react/testing.d.mts +8 -1
- package/dist/react/testing.d.ts +8 -1
- package/dist/react/testing.js +100 -16
- package/dist/react/testing.mjs +100 -16
- package/dist/react/vitest-config.js +195 -32
- package/dist/react/vitest-config.mjs +195 -32
- package/dist/rules/react-test-boundary-plugin.js +97 -0
- package/oxlint-config.json +4 -2
- package/package.json +1 -1
|
@@ -40,6 +40,7 @@ var import_config = require("vitest/config");
|
|
|
40
40
|
// src/react/react-playthrough-reporter.ts
|
|
41
41
|
var import_node_fs = require("fs");
|
|
42
42
|
var import_node_path = require("path");
|
|
43
|
+
var import_node_util = require("util");
|
|
43
44
|
|
|
44
45
|
// src/react/react-playthrough.ts
|
|
45
46
|
var import_react2 = require("@testing-library/react");
|
|
@@ -102,20 +103,50 @@ var INPUT_EVENTS = [
|
|
|
102
103
|
"touchend"
|
|
103
104
|
];
|
|
104
105
|
var MIN_CHECKPOINTS = 2;
|
|
106
|
+
var MAX_FORMATTED_OBSERVATION_LENGTH = 500;
|
|
107
|
+
function formatObservation(fingerprint) {
|
|
108
|
+
if (fingerprint.length <= MAX_FORMATTED_OBSERVATION_LENGTH) return fingerprint;
|
|
109
|
+
return `${fingerprint.slice(0, MAX_FORMATTED_OBSERVATION_LENGTH)}\u2026 (${fingerprint.length} chars)`;
|
|
110
|
+
}
|
|
111
|
+
function sampleObservation(observe, stage) {
|
|
112
|
+
let value;
|
|
113
|
+
try {
|
|
114
|
+
value = observe();
|
|
115
|
+
} catch (error) {
|
|
116
|
+
throw new Error(`observe() threw at ${stage}: ${String(error)}`);
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const fingerprint = JSON.stringify(value);
|
|
120
|
+
if (fingerprint === void 0) throw new Error("unsupported value");
|
|
121
|
+
return { fingerprint, formatted: formatObservation(fingerprint) };
|
|
122
|
+
} catch {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`observe() must return JSON-serializable read-only state; sampling failed at ${stage}.`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function formatObservationTimeline(entered, afterPrimary, outcome) {
|
|
129
|
+
return [
|
|
130
|
+
`entered=${entered.formatted}`,
|
|
131
|
+
`after-primary=${afterPrimary?.formatted ?? "<not sampled>"}`,
|
|
132
|
+
`outcome=${outcome.formatted}`
|
|
133
|
+
].join(", ");
|
|
134
|
+
}
|
|
105
135
|
function describeMissingEvidence(evidence) {
|
|
106
|
-
if (!evidence || evidence.entryInputs === 0) return "entry
|
|
107
|
-
if (evidence.primaryInputs === 0) return "primary
|
|
136
|
+
if (!evidence || evidence.entryInputs === 0) return "an entry input";
|
|
137
|
+
if (evidence.primaryInputs === 0) return "a primary gameplay input";
|
|
108
138
|
if (!evidence.checkpoints.includes("entered")) return "entered checkpoint";
|
|
109
|
-
if (evidence.boundedRuns === 0) return "
|
|
110
|
-
if (evidence.assertionsAfterOutcome === 0)
|
|
139
|
+
if (evidence.boundedRuns === 0) return "a bounded stepUntil call";
|
|
140
|
+
if (evidence.assertionsAfterOutcome === 0)
|
|
141
|
+
return "an outcome assertion after stepUntil";
|
|
111
142
|
if (evidence.checkpoints.length < MIN_CHECKPOINTS)
|
|
112
|
-
return
|
|
143
|
+
return `at least ${MIN_CHECKPOINTS} checkpoints`;
|
|
113
144
|
if (!evidence.checkpoints.some(
|
|
114
145
|
(checkpoint) => checkpoint === "progress" || checkpoint === "terminal"
|
|
115
146
|
)) {
|
|
116
147
|
return "progress/terminal checkpoint";
|
|
117
148
|
}
|
|
118
|
-
return "
|
|
149
|
+
return "a complete playthrough verification marker";
|
|
119
150
|
}
|
|
120
151
|
function createMetadata(waiverReason) {
|
|
121
152
|
return {
|
|
@@ -132,7 +163,7 @@ function createMetadata(waiverReason) {
|
|
|
132
163
|
}
|
|
133
164
|
};
|
|
134
165
|
}
|
|
135
|
-
function definePlaythrough(element, run, waiverReason) {
|
|
166
|
+
function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
136
167
|
const reason = normalizePlaythroughWaiverReason(waiverReason);
|
|
137
168
|
const metadata = createMetadata(reason);
|
|
138
169
|
(0, import_vitest.test)("production game completes a bounded playthrough", {
|
|
@@ -142,6 +173,9 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
142
173
|
const evidence = metadata.evidence;
|
|
143
174
|
let assertionsAtOutcome;
|
|
144
175
|
let enteredRecorded = false;
|
|
176
|
+
let domTextAtEntered;
|
|
177
|
+
let enteredObservation;
|
|
178
|
+
let afterPrimaryObservation;
|
|
145
179
|
const recordInput = () => {
|
|
146
180
|
evidence.domInputEvents += 1;
|
|
147
181
|
};
|
|
@@ -179,7 +213,15 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
179
213
|
);
|
|
180
214
|
}
|
|
181
215
|
if (kind === "entry") evidence.entryInputs += 1;
|
|
182
|
-
else
|
|
216
|
+
else {
|
|
217
|
+
evidence.primaryInputs += 1;
|
|
218
|
+
if (playthroughOptions?.observe) {
|
|
219
|
+
afterPrimaryObservation = sampleObservation(
|
|
220
|
+
playthroughOptions.observe,
|
|
221
|
+
"after-primary"
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
183
225
|
},
|
|
184
226
|
checkpoint(kind) {
|
|
185
227
|
if (kind === "entered") {
|
|
@@ -194,6 +236,14 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
194
236
|
);
|
|
195
237
|
}
|
|
196
238
|
enteredRecorded = true;
|
|
239
|
+
if (playthroughOptions?.observe) {
|
|
240
|
+
enteredObservation = sampleObservation(
|
|
241
|
+
playthroughOptions.observe,
|
|
242
|
+
"entered"
|
|
243
|
+
);
|
|
244
|
+
} else {
|
|
245
|
+
domTextAtEntered = document.body.textContent ?? "";
|
|
246
|
+
}
|
|
197
247
|
evidence.checkpoints.push(kind);
|
|
198
248
|
return;
|
|
199
249
|
}
|
|
@@ -214,13 +264,37 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
214
264
|
}
|
|
215
265
|
evidence.checkpoints.push(kind);
|
|
216
266
|
},
|
|
217
|
-
async stepUntil(condition,
|
|
218
|
-
const
|
|
267
|
+
async stepUntil(condition, stepOptions = {}) {
|
|
268
|
+
const boundedOptions = stepOptions.diagnostics || !playthroughOptions?.observe ? stepOptions : {
|
|
269
|
+
...stepOptions,
|
|
270
|
+
diagnostics: playthroughOptions.observe
|
|
271
|
+
};
|
|
272
|
+
const steps = await runBoundedUntil(condition, boundedOptions);
|
|
219
273
|
if (evidence.primaryInputs === 0) {
|
|
220
274
|
throw new Error(
|
|
221
275
|
'stepUntil must follow performInput("primary", ...). A menu/help click is not gameplay evidence.'
|
|
222
276
|
);
|
|
223
277
|
}
|
|
278
|
+
if (playthroughOptions?.observe) {
|
|
279
|
+
if (!enteredObservation) {
|
|
280
|
+
throw new Error(
|
|
281
|
+
'observe requires checkpoint("entered") before primary gameplay input.'
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
const outcomeObservation = sampleObservation(
|
|
285
|
+
playthroughOptions.observe,
|
|
286
|
+
"outcome"
|
|
287
|
+
);
|
|
288
|
+
if (outcomeObservation.fingerprint === enteredObservation.fingerprint) {
|
|
289
|
+
throw new Error(
|
|
290
|
+
`The authoritative observation did not change from checkpoint("entered") to the outcome. Timeline: ${formatObservationTimeline(enteredObservation, afterPrimaryObservation, outcomeObservation)}`
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
} else if (steps === 0 && domTextAtEntered !== void 0 && (document.body.textContent ?? "") === domTextAtEntered) {
|
|
294
|
+
throw new Error(
|
|
295
|
+
'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.'
|
|
296
|
+
);
|
|
297
|
+
}
|
|
224
298
|
evidence.boundedRuns += 1;
|
|
225
299
|
assertionsAtOutcome = expect.getState().assertionCalls;
|
|
226
300
|
return steps;
|
|
@@ -262,9 +336,16 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
262
336
|
});
|
|
263
337
|
}
|
|
264
338
|
var playthroughTest = Object.assign(
|
|
265
|
-
(element,
|
|
339
|
+
(element, optionsOrRun, maybeRun) => {
|
|
340
|
+
if (typeof optionsOrRun === "function") {
|
|
341
|
+
definePlaythrough(element, optionsOrRun);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (!maybeRun) throw new TypeError("playthroughTest requires a run callback.");
|
|
345
|
+
definePlaythrough(element, maybeRun, optionsOrRun);
|
|
346
|
+
},
|
|
266
347
|
{
|
|
267
|
-
skip: (reason, element, run) => definePlaythrough(element, run, reason)
|
|
348
|
+
skip: (reason, element, run) => definePlaythrough(element, run, void 0, reason)
|
|
268
349
|
}
|
|
269
350
|
);
|
|
270
351
|
function auditReactPlaythroughRun(tests) {
|
|
@@ -281,7 +362,7 @@ function auditReactPlaythroughRun(tests) {
|
|
|
281
362
|
const issues = [];
|
|
282
363
|
if (declared.length === 0) {
|
|
283
364
|
issues.push(
|
|
284
|
-
'
|
|
365
|
+
'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").'
|
|
285
366
|
);
|
|
286
367
|
} else {
|
|
287
368
|
for (const candidate of declared) {
|
|
@@ -290,13 +371,17 @@ function auditReactPlaythroughRun(tests) {
|
|
|
290
371
|
if (isValid || isWaived) continue;
|
|
291
372
|
if (candidate.state === "skipped") {
|
|
292
373
|
issues.push(
|
|
293
|
-
|
|
374
|
+
`Playthrough ${JSON.stringify(candidate.name)} was skipped without an explicit reason of at least 20 characters.`
|
|
294
375
|
);
|
|
295
376
|
} else if (candidate.state !== "passed") {
|
|
296
|
-
issues.push(
|
|
377
|
+
issues.push(
|
|
378
|
+
`Playthrough ${JSON.stringify(candidate.name)} finished with state ${candidate.state}.`
|
|
379
|
+
);
|
|
297
380
|
} else {
|
|
298
381
|
const missing = describeMissingEvidence(candidate.metadata?.evidence);
|
|
299
|
-
issues.push(
|
|
382
|
+
issues.push(
|
|
383
|
+
`Playthrough ${JSON.stringify(candidate.name)} is missing ${missing}.`
|
|
384
|
+
);
|
|
300
385
|
}
|
|
301
386
|
}
|
|
302
387
|
}
|
|
@@ -323,7 +408,18 @@ function toAuditInput(test2) {
|
|
|
323
408
|
}
|
|
324
409
|
function firstLine(value) {
|
|
325
410
|
if (typeof value !== "string") return void 0;
|
|
326
|
-
return value.split("\n").map((line) => line.trim()).find(Boolean);
|
|
411
|
+
return (0, import_node_util.stripVTControlCharacters)(value).split("\n").map((line) => line.trim()).find(Boolean);
|
|
412
|
+
}
|
|
413
|
+
function failureHint(value) {
|
|
414
|
+
const names = [...value.matchAll(/button\s*\n\s*Name "([^"]+)"/g)].map(
|
|
415
|
+
(match) => match[1]
|
|
416
|
+
);
|
|
417
|
+
if (names.length === 0) return void 0;
|
|
418
|
+
return `Available button names: ${names.map((name) => JSON.stringify(name)).join(", ")}`;
|
|
419
|
+
}
|
|
420
|
+
function errorLocation(value) {
|
|
421
|
+
const match = value.match(/(?:^|\n)\s*at\s+(.*?\.(?:test|spec)\.[^\n]+:\d+:\d+)/);
|
|
422
|
+
return match?.[1];
|
|
327
423
|
}
|
|
328
424
|
function toModuleResult(module2, projectRoot) {
|
|
329
425
|
const tests = [...module2.children.allTests()];
|
|
@@ -333,13 +429,71 @@ function toModuleResult(module2, projectRoot) {
|
|
|
333
429
|
(test2) => (test2.result().errors ?? []).map((error) => firstLine(error.message))
|
|
334
430
|
)
|
|
335
431
|
].filter((message) => Boolean(message));
|
|
432
|
+
const failures = tests.filter((test2) => test2.result().state === "failed").map((test2) => {
|
|
433
|
+
const raw = test2.result().errors?.[0]?.message ?? module2.errors()[0]?.message ?? "Unknown failure";
|
|
434
|
+
return {
|
|
435
|
+
test: test2.fullName,
|
|
436
|
+
cause: firstLine(raw) ?? "Unknown failure",
|
|
437
|
+
location: test2.location ? `${(0, import_node_path.relative)(projectRoot, module2.moduleId).replaceAll("\\", "/")}:${test2.location.line}:${test2.location.column}` : errorLocation(raw),
|
|
438
|
+
hint: failureHint(raw)
|
|
439
|
+
};
|
|
440
|
+
});
|
|
441
|
+
if (failures.length === 0 && tests.length === 0 && module2.errors().length > 0) {
|
|
442
|
+
const raw = module2.errors()[0]?.message ?? "Module failed to load";
|
|
443
|
+
failures.push({
|
|
444
|
+
test: "<collection>",
|
|
445
|
+
cause: firstLine(raw) ?? "Module failed to load",
|
|
446
|
+
location: errorLocation(raw),
|
|
447
|
+
hint: failureHint(raw)
|
|
448
|
+
});
|
|
449
|
+
}
|
|
336
450
|
return {
|
|
337
451
|
file: (0, import_node_path.relative)(projectRoot, module2.moduleId).replaceAll("\\", "/"),
|
|
338
452
|
state: module2.state(),
|
|
339
453
|
errors,
|
|
340
|
-
tests: tests.map(toAuditInput)
|
|
454
|
+
tests: tests.map(toAuditInput),
|
|
455
|
+
failures
|
|
341
456
|
};
|
|
342
457
|
}
|
|
458
|
+
function formatFailureSummary(modules) {
|
|
459
|
+
const failures = modules.flatMap(
|
|
460
|
+
(module2) => (module2.failures ?? []).map((failure) => ({ ...failure, file: module2.file }))
|
|
461
|
+
);
|
|
462
|
+
if (failures.length === 0) return ["TEST_RESULT: PASS"];
|
|
463
|
+
const lines = [`FAILED_TESTS: ${failures.length}`];
|
|
464
|
+
for (const [index, failure] of failures.entries()) {
|
|
465
|
+
lines.push(`FAILURE_${index + 1}: ${failure.file}`);
|
|
466
|
+
lines.push(`TEST: ${failure.test}`);
|
|
467
|
+
lines.push(`CAUSE: ${failure.cause}`);
|
|
468
|
+
if (failure.location) lines.push(`AT: ${failure.location}`);
|
|
469
|
+
if (failure.hint) lines.push(`HINT: ${failure.hint}`);
|
|
470
|
+
}
|
|
471
|
+
lines.push("TEST_RESULT: FAIL");
|
|
472
|
+
return lines;
|
|
473
|
+
}
|
|
474
|
+
function repairGuidance(cause) {
|
|
475
|
+
if (/snapshot\(\) returned the same reference/i.test(cause)) {
|
|
476
|
+
return "The game mutated state without publishing a new snapshot reference, so React skipped the render after an Object.is comparison. Publish a new top-level object before notifying subscribers, for example cachedSnapshot = { ...state }, and never expose a mutable internal object as the snapshot.";
|
|
477
|
+
}
|
|
478
|
+
if (/(?:already true|found the outcome) at step 0.*DOM has not changed/i.test(
|
|
479
|
+
cause
|
|
480
|
+
)) {
|
|
481
|
+
return 'Make the stepUntil condition false at checkpoint("entered"). Verify that the primary input reaches the production control, then wait for a post-input outcome such as a changed score, a removed entry overlay, a completed turn, or a result screen. For Canvas or Controller state outside the DOM, declare observe once on playthroughTest and return the read-only production Telemetry snapshot.';
|
|
482
|
+
}
|
|
483
|
+
if (/authoritative observation did not change/i.test(cause)) {
|
|
484
|
+
return "The flow reached its condition while observe still returned the same authoritative state. Make observe read the same production Controller that React renders, verify the primary input changes that Controller, and wait for a post-input result. Inspect the Timeline values to locate the disconnected stage.";
|
|
485
|
+
}
|
|
486
|
+
if (/No step callback was provided/i.test(cause)) {
|
|
487
|
+
return "This flow is driven by time or frames, but stepUntil did not advance the game clock. Inject the devkit GameClock into the production game and pass step: () => clock.stepFrame(). Do not replace deterministic advancement with a real setTimeout.";
|
|
488
|
+
}
|
|
489
|
+
if (/outcome was not reached within \d+ steps/i.test(cause)) {
|
|
490
|
+
return "The real input was dispatched, but gameplay did not reach the outcome within the bound. Confirm that the production control received the input, then inspect Last diagnostics to determine whether the game loop, rule state, or UI synchronization failed to advance.";
|
|
491
|
+
}
|
|
492
|
+
if (/performInput|checkpoint/.test(cause)) {
|
|
493
|
+
return 'Complete the evidence sequence in order: performInput("entry"), checkpoint("entered"), performInput("primary"), bounded stepUntil, an authoritative result assertion using the provided expect, then checkpoint("progress") or checkpoint("terminal").';
|
|
494
|
+
}
|
|
495
|
+
return "Start from the production entry, dispatch real DOM input, and use stepUntil to reach a bounded player-visible or authoritative game outcome before asserting it. Do not jump to an internal level or mutate gameplay state.";
|
|
496
|
+
}
|
|
343
497
|
function assessReactPlaythroughReport(input) {
|
|
344
498
|
const base = { file: input.expectedFile };
|
|
345
499
|
if (!input.expectedFileScheduled) {
|
|
@@ -347,15 +501,15 @@ function assessReactPlaythroughReport(input) {
|
|
|
347
501
|
return {
|
|
348
502
|
...base,
|
|
349
503
|
status: "NOT_CHECKED",
|
|
350
|
-
next:
|
|
504
|
+
next: `This focused run did not execute the minimum production playthrough. Run pnpm test before submitting and make sure ${input.expectedFile} passes.`,
|
|
351
505
|
failsRun: false
|
|
352
506
|
};
|
|
353
507
|
}
|
|
354
508
|
return {
|
|
355
509
|
...base,
|
|
356
510
|
status: "FAILED",
|
|
357
|
-
cause: "
|
|
358
|
-
next: '
|
|
511
|
+
cause: "The required production playthrough test file does not exist.",
|
|
512
|
+
next: 'Create the file and render <App />. Drive a legal entry with performInput("entry"), record checkpoint("entered"), perform a core game action with performInput("primary"), and use a bounded stepUntil. Assert the authoritative result with the provided expect, then record checkpoint("progress") or checkpoint("terminal").',
|
|
359
513
|
failsRun: true
|
|
360
514
|
};
|
|
361
515
|
}
|
|
@@ -369,7 +523,7 @@ function assessReactPlaythroughReport(input) {
|
|
|
369
523
|
return {
|
|
370
524
|
...base,
|
|
371
525
|
status: "NOT_CHECKED",
|
|
372
|
-
next:
|
|
526
|
+
next: `The name or line filter did not execute the minimum production playthrough. Run pnpm test before submitting and make sure ${input.expectedFile} passes.`,
|
|
373
527
|
failsRun: false
|
|
374
528
|
};
|
|
375
529
|
}
|
|
@@ -377,23 +531,20 @@ function assessReactPlaythroughReport(input) {
|
|
|
377
531
|
return {
|
|
378
532
|
...base,
|
|
379
533
|
status: "NOT_RUN",
|
|
380
|
-
cause: productionModule?.errors[0] ?? input.unhandledErrors?.[0] ?? "
|
|
381
|
-
next: "
|
|
534
|
+
cause: productionModule?.errors[0] ?? input.unhandledErrors?.[0] ?? "The production playthrough could not be collected or executed.",
|
|
535
|
+
next: "Fix the first Vitest syntax, import, environment, or collection error shown above, then run pnpm test again. Do not use skip to hide a load failure.",
|
|
382
536
|
failsRun: true
|
|
383
537
|
};
|
|
384
538
|
}
|
|
385
539
|
const tests = input.modules.flatMap((module2) => module2.tests);
|
|
386
540
|
const audit = auditReactPlaythroughRun(tests);
|
|
387
541
|
if (!audit.passed) {
|
|
388
|
-
const cause = productionModule.errors[0] ?? input.unhandledErrors?.[0] ?? audit.issues[0] ?? "
|
|
389
|
-
const timedOut = /outcome was not reached within \d+ steps/i.test(cause);
|
|
390
|
-
const missingStep = /No step callback was provided/i.test(cause);
|
|
391
|
-
const missingStructuredEvidence = /performInput|checkpoint/.test(cause);
|
|
542
|
+
const cause = productionModule.errors[0] ?? input.unhandledErrors?.[0] ?? audit.issues[0] ?? "The production playthrough did not leave complete gameplay evidence.";
|
|
392
543
|
return {
|
|
393
544
|
...base,
|
|
394
545
|
status: "FAILED",
|
|
395
546
|
cause,
|
|
396
|
-
next:
|
|
547
|
+
next: repairGuidance(cause),
|
|
397
548
|
failsRun: true
|
|
398
549
|
};
|
|
399
550
|
}
|
|
@@ -411,7 +562,7 @@ function formatReactPlaythroughReport(report) {
|
|
|
411
562
|
const lines = [`REACT_PLAYTHROUGH: ${report.status}`, `FILE: ${report.file}`];
|
|
412
563
|
if (report.cause) lines.push(`CAUSE: ${report.cause}`);
|
|
413
564
|
if (report.waiverReasons?.length) {
|
|
414
|
-
lines.push(`REASON: ${report.waiverReasons.join("
|
|
565
|
+
lines.push(`REASON: ${report.waiverReasons.join("; ")}`);
|
|
415
566
|
}
|
|
416
567
|
if (report.next) lines.push(`NEXT: ${report.next}`);
|
|
417
568
|
return `
|
|
@@ -458,6 +609,14 @@ var ReactPlaythroughReporter = class {
|
|
|
458
609
|
} else {
|
|
459
610
|
console.log(output);
|
|
460
611
|
}
|
|
612
|
+
const summary = formatFailureSummary(
|
|
613
|
+
testModules.map((module2) => toModuleResult(module2, this.projectRoot))
|
|
614
|
+
);
|
|
615
|
+
if (report.failsRun && summary[0] === "TEST_RESULT: PASS") {
|
|
616
|
+
summary[0] = "TEST_RESULT: FAIL";
|
|
617
|
+
}
|
|
618
|
+
console.log(`
|
|
619
|
+
${summary.join("\n")}`);
|
|
461
620
|
}
|
|
462
621
|
};
|
|
463
622
|
|
|
@@ -486,6 +645,9 @@ function resolvePhaser3BrowserEntry(projectRoot) {
|
|
|
486
645
|
function defineReactGameVitestConfig(options) {
|
|
487
646
|
const phaser3BrowserEntry = resolvePhaser3BrowserEntry(options.projectRoot);
|
|
488
647
|
return (0, import_config.defineConfig)({
|
|
648
|
+
// Keep discovery and dependency resolution anchored to the generated app even
|
|
649
|
+
// when an external runner invokes Vitest from a parent workspace directory.
|
|
650
|
+
root: options.projectRoot,
|
|
489
651
|
resolve: {
|
|
490
652
|
alias: {
|
|
491
653
|
...phaser3BrowserEntry ? { phaser: phaser3BrowserEntry } : {},
|
|
@@ -506,6 +668,7 @@ function defineReactGameVitestConfig(options) {
|
|
|
506
668
|
environmentOptions: {
|
|
507
669
|
jsdom: { url: "http://localhost/", pretendToBeVisual: true }
|
|
508
670
|
},
|
|
671
|
+
includeTaskLocation: true,
|
|
509
672
|
setupFiles: [
|
|
510
673
|
"miaoda-game-devkit/react/vitest-setup",
|
|
511
674
|
...options.additionalSetupFiles ?? []
|
|
@@ -513,8 +676,8 @@ function defineReactGameVitestConfig(options) {
|
|
|
513
676
|
sequence: {
|
|
514
677
|
setupFiles: "list"
|
|
515
678
|
},
|
|
516
|
-
//
|
|
517
|
-
reporters: [
|
|
679
|
+
// 单一 reporter 输出无 ANSI 的紧凑失败摘要和项目级可玩性门禁。
|
|
680
|
+
reporters: [new ReactPlaythroughReporter(options.projectRoot)],
|
|
518
681
|
restoreMocks: true,
|
|
519
682
|
clearMocks: true,
|
|
520
683
|
testTimeout: options.testTimeout,
|