miaoda-game-devkit 0.6.4 → 0.6.6
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/dist/cli/phaser-lint.js +2 -2
- package/dist/cli/react-lint.js +2 -2
- package/dist/react/testing.d.mts +12 -0
- package/dist/react/testing.d.ts +12 -0
- package/dist/react/testing.js +112 -14
- package/dist/react/testing.mjs +112 -14
- package/dist/react/vitest-config.js +636 -440
- package/dist/react/vitest-config.mjs +640 -444
- package/dist/react/vitest-setup.js +112 -14
- package/dist/react/vitest-setup.mjs +112 -14
- package/dist/rules/react-test-boundary-plugin.js +1 -1
- package/package.json +1 -1
|
@@ -34,251 +34,12 @@ __export(react_vitest_config_exports, {
|
|
|
34
34
|
});
|
|
35
35
|
module.exports = __toCommonJS(react_vitest_config_exports);
|
|
36
36
|
var import_node_fs3 = require("fs");
|
|
37
|
-
var
|
|
37
|
+
var import_node_path4 = require("path");
|
|
38
38
|
var import_config = require("vitest/config");
|
|
39
39
|
|
|
40
40
|
// src/react/react-playthrough-reporter.ts
|
|
41
41
|
var import_node_fs2 = require("fs");
|
|
42
|
-
var
|
|
43
|
-
var import_node_util = require("util");
|
|
44
|
-
|
|
45
|
-
// src/cli/react-authoritative-playthrough.ts
|
|
46
|
-
var import_node_fs = require("fs");
|
|
47
|
-
var import_node_path = require("path");
|
|
48
|
-
var PRODUCTION_PLAYTHROUGH = "tests/production-playthrough.test.tsx";
|
|
49
|
-
var PRODUCTION_APP = "src/App.tsx";
|
|
50
|
-
var EXAMPLE_IMPORT_PREFIX = "@/game/example/";
|
|
51
|
-
var REACT_RUNTIME_ENTRY = "miaoda-game-devkit/react";
|
|
52
|
-
var REACT_TESTING_ENTRY = "miaoda-game-devkit/react/testing";
|
|
53
|
-
var CLOCK_IMPORTS = /* @__PURE__ */ new Set(["browserGameClock"]);
|
|
54
|
-
var CONTROLLER_IMPORTS = /* @__PURE__ */ new Set([
|
|
55
|
-
"useGameController",
|
|
56
|
-
"useOwnedGameController"
|
|
57
|
-
]);
|
|
58
|
-
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
|
|
59
|
-
function runnableTestFiles(root, directory = (0, import_node_path.join)(root, "tests")) {
|
|
60
|
-
if (!(0, import_node_fs.existsSync)(directory)) return [];
|
|
61
|
-
const files = [];
|
|
62
|
-
for (const entry of (0, import_node_fs.readdirSync)(directory, { withFileTypes: true })) {
|
|
63
|
-
const path = (0, import_node_path.join)(directory, entry.name);
|
|
64
|
-
if (entry.isDirectory()) {
|
|
65
|
-
files.push(...runnableTestFiles(root, path));
|
|
66
|
-
} else if (entry.isFile() && /\.test\.[cm]?[jt]sx?$/.test(entry.name)) {
|
|
67
|
-
files.push(path);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return files;
|
|
71
|
-
}
|
|
72
|
-
function extension(path) {
|
|
73
|
-
const index = path.lastIndexOf(".");
|
|
74
|
-
return index < 0 ? "" : path.slice(index);
|
|
75
|
-
}
|
|
76
|
-
function sourceFiles(root, directory = (0, import_node_path.join)(root, "src")) {
|
|
77
|
-
if (!(0, import_node_fs.existsSync)(directory)) return [];
|
|
78
|
-
const files = [];
|
|
79
|
-
for (const entry of (0, import_node_fs.readdirSync)(directory, { withFileTypes: true })) {
|
|
80
|
-
const path = (0, import_node_path.join)(directory, entry.name);
|
|
81
|
-
const projectPath = (0, import_node_path.relative)(root, path).replaceAll("\\", "/");
|
|
82
|
-
if (entry.isDirectory()) {
|
|
83
|
-
if (projectPath === "src/game/example") continue;
|
|
84
|
-
files.push(...sourceFiles(root, path));
|
|
85
|
-
} else if (entry.isFile() && SOURCE_EXTENSIONS.has(extension(entry.name))) {
|
|
86
|
-
files.push(path);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return files;
|
|
90
|
-
}
|
|
91
|
-
function withoutComments(source) {
|
|
92
|
-
let output = "";
|
|
93
|
-
let state = "code";
|
|
94
|
-
for (let index = 0; index < source.length; index += 1) {
|
|
95
|
-
const char = source[index];
|
|
96
|
-
const next = source[index + 1];
|
|
97
|
-
if (state === "line") {
|
|
98
|
-
if (char === "\n") {
|
|
99
|
-
state = "code";
|
|
100
|
-
output += char;
|
|
101
|
-
} else {
|
|
102
|
-
output += " ";
|
|
103
|
-
}
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
if (state === "block") {
|
|
107
|
-
if (char === "*" && next === "/") {
|
|
108
|
-
output += " ";
|
|
109
|
-
index += 1;
|
|
110
|
-
state = "code";
|
|
111
|
-
} else {
|
|
112
|
-
output += char === "\n" ? "\n" : " ";
|
|
113
|
-
}
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
if (state === "code" && char === "/" && next === "/") {
|
|
117
|
-
output += " ";
|
|
118
|
-
index += 1;
|
|
119
|
-
state = "line";
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
if (state === "code" && char === "/" && next === "*") {
|
|
123
|
-
output += " ";
|
|
124
|
-
index += 1;
|
|
125
|
-
state = "block";
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
if (state === "code" && char === "'") state = "single";
|
|
129
|
-
else if (state === "code" && char === '"') state = "double";
|
|
130
|
-
else if (state === "code" && char === "`") state = "template";
|
|
131
|
-
else if (state === "single" && char === "'" && source[index - 1] !== "\\") {
|
|
132
|
-
state = "code";
|
|
133
|
-
} else if (state === "double" && char === '"' && source[index - 1] !== "\\") {
|
|
134
|
-
state = "code";
|
|
135
|
-
} else if (state === "template" && char === "`" && source[index - 1] !== "\\") {
|
|
136
|
-
state = "code";
|
|
137
|
-
}
|
|
138
|
-
output += char;
|
|
139
|
-
}
|
|
140
|
-
return output;
|
|
141
|
-
}
|
|
142
|
-
function codePositions(source) {
|
|
143
|
-
const positions = Array.from({ length: source.length }, () => false);
|
|
144
|
-
let state = "code";
|
|
145
|
-
for (let index = 0; index < source.length; index += 1) {
|
|
146
|
-
const char = source[index];
|
|
147
|
-
if (state === "code") positions[index] = true;
|
|
148
|
-
if (state === "code" && char === "'") state = "single";
|
|
149
|
-
else if (state === "code" && char === '"') state = "double";
|
|
150
|
-
else if (state === "code" && char === "`") state = "template";
|
|
151
|
-
else if (state === "single" && char === "'" && source[index - 1] !== "\\") {
|
|
152
|
-
state = "code";
|
|
153
|
-
} else if (state === "double" && char === '"' && source[index - 1] !== "\\") {
|
|
154
|
-
state = "code";
|
|
155
|
-
} else if (state === "template" && char === "`" && source[index - 1] !== "\\") {
|
|
156
|
-
state = "code";
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
return positions;
|
|
160
|
-
}
|
|
161
|
-
function importedModuleSpecifiers(source) {
|
|
162
|
-
const clean = withoutComments(source);
|
|
163
|
-
const positions = codePositions(clean);
|
|
164
|
-
const modules = [];
|
|
165
|
-
const pattern = /\b(?:from\s+|import\s*(?:\(\s*)?)["']([^"']+)["']\s*\)?/g;
|
|
166
|
-
for (const match of clean.matchAll(pattern)) {
|
|
167
|
-
if (positions[match.index]) modules.push(match[1]);
|
|
168
|
-
}
|
|
169
|
-
return modules;
|
|
170
|
-
}
|
|
171
|
-
function productionFileImportsExample(file, projectRoot) {
|
|
172
|
-
const exampleRoot = (0, import_node_path.join)(projectRoot, "src/game/example");
|
|
173
|
-
return importedModuleSpecifiers((0, import_node_fs.readFileSync)(file, "utf8")).some(
|
|
174
|
-
(moduleName) => {
|
|
175
|
-
if (moduleName.startsWith(EXAMPLE_IMPORT_PREFIX)) return true;
|
|
176
|
-
if (!moduleName.startsWith(".")) return false;
|
|
177
|
-
const target = (0, import_node_path.resolve)((0, import_node_path.dirname)(file), moduleName);
|
|
178
|
-
return target === exampleRoot || target.startsWith(`${exampleRoot}${import_node_path.sep}`);
|
|
179
|
-
}
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
function importsExampleAlias(source) {
|
|
183
|
-
return importedModuleSpecifiers(source).some(
|
|
184
|
-
(moduleName) => moduleName.startsWith(EXAMPLE_IMPORT_PREFIX)
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
function namedImports(source, moduleName) {
|
|
188
|
-
const names = /* @__PURE__ */ new Set();
|
|
189
|
-
const clean = withoutComments(source);
|
|
190
|
-
const positions = codePositions(clean);
|
|
191
|
-
const escapedModule = moduleName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
192
|
-
const pattern = new RegExp(
|
|
193
|
-
`^\\s*import\\s+(?:type\\s+)?\\{([\\s\\S]*?)\\}\\s+from\\s+["']${escapedModule}["']`,
|
|
194
|
-
"gm"
|
|
195
|
-
);
|
|
196
|
-
for (const match of clean.matchAll(pattern)) {
|
|
197
|
-
if (!positions[match.index]) continue;
|
|
198
|
-
for (const specifier of match[1].split(",")) {
|
|
199
|
-
const imported = specifier.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0].trim();
|
|
200
|
-
if (imported) names.add(imported);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
return names;
|
|
204
|
-
}
|
|
205
|
-
function containsAny(values, expected) {
|
|
206
|
-
return [...values].some((value) => expected.has(value));
|
|
207
|
-
}
|
|
208
|
-
function declaresObserve(source) {
|
|
209
|
-
const clean = withoutComments(source);
|
|
210
|
-
const positions = codePositions(clean);
|
|
211
|
-
for (const match of clean.matchAll(/\bobserve\s*:/g)) {
|
|
212
|
-
if (positions[match.index]) return true;
|
|
213
|
-
}
|
|
214
|
-
return false;
|
|
215
|
-
}
|
|
216
|
-
function auditReactAuthoritativePlaythrough(projectRoot) {
|
|
217
|
-
const clockFiles = [];
|
|
218
|
-
const controllerFiles = [];
|
|
219
|
-
const productionFiles = sourceFiles(projectRoot);
|
|
220
|
-
for (const file of productionFiles) {
|
|
221
|
-
const imports = namedImports(
|
|
222
|
-
(0, import_node_fs.readFileSync)(file, "utf8"),
|
|
223
|
-
REACT_RUNTIME_ENTRY
|
|
224
|
-
);
|
|
225
|
-
const projectPath = (0, import_node_path.relative)(projectRoot, file).replaceAll("\\", "/");
|
|
226
|
-
if (containsAny(imports, CLOCK_IMPORTS)) clockFiles.push(projectPath);
|
|
227
|
-
if (containsAny(imports, CONTROLLER_IMPORTS))
|
|
228
|
-
controllerFiles.push(projectPath);
|
|
229
|
-
}
|
|
230
|
-
const appPath = (0, import_node_path.join)(projectRoot, PRODUCTION_APP);
|
|
231
|
-
const productionEntryExists = (0, import_node_fs.existsSync)(appPath);
|
|
232
|
-
const productionUsesExample = productionFiles.some(
|
|
233
|
-
(file) => productionFileImportsExample(file, projectRoot)
|
|
234
|
-
);
|
|
235
|
-
const staleExampleTestFiles = productionUsesExample ? [] : runnableTestFiles(projectRoot).filter((file) => importsExampleAlias((0, import_node_fs.readFileSync)(file, "utf8"))).map((file) => (0, import_node_path.relative)(projectRoot, file).replaceAll("\\", "/"));
|
|
236
|
-
const issues = [];
|
|
237
|
-
if (staleExampleTestFiles.length > 0) {
|
|
238
|
-
issues.push(
|
|
239
|
-
`Production ${PRODUCTION_APP} no longer imports ${EXAMPLE_IMPORT_PREFIX}*, but runnable product tests still do: ${staleExampleTestFiles.join(", ")}. Replace those CollectGame tests with tests for the production game or delete genuinely inapplicable slots. Teaching examples under tests/examples/**/*.example.* remain allowed.`
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
if (clockFiles.length === 0 && controllerFiles.length === 0) {
|
|
243
|
-
return {
|
|
244
|
-
ok: issues.length === 0,
|
|
245
|
-
issues,
|
|
246
|
-
clockFiles,
|
|
247
|
-
controllerFiles,
|
|
248
|
-
productionEntryExists,
|
|
249
|
-
productionUsesExample,
|
|
250
|
-
staleExampleTestFiles
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
const testPath = (0, import_node_path.join)(projectRoot, PRODUCTION_PLAYTHROUGH);
|
|
254
|
-
const testSource = (0, import_node_fs.existsSync)(testPath) ? (0, import_node_fs.readFileSync)(testPath, "utf8") : "";
|
|
255
|
-
const testingImports = namedImports(testSource, REACT_TESTING_ENTRY);
|
|
256
|
-
const hasObserve = declaresObserve(testSource);
|
|
257
|
-
if (!hasObserve) {
|
|
258
|
-
issues.push(
|
|
259
|
-
`${PRODUCTION_PLAYTHROUGH} must declare observe because production uses the devkit clock or Controller ownership hook. Observe the same production Controller rendered by <App /> through Telemetry; DOM labels are not an authoritative gameplay boundary.`
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
if (clockFiles.length > 0 && !testingImports.has("ManualGameClock")) {
|
|
263
|
-
issues.push(
|
|
264
|
-
`${PRODUCTION_PLAYTHROUGH} must import ManualGameClock because production owns gameplay time in ${clockFiles.join(", ")}. Inject it through the production <App /> factory and advance it with a non-empty deterministic step.`
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
return {
|
|
268
|
-
ok: issues.length === 0,
|
|
269
|
-
issues,
|
|
270
|
-
clockFiles,
|
|
271
|
-
controllerFiles,
|
|
272
|
-
productionEntryExists,
|
|
273
|
-
productionUsesExample,
|
|
274
|
-
staleExampleTestFiles
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// src/react/react-playthrough.ts
|
|
279
|
-
var import_react2 = require("@testing-library/react");
|
|
280
|
-
var import_user_event = __toESM(require("@testing-library/user-event"));
|
|
281
|
-
var import_vitest = require("vitest");
|
|
42
|
+
var import_node_path3 = require("path");
|
|
282
43
|
|
|
283
44
|
// src/react/react-error-diagnostics.ts
|
|
284
45
|
var MAX_DIAGNOSTIC_LENGTH = 1e3;
|
|
@@ -306,47 +67,145 @@ function safeJson(value) {
|
|
|
306
67
|
return void 0;
|
|
307
68
|
}
|
|
308
69
|
}
|
|
309
|
-
function
|
|
70
|
+
function diagnosticValue(value) {
|
|
71
|
+
if (value === void 0) return void 0;
|
|
72
|
+
if (typeof value === "string") {
|
|
73
|
+
return truncate(value.replace(/\s+/g, " ").trim());
|
|
74
|
+
}
|
|
75
|
+
if (value === null || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
76
|
+
return String(value);
|
|
77
|
+
}
|
|
78
|
+
if (typeof value === "symbol") return value.toString();
|
|
79
|
+
if (typeof value === "function") {
|
|
80
|
+
return `Function<${value.name || "anonymous"}>`;
|
|
81
|
+
}
|
|
82
|
+
const json = safeJson(value);
|
|
83
|
+
return truncate(json ?? String(value));
|
|
84
|
+
}
|
|
85
|
+
function normalizeFile(value) {
|
|
86
|
+
return value.replaceAll("\\", "/").replace(/^file:\/\//, "");
|
|
87
|
+
}
|
|
88
|
+
function failureLayer(file) {
|
|
89
|
+
const normalized = normalizeFile(file);
|
|
90
|
+
if (normalized.includes("/miaoda-game-devkit/") || normalized.includes("/packages/game-devkit/")) {
|
|
91
|
+
return "harness";
|
|
92
|
+
}
|
|
93
|
+
if (normalized.includes("/node_modules/")) return "dependency";
|
|
94
|
+
if (/(?:^|\/)tests?\//.test(normalized) || /\.(?:test|spec)\.[cm]?[jt]sx?$/.test(normalized)) {
|
|
95
|
+
return "test";
|
|
96
|
+
}
|
|
97
|
+
if (/(?:^|\/)src\//.test(normalized)) return "product";
|
|
98
|
+
return "unknown";
|
|
99
|
+
}
|
|
100
|
+
function parsedOrigin(value) {
|
|
101
|
+
if (!value || typeof value !== "object") return void 0;
|
|
102
|
+
const frame = value;
|
|
103
|
+
if (typeof frame.file !== "string" || typeof frame.line !== "number" || typeof frame.column !== "number") {
|
|
104
|
+
return void 0;
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
file: normalizeFile(frame.file),
|
|
108
|
+
line: frame.line,
|
|
109
|
+
column: frame.column
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function stackOrigins(record) {
|
|
113
|
+
if (Array.isArray(record.stacks)) {
|
|
114
|
+
const parsed = record.stacks.map(parsedOrigin).filter((origin) => Boolean(origin));
|
|
115
|
+
if (parsed.length > 0) return parsed;
|
|
116
|
+
}
|
|
117
|
+
if (typeof record.stack !== "string") return [];
|
|
118
|
+
const origins = [];
|
|
119
|
+
const pattern = /(?:at\s+.*?\()?((?:file:\/\/)?[^()\s]+):(\d+):(\d+)\)?/g;
|
|
120
|
+
for (const match of record.stack.matchAll(pattern)) {
|
|
121
|
+
origins.push({
|
|
122
|
+
file: normalizeFile(match[1]),
|
|
123
|
+
line: Number(match[2]),
|
|
124
|
+
column: Number(match[3])
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return origins;
|
|
128
|
+
}
|
|
129
|
+
function selectOrigin(record) {
|
|
130
|
+
const origins = stackOrigins(record);
|
|
131
|
+
return origins.find((origin) => {
|
|
132
|
+
const layer = failureLayer(origin.file);
|
|
133
|
+
return layer === "product" || layer === "test";
|
|
134
|
+
}) ?? origins.find((origin) => failureLayer(origin.file) !== "dependency");
|
|
135
|
+
}
|
|
136
|
+
function fallbackCode(record, origin, defaultCode) {
|
|
137
|
+
if (typeof record.code === "string" && record.code.trim()) {
|
|
138
|
+
return record.code.trim();
|
|
139
|
+
}
|
|
140
|
+
if (record.name === "AssertionError" && record.actual !== void 0 && record.expected !== void 0) {
|
|
141
|
+
return "EXPECTATION_MISMATCH";
|
|
142
|
+
}
|
|
143
|
+
if (record.name === "TypeError" && origin) {
|
|
144
|
+
const layer = failureLayer(origin.file);
|
|
145
|
+
if (layer === "product") return "PRODUCT_RUNTIME_TYPE_ERROR";
|
|
146
|
+
if (layer === "test") return "TEST_API_MISMATCH";
|
|
147
|
+
}
|
|
148
|
+
return defaultCode;
|
|
149
|
+
}
|
|
150
|
+
function structuredEntry(record, message, defaultCode) {
|
|
151
|
+
const origin = selectOrigin(record);
|
|
152
|
+
return {
|
|
153
|
+
code: fallbackCode(record, origin, defaultCode),
|
|
154
|
+
message: truncate(message),
|
|
155
|
+
errorName: typeof record.name === "string" && record.name.trim() ? record.name.trim() : void 0,
|
|
156
|
+
actual: diagnosticValue(record.actual),
|
|
157
|
+
expected: diagnosticValue(record.expected),
|
|
158
|
+
origin,
|
|
159
|
+
layer: origin ? failureLayer(origin.file) : void 0
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function collectEntries(value, fallbackCode2, seen) {
|
|
310
163
|
if (typeof value === "string") {
|
|
311
|
-
return value.trim() ? [{ code:
|
|
164
|
+
return value.trim() ? [{ code: fallbackCode2, message: truncate(value) }] : [];
|
|
312
165
|
}
|
|
313
166
|
if (value === null || value === void 0 || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "symbol") {
|
|
314
|
-
return [{ code:
|
|
167
|
+
return [{ code: fallbackCode2, message: String(value) }];
|
|
315
168
|
}
|
|
316
169
|
if (typeof value === "function") {
|
|
317
170
|
return [
|
|
318
|
-
{ code:
|
|
171
|
+
{ code: fallbackCode2, message: `Function<${value.name || "anonymous"}>` }
|
|
319
172
|
];
|
|
320
173
|
}
|
|
321
174
|
if (seen.has(value)) return [];
|
|
322
175
|
seen.add(value);
|
|
323
176
|
if (Array.isArray(value)) {
|
|
324
|
-
return value.flatMap((item) => collectEntries(item,
|
|
177
|
+
return value.flatMap((item) => collectEntries(item, fallbackCode2, seen));
|
|
325
178
|
}
|
|
326
179
|
const record = value;
|
|
327
|
-
const code = typeof record.code === "string" && record.code.trim() ? record.code.trim() : fallbackCode;
|
|
328
180
|
const entries = [];
|
|
329
181
|
if (typeof record.message === "string" && record.message.trim()) {
|
|
330
|
-
entries.push(
|
|
182
|
+
entries.push(structuredEntry(record, record.message, fallbackCode2));
|
|
331
183
|
}
|
|
332
184
|
if (record.cause !== void 0) {
|
|
333
|
-
entries.push(...collectEntries(record.cause,
|
|
185
|
+
entries.push(...collectEntries(record.cause, fallbackCode2, seen));
|
|
334
186
|
}
|
|
335
187
|
if (Array.isArray(record.errors)) {
|
|
336
|
-
entries.push(...collectEntries(record.errors,
|
|
188
|
+
entries.push(...collectEntries(record.errors, fallbackCode2, seen));
|
|
337
189
|
}
|
|
338
190
|
if (entries.length > 0) return entries;
|
|
339
191
|
if (typeof record.stack === "string" && record.stack.trim()) {
|
|
340
|
-
return [
|
|
192
|
+
return [structuredEntry(record, record.stack, fallbackCode2)];
|
|
341
193
|
}
|
|
342
194
|
const json = safeJson(value);
|
|
343
|
-
return json && json !== "{}" ? [
|
|
195
|
+
return json && json !== "{}" ? [structuredEntry(record, json, fallbackCode2)] : [];
|
|
344
196
|
}
|
|
345
|
-
function extractFailureEntries(value,
|
|
346
|
-
const entries = collectEntries(value,
|
|
197
|
+
function extractFailureEntries(value, fallbackCode2 = "TEST_FAILURE") {
|
|
198
|
+
const entries = collectEntries(value, fallbackCode2, /* @__PURE__ */ new WeakSet());
|
|
347
199
|
const keys = /* @__PURE__ */ new Set();
|
|
348
200
|
return entries.filter((entry) => {
|
|
349
|
-
const key =
|
|
201
|
+
const key = [
|
|
202
|
+
entry.code,
|
|
203
|
+
entry.message,
|
|
204
|
+
entry.errorName,
|
|
205
|
+
entry.actual,
|
|
206
|
+
entry.expected,
|
|
207
|
+
entry.origin ? `${entry.origin.file}:${entry.origin.line}:${entry.origin.column}` : void 0
|
|
208
|
+
].join("\0");
|
|
350
209
|
if (keys.has(key)) return false;
|
|
351
210
|
keys.add(key);
|
|
352
211
|
return true;
|
|
@@ -377,6 +236,15 @@ function codedError(code, message) {
|
|
|
377
236
|
return error;
|
|
378
237
|
}
|
|
379
238
|
|
|
239
|
+
// src/react/react-playthrough-results.ts
|
|
240
|
+
var import_node_path = require("path");
|
|
241
|
+
var import_node_util = require("util");
|
|
242
|
+
|
|
243
|
+
// src/react/react-playthrough.ts
|
|
244
|
+
var import_react2 = require("@testing-library/react");
|
|
245
|
+
var import_user_event = __toESM(require("@testing-library/user-event"));
|
|
246
|
+
var import_vitest = require("vitest");
|
|
247
|
+
|
|
380
248
|
// src/react/react-playthrough-core.ts
|
|
381
249
|
var import_react = require("@testing-library/react");
|
|
382
250
|
function throwIfAborted(signal) {
|
|
@@ -858,28 +726,7 @@ function auditReactPlaythroughRun(tests) {
|
|
|
858
726
|
return { passed: false, waived: false, issues };
|
|
859
727
|
}
|
|
860
728
|
|
|
861
|
-
// src/react/react-playthrough-
|
|
862
|
-
var PRODUCTION_PLAYTHROUGH_FILE = "tests/production-playthrough.test.tsx";
|
|
863
|
-
var REPAIR_CONSTRAINT = "Preserve the intended gameplay outcome. Fix the production mechanic, deterministic driver, or authoritative observation that prevents it. Do not make the test pass by weakening or deleting assertions, replacing the outcome with back/menu/exit navigation, observing arbitrary UI text only to change a fingerprint, using a no-op step, or treating an intermediate active/in-flight/running phase as meaningful progress.";
|
|
864
|
-
function assessProductTestAlignment(projectRoot) {
|
|
865
|
-
const audit = auditReactAuthoritativePlaythrough(projectRoot);
|
|
866
|
-
if (audit.staleExampleTestFiles.length > 0) {
|
|
867
|
-
return { status: "FAILED", cause: audit.issues[0] };
|
|
868
|
-
}
|
|
869
|
-
if (!audit.productionEntryExists) {
|
|
870
|
-
return {
|
|
871
|
-
status: "NOT_VERIFIED",
|
|
872
|
-
cause: "The production src/App.tsx entry does not exist, so product-test alignment could not be verified."
|
|
873
|
-
};
|
|
874
|
-
}
|
|
875
|
-
if (audit.productionUsesExample) {
|
|
876
|
-
return {
|
|
877
|
-
status: "NOT_VERIFIED",
|
|
878
|
-
cause: "The production App still uses the replaceable src/game/example teaching game, so replacement-game test alignment is not applicable yet."
|
|
879
|
-
};
|
|
880
|
-
}
|
|
881
|
-
return { status: "PASS" };
|
|
882
|
-
}
|
|
729
|
+
// src/react/react-playthrough-results.ts
|
|
883
730
|
function isMetadata(value) {
|
|
884
731
|
if (!value || typeof value !== "object") return false;
|
|
885
732
|
const metadata = value;
|
|
@@ -897,17 +744,22 @@ function isMetadata(value) {
|
|
|
897
744
|
(stage) => Boolean(stage) && typeof stage === "object" && typeof stage.name === "string" && typeof stage.kind === "string" && typeof stage.domInputEvents === "number" && typeof stage.advancedSteps === "number" && typeof stage.assertions === "number" && typeof stage.stateChanged === "boolean" && typeof stage.before === "string" && typeof stage.after === "string"
|
|
898
745
|
);
|
|
899
746
|
}
|
|
747
|
+
function firstFailureLine(value) {
|
|
748
|
+
if (typeof value !== "string") return void 0;
|
|
749
|
+
return (0, import_node_util.stripVTControlCharacters)(value).split("\n").map((line) => line.trim()).find(Boolean);
|
|
750
|
+
}
|
|
900
751
|
function toAuditInput(test2) {
|
|
901
752
|
const metadata = test2.meta().reactPlaythrough;
|
|
902
753
|
return {
|
|
903
754
|
name: test2.fullName,
|
|
904
755
|
state: test2.result().state,
|
|
756
|
+
mode: test2.options.mode,
|
|
905
757
|
metadata: isMetadata(metadata) ? metadata : void 0
|
|
906
758
|
};
|
|
907
759
|
}
|
|
908
760
|
function findPendingProductTests(modules, projectRoot) {
|
|
909
761
|
return modules.flatMap((module2) => {
|
|
910
|
-
const file = (0,
|
|
762
|
+
const file = (0, import_node_path.relative)(projectRoot, module2.moduleId).replaceAll("\\", "/");
|
|
911
763
|
if (file.split("/").includes("examples")) return [];
|
|
912
764
|
const tests = [...module2.children.allTests()];
|
|
913
765
|
const pending = tests.filter((test2) => {
|
|
@@ -926,6 +778,131 @@ function findPendingProductTests(modules, projectRoot) {
|
|
|
926
778
|
}));
|
|
927
779
|
});
|
|
928
780
|
}
|
|
781
|
+
function selectReactFailure(values, errorRecordCount = values.length) {
|
|
782
|
+
const entries = extractFailureEntries(values);
|
|
783
|
+
if (entries.length === 0) {
|
|
784
|
+
return {
|
|
785
|
+
code: "MISSING_FAILURE_DETAILS",
|
|
786
|
+
cause: `Vitest marked this test as failed but returned no readable message in ${errorRecordCount} error record${errorRecordCount === 1 ? "" : "s"}.`,
|
|
787
|
+
rawCause: "",
|
|
788
|
+
related: []
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
const primary = entries[0];
|
|
792
|
+
const rawCause = primary.message;
|
|
793
|
+
return {
|
|
794
|
+
code: primary.code,
|
|
795
|
+
cause: firstFailureLine(rawCause) ?? rawCause,
|
|
796
|
+
rawCause,
|
|
797
|
+
errorName: primary.errorName,
|
|
798
|
+
actual: primary.actual,
|
|
799
|
+
expected: primary.expected,
|
|
800
|
+
origin: primary.origin,
|
|
801
|
+
layer: primary.layer,
|
|
802
|
+
related: entries.slice(1)
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
function failureHint(value) {
|
|
806
|
+
const names = [...value.matchAll(/button\s*\n\s*Name "([^"]+)"/g)].map(
|
|
807
|
+
(match) => match[1]
|
|
808
|
+
);
|
|
809
|
+
if (names.length === 0) return void 0;
|
|
810
|
+
return `Available button names: ${names.map((name) => JSON.stringify(name)).join(", ")}`;
|
|
811
|
+
}
|
|
812
|
+
function errorLocation(value) {
|
|
813
|
+
const match = value.match(
|
|
814
|
+
/(?:^|\n)\s*at\s+(.*?\.(?:test|spec)\.[^\n]+:\d+:\d+)/
|
|
815
|
+
);
|
|
816
|
+
return match?.[1];
|
|
817
|
+
}
|
|
818
|
+
function truncateReporterLine(value, limit) {
|
|
819
|
+
const compact = (0, import_node_util.stripVTControlCharacters)(value).replace(/\s+/g, " ").trim();
|
|
820
|
+
if (compact.length <= limit) return compact;
|
|
821
|
+
return `${compact.slice(0, Math.max(0, limit - 1))}\u2026`;
|
|
822
|
+
}
|
|
823
|
+
function failureTrace(test2) {
|
|
824
|
+
const annotations = test2.annotations();
|
|
825
|
+
let annotationTrace;
|
|
826
|
+
for (let index = annotations.length - 1; index >= 0; index -= 1) {
|
|
827
|
+
if (annotations[index].type === REACT_PLAYTHROUGH_TRACE_ANNOTATION) {
|
|
828
|
+
annotationTrace = annotations[index].message;
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
const metadata = test2.meta().reactPlaythrough;
|
|
833
|
+
const trace = annotationTrace ?? (isMetadata(metadata) ? metadata.trace : void 0);
|
|
834
|
+
return trace ? truncateReporterLine(trace, 720) : void 0;
|
|
835
|
+
}
|
|
836
|
+
function toReactPlaythroughModuleResult(module2, projectRoot) {
|
|
837
|
+
const tests = [...module2.children.allTests()];
|
|
838
|
+
const moduleFailure = selectReactFailure(
|
|
839
|
+
module2.errors(),
|
|
840
|
+
module2.errors().length
|
|
841
|
+
);
|
|
842
|
+
const moduleErrors = extractFailureEntries(module2.errors()).map(
|
|
843
|
+
(entry) => firstFailureLine(entry.message) ?? entry.message
|
|
844
|
+
);
|
|
845
|
+
const errors = [
|
|
846
|
+
...moduleErrors,
|
|
847
|
+
...tests.flatMap(
|
|
848
|
+
(test2) => extractFailureEntries(test2.result().errors ?? []).map(
|
|
849
|
+
(entry) => firstFailureLine(entry.message) ?? entry.message
|
|
850
|
+
)
|
|
851
|
+
)
|
|
852
|
+
];
|
|
853
|
+
const failures = tests.filter((test2) => test2.result().state === "failed").map((test2) => {
|
|
854
|
+
const metadata = test2.meta().reactPlaythrough;
|
|
855
|
+
const testErrors = test2.result().errors ?? [];
|
|
856
|
+
const metadataErrors = isMetadata(metadata) ? metadata.failure?.entries ?? [] : [];
|
|
857
|
+
const attemptErrors = metadataErrors.length > 0 ? metadataErrors : testErrors;
|
|
858
|
+
const selected = selectReactFailure(
|
|
859
|
+
[...attemptErrors, ...module2.errors()],
|
|
860
|
+
testErrors.length + module2.errors().length
|
|
861
|
+
);
|
|
862
|
+
return {
|
|
863
|
+
test: test2.fullName,
|
|
864
|
+
causeCode: selected.code,
|
|
865
|
+
cause: selected.cause,
|
|
866
|
+
errorName: selected.errorName,
|
|
867
|
+
actual: selected.actual,
|
|
868
|
+
expected: selected.expected,
|
|
869
|
+
origin: selected.origin,
|
|
870
|
+
layer: selected.layer,
|
|
871
|
+
related: selected.related,
|
|
872
|
+
location: test2.location ? `${(0, import_node_path.relative)(projectRoot, module2.moduleId).replaceAll("\\", "/")}:${test2.location.line}:${test2.location.column}` : errorLocation(selected.rawCause),
|
|
873
|
+
hint: failureHint(selected.rawCause),
|
|
874
|
+
trace: failureTrace(test2)
|
|
875
|
+
};
|
|
876
|
+
});
|
|
877
|
+
if (failures.length === 0 && tests.length === 0 && module2.errors().length > 0) {
|
|
878
|
+
failures.push({
|
|
879
|
+
test: "<collection>",
|
|
880
|
+
causeCode: moduleFailure.code,
|
|
881
|
+
cause: moduleFailure.cause,
|
|
882
|
+
errorName: moduleFailure.errorName,
|
|
883
|
+
actual: moduleFailure.actual,
|
|
884
|
+
expected: moduleFailure.expected,
|
|
885
|
+
origin: moduleFailure.origin,
|
|
886
|
+
layer: moduleFailure.layer,
|
|
887
|
+
related: moduleFailure.related,
|
|
888
|
+
location: errorLocation(moduleFailure.rawCause),
|
|
889
|
+
hint: failureHint(moduleFailure.rawCause)
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
return {
|
|
893
|
+
file: (0, import_node_path.relative)(projectRoot, module2.moduleId).replaceAll("\\", "/"),
|
|
894
|
+
state: module2.state(),
|
|
895
|
+
errors,
|
|
896
|
+
primaryError: moduleErrors[0] ?? failures[0]?.cause,
|
|
897
|
+
primaryCauseCode: failures[0]?.causeCode,
|
|
898
|
+
relatedErrors: failures[0]?.related,
|
|
899
|
+
tests: tests.map(toAuditInput),
|
|
900
|
+
failures
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// src/react/react-playthrough-report-format.ts
|
|
905
|
+
var REPAIR_CONSTRAINT = "Preserve the intended gameplay outcome. Fix the production mechanic, deterministic driver, or authoritative observation that prevents it. Do not make the test pass by weakening or deleting assertions, replacing the outcome with back/menu/exit navigation, observing arbitrary UI text only to change a fingerprint, using a no-op step, or treating an intermediate active/in-flight/running phase as meaningful progress.";
|
|
929
906
|
function formatPendingProductTestReport(pending) {
|
|
930
907
|
if (pending.length === 0) return void 0;
|
|
931
908
|
const lines = [
|
|
@@ -942,149 +919,383 @@ function formatPendingProductTestReport(pending) {
|
|
|
942
919
|
);
|
|
943
920
|
reportedIncompleteFiles.add(item.file);
|
|
944
921
|
}
|
|
945
|
-
lines.push(`FILE: ${item.file}`);
|
|
946
|
-
lines.push(`TEST: ${item.test}`);
|
|
947
|
-
lines.push(`MODE: ${item.mode}`);
|
|
922
|
+
lines.push(`FILE: ${item.file}`);
|
|
923
|
+
lines.push(`TEST: ${item.test}`);
|
|
924
|
+
lines.push(`MODE: ${item.mode}`);
|
|
925
|
+
}
|
|
926
|
+
lines.push(
|
|
927
|
+
"NEXT: Implement each focused product test or delete a genuinely inapplicable slot. Do not replace todo with skip."
|
|
928
|
+
);
|
|
929
|
+
return `
|
|
930
|
+
${lines.join("\n")}`;
|
|
931
|
+
}
|
|
932
|
+
function formatOrigin(origin) {
|
|
933
|
+
return `${origin.file}:${origin.line}:${origin.column}`;
|
|
934
|
+
}
|
|
935
|
+
function appendFailureDetails(lines, failure) {
|
|
936
|
+
if (failure.errorName) lines.push(`ERROR_NAME: ${failure.errorName}`);
|
|
937
|
+
if (failure.layer) lines.push(`CAUSE_LAYER: ${failure.layer}`);
|
|
938
|
+
if (failure.origin) lines.push(`ORIGIN: ${formatOrigin(failure.origin)}`);
|
|
939
|
+
if (failure.expected !== void 0) {
|
|
940
|
+
lines.push(`EXPECTED: ${failure.expected}`);
|
|
941
|
+
}
|
|
942
|
+
if (failure.actual !== void 0) lines.push(`ACTUAL: ${failure.actual}`);
|
|
943
|
+
}
|
|
944
|
+
function formatReactFailureSummary(modules) {
|
|
945
|
+
const failures = modules.flatMap(
|
|
946
|
+
(module2) => (module2.failures ?? []).map((failure) => ({
|
|
947
|
+
...failure,
|
|
948
|
+
file: module2.file
|
|
949
|
+
}))
|
|
950
|
+
);
|
|
951
|
+
if (failures.length === 0) return ["TEST_RESULT: PASS"];
|
|
952
|
+
const lines = [`FAILED_TESTS: ${failures.length}`];
|
|
953
|
+
for (const [index, failure] of failures.entries()) {
|
|
954
|
+
lines.push(`FAILURE_${index + 1}: ${failure.file}`);
|
|
955
|
+
lines.push(`TEST: ${failure.test}`);
|
|
956
|
+
lines.push(`CAUSE_CODE: ${failure.causeCode ?? "TEST_FAILURE"}`);
|
|
957
|
+
lines.push(`CAUSE: ${failure.cause}`);
|
|
958
|
+
appendFailureDetails(lines, failure);
|
|
959
|
+
for (const [relatedIndex, related] of (failure.related ?? []).entries()) {
|
|
960
|
+
lines.push(`RELATED_${relatedIndex + 1}_CODE: ${related.code}`);
|
|
961
|
+
lines.push(
|
|
962
|
+
`RELATED_${relatedIndex + 1}: ${firstFailureLine(related.message) ?? related.message}`
|
|
963
|
+
);
|
|
964
|
+
if (related.layer) {
|
|
965
|
+
lines.push(`RELATED_${relatedIndex + 1}_LAYER: ${related.layer}`);
|
|
966
|
+
}
|
|
967
|
+
if (related.origin) {
|
|
968
|
+
lines.push(
|
|
969
|
+
`RELATED_${relatedIndex + 1}_ORIGIN: ${formatOrigin(related.origin)}`
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
if (failure.trace) lines.push(`TRACE: ${failure.trace}`);
|
|
974
|
+
if (failure.location) lines.push(`AT: ${failure.location}`);
|
|
975
|
+
if (failure.hint) lines.push(`HINT: ${failure.hint}`);
|
|
976
|
+
}
|
|
977
|
+
lines.push("TEST_RESULT: FAIL");
|
|
978
|
+
return lines;
|
|
979
|
+
}
|
|
980
|
+
function formatReactTestCountSummary(modules) {
|
|
981
|
+
const files = {
|
|
982
|
+
total: modules.length,
|
|
983
|
+
passed: 0,
|
|
984
|
+
failed: 0,
|
|
985
|
+
skipped: 0,
|
|
986
|
+
pending: 0,
|
|
987
|
+
queued: 0
|
|
988
|
+
};
|
|
989
|
+
const tests = {
|
|
990
|
+
total: 0,
|
|
991
|
+
passed: 0,
|
|
992
|
+
failed: 0,
|
|
993
|
+
skipped: 0,
|
|
994
|
+
todo: 0,
|
|
995
|
+
pending: 0
|
|
996
|
+
};
|
|
997
|
+
let declaredPlaythroughs = 0;
|
|
998
|
+
let verifiedPlaythroughs = 0;
|
|
999
|
+
let waivedPlaythroughs = 0;
|
|
1000
|
+
for (const module2 of modules) {
|
|
1001
|
+
if (module2.state in files && module2.state !== "total") {
|
|
1002
|
+
files[module2.state] += 1;
|
|
1003
|
+
}
|
|
1004
|
+
for (const test2 of module2.tests) {
|
|
1005
|
+
tests.total += 1;
|
|
1006
|
+
if (test2.mode === "todo") tests.todo += 1;
|
|
1007
|
+
else tests[test2.state] += 1;
|
|
1008
|
+
if (test2.metadata) {
|
|
1009
|
+
declaredPlaythroughs += 1;
|
|
1010
|
+
if (test2.metadata.evidence.verified) verifiedPlaythroughs += 1;
|
|
1011
|
+
if (test2.metadata.waiverReason) waivedPlaythroughs += 1;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
return [
|
|
1016
|
+
`TEST_FILES: total=${files.total} passed=${files.passed} failed=${files.failed} skipped=${files.skipped} pending=${files.pending} queued=${files.queued}`,
|
|
1017
|
+
`TESTS: total=${tests.total} passed=${tests.passed} failed=${tests.failed} skipped=${tests.skipped} todo=${tests.todo} pending=${tests.pending}`,
|
|
1018
|
+
`PLAYTHROUGHS: declared=${declaredPlaythroughs} verified=${verifiedPlaythroughs} waived=${waivedPlaythroughs}`
|
|
1019
|
+
];
|
|
1020
|
+
}
|
|
1021
|
+
function formatReactPlaythroughReport(report, alignment) {
|
|
1022
|
+
const lines = [
|
|
1023
|
+
`REACT_PLAYTHROUGH_STRUCTURE: ${report.status}`,
|
|
1024
|
+
`PRODUCT_TEST_ALIGNMENT: ${alignment?.status ?? "NOT_VERIFIED"}`,
|
|
1025
|
+
`FILE: ${report.file}`
|
|
1026
|
+
];
|
|
1027
|
+
if (alignment?.cause) lines.push(`ALIGNMENT_CAUSE: ${alignment.cause}`);
|
|
1028
|
+
if (report.causeCode) lines.push(`CAUSE_CODE: ${report.causeCode}`);
|
|
1029
|
+
if (report.cause) lines.push(`CAUSE: ${report.cause}`);
|
|
1030
|
+
for (const [index, related] of (report.related ?? []).entries()) {
|
|
1031
|
+
lines.push(`RELATED_${index + 1}_CODE: ${related.code}`);
|
|
1032
|
+
lines.push(
|
|
1033
|
+
`RELATED_${index + 1}: ${firstFailureLine(related.message) ?? related.message}`
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
if (report.waiverReasons?.length) {
|
|
1037
|
+
lines.push(`REASON: ${report.waiverReasons.join("; ")}`);
|
|
1038
|
+
}
|
|
1039
|
+
if (report.next) lines.push(`NEXT: ${report.next}`);
|
|
1040
|
+
if (report.status === "FAILED") {
|
|
1041
|
+
lines.push(`REPAIR_CONSTRAINT: ${REPAIR_CONSTRAINT}`);
|
|
1042
|
+
}
|
|
1043
|
+
return `
|
|
1044
|
+
${lines.join("\n")}`;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// src/cli/react-authoritative-playthrough.ts
|
|
1048
|
+
var import_node_fs = require("fs");
|
|
1049
|
+
var import_node_path2 = require("path");
|
|
1050
|
+
var PRODUCTION_PLAYTHROUGH = "tests/production-playthrough.test.tsx";
|
|
1051
|
+
var PRODUCTION_APP = "src/App.tsx";
|
|
1052
|
+
var EXAMPLE_IMPORT_PREFIX = "@/game/example/";
|
|
1053
|
+
var REACT_RUNTIME_ENTRY = "miaoda-game-devkit/react";
|
|
1054
|
+
var REACT_TESTING_ENTRY = "miaoda-game-devkit/react/testing";
|
|
1055
|
+
var CLOCK_IMPORTS = /* @__PURE__ */ new Set(["browserGameClock"]);
|
|
1056
|
+
var CONTROLLER_IMPORTS = /* @__PURE__ */ new Set([
|
|
1057
|
+
"useGameController",
|
|
1058
|
+
"useOwnedGameController"
|
|
1059
|
+
]);
|
|
1060
|
+
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
|
|
1061
|
+
function runnableTestFiles(root, directory = (0, import_node_path2.join)(root, "tests")) {
|
|
1062
|
+
if (!(0, import_node_fs.existsSync)(directory)) return [];
|
|
1063
|
+
const files = [];
|
|
1064
|
+
for (const entry of (0, import_node_fs.readdirSync)(directory, { withFileTypes: true })) {
|
|
1065
|
+
const path = (0, import_node_path2.join)(directory, entry.name);
|
|
1066
|
+
if (entry.isDirectory()) {
|
|
1067
|
+
files.push(...runnableTestFiles(root, path));
|
|
1068
|
+
} else if (entry.isFile() && /\.test\.[cm]?[jt]sx?$/.test(entry.name)) {
|
|
1069
|
+
files.push(path);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
return files;
|
|
1073
|
+
}
|
|
1074
|
+
function extension(path) {
|
|
1075
|
+
const index = path.lastIndexOf(".");
|
|
1076
|
+
return index < 0 ? "" : path.slice(index);
|
|
1077
|
+
}
|
|
1078
|
+
function sourceFiles(root, directory = (0, import_node_path2.join)(root, "src")) {
|
|
1079
|
+
if (!(0, import_node_fs.existsSync)(directory)) return [];
|
|
1080
|
+
const files = [];
|
|
1081
|
+
for (const entry of (0, import_node_fs.readdirSync)(directory, { withFileTypes: true })) {
|
|
1082
|
+
const path = (0, import_node_path2.join)(directory, entry.name);
|
|
1083
|
+
const projectPath = (0, import_node_path2.relative)(root, path).replaceAll("\\", "/");
|
|
1084
|
+
if (entry.isDirectory()) {
|
|
1085
|
+
if (projectPath === "src/game/example") continue;
|
|
1086
|
+
files.push(...sourceFiles(root, path));
|
|
1087
|
+
} else if (entry.isFile() && SOURCE_EXTENSIONS.has(extension(entry.name))) {
|
|
1088
|
+
files.push(path);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
return files;
|
|
1092
|
+
}
|
|
1093
|
+
function withoutComments(source) {
|
|
1094
|
+
let output = "";
|
|
1095
|
+
let state = "code";
|
|
1096
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
1097
|
+
const char = source[index];
|
|
1098
|
+
const next = source[index + 1];
|
|
1099
|
+
if (state === "line") {
|
|
1100
|
+
if (char === "\n") {
|
|
1101
|
+
state = "code";
|
|
1102
|
+
output += char;
|
|
1103
|
+
} else {
|
|
1104
|
+
output += " ";
|
|
1105
|
+
}
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
if (state === "block") {
|
|
1109
|
+
if (char === "*" && next === "/") {
|
|
1110
|
+
output += " ";
|
|
1111
|
+
index += 1;
|
|
1112
|
+
state = "code";
|
|
1113
|
+
} else {
|
|
1114
|
+
output += char === "\n" ? "\n" : " ";
|
|
1115
|
+
}
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
if (state === "code" && char === "/" && next === "/") {
|
|
1119
|
+
output += " ";
|
|
1120
|
+
index += 1;
|
|
1121
|
+
state = "line";
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
if (state === "code" && char === "/" && next === "*") {
|
|
1125
|
+
output += " ";
|
|
1126
|
+
index += 1;
|
|
1127
|
+
state = "block";
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
if (state === "code" && char === "'") state = "single";
|
|
1131
|
+
else if (state === "code" && char === '"') state = "double";
|
|
1132
|
+
else if (state === "code" && char === "`") state = "template";
|
|
1133
|
+
else if (state === "single" && char === "'" && source[index - 1] !== "\\") {
|
|
1134
|
+
state = "code";
|
|
1135
|
+
} else if (state === "double" && char === '"' && source[index - 1] !== "\\") {
|
|
1136
|
+
state = "code";
|
|
1137
|
+
} else if (state === "template" && char === "`" && source[index - 1] !== "\\") {
|
|
1138
|
+
state = "code";
|
|
1139
|
+
}
|
|
1140
|
+
output += char;
|
|
1141
|
+
}
|
|
1142
|
+
return output;
|
|
1143
|
+
}
|
|
1144
|
+
function codePositions(source) {
|
|
1145
|
+
const positions = Array.from({ length: source.length }, () => false);
|
|
1146
|
+
let state = "code";
|
|
1147
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
1148
|
+
const char = source[index];
|
|
1149
|
+
if (state === "code") positions[index] = true;
|
|
1150
|
+
if (state === "code" && char === "'") state = "single";
|
|
1151
|
+
else if (state === "code" && char === '"') state = "double";
|
|
1152
|
+
else if (state === "code" && char === "`") state = "template";
|
|
1153
|
+
else if (state === "single" && char === "'" && source[index - 1] !== "\\") {
|
|
1154
|
+
state = "code";
|
|
1155
|
+
} else if (state === "double" && char === '"' && source[index - 1] !== "\\") {
|
|
1156
|
+
state = "code";
|
|
1157
|
+
} else if (state === "template" && char === "`" && source[index - 1] !== "\\") {
|
|
1158
|
+
state = "code";
|
|
1159
|
+
}
|
|
948
1160
|
}
|
|
949
|
-
|
|
950
|
-
"NEXT: Implement each focused product test or delete a genuinely inapplicable slot. Do not replace todo with skip."
|
|
951
|
-
);
|
|
952
|
-
return `
|
|
953
|
-
${lines.join("\n")}`;
|
|
954
|
-
}
|
|
955
|
-
function firstLine(value) {
|
|
956
|
-
if (typeof value !== "string") return void 0;
|
|
957
|
-
return (0, import_node_util.stripVTControlCharacters)(value).split("\n").map((line) => line.trim()).find(Boolean);
|
|
1161
|
+
return positions;
|
|
958
1162
|
}
|
|
959
|
-
function
|
|
960
|
-
const
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
related: []
|
|
967
|
-
};
|
|
1163
|
+
function importedModuleSpecifiers(source) {
|
|
1164
|
+
const clean = withoutComments(source);
|
|
1165
|
+
const positions = codePositions(clean);
|
|
1166
|
+
const modules = [];
|
|
1167
|
+
const pattern = /\b(?:from\s+|import\s*(?:\(\s*)?)["']([^"']+)["']\s*\)?/g;
|
|
1168
|
+
for (const match of clean.matchAll(pattern)) {
|
|
1169
|
+
if (positions[match.index]) modules.push(match[1]);
|
|
968
1170
|
}
|
|
969
|
-
|
|
970
|
-
const rawCause = primary.message;
|
|
971
|
-
const cause = firstLine(rawCause) ?? rawCause;
|
|
972
|
-
return { code: primary.code, cause, rawCause, related: entries.slice(1) };
|
|
1171
|
+
return modules;
|
|
973
1172
|
}
|
|
974
|
-
function
|
|
975
|
-
const
|
|
976
|
-
|
|
1173
|
+
function productionFileImportsExample(file, projectRoot) {
|
|
1174
|
+
const exampleRoot = (0, import_node_path2.join)(projectRoot, "src/game/example");
|
|
1175
|
+
return importedModuleSpecifiers((0, import_node_fs.readFileSync)(file, "utf8")).some(
|
|
1176
|
+
(moduleName) => {
|
|
1177
|
+
if (moduleName.startsWith(EXAMPLE_IMPORT_PREFIX)) return true;
|
|
1178
|
+
if (!moduleName.startsWith(".")) return false;
|
|
1179
|
+
const target = (0, import_node_path2.resolve)((0, import_node_path2.dirname)(file), moduleName);
|
|
1180
|
+
return target === exampleRoot || target.startsWith(`${exampleRoot}${import_node_path2.sep}`);
|
|
1181
|
+
}
|
|
977
1182
|
);
|
|
978
|
-
if (names.length === 0) return void 0;
|
|
979
|
-
return `Available button names: ${names.map((name) => JSON.stringify(name)).join(", ")}`;
|
|
980
1183
|
}
|
|
981
|
-
function
|
|
982
|
-
|
|
983
|
-
|
|
1184
|
+
function importsExampleAlias(source) {
|
|
1185
|
+
return importedModuleSpecifiers(source).some(
|
|
1186
|
+
(moduleName) => moduleName.startsWith(EXAMPLE_IMPORT_PREFIX)
|
|
984
1187
|
);
|
|
985
|
-
return match?.[1];
|
|
986
1188
|
}
|
|
987
|
-
function
|
|
988
|
-
const
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1189
|
+
function namedImports(source, moduleName) {
|
|
1190
|
+
const names = /* @__PURE__ */ new Set();
|
|
1191
|
+
const clean = withoutComments(source);
|
|
1192
|
+
const positions = codePositions(clean);
|
|
1193
|
+
const escapedModule = moduleName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1194
|
+
const pattern = new RegExp(
|
|
1195
|
+
`^\\s*import\\s+(?:type\\s+)?\\{([\\s\\S]*?)\\}\\s+from\\s+["']${escapedModule}["']`,
|
|
1196
|
+
"gm"
|
|
1197
|
+
);
|
|
1198
|
+
for (const match of clean.matchAll(pattern)) {
|
|
1199
|
+
if (!positions[match.index]) continue;
|
|
1200
|
+
for (const specifier of match[1].split(",")) {
|
|
1201
|
+
const imported = specifier.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0].trim();
|
|
1202
|
+
if (imported) names.add(imported);
|
|
994
1203
|
}
|
|
995
1204
|
}
|
|
996
|
-
|
|
997
|
-
const trace = annotationTrace ?? (isMetadata(metadata) ? metadata.trace : void 0);
|
|
998
|
-
return trace ? truncateReporterLine(trace, 720) : void 0;
|
|
1205
|
+
return names;
|
|
999
1206
|
}
|
|
1000
|
-
function
|
|
1001
|
-
|
|
1002
|
-
if (compact.length <= limit) return compact;
|
|
1003
|
-
return `${compact.slice(0, Math.max(0, limit - 1))}\u2026`;
|
|
1207
|
+
function containsAny(values, expected) {
|
|
1208
|
+
return [...values].some((value) => expected.has(value));
|
|
1004
1209
|
}
|
|
1005
|
-
function
|
|
1006
|
-
const
|
|
1007
|
-
const
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1210
|
+
function declaresObserve(source) {
|
|
1211
|
+
const clean = withoutComments(source);
|
|
1212
|
+
const positions = codePositions(clean);
|
|
1213
|
+
for (const match of clean.matchAll(/\bobserve\s*:/g)) {
|
|
1214
|
+
if (positions[match.index]) return true;
|
|
1215
|
+
}
|
|
1216
|
+
return false;
|
|
1217
|
+
}
|
|
1218
|
+
function auditReactAuthoritativePlaythrough(projectRoot) {
|
|
1219
|
+
const clockFiles = [];
|
|
1220
|
+
const controllerFiles = [];
|
|
1221
|
+
const productionFiles = sourceFiles(projectRoot);
|
|
1222
|
+
for (const file of productionFiles) {
|
|
1223
|
+
const imports = namedImports(
|
|
1224
|
+
(0, import_node_fs.readFileSync)(file, "utf8"),
|
|
1225
|
+
REACT_RUNTIME_ENTRY
|
|
1226
|
+
);
|
|
1227
|
+
const projectPath = (0, import_node_path2.relative)(projectRoot, file).replaceAll("\\", "/");
|
|
1228
|
+
if (containsAny(imports, CLOCK_IMPORTS)) clockFiles.push(projectPath);
|
|
1229
|
+
if (containsAny(imports, CONTROLLER_IMPORTS))
|
|
1230
|
+
controllerFiles.push(projectPath);
|
|
1231
|
+
}
|
|
1232
|
+
const appPath = (0, import_node_path2.join)(projectRoot, PRODUCTION_APP);
|
|
1233
|
+
const productionEntryExists = (0, import_node_fs.existsSync)(appPath);
|
|
1234
|
+
const productionUsesExample = productionFiles.some(
|
|
1235
|
+
(file) => productionFileImportsExample(file, projectRoot)
|
|
1013
1236
|
);
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
)
|
|
1020
|
-
)
|
|
1021
|
-
];
|
|
1022
|
-
const failures = tests.filter((test2) => test2.result().state === "failed").map((test2) => {
|
|
1023
|
-
const metadata = test2.meta().reactPlaythrough;
|
|
1024
|
-
const testErrors = test2.result().errors ?? [];
|
|
1025
|
-
const metadataErrors = isMetadata(metadata) ? metadata.failure?.entries ?? [] : [];
|
|
1026
|
-
const attemptErrors = metadataErrors.length > 0 ? metadataErrors : testErrors;
|
|
1027
|
-
const selected = selectReactFailure(
|
|
1028
|
-
[...attemptErrors, ...module2.errors()],
|
|
1029
|
-
testErrors.length + module2.errors().length
|
|
1237
|
+
const staleExampleTestFiles = productionUsesExample ? [] : runnableTestFiles(projectRoot).filter((file) => importsExampleAlias((0, import_node_fs.readFileSync)(file, "utf8"))).map((file) => (0, import_node_path2.relative)(projectRoot, file).replaceAll("\\", "/"));
|
|
1238
|
+
const issues = [];
|
|
1239
|
+
if (staleExampleTestFiles.length > 0) {
|
|
1240
|
+
issues.push(
|
|
1241
|
+
`Production ${PRODUCTION_APP} no longer imports ${EXAMPLE_IMPORT_PREFIX}*, but runnable product tests still do: ${staleExampleTestFiles.join(", ")}. Replace those CollectGame tests with tests for the production game or delete genuinely inapplicable slots. Teaching examples under tests/examples/**/*.example.* remain allowed.`
|
|
1030
1242
|
);
|
|
1243
|
+
}
|
|
1244
|
+
if (clockFiles.length === 0 && controllerFiles.length === 0) {
|
|
1031
1245
|
return {
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1246
|
+
ok: issues.length === 0,
|
|
1247
|
+
issues,
|
|
1248
|
+
clockFiles,
|
|
1249
|
+
controllerFiles,
|
|
1250
|
+
productionEntryExists,
|
|
1251
|
+
productionUsesExample,
|
|
1252
|
+
staleExampleTestFiles
|
|
1039
1253
|
};
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1254
|
+
}
|
|
1255
|
+
const testPath = (0, import_node_path2.join)(projectRoot, PRODUCTION_PLAYTHROUGH);
|
|
1256
|
+
const testSource = (0, import_node_fs.existsSync)(testPath) ? (0, import_node_fs.readFileSync)(testPath, "utf8") : "";
|
|
1257
|
+
const testingImports = namedImports(testSource, REACT_TESTING_ENTRY);
|
|
1258
|
+
const hasObserve = declaresObserve(testSource);
|
|
1259
|
+
if (!hasObserve) {
|
|
1260
|
+
issues.push(
|
|
1261
|
+
`${PRODUCTION_PLAYTHROUGH} must declare observe because production uses the devkit clock or Controller ownership hook. Pass { observe: () => telemetry.read.session() } to playthroughTest, using Telemetry backed by the same production Controller rendered by <App />; DOM labels are not an authoritative gameplay boundary.`
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
if (clockFiles.length > 0 && !testingImports.has("ManualGameClock")) {
|
|
1265
|
+
issues.push(
|
|
1266
|
+
`${PRODUCTION_PLAYTHROUGH} must import ManualGameClock because production owns gameplay time in ${clockFiles.join(", ")}. Add this import to the test: import { ManualGameClock } from "miaoda-game-devkit/react/testing"; Then create one, inject it through the production <App /> factory, and advance it with step: () => clock.stepFrame().`
|
|
1267
|
+
);
|
|
1050
1268
|
}
|
|
1051
1269
|
return {
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
failures
|
|
1270
|
+
ok: issues.length === 0,
|
|
1271
|
+
issues,
|
|
1272
|
+
clockFiles,
|
|
1273
|
+
controllerFiles,
|
|
1274
|
+
productionEntryExists,
|
|
1275
|
+
productionUsesExample,
|
|
1276
|
+
staleExampleTestFiles
|
|
1060
1277
|
};
|
|
1061
1278
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
}
|
|
1068
|
-
);
|
|
1069
|
-
if (failures.length === 0) return ["TEST_RESULT: PASS"];
|
|
1070
|
-
const lines = [`FAILED_TESTS: ${failures.length}`];
|
|
1071
|
-
for (const [index, failure] of failures.entries()) {
|
|
1072
|
-
lines.push(`FAILURE_${index + 1}: ${failure.file}`);
|
|
1073
|
-
lines.push(`TEST: ${failure.test}`);
|
|
1074
|
-
lines.push(`CAUSE_CODE: ${failure.causeCode ?? "TEST_FAILURE"}`);
|
|
1075
|
-
lines.push(`CAUSE: ${failure.cause}`);
|
|
1076
|
-
for (const [relatedIndex, related] of (failure.related ?? []).entries()) {
|
|
1077
|
-
lines.push(`RELATED_${relatedIndex + 1}_CODE: ${related.code}`);
|
|
1078
|
-
lines.push(
|
|
1079
|
-
`RELATED_${relatedIndex + 1}: ${firstLine(related.message) ?? related.message}`
|
|
1080
|
-
);
|
|
1081
|
-
}
|
|
1082
|
-
if (failure.trace) lines.push(`TRACE: ${failure.trace}`);
|
|
1083
|
-
if (failure.location) lines.push(`AT: ${failure.location}`);
|
|
1084
|
-
if (failure.hint) lines.push(`HINT: ${failure.hint}`);
|
|
1279
|
+
|
|
1280
|
+
// src/react/react-playthrough-policy.ts
|
|
1281
|
+
function assessProductTestAlignment(projectRoot) {
|
|
1282
|
+
const audit = auditReactAuthoritativePlaythrough(projectRoot);
|
|
1283
|
+
if (audit.staleExampleTestFiles.length > 0) {
|
|
1284
|
+
return { status: "FAILED", cause: audit.issues[0] };
|
|
1085
1285
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1286
|
+
if (!audit.productionEntryExists) {
|
|
1287
|
+
return {
|
|
1288
|
+
status: "NOT_VERIFIED",
|
|
1289
|
+
cause: "The production src/App.tsx entry does not exist, so product-test alignment could not be verified."
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
if (audit.productionUsesExample) {
|
|
1293
|
+
return {
|
|
1294
|
+
status: "NOT_VERIFIED",
|
|
1295
|
+
cause: "The production App still uses the replaceable src/game/example teaching game, so replacement-game test alignment is not applicable yet."
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
return { status: "PASS" };
|
|
1088
1299
|
}
|
|
1089
1300
|
function repairGuidance(code) {
|
|
1090
1301
|
switch (code) {
|
|
@@ -1102,6 +1313,12 @@ function repairGuidance(code) {
|
|
|
1102
1313
|
return "Keep the intended outcome unchanged. The stage driver ran, but gameplay did not reach it within the bound. Inspect TRACE and Last diagnostics, then confirm that each deterministic step advances the same production Controller rendered by <App />. If state remains unchanged, inject ManualGameClock through the production App factory and observe that Controller through Telemetry. Do not replace the outcome with navigation, an intermediate phase, a no-op step, or a weaker assertion.";
|
|
1103
1314
|
case "PLAYTHROUGH_CLOCK_NOT_ADVANCED":
|
|
1104
1315
|
return "This stage is driven by time or frames, but it 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.";
|
|
1316
|
+
case "EXPECTATION_MISMATCH":
|
|
1317
|
+
return "The test assertion observed a different value than it expected. Compare EXPECTED and ACTUAL with the game-owned state type and inspect ORIGIN before editing. Fix production only when the observed value violates the product contract; otherwise correct the test expectation without weakening the intended outcome.";
|
|
1318
|
+
case "PRODUCT_RUNTIME_TYPE_ERROR":
|
|
1319
|
+
return "A TypeError originated in production source. Open ORIGIN, validate the referenced dependency or state before mutation, and fix the owning initialization or lifecycle boundary. Do not bypass the crashing path in the test.";
|
|
1320
|
+
case "TEST_API_MISMATCH":
|
|
1321
|
+
return "A TypeError originated in test code. Open ORIGIN and align the call with the installed public API or the game-owned typed Session. Do not add a production shim solely for an invented test helper.";
|
|
1105
1322
|
case "INVALID_STAGE_ORDER":
|
|
1106
1323
|
case "INCOMPLETE_PLAYTHROUGH_EVIDENCE":
|
|
1107
1324
|
case "INVALID_STAGE_NAME":
|
|
@@ -1187,36 +1404,14 @@ function assessReactPlaythroughReport(input) {
|
|
|
1187
1404
|
}
|
|
1188
1405
|
return { ...base, status: "PASS", failsRun: false };
|
|
1189
1406
|
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
`PRODUCT_TEST_ALIGNMENT: ${alignment?.status ?? "NOT_VERIFIED"}`,
|
|
1194
|
-
`FILE: ${report.file}`
|
|
1195
|
-
];
|
|
1196
|
-
if (alignment?.cause) lines.push(`ALIGNMENT_CAUSE: ${alignment.cause}`);
|
|
1197
|
-
if (report.causeCode) lines.push(`CAUSE_CODE: ${report.causeCode}`);
|
|
1198
|
-
if (report.cause) lines.push(`CAUSE: ${report.cause}`);
|
|
1199
|
-
for (const [index, related] of (report.related ?? []).entries()) {
|
|
1200
|
-
lines.push(`RELATED_${index + 1}_CODE: ${related.code}`);
|
|
1201
|
-
lines.push(
|
|
1202
|
-
`RELATED_${index + 1}: ${firstLine(related.message) ?? related.message}`
|
|
1203
|
-
);
|
|
1204
|
-
}
|
|
1205
|
-
if (report.waiverReasons?.length) {
|
|
1206
|
-
lines.push(`REASON: ${report.waiverReasons.join("; ")}`);
|
|
1207
|
-
}
|
|
1208
|
-
if (report.next) lines.push(`NEXT: ${report.next}`);
|
|
1209
|
-
if (report.status === "FAILED") {
|
|
1210
|
-
lines.push(`REPAIR_CONSTRAINT: ${REPAIR_CONSTRAINT}`);
|
|
1211
|
-
}
|
|
1212
|
-
return `
|
|
1213
|
-
${lines.join("\n")}`;
|
|
1214
|
-
}
|
|
1407
|
+
|
|
1408
|
+
// src/react/react-playthrough-reporter.ts
|
|
1409
|
+
var PRODUCTION_PLAYTHROUGH_FILE = "tests/production-playthrough.test.tsx";
|
|
1215
1410
|
var ReactPlaythroughReporter = class {
|
|
1216
1411
|
/** 绑定模板根目录,以稳定识别生产可玩性测试而不依赖测试名称。 */
|
|
1217
1412
|
constructor(projectRoot) {
|
|
1218
1413
|
this.projectRoot = projectRoot;
|
|
1219
|
-
this.expectedModuleId = (0,
|
|
1414
|
+
this.expectedModuleId = (0, import_node_path3.resolve)(projectRoot, this.expectedFile);
|
|
1220
1415
|
}
|
|
1221
1416
|
projectRoot;
|
|
1222
1417
|
expectedFile = PRODUCTION_PLAYTHROUGH_FILE;
|
|
@@ -1226,7 +1421,7 @@ var ReactPlaythroughReporter = class {
|
|
|
1226
1421
|
/** 记录本轮是否实际选择了生产流程文件,用于区分聚焦运行与门禁失败。 */
|
|
1227
1422
|
onTestRunStart(specifications) {
|
|
1228
1423
|
this.expectedFileScheduled = specifications.some(
|
|
1229
|
-
(specification) => (0,
|
|
1424
|
+
(specification) => (0, import_node_path3.resolve)(specification.moduleId) === this.expectedModuleId
|
|
1230
1425
|
);
|
|
1231
1426
|
this.focusedSelection = specifications.some(
|
|
1232
1427
|
(specification) => Boolean(specification.project.globalConfig.testNamePattern) || Boolean(specification.testNamePattern) || Boolean(specification.testLines?.length)
|
|
@@ -1238,16 +1433,17 @@ var ReactPlaythroughReporter = class {
|
|
|
1238
1433
|
testModules,
|
|
1239
1434
|
this.projectRoot
|
|
1240
1435
|
);
|
|
1436
|
+
const moduleResults = testModules.map(
|
|
1437
|
+
(module2) => toReactPlaythroughModuleResult(module2, this.projectRoot)
|
|
1438
|
+
);
|
|
1241
1439
|
const report = assessReactPlaythroughReport({
|
|
1242
1440
|
expectedFile: this.expectedFile,
|
|
1243
1441
|
expectedFileExists: (0, import_node_fs2.existsSync)(this.expectedModuleId),
|
|
1244
1442
|
expectedFileScheduled: this.expectedFileScheduled,
|
|
1245
1443
|
focusedSelection: this.focusedSelection,
|
|
1246
|
-
modules:
|
|
1247
|
-
(module2) => toModuleResult(module2, this.projectRoot)
|
|
1248
|
-
),
|
|
1444
|
+
modules: moduleResults,
|
|
1249
1445
|
unhandledErrors: extractFailureEntries(unhandledErrors).map(
|
|
1250
|
-
(entry) =>
|
|
1446
|
+
(entry) => firstFailureLine(entry.message) ?? entry.message
|
|
1251
1447
|
)
|
|
1252
1448
|
});
|
|
1253
1449
|
const alignment = assessProductTestAlignment(this.projectRoot);
|
|
@@ -1265,14 +1461,14 @@ var ReactPlaythroughReporter = class {
|
|
|
1265
1461
|
console.error(pendingOutput);
|
|
1266
1462
|
process.exitCode = 1;
|
|
1267
1463
|
}
|
|
1268
|
-
const summary = formatReactFailureSummary(
|
|
1269
|
-
testModules.map((module2) => toModuleResult(module2, this.projectRoot))
|
|
1270
|
-
);
|
|
1464
|
+
const summary = formatReactFailureSummary(moduleResults);
|
|
1271
1465
|
if ((report.failsRun || alignment.status === "FAILED" || pendingProductTests.length > 0) && summary[0] === "TEST_RESULT: PASS") {
|
|
1272
1466
|
summary[0] = "TEST_RESULT: FAIL";
|
|
1273
1467
|
}
|
|
1274
|
-
console.log(
|
|
1275
|
-
|
|
1468
|
+
console.log(
|
|
1469
|
+
`
|
|
1470
|
+
${[...formatReactTestCountSummary(moduleResults), ...summary].join("\n")}`
|
|
1471
|
+
);
|
|
1276
1472
|
}
|
|
1277
1473
|
};
|
|
1278
1474
|
|
|
@@ -1284,13 +1480,13 @@ function getJSDOMWorkerExecArgv() {
|
|
|
1284
1480
|
|
|
1285
1481
|
// src/react-vitest-config.ts
|
|
1286
1482
|
function resolvePhaser3BrowserEntry(projectRoot) {
|
|
1287
|
-
const manifestPath = (0,
|
|
1483
|
+
const manifestPath = (0, import_node_path4.resolve)(projectRoot, "node_modules/phaser/package.json");
|
|
1288
1484
|
if (!(0, import_node_fs3.existsSync)(manifestPath)) return void 0;
|
|
1289
1485
|
try {
|
|
1290
1486
|
const manifest = JSON.parse((0, import_node_fs3.readFileSync)(manifestPath, "utf8"));
|
|
1291
1487
|
if (!manifest.version?.startsWith("3.")) return void 0;
|
|
1292
|
-
const browserEntry = (0,
|
|
1293
|
-
(0,
|
|
1488
|
+
const browserEntry = (0, import_node_path4.resolve)(
|
|
1489
|
+
(0, import_node_path4.dirname)(manifestPath),
|
|
1294
1490
|
manifest.browser ?? "dist/phaser.js"
|
|
1295
1491
|
);
|
|
1296
1492
|
return (0, import_node_fs3.existsSync)(browserEntry) ? browserEntry : void 0;
|
|
@@ -1308,7 +1504,7 @@ function defineReactGameVitestConfig(options) {
|
|
|
1308
1504
|
alias: {
|
|
1309
1505
|
...phaser3BrowserEntry ? { phaser: phaser3BrowserEntry } : {},
|
|
1310
1506
|
...options.aliases,
|
|
1311
|
-
"@": (0,
|
|
1507
|
+
"@": (0, import_node_path4.resolve)(options.projectRoot, "src")
|
|
1312
1508
|
}
|
|
1313
1509
|
},
|
|
1314
1510
|
test: {
|