miaoda-game-devkit 0.2.15 → 0.2.17

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,185 @@ 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 userEvent from "@testing-library/user-event";
59
+ import { expect, test } from "vitest";
60
+
61
+ // src/react/react-playthrough-core.ts
62
+ import { act } from "@testing-library/react";
63
+ function formatDiagnostics(read) {
64
+ if (!read) return void 0;
65
+ try {
66
+ const value = read();
67
+ if (typeof value === "string") return value;
68
+ return JSON.stringify(value);
69
+ } catch (error) {
70
+ return `diagnostics() threw: ${String(error)}`;
71
+ }
72
+ }
73
+ function normalizePlaythroughWaiverReason(waiverReason) {
74
+ if (waiverReason === void 0) return void 0;
75
+ const reason = waiverReason.trim();
76
+ if (reason.length < 20) {
77
+ throw new Error(
78
+ "playthroughTest.skip reason must contain at least 20 characters."
79
+ );
80
+ }
81
+ return reason;
82
+ }
83
+ async function runBoundedUntil(condition, options = {}) {
84
+ const maxSteps = options.maxSteps ?? 120;
85
+ if (!Number.isSafeInteger(maxSteps) || maxSteps < 0 || maxSteps > 1e4) {
86
+ throw new RangeError(
87
+ "stepUntil maxSteps must be a safe integer between 0 and 10000."
88
+ );
89
+ }
90
+ for (let step = 0; step <= maxSteps; step += 1) {
91
+ if (condition()) return step;
92
+ if (step < maxSteps) {
93
+ await act(async () => {
94
+ await options.step?.(step + 1);
95
+ });
96
+ }
97
+ }
98
+ const diagnostics = formatDiagnostics(options.diagnostics);
99
+ const guidance = options.step ? "The step callback ran, but the authoritative outcome did not change." : "No step callback was provided, so time-driven gameplay was not advanced. Inject a ManualGameClock for this test and pass step: () => clock.stepFrame().";
100
+ const suffix = diagnostics ? ` Last diagnostics: ${diagnostics}` : "";
101
+ throw new Error(
102
+ `Playthrough outcome was not reached within ${maxSteps} steps. ${guidance}${suffix}`
103
+ );
104
+ }
105
+
106
+ // src/react/react-playthrough.ts
107
+ var INPUT_EVENTS = [
108
+ "click",
109
+ "keydown",
110
+ "keyup",
111
+ "pointerdown",
112
+ "pointerup",
113
+ "touchstart",
114
+ "touchend"
115
+ ];
116
+ function createMetadata(waiverReason) {
117
+ return {
118
+ version: 1,
119
+ waiverReason,
120
+ evidence: {
121
+ domInputEvents: 0,
122
+ boundedRuns: 0,
123
+ assertionsAfterOutcome: 0,
124
+ verified: false
125
+ }
126
+ };
127
+ }
128
+ function definePlaythrough(element, run, waiverReason) {
129
+ const reason = normalizePlaythroughWaiverReason(waiverReason);
130
+ const metadata = createMetadata(reason);
131
+ test("production game completes a bounded playthrough", {
132
+ skip: Boolean(reason),
133
+ meta: { reactPlaythrough: metadata }
134
+ }, async () => {
135
+ const evidence = metadata.evidence;
136
+ let assertionsAtOutcome;
137
+ const recordInput = () => {
138
+ evidence.domInputEvents += 1;
139
+ };
140
+ for (const event of INPUT_EVENTS) {
141
+ document.addEventListener(event, recordInput, true);
142
+ }
143
+ try {
144
+ const view = render(element);
145
+ if (view.container.childNodes.length === 0) {
146
+ throw new Error(
147
+ "playthroughTest must render the production game entry."
148
+ );
149
+ }
150
+ const user = userEvent.setup();
151
+ await run({
152
+ view,
153
+ user,
154
+ async stepUntil(condition, options = {}) {
155
+ const steps = await runBoundedUntil(condition, options);
156
+ if (evidence.domInputEvents === 0) {
157
+ throw new Error(
158
+ "stepUntil reached an outcome before any production DOM input."
159
+ );
160
+ }
161
+ evidence.boundedRuns += 1;
162
+ assertionsAtOutcome = expect.getState().assertionCalls;
163
+ return steps;
164
+ }
165
+ });
166
+ const assertionCalls = expect.getState().assertionCalls;
167
+ evidence.assertionsAfterOutcome = assertionsAtOutcome === void 0 ? 0 : assertionCalls - assertionsAtOutcome;
168
+ if (evidence.domInputEvents === 0) {
169
+ throw new Error(
170
+ "playthroughTest did not observe production DOM input."
171
+ );
172
+ }
173
+ if (evidence.boundedRuns === 0) {
174
+ throw new Error("playthroughTest must complete one bounded stepUntil.");
175
+ }
176
+ if (evidence.assertionsAfterOutcome === 0) {
177
+ throw new Error(
178
+ "Assert an authoritative game outcome after stepUntil returns."
179
+ );
180
+ }
181
+ evidence.verified = true;
182
+ } finally {
183
+ for (const event of INPUT_EVENTS) {
184
+ document.removeEventListener(event, recordInput, true);
185
+ }
186
+ }
187
+ });
188
+ }
189
+ var playthroughTest = Object.assign(
190
+ (element, run) => definePlaythrough(element, run),
191
+ {
192
+ skip: (reason, element, run) => definePlaythrough(element, run, reason)
193
+ }
194
+ );
195
+ function auditReactPlaythroughRun(tests) {
196
+ const declared = tests.filter((candidate) => candidate.metadata);
197
+ const valid = declared.filter(({ state, metadata }) => {
198
+ const evidence = metadata?.evidence;
199
+ return state === "passed" && evidence?.verified === true && evidence.domInputEvents > 0 && evidence.boundedRuns > 0 && evidence.assertionsAfterOutcome > 0;
200
+ });
201
+ const waivers = declared.filter(
202
+ ({ state, metadata }) => state === "skipped" && (metadata?.waiverReason?.trim().length ?? 0) >= 20
203
+ );
204
+ const issues = [];
205
+ if (declared.length === 0) {
206
+ issues.push(
207
+ "\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"
208
+ );
209
+ } else {
210
+ for (const candidate of declared) {
211
+ const isValid = valid.includes(candidate);
212
+ const isWaived = waivers.includes(candidate);
213
+ if (isValid || isWaived) continue;
214
+ if (candidate.state === "skipped") {
215
+ issues.push(
216
+ `\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`
217
+ );
218
+ } else if (candidate.state !== "passed") {
219
+ issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7684\u72B6\u6001\u4E3A ${candidate.state}\u3002`);
220
+ } else {
221
+ issues.push(
222
+ `\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`
223
+ );
224
+ }
225
+ }
226
+ }
227
+ if (issues.length > 0) return { passed: false, waived: false, issues };
228
+ if (valid.length > 0) return { passed: true, waived: false, issues: [] };
229
+ if (waivers.length > 0) return { passed: true, waived: true, issues: [] };
230
+ return { passed: false, waived: false, issues };
231
+ }
55
232
  export {
56
- ManualGameClock
233
+ ManualGameClock,
234
+ auditReactPlaythroughRun,
235
+ playthroughTest
57
236
  };
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/react-vitest-config.ts
@@ -23,9 +33,344 @@ __export(react_vitest_config_exports, {
23
33
  defineReactGameVitestConfig: () => defineReactGameVitestConfig
24
34
  });
25
35
  module.exports = __toCommonJS(react_vitest_config_exports);
36
+ var import_node_fs2 = require("fs");
37
+ var import_node_path2 = require("path");
38
+ var import_config = require("vitest/config");
39
+
40
+ // src/react/react-playthrough-reporter.ts
26
41
  var import_node_fs = require("fs");
27
42
  var import_node_path = require("path");
28
- var import_config = require("vitest/config");
43
+
44
+ // src/react/react-playthrough.ts
45
+ var import_react2 = require("@testing-library/react");
46
+ var import_user_event = __toESM(require("@testing-library/user-event"));
47
+ var import_vitest = require("vitest");
48
+
49
+ // src/react/react-playthrough-core.ts
50
+ var import_react = require("@testing-library/react");
51
+ function formatDiagnostics(read) {
52
+ if (!read) return void 0;
53
+ try {
54
+ const value = read();
55
+ if (typeof value === "string") return value;
56
+ return JSON.stringify(value);
57
+ } catch (error) {
58
+ return `diagnostics() threw: ${String(error)}`;
59
+ }
60
+ }
61
+ function normalizePlaythroughWaiverReason(waiverReason) {
62
+ if (waiverReason === void 0) return void 0;
63
+ const reason = waiverReason.trim();
64
+ if (reason.length < 20) {
65
+ throw new Error(
66
+ "playthroughTest.skip reason must contain at least 20 characters."
67
+ );
68
+ }
69
+ return reason;
70
+ }
71
+ async function runBoundedUntil(condition, options = {}) {
72
+ const maxSteps = options.maxSteps ?? 120;
73
+ if (!Number.isSafeInteger(maxSteps) || maxSteps < 0 || maxSteps > 1e4) {
74
+ throw new RangeError(
75
+ "stepUntil maxSteps must be a safe integer between 0 and 10000."
76
+ );
77
+ }
78
+ for (let step = 0; step <= maxSteps; step += 1) {
79
+ if (condition()) return step;
80
+ if (step < maxSteps) {
81
+ await (0, import_react.act)(async () => {
82
+ await options.step?.(step + 1);
83
+ });
84
+ }
85
+ }
86
+ const diagnostics = formatDiagnostics(options.diagnostics);
87
+ const guidance = options.step ? "The step callback ran, but the authoritative outcome did not change." : "No step callback was provided, so time-driven gameplay was not advanced. Inject a ManualGameClock for this test and pass step: () => clock.stepFrame().";
88
+ const suffix = diagnostics ? ` Last diagnostics: ${diagnostics}` : "";
89
+ throw new Error(
90
+ `Playthrough outcome was not reached within ${maxSteps} steps. ${guidance}${suffix}`
91
+ );
92
+ }
93
+
94
+ // src/react/react-playthrough.ts
95
+ var INPUT_EVENTS = [
96
+ "click",
97
+ "keydown",
98
+ "keyup",
99
+ "pointerdown",
100
+ "pointerup",
101
+ "touchstart",
102
+ "touchend"
103
+ ];
104
+ function createMetadata(waiverReason) {
105
+ return {
106
+ version: 1,
107
+ waiverReason,
108
+ evidence: {
109
+ domInputEvents: 0,
110
+ boundedRuns: 0,
111
+ assertionsAfterOutcome: 0,
112
+ verified: false
113
+ }
114
+ };
115
+ }
116
+ function definePlaythrough(element, run, waiverReason) {
117
+ const reason = normalizePlaythroughWaiverReason(waiverReason);
118
+ const metadata = createMetadata(reason);
119
+ (0, import_vitest.test)("production game completes a bounded playthrough", {
120
+ skip: Boolean(reason),
121
+ meta: { reactPlaythrough: metadata }
122
+ }, async () => {
123
+ const evidence = metadata.evidence;
124
+ let assertionsAtOutcome;
125
+ const recordInput = () => {
126
+ evidence.domInputEvents += 1;
127
+ };
128
+ for (const event of INPUT_EVENTS) {
129
+ document.addEventListener(event, recordInput, true);
130
+ }
131
+ try {
132
+ const view = (0, import_react2.render)(element);
133
+ if (view.container.childNodes.length === 0) {
134
+ throw new Error(
135
+ "playthroughTest must render the production game entry."
136
+ );
137
+ }
138
+ const user = import_user_event.default.setup();
139
+ await run({
140
+ view,
141
+ user,
142
+ async stepUntil(condition, options = {}) {
143
+ const steps = await runBoundedUntil(condition, options);
144
+ if (evidence.domInputEvents === 0) {
145
+ throw new Error(
146
+ "stepUntil reached an outcome before any production DOM input."
147
+ );
148
+ }
149
+ evidence.boundedRuns += 1;
150
+ assertionsAtOutcome = import_vitest.expect.getState().assertionCalls;
151
+ return steps;
152
+ }
153
+ });
154
+ const assertionCalls = import_vitest.expect.getState().assertionCalls;
155
+ evidence.assertionsAfterOutcome = assertionsAtOutcome === void 0 ? 0 : assertionCalls - assertionsAtOutcome;
156
+ if (evidence.domInputEvents === 0) {
157
+ throw new Error(
158
+ "playthroughTest did not observe production DOM input."
159
+ );
160
+ }
161
+ if (evidence.boundedRuns === 0) {
162
+ throw new Error("playthroughTest must complete one bounded stepUntil.");
163
+ }
164
+ if (evidence.assertionsAfterOutcome === 0) {
165
+ throw new Error(
166
+ "Assert an authoritative game outcome after stepUntil returns."
167
+ );
168
+ }
169
+ evidence.verified = true;
170
+ } finally {
171
+ for (const event of INPUT_EVENTS) {
172
+ document.removeEventListener(event, recordInput, true);
173
+ }
174
+ }
175
+ });
176
+ }
177
+ var playthroughTest = Object.assign(
178
+ (element, run) => definePlaythrough(element, run),
179
+ {
180
+ skip: (reason, element, run) => definePlaythrough(element, run, reason)
181
+ }
182
+ );
183
+ function auditReactPlaythroughRun(tests) {
184
+ const declared = tests.filter((candidate) => candidate.metadata);
185
+ const valid = declared.filter(({ state, metadata }) => {
186
+ const evidence = metadata?.evidence;
187
+ return state === "passed" && evidence?.verified === true && evidence.domInputEvents > 0 && evidence.boundedRuns > 0 && evidence.assertionsAfterOutcome > 0;
188
+ });
189
+ const waivers = declared.filter(
190
+ ({ state, metadata }) => state === "skipped" && (metadata?.waiverReason?.trim().length ?? 0) >= 20
191
+ );
192
+ const issues = [];
193
+ if (declared.length === 0) {
194
+ issues.push(
195
+ "\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"
196
+ );
197
+ } else {
198
+ for (const candidate of declared) {
199
+ const isValid = valid.includes(candidate);
200
+ const isWaived = waivers.includes(candidate);
201
+ if (isValid || isWaived) continue;
202
+ if (candidate.state === "skipped") {
203
+ issues.push(
204
+ `\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`
205
+ );
206
+ } else if (candidate.state !== "passed") {
207
+ issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7684\u72B6\u6001\u4E3A ${candidate.state}\u3002`);
208
+ } else {
209
+ issues.push(
210
+ `\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`
211
+ );
212
+ }
213
+ }
214
+ }
215
+ if (issues.length > 0) return { passed: false, waived: false, issues };
216
+ if (valid.length > 0) return { passed: true, waived: false, issues: [] };
217
+ if (waivers.length > 0) return { passed: true, waived: true, issues: [] };
218
+ return { passed: false, waived: false, issues };
219
+ }
220
+
221
+ // src/react/react-playthrough-reporter.ts
222
+ var PRODUCTION_PLAYTHROUGH_FILE = "tests/production-playthrough.test.tsx";
223
+ function isMetadata(value) {
224
+ if (!value || typeof value !== "object") return false;
225
+ const metadata = value;
226
+ return metadata.version === 1 && Boolean(metadata.evidence);
227
+ }
228
+ function toAuditInput(test2) {
229
+ const metadata = test2.meta().reactPlaythrough;
230
+ return {
231
+ name: test2.fullName,
232
+ state: test2.result().state,
233
+ metadata: isMetadata(metadata) ? metadata : void 0
234
+ };
235
+ }
236
+ function firstLine(value) {
237
+ if (typeof value !== "string") return void 0;
238
+ return value.split("\n").map((line) => line.trim()).find(Boolean);
239
+ }
240
+ function toModuleResult(module2, projectRoot) {
241
+ const tests = [...module2.children.allTests()];
242
+ const errors = [
243
+ ...module2.errors().map((error) => firstLine(error.message)),
244
+ ...tests.flatMap(
245
+ (test2) => (test2.result().errors ?? []).map((error) => firstLine(error.message))
246
+ )
247
+ ].filter((message) => Boolean(message));
248
+ return {
249
+ file: (0, import_node_path.relative)(projectRoot, module2.moduleId).replaceAll("\\", "/"),
250
+ state: module2.state(),
251
+ errors,
252
+ tests: tests.map(toAuditInput)
253
+ };
254
+ }
255
+ function assessReactPlaythroughReport(input) {
256
+ const base = { file: input.expectedFile };
257
+ if (!input.expectedFileScheduled) {
258
+ if (input.expectedFileExists) {
259
+ return {
260
+ ...base,
261
+ status: "NOT_CHECKED",
262
+ 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`,
263
+ failsRun: false
264
+ };
265
+ }
266
+ return {
267
+ ...base,
268
+ status: "FAILED",
269
+ cause: "\u751F\u4EA7\u53EF\u73A9\u6027\u6D4B\u8BD5\u6587\u4EF6\u4E0D\u5B58\u5728\u3002",
270
+ 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",
271
+ failsRun: true
272
+ };
273
+ }
274
+ const productionModule = input.modules.find(
275
+ (module2) => module2.file === input.expectedFile
276
+ );
277
+ const executedProductionFlow = productionModule?.tests.some(
278
+ (test2) => test2.state !== "skipped" && test2.state !== "pending"
279
+ );
280
+ if (input.focusedSelection && !executedProductionFlow) {
281
+ return {
282
+ ...base,
283
+ status: "NOT_CHECKED",
284
+ 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`,
285
+ failsRun: false
286
+ };
287
+ }
288
+ if (!productionModule || productionModule.tests.length === 0) {
289
+ return {
290
+ ...base,
291
+ status: "NOT_RUN",
292
+ cause: productionModule?.errors[0] ?? input.unhandledErrors?.[0] ?? "\u751F\u4EA7\u53EF\u73A9\u6027\u6D4B\u8BD5\u672A\u5B8C\u6210\u6536\u96C6\u6216\u6267\u884C\u3002",
293
+ 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",
294
+ failsRun: true
295
+ };
296
+ }
297
+ const tests = input.modules.flatMap((module2) => module2.tests);
298
+ const audit = auditReactPlaythroughRun(tests);
299
+ if (!audit.passed) {
300
+ 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";
301
+ const timedOut = /outcome was not reached within \d+ steps/i.test(cause);
302
+ const missingStep = /No step callback was provided/i.test(cause);
303
+ return {
304
+ ...base,
305
+ status: "FAILED",
306
+ cause,
307
+ next: missingStep ? "\u8BE5\u6D41\u7A0B\u662F\u65F6\u95F4\u6216\u5E27\u9A71\u52A8\u7684\uFF0C\u4F46 stepUntil \u6CA1\u6709\u63A8\u8FDB\u6E38\u620F\u65F6\u95F4\uFF1B\u4E3A\u751F\u4EA7\u6E38\u620F\u6CE8\u5165 devkit \u7684 GameClock\uFF0C\u5E76\u4F20\u5165 step: () => clock.stepFrame()\u3002\u4E0D\u8981\u7528\u771F\u5B9E setTimeout\u3002" : 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\u751F\u4EA7\u63A7\u5236\u662F\u5426\u6536\u5230\u8F93\u5165\uFF0C\u518D\u67E5\u770B\u8D85\u65F6\u9519\u8BEF\u4E2D\u7684 Last diagnostics \u5224\u65AD\u662F\u6E38\u620F\u5FAA\u73AF\u3001\u89C4\u5219\u72B6\u6001\u8FD8\u662F UI \u540C\u6B65\u672A\u63A8\u8FDB\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",
308
+ failsRun: true
309
+ };
310
+ }
311
+ if (audit.waived) {
312
+ return {
313
+ ...base,
314
+ status: "WAIVED",
315
+ waiverReasons: tests.map((test2) => test2.metadata?.waiverReason).filter((reason) => Boolean(reason)),
316
+ failsRun: false
317
+ };
318
+ }
319
+ return { ...base, status: "PASS", failsRun: false };
320
+ }
321
+ function formatReactPlaythroughReport(report) {
322
+ const lines = [`REACT_PLAYTHROUGH: ${report.status}`, `FILE: ${report.file}`];
323
+ if (report.cause) lines.push(`CAUSE: ${report.cause}`);
324
+ if (report.waiverReasons?.length) {
325
+ lines.push(`REASON: ${report.waiverReasons.join("\uFF1B")}`);
326
+ }
327
+ if (report.next) lines.push(`NEXT: ${report.next}`);
328
+ return `
329
+ ${lines.join("\n")}`;
330
+ }
331
+ var ReactPlaythroughReporter = class {
332
+ /** 绑定模板根目录,以稳定识别生产可玩性测试而不依赖测试名称。 */
333
+ constructor(projectRoot) {
334
+ this.projectRoot = projectRoot;
335
+ this.expectedModuleId = (0, import_node_path.resolve)(projectRoot, this.expectedFile);
336
+ }
337
+ projectRoot;
338
+ expectedFile = PRODUCTION_PLAYTHROUGH_FILE;
339
+ expectedModuleId;
340
+ expectedFileScheduled = false;
341
+ focusedSelection = false;
342
+ /** 记录本轮是否实际选择了生产流程文件,用于区分聚焦运行与门禁失败。 */
343
+ onTestRunStart(specifications) {
344
+ this.expectedFileScheduled = specifications.some(
345
+ (specification) => (0, import_node_path.resolve)(specification.moduleId) === this.expectedModuleId
346
+ );
347
+ this.focusedSelection = specifications.some(
348
+ (specification) => Boolean(specification.project.globalConfig.testNamePattern) || Boolean(specification.testNamePattern) || Boolean(specification.testLines?.length)
349
+ );
350
+ }
351
+ /** 测试运行结束后执行项目级主流程门禁,并写入最终退出码。 */
352
+ onTestRunEnd(testModules, unhandledErrors) {
353
+ const report = assessReactPlaythroughReport({
354
+ expectedFile: this.expectedFile,
355
+ expectedFileExists: (0, import_node_fs.existsSync)(this.expectedModuleId),
356
+ expectedFileScheduled: this.expectedFileScheduled,
357
+ focusedSelection: this.focusedSelection,
358
+ modules: testModules.map(
359
+ (module2) => toModuleResult(module2, this.projectRoot)
360
+ ),
361
+ unhandledErrors: unhandledErrors.map((error) => firstLine(error.message)).filter((message) => Boolean(message))
362
+ });
363
+ const output = formatReactPlaythroughReport(report);
364
+ if (report.failsRun) {
365
+ console.error(output);
366
+ process.exitCode = 1;
367
+ } else if (report.status === "WAIVED" || report.status === "NOT_CHECKED") {
368
+ console.warn(output);
369
+ } else {
370
+ console.log(output);
371
+ }
372
+ }
373
+ };
29
374
 
30
375
  // src/testing/vitest-node-args.ts
31
376
  function getJSDOMWorkerExecArgv() {
@@ -35,16 +380,16 @@ function getJSDOMWorkerExecArgv() {
35
380
 
36
381
  // src/react-vitest-config.ts
37
382
  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;
383
+ const manifestPath = (0, import_node_path2.resolve)(projectRoot, "node_modules/phaser/package.json");
384
+ if (!(0, import_node_fs2.existsSync)(manifestPath)) return void 0;
40
385
  try {
41
- const manifest = JSON.parse((0, import_node_fs.readFileSync)(manifestPath, "utf8"));
386
+ const manifest = JSON.parse((0, import_node_fs2.readFileSync)(manifestPath, "utf8"));
42
387
  if (!manifest.version?.startsWith("3.")) return void 0;
43
- const browserEntry = (0, import_node_path.resolve)(
44
- (0, import_node_path.dirname)(manifestPath),
388
+ const browserEntry = (0, import_node_path2.resolve)(
389
+ (0, import_node_path2.dirname)(manifestPath),
45
390
  manifest.browser ?? "dist/phaser.js"
46
391
  );
47
- return (0, import_node_fs.existsSync)(browserEntry) ? browserEntry : void 0;
392
+ return (0, import_node_fs2.existsSync)(browserEntry) ? browserEntry : void 0;
48
393
  } catch {
49
394
  return void 0;
50
395
  }
@@ -56,7 +401,7 @@ function defineReactGameVitestConfig(options) {
56
401
  alias: {
57
402
  ...phaser3BrowserEntry ? { phaser: phaser3BrowserEntry } : {},
58
403
  ...options.aliases,
59
- "@": (0, import_node_path.resolve)(options.projectRoot, "src")
404
+ "@": (0, import_node_path2.resolve)(options.projectRoot, "src")
60
405
  }
61
406
  },
62
407
  test: {
@@ -79,7 +424,8 @@ function defineReactGameVitestConfig(options) {
79
424
  sequence: {
80
425
  setupFiles: "list"
81
426
  },
82
- reporters: ["minimal"],
427
+ // minimal 保留业务失败;附加 reporter 负责项目级最低可玩性门禁。
428
+ reporters: ["minimal", new ReactPlaythroughReporter(options.projectRoot)],
83
429
  restoreMocks: true,
84
430
  clearMocks: true,
85
431
  testTimeout: options.testTimeout,