miaoda-game-devkit 0.2.14 → 0.2.16

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.
@@ -52,6 +52,169 @@ var ManualGameClock = class {
52
52
  return this.timers.size;
53
53
  }
54
54
  };
55
+
56
+ // src/react/react-playthrough.ts
57
+ import { render } from "@testing-library/react";
58
+ import { expect, test } from "vitest";
59
+
60
+ // src/react/react-playthrough-core.ts
61
+ import { act } from "@testing-library/react";
62
+ function normalizePlaythroughWaiverReason(waiverReason) {
63
+ if (waiverReason === void 0) return void 0;
64
+ const reason = waiverReason.trim();
65
+ if (reason.length < 20) {
66
+ throw new Error(
67
+ "playthroughTest.skip reason must contain at least 20 characters."
68
+ );
69
+ }
70
+ return reason;
71
+ }
72
+ async function runBoundedUntil(condition, options = {}) {
73
+ const maxSteps = options.maxSteps ?? 120;
74
+ if (!Number.isSafeInteger(maxSteps) || maxSteps < 0 || maxSteps > 1e4) {
75
+ throw new RangeError(
76
+ "stepUntil maxSteps must be a safe integer between 0 and 10000."
77
+ );
78
+ }
79
+ for (let step = 0; step <= maxSteps; step += 1) {
80
+ if (condition()) return step;
81
+ if (step < maxSteps) {
82
+ await act(async () => {
83
+ await options.step?.(step + 1);
84
+ });
85
+ }
86
+ }
87
+ throw new Error(
88
+ `Playthrough outcome was not reached within ${maxSteps} steps.`
89
+ );
90
+ }
91
+
92
+ // src/react/react-playthrough.ts
93
+ var INPUT_EVENTS = [
94
+ "click",
95
+ "keydown",
96
+ "keyup",
97
+ "pointerdown",
98
+ "pointerup",
99
+ "touchstart",
100
+ "touchend"
101
+ ];
102
+ function createMetadata(waiverReason) {
103
+ return {
104
+ version: 1,
105
+ waiverReason,
106
+ evidence: {
107
+ domInputEvents: 0,
108
+ boundedRuns: 0,
109
+ assertionsAfterOutcome: 0,
110
+ verified: false
111
+ }
112
+ };
113
+ }
114
+ function definePlaythrough(element, run, waiverReason) {
115
+ const reason = normalizePlaythroughWaiverReason(waiverReason);
116
+ const metadata = createMetadata(reason);
117
+ test("production game completes a bounded playthrough", {
118
+ skip: Boolean(reason),
119
+ meta: { reactPlaythrough: metadata }
120
+ }, async () => {
121
+ const evidence = metadata.evidence;
122
+ let assertionsAtOutcome;
123
+ const recordInput = () => {
124
+ evidence.domInputEvents += 1;
125
+ };
126
+ for (const event of INPUT_EVENTS) {
127
+ document.addEventListener(event, recordInput, true);
128
+ }
129
+ try {
130
+ const view = render(element);
131
+ if (view.container.childNodes.length === 0) {
132
+ throw new Error(
133
+ "playthroughTest must render the production game entry."
134
+ );
135
+ }
136
+ await run({
137
+ view,
138
+ async stepUntil(condition, options = {}) {
139
+ const steps = await runBoundedUntil(condition, options);
140
+ if (evidence.domInputEvents === 0) {
141
+ throw new Error(
142
+ "stepUntil reached an outcome before any production DOM input."
143
+ );
144
+ }
145
+ evidence.boundedRuns += 1;
146
+ assertionsAtOutcome = expect.getState().assertionCalls;
147
+ return steps;
148
+ }
149
+ });
150
+ const assertionCalls = expect.getState().assertionCalls;
151
+ evidence.assertionsAfterOutcome = assertionsAtOutcome === void 0 ? 0 : assertionCalls - assertionsAtOutcome;
152
+ if (evidence.domInputEvents === 0) {
153
+ throw new Error(
154
+ "playthroughTest did not observe production DOM input."
155
+ );
156
+ }
157
+ if (evidence.boundedRuns === 0) {
158
+ throw new Error("playthroughTest must complete one bounded stepUntil.");
159
+ }
160
+ if (evidence.assertionsAfterOutcome === 0) {
161
+ throw new Error(
162
+ "Assert an authoritative game outcome after stepUntil returns."
163
+ );
164
+ }
165
+ evidence.verified = true;
166
+ } finally {
167
+ for (const event of INPUT_EVENTS) {
168
+ document.removeEventListener(event, recordInput, true);
169
+ }
170
+ }
171
+ });
172
+ }
173
+ var playthroughTest = Object.assign(
174
+ (element, run) => definePlaythrough(element, run),
175
+ {
176
+ skip: (reason, element, run) => definePlaythrough(element, run, reason)
177
+ }
178
+ );
179
+ function auditReactPlaythroughRun(tests) {
180
+ const declared = tests.filter((candidate) => candidate.metadata);
181
+ const valid = declared.filter(({ state, metadata }) => {
182
+ const evidence = metadata?.evidence;
183
+ return state === "passed" && evidence?.verified === true && evidence.domInputEvents > 0 && evidence.boundedRuns > 0 && evidence.assertionsAfterOutcome > 0;
184
+ });
185
+ const waivers = declared.filter(
186
+ ({ state, metadata }) => state === "skipped" && (metadata?.waiverReason?.trim().length ?? 0) >= 20
187
+ );
188
+ const issues = [];
189
+ if (declared.length === 0) {
190
+ issues.push(
191
+ "\u7F3A\u5C11\u751F\u4EA7\u6E38\u620F\u53EF\u73A9\u6027\u9A8C\u8BC1\uFF1A\u4F7F\u7528 playthroughTest \u6E32\u67D3 <App />\uFF0C\u901A\u8FC7\u771F\u5B9E DOM \u8F93\u5165\u5B8C\u6210\u4E00\u4E2A\u6709\u6B65\u6570\u4E0A\u9650\u7684\u5173\u952E\u6D41\u7A0B\u3002"
192
+ );
193
+ } else {
194
+ for (const candidate of declared) {
195
+ const isValid = valid.includes(candidate);
196
+ const isWaived = waivers.includes(candidate);
197
+ if (isValid || isWaived) continue;
198
+ if (candidate.state === "skipped") {
199
+ issues.push(
200
+ `\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u88AB\u8DF3\u8FC7\uFF0C\u4F46\u6CA1\u6709\u81F3\u5C11 20 \u4E2A\u5B57\u7B26\u7684\u660E\u786E\u7406\u7531\u3002`
201
+ );
202
+ } else if (candidate.state !== "passed") {
203
+ issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7684\u72B6\u6001\u4E3A ${candidate.state}\u3002`);
204
+ } else {
205
+ issues.push(
206
+ `\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u6CA1\u6709\u7559\u4E0B\u5B8C\u6574\u7684\u8F93\u5165\u3001\u6709\u9650\u63A8\u8FDB\u548C\u7ED3\u679C\u65AD\u8A00\u8BC1\u636E\u3002`
207
+ );
208
+ }
209
+ }
210
+ }
211
+ if (issues.length > 0) return { passed: false, waived: false, issues };
212
+ if (valid.length > 0) return { passed: true, waived: false, issues: [] };
213
+ if (waivers.length > 0) return { passed: true, waived: true, issues: [] };
214
+ return { passed: false, waived: false, issues };
215
+ }
55
216
  export {
56
- ManualGameClock
217
+ ManualGameClock,
218
+ auditReactPlaythroughRun,
219
+ playthroughTest
57
220
  };
@@ -23,9 +23,327 @@ __export(react_vitest_config_exports, {
23
23
  defineReactGameVitestConfig: () => defineReactGameVitestConfig
24
24
  });
25
25
  module.exports = __toCommonJS(react_vitest_config_exports);
26
+ var import_node_fs2 = require("fs");
27
+ var import_node_path2 = require("path");
28
+ var import_config = require("vitest/config");
29
+
30
+ // src/react/react-playthrough-reporter.ts
26
31
  var import_node_fs = require("fs");
27
32
  var import_node_path = require("path");
28
- var import_config = require("vitest/config");
33
+
34
+ // src/react/react-playthrough.ts
35
+ var import_react2 = require("@testing-library/react");
36
+ var import_vitest = require("vitest");
37
+
38
+ // src/react/react-playthrough-core.ts
39
+ var import_react = require("@testing-library/react");
40
+ function normalizePlaythroughWaiverReason(waiverReason) {
41
+ if (waiverReason === void 0) return void 0;
42
+ const reason = waiverReason.trim();
43
+ if (reason.length < 20) {
44
+ throw new Error(
45
+ "playthroughTest.skip reason must contain at least 20 characters."
46
+ );
47
+ }
48
+ return reason;
49
+ }
50
+ async function runBoundedUntil(condition, options = {}) {
51
+ const maxSteps = options.maxSteps ?? 120;
52
+ if (!Number.isSafeInteger(maxSteps) || maxSteps < 0 || maxSteps > 1e4) {
53
+ throw new RangeError(
54
+ "stepUntil maxSteps must be a safe integer between 0 and 10000."
55
+ );
56
+ }
57
+ for (let step = 0; step <= maxSteps; step += 1) {
58
+ if (condition()) return step;
59
+ if (step < maxSteps) {
60
+ await (0, import_react.act)(async () => {
61
+ await options.step?.(step + 1);
62
+ });
63
+ }
64
+ }
65
+ throw new Error(
66
+ `Playthrough outcome was not reached within ${maxSteps} steps.`
67
+ );
68
+ }
69
+
70
+ // src/react/react-playthrough.ts
71
+ var INPUT_EVENTS = [
72
+ "click",
73
+ "keydown",
74
+ "keyup",
75
+ "pointerdown",
76
+ "pointerup",
77
+ "touchstart",
78
+ "touchend"
79
+ ];
80
+ function createMetadata(waiverReason) {
81
+ return {
82
+ version: 1,
83
+ waiverReason,
84
+ evidence: {
85
+ domInputEvents: 0,
86
+ boundedRuns: 0,
87
+ assertionsAfterOutcome: 0,
88
+ verified: false
89
+ }
90
+ };
91
+ }
92
+ function definePlaythrough(element, run, waiverReason) {
93
+ const reason = normalizePlaythroughWaiverReason(waiverReason);
94
+ const metadata = createMetadata(reason);
95
+ (0, import_vitest.test)("production game completes a bounded playthrough", {
96
+ skip: Boolean(reason),
97
+ meta: { reactPlaythrough: metadata }
98
+ }, async () => {
99
+ const evidence = metadata.evidence;
100
+ let assertionsAtOutcome;
101
+ const recordInput = () => {
102
+ evidence.domInputEvents += 1;
103
+ };
104
+ for (const event of INPUT_EVENTS) {
105
+ document.addEventListener(event, recordInput, true);
106
+ }
107
+ try {
108
+ const view = (0, import_react2.render)(element);
109
+ if (view.container.childNodes.length === 0) {
110
+ throw new Error(
111
+ "playthroughTest must render the production game entry."
112
+ );
113
+ }
114
+ await run({
115
+ view,
116
+ async stepUntil(condition, options = {}) {
117
+ const steps = await runBoundedUntil(condition, options);
118
+ if (evidence.domInputEvents === 0) {
119
+ throw new Error(
120
+ "stepUntil reached an outcome before any production DOM input."
121
+ );
122
+ }
123
+ evidence.boundedRuns += 1;
124
+ assertionsAtOutcome = import_vitest.expect.getState().assertionCalls;
125
+ return steps;
126
+ }
127
+ });
128
+ const assertionCalls = import_vitest.expect.getState().assertionCalls;
129
+ evidence.assertionsAfterOutcome = assertionsAtOutcome === void 0 ? 0 : assertionCalls - assertionsAtOutcome;
130
+ if (evidence.domInputEvents === 0) {
131
+ throw new Error(
132
+ "playthroughTest did not observe production DOM input."
133
+ );
134
+ }
135
+ if (evidence.boundedRuns === 0) {
136
+ throw new Error("playthroughTest must complete one bounded stepUntil.");
137
+ }
138
+ if (evidence.assertionsAfterOutcome === 0) {
139
+ throw new Error(
140
+ "Assert an authoritative game outcome after stepUntil returns."
141
+ );
142
+ }
143
+ evidence.verified = true;
144
+ } finally {
145
+ for (const event of INPUT_EVENTS) {
146
+ document.removeEventListener(event, recordInput, true);
147
+ }
148
+ }
149
+ });
150
+ }
151
+ var playthroughTest = Object.assign(
152
+ (element, run) => definePlaythrough(element, run),
153
+ {
154
+ skip: (reason, element, run) => definePlaythrough(element, run, reason)
155
+ }
156
+ );
157
+ function auditReactPlaythroughRun(tests) {
158
+ const declared = tests.filter((candidate) => candidate.metadata);
159
+ const valid = declared.filter(({ state, metadata }) => {
160
+ const evidence = metadata?.evidence;
161
+ return state === "passed" && evidence?.verified === true && evidence.domInputEvents > 0 && evidence.boundedRuns > 0 && evidence.assertionsAfterOutcome > 0;
162
+ });
163
+ const waivers = declared.filter(
164
+ ({ state, metadata }) => state === "skipped" && (metadata?.waiverReason?.trim().length ?? 0) >= 20
165
+ );
166
+ const issues = [];
167
+ if (declared.length === 0) {
168
+ issues.push(
169
+ "\u7F3A\u5C11\u751F\u4EA7\u6E38\u620F\u53EF\u73A9\u6027\u9A8C\u8BC1\uFF1A\u4F7F\u7528 playthroughTest \u6E32\u67D3 <App />\uFF0C\u901A\u8FC7\u771F\u5B9E DOM \u8F93\u5165\u5B8C\u6210\u4E00\u4E2A\u6709\u6B65\u6570\u4E0A\u9650\u7684\u5173\u952E\u6D41\u7A0B\u3002"
170
+ );
171
+ } else {
172
+ for (const candidate of declared) {
173
+ const isValid = valid.includes(candidate);
174
+ const isWaived = waivers.includes(candidate);
175
+ if (isValid || isWaived) continue;
176
+ if (candidate.state === "skipped") {
177
+ issues.push(
178
+ `\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u88AB\u8DF3\u8FC7\uFF0C\u4F46\u6CA1\u6709\u81F3\u5C11 20 \u4E2A\u5B57\u7B26\u7684\u660E\u786E\u7406\u7531\u3002`
179
+ );
180
+ } else if (candidate.state !== "passed") {
181
+ issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7684\u72B6\u6001\u4E3A ${candidate.state}\u3002`);
182
+ } else {
183
+ issues.push(
184
+ `\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u6CA1\u6709\u7559\u4E0B\u5B8C\u6574\u7684\u8F93\u5165\u3001\u6709\u9650\u63A8\u8FDB\u548C\u7ED3\u679C\u65AD\u8A00\u8BC1\u636E\u3002`
185
+ );
186
+ }
187
+ }
188
+ }
189
+ if (issues.length > 0) return { passed: false, waived: false, issues };
190
+ if (valid.length > 0) return { passed: true, waived: false, issues: [] };
191
+ if (waivers.length > 0) return { passed: true, waived: true, issues: [] };
192
+ return { passed: false, waived: false, issues };
193
+ }
194
+
195
+ // src/react/react-playthrough-reporter.ts
196
+ var PRODUCTION_PLAYTHROUGH_FILE = "tests/production-playthrough.test.tsx";
197
+ function isMetadata(value) {
198
+ if (!value || typeof value !== "object") return false;
199
+ const metadata = value;
200
+ return metadata.version === 1 && Boolean(metadata.evidence);
201
+ }
202
+ function toAuditInput(test2) {
203
+ const metadata = test2.meta().reactPlaythrough;
204
+ return {
205
+ name: test2.fullName,
206
+ state: test2.result().state,
207
+ metadata: isMetadata(metadata) ? metadata : void 0
208
+ };
209
+ }
210
+ function firstLine(value) {
211
+ if (typeof value !== "string") return void 0;
212
+ return value.split("\n").map((line) => line.trim()).find(Boolean);
213
+ }
214
+ function toModuleResult(module2, projectRoot) {
215
+ const tests = [...module2.children.allTests()];
216
+ const errors = [
217
+ ...module2.errors().map((error) => firstLine(error.message)),
218
+ ...tests.flatMap(
219
+ (test2) => (test2.result().errors ?? []).map((error) => firstLine(error.message))
220
+ )
221
+ ].filter((message) => Boolean(message));
222
+ return {
223
+ file: (0, import_node_path.relative)(projectRoot, module2.moduleId).replaceAll("\\", "/"),
224
+ state: module2.state(),
225
+ errors,
226
+ tests: tests.map(toAuditInput)
227
+ };
228
+ }
229
+ function assessReactPlaythroughReport(input) {
230
+ const base = { file: input.expectedFile };
231
+ if (!input.expectedFileScheduled) {
232
+ if (input.expectedFileExists) {
233
+ return {
234
+ ...base,
235
+ status: "NOT_CHECKED",
236
+ next: `\u672C\u6B21\u662F\u805A\u7126\u8FD0\u884C\uFF0C\u672A\u68C0\u67E5\u6700\u4F4E\u53EF\u73A9\u6D41\u7A0B\uFF1B\u63D0\u4EA4\u524D\u8FD0\u884C pnpm test\uFF0C\u786E\u4FDD ${input.expectedFile} \u901A\u8FC7\u3002`,
237
+ failsRun: false
238
+ };
239
+ }
240
+ return {
241
+ ...base,
242
+ status: "FAILED",
243
+ cause: "\u751F\u4EA7\u53EF\u73A9\u6027\u6D4B\u8BD5\u6587\u4EF6\u4E0D\u5B58\u5728\u3002",
244
+ next: "\u521B\u5EFA\u8BE5\u6587\u4EF6\uFF1A\u4ECE <App /> \u89E6\u53D1\u771F\u5B9E DOM \u8F93\u5165\uFF0C\u7528 stepUntil \u6709\u754C\u63A8\u8FDB\u5230\u73A9\u5BB6\u53EF\u89C1\u7ED3\u679C\u6216\u6E38\u620F\u771F\u5B9E\u72B6\u6001\u7684\u53EA\u8BFB snapshot\uFF0C\u518D\u65AD\u8A00\u7ED3\u679C\u3002",
245
+ failsRun: true
246
+ };
247
+ }
248
+ const productionModule = input.modules.find(
249
+ (module2) => module2.file === input.expectedFile
250
+ );
251
+ const executedProductionFlow = productionModule?.tests.some(
252
+ (test2) => test2.state !== "skipped" && test2.state !== "pending"
253
+ );
254
+ if (input.focusedSelection && !executedProductionFlow) {
255
+ return {
256
+ ...base,
257
+ status: "NOT_CHECKED",
258
+ next: `\u672C\u6B21\u540D\u79F0\u6216\u884C\u53F7\u8FC7\u6EE4\u6CA1\u6709\u6267\u884C\u6700\u4F4E\u53EF\u73A9\u6D41\u7A0B\uFF1B\u63D0\u4EA4\u524D\u8FD0\u884C pnpm test\uFF0C\u786E\u4FDD ${input.expectedFile} \u901A\u8FC7\u3002`,
259
+ failsRun: false
260
+ };
261
+ }
262
+ if (!productionModule || productionModule.tests.length === 0) {
263
+ return {
264
+ ...base,
265
+ status: "NOT_RUN",
266
+ cause: productionModule?.errors[0] ?? input.unhandledErrors?.[0] ?? "\u751F\u4EA7\u53EF\u73A9\u6027\u6D4B\u8BD5\u672A\u5B8C\u6210\u6536\u96C6\u6216\u6267\u884C\u3002",
267
+ next: "\u5148\u4FEE\u590D Vitest \u4E0A\u65B9\u9996\u4E2A\u8BED\u6CD5\u3001\u5BFC\u5165\u6216\u6536\u96C6\u9519\u8BEF\uFF0C\u518D\u8FD0\u884C pnpm test\uFF1B\u4E0D\u8981\u7528 skip \u63A9\u76D6\u52A0\u8F7D\u5931\u8D25\u3002",
268
+ failsRun: true
269
+ };
270
+ }
271
+ const tests = input.modules.flatMap((module2) => module2.tests);
272
+ const audit = auditReactPlaythroughRun(tests);
273
+ if (!audit.passed) {
274
+ const cause = productionModule.errors[0] ?? input.unhandledErrors?.[0] ?? audit.issues[0] ?? "\u751F\u4EA7\u53EF\u73A9\u6D41\u7A0B\u6CA1\u6709\u7559\u4E0B\u5B8C\u6574\u8BC1\u636E\u3002";
275
+ const timedOut = /outcome was not reached within \d+ steps/i.test(cause);
276
+ return {
277
+ ...base,
278
+ status: "FAILED",
279
+ cause,
280
+ next: timedOut ? "\u771F\u5B9E\u8F93\u5165\u5DF2\u6267\u884C\uFF0C\u4F46\u73A9\u6CD5\u6CA1\u6709\u5728\u4E0A\u9650\u5185\u4EA7\u751F\u7ED3\u679C\uFF1B\u68C0\u67E5\u70B9\u51FB\u6216\u6309\u952E\u662F\u5426\u771F\u7684\u8C03\u7528\u6E38\u620F\u7684\u751F\u4EA7\u63A7\u5236\u65B9\u6CD5\uFF0C\u4EE5\u53CA\u6E38\u620F\u771F\u5B9E\u72B6\u6001\u662F\u5426\u5230\u8FBE\u5E76\u663E\u793A\u7ED3\u679C\u3002" : "\u4ECE\u751F\u4EA7\u5165\u53E3\u6267\u884C\u771F\u5B9E DOM \u8F93\u5165\uFF0C\u7528 stepUntil \u6709\u754C\u63A8\u8FDB\u5230\u73A9\u5BB6\u53EF\u89C1\u7ED3\u679C\u6216\u6E38\u620F\u771F\u5B9E\u72B6\u6001\u7684\u53EA\u8BFB snapshot\uFF0C\u5E76\u5728\u5176\u540E\u65AD\u8A00\uFF1B\u4E0D\u8981\u76F4\u8FBE\u5185\u90E8\u5173\u5361\u6216\u4FEE\u6539\u73A9\u6CD5\u72B6\u6001\u3002",
281
+ failsRun: true
282
+ };
283
+ }
284
+ if (audit.waived) {
285
+ return {
286
+ ...base,
287
+ status: "WAIVED",
288
+ waiverReasons: tests.map((test2) => test2.metadata?.waiverReason).filter((reason) => Boolean(reason)),
289
+ failsRun: false
290
+ };
291
+ }
292
+ return { ...base, status: "PASS", failsRun: false };
293
+ }
294
+ function formatReactPlaythroughReport(report) {
295
+ const lines = [`REACT_PLAYTHROUGH: ${report.status}`, `FILE: ${report.file}`];
296
+ if (report.cause) lines.push(`CAUSE: ${report.cause}`);
297
+ if (report.waiverReasons?.length) {
298
+ lines.push(`REASON: ${report.waiverReasons.join("\uFF1B")}`);
299
+ }
300
+ if (report.next) lines.push(`NEXT: ${report.next}`);
301
+ return `
302
+ ${lines.join("\n")}`;
303
+ }
304
+ var ReactPlaythroughReporter = class {
305
+ /** 绑定模板根目录,以稳定识别生产可玩性测试而不依赖测试名称。 */
306
+ constructor(projectRoot) {
307
+ this.projectRoot = projectRoot;
308
+ this.expectedModuleId = (0, import_node_path.resolve)(projectRoot, this.expectedFile);
309
+ }
310
+ projectRoot;
311
+ expectedFile = PRODUCTION_PLAYTHROUGH_FILE;
312
+ expectedModuleId;
313
+ expectedFileScheduled = false;
314
+ focusedSelection = false;
315
+ /** 记录本轮是否实际选择了生产流程文件,用于区分聚焦运行与门禁失败。 */
316
+ onTestRunStart(specifications) {
317
+ this.expectedFileScheduled = specifications.some(
318
+ (specification) => (0, import_node_path.resolve)(specification.moduleId) === this.expectedModuleId
319
+ );
320
+ this.focusedSelection = specifications.some(
321
+ (specification) => Boolean(specification.project.globalConfig.testNamePattern) || Boolean(specification.testNamePattern) || Boolean(specification.testLines?.length)
322
+ );
323
+ }
324
+ /** 测试运行结束后执行项目级主流程门禁,并写入最终退出码。 */
325
+ onTestRunEnd(testModules, unhandledErrors) {
326
+ const report = assessReactPlaythroughReport({
327
+ expectedFile: this.expectedFile,
328
+ expectedFileExists: (0, import_node_fs.existsSync)(this.expectedModuleId),
329
+ expectedFileScheduled: this.expectedFileScheduled,
330
+ focusedSelection: this.focusedSelection,
331
+ modules: testModules.map(
332
+ (module2) => toModuleResult(module2, this.projectRoot)
333
+ ),
334
+ unhandledErrors: unhandledErrors.map((error) => firstLine(error.message)).filter((message) => Boolean(message))
335
+ });
336
+ const output = formatReactPlaythroughReport(report);
337
+ if (report.failsRun) {
338
+ console.error(output);
339
+ process.exitCode = 1;
340
+ } else if (report.status === "WAIVED" || report.status === "NOT_CHECKED") {
341
+ console.warn(output);
342
+ } else {
343
+ console.log(output);
344
+ }
345
+ }
346
+ };
29
347
 
30
348
  // src/testing/vitest-node-args.ts
31
349
  function getJSDOMWorkerExecArgv() {
@@ -35,16 +353,16 @@ function getJSDOMWorkerExecArgv() {
35
353
 
36
354
  // src/react-vitest-config.ts
37
355
  function resolvePhaser3BrowserEntry(projectRoot) {
38
- const manifestPath = (0, import_node_path.resolve)(projectRoot, "node_modules/phaser/package.json");
39
- if (!(0, import_node_fs.existsSync)(manifestPath)) return void 0;
356
+ const manifestPath = (0, import_node_path2.resolve)(projectRoot, "node_modules/phaser/package.json");
357
+ if (!(0, import_node_fs2.existsSync)(manifestPath)) return void 0;
40
358
  try {
41
- const manifest = JSON.parse((0, import_node_fs.readFileSync)(manifestPath, "utf8"));
359
+ const manifest = JSON.parse((0, import_node_fs2.readFileSync)(manifestPath, "utf8"));
42
360
  if (!manifest.version?.startsWith("3.")) return void 0;
43
- const browserEntry = (0, import_node_path.resolve)(
44
- (0, import_node_path.dirname)(manifestPath),
361
+ const browserEntry = (0, import_node_path2.resolve)(
362
+ (0, import_node_path2.dirname)(manifestPath),
45
363
  manifest.browser ?? "dist/phaser.js"
46
364
  );
47
- return (0, import_node_fs.existsSync)(browserEntry) ? browserEntry : void 0;
365
+ return (0, import_node_fs2.existsSync)(browserEntry) ? browserEntry : void 0;
48
366
  } catch {
49
367
  return void 0;
50
368
  }
@@ -56,7 +374,7 @@ function defineReactGameVitestConfig(options) {
56
374
  alias: {
57
375
  ...phaser3BrowserEntry ? { phaser: phaser3BrowserEntry } : {},
58
376
  ...options.aliases,
59
- "@": (0, import_node_path.resolve)(options.projectRoot, "src")
377
+ "@": (0, import_node_path2.resolve)(options.projectRoot, "src")
60
378
  }
61
379
  },
62
380
  test: {
@@ -79,7 +397,8 @@ function defineReactGameVitestConfig(options) {
79
397
  sequence: {
80
398
  setupFiles: "list"
81
399
  },
82
- reporters: ["minimal"],
400
+ // minimal 保留业务失败;附加 reporter 负责项目级最低可玩性门禁。
401
+ reporters: ["minimal", new ReactPlaythroughReporter(options.projectRoot)],
83
402
  restoreMocks: true,
84
403
  clearMocks: true,
85
404
  testTimeout: options.testTimeout,