miaoda-game-devkit 0.6.5 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/phaser-lint.js +62 -105
- package/dist/cli/react-lint.js +62 -105
- package/dist/react/index.d.mts +3 -0
- package/dist/react/index.d.ts +3 -0
- package/dist/react/testing.d.mts +22 -0
- package/dist/react/testing.d.ts +22 -0
- package/dist/react/testing.js +114 -16
- package/dist/react/testing.mjs +114 -16
- package/dist/react/vitest-config.d.mts +5 -0
- package/dist/react/vitest-config.d.ts +5 -0
- package/dist/react/vitest-config.js +522 -365
- package/dist/react/vitest-config.mjs +515 -358
- package/dist/react/vitest-setup.js +112 -14
- package/dist/react/vitest-setup.mjs +112 -14
- package/package.json +2 -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. 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.`
|
|
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(", ")}. 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().`
|
|
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) {
|
|
@@ -427,10 +295,10 @@ async function runBoundedUntil(condition, options = {}) {
|
|
|
427
295
|
}
|
|
428
296
|
}
|
|
429
297
|
const diagnostics = formatDiagnostics(options.diagnostics);
|
|
430
|
-
const guidance = options.step ? "The step callback ran, but the authoritative outcome did not change." : "No step callback was provided,
|
|
298
|
+
const guidance = options.step ? "The step callback ran, but the authoritative outcome did not change." : "No step callback was provided, and the condition did not become true after the stage action. This does not identify a clock problem; inspect the condition, production input wiring, and observed state boundary.";
|
|
431
299
|
const suffix = diagnostics ? ` Last diagnostics: ${diagnostics}` : "";
|
|
432
300
|
throw codedError(
|
|
433
|
-
options.step ? "PLAYTHROUGH_BOUND_EXHAUSTED" : "
|
|
301
|
+
options.step ? "PLAYTHROUGH_BOUND_EXHAUSTED" : "PLAYTHROUGH_OUTCOME_NOT_REACHED",
|
|
434
302
|
`Playthrough outcome was not reached within ${maxSteps} steps. ${guidance}${suffix}`
|
|
435
303
|
);
|
|
436
304
|
}
|
|
@@ -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,36 +778,6 @@ function findPendingProductTests(modules, projectRoot) {
|
|
|
926
778
|
}));
|
|
927
779
|
});
|
|
928
780
|
}
|
|
929
|
-
function formatPendingProductTestReport(pending) {
|
|
930
|
-
if (pending.length === 0) return void 0;
|
|
931
|
-
const lines = [
|
|
932
|
-
"REACT_FOCUSED_TESTS: FAILED",
|
|
933
|
-
"CAUSE_CODE: TODO_OR_SKIP_TESTS",
|
|
934
|
-
"CAUSE: Product tests still contain explicit todo/skip cases."
|
|
935
|
-
];
|
|
936
|
-
const reportedIncompleteFiles = /* @__PURE__ */ new Set();
|
|
937
|
-
for (const item of pending) {
|
|
938
|
-
if (item.fileOnlyContainsPendingTests && !reportedIncompleteFiles.has(item.file)) {
|
|
939
|
-
lines.push("FILE_CAUSE_CODE: PRODUCT_TEST_FILE_NOT_IMPLEMENTED");
|
|
940
|
-
lines.push(
|
|
941
|
-
`FILE_CAUSE: Every collected test in ${item.file} is marked todo/skip.`
|
|
942
|
-
);
|
|
943
|
-
reportedIncompleteFiles.add(item.file);
|
|
944
|
-
}
|
|
945
|
-
lines.push(`FILE: ${item.file}`);
|
|
946
|
-
lines.push(`TEST: ${item.test}`);
|
|
947
|
-
lines.push(`MODE: ${item.mode}`);
|
|
948
|
-
}
|
|
949
|
-
lines.push(
|
|
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);
|
|
958
|
-
}
|
|
959
781
|
function selectReactFailure(values, errorRecordCount = values.length) {
|
|
960
782
|
const entries = extractFailureEntries(values);
|
|
961
783
|
if (entries.length === 0) {
|
|
@@ -968,8 +790,17 @@ function selectReactFailure(values, errorRecordCount = values.length) {
|
|
|
968
790
|
}
|
|
969
791
|
const primary = entries[0];
|
|
970
792
|
const rawCause = primary.message;
|
|
971
|
-
|
|
972
|
-
|
|
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
|
+
};
|
|
973
804
|
}
|
|
974
805
|
function failureHint(value) {
|
|
975
806
|
const names = [...value.matchAll(/button\s*\n\s*Name "([^"]+)"/g)].map(
|
|
@@ -984,6 +815,11 @@ function errorLocation(value) {
|
|
|
984
815
|
);
|
|
985
816
|
return match?.[1];
|
|
986
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
|
+
}
|
|
987
823
|
function failureTrace(test2) {
|
|
988
824
|
const annotations = test2.annotations();
|
|
989
825
|
let annotationTrace;
|
|
@@ -997,25 +833,20 @@ function failureTrace(test2) {
|
|
|
997
833
|
const trace = annotationTrace ?? (isMetadata(metadata) ? metadata.trace : void 0);
|
|
998
834
|
return trace ? truncateReporterLine(trace, 720) : void 0;
|
|
999
835
|
}
|
|
1000
|
-
function
|
|
1001
|
-
const compact = (0, import_node_util.stripVTControlCharacters)(value).replace(/\s+/g, " ").trim();
|
|
1002
|
-
if (compact.length <= limit) return compact;
|
|
1003
|
-
return `${compact.slice(0, Math.max(0, limit - 1))}\u2026`;
|
|
1004
|
-
}
|
|
1005
|
-
function toModuleResult(module2, projectRoot) {
|
|
836
|
+
function toReactPlaythroughModuleResult(module2, projectRoot) {
|
|
1006
837
|
const tests = [...module2.children.allTests()];
|
|
1007
838
|
const moduleFailure = selectReactFailure(
|
|
1008
839
|
module2.errors(),
|
|
1009
840
|
module2.errors().length
|
|
1010
841
|
);
|
|
1011
842
|
const moduleErrors = extractFailureEntries(module2.errors()).map(
|
|
1012
|
-
(entry) =>
|
|
843
|
+
(entry) => firstFailureLine(entry.message) ?? entry.message
|
|
1013
844
|
);
|
|
1014
845
|
const errors = [
|
|
1015
846
|
...moduleErrors,
|
|
1016
847
|
...tests.flatMap(
|
|
1017
848
|
(test2) => extractFailureEntries(test2.result().errors ?? []).map(
|
|
1018
|
-
(entry) =>
|
|
849
|
+
(entry) => firstFailureLine(entry.message) ?? entry.message
|
|
1019
850
|
)
|
|
1020
851
|
)
|
|
1021
852
|
];
|
|
@@ -1032,8 +863,13 @@ function toModuleResult(module2, projectRoot) {
|
|
|
1032
863
|
test: test2.fullName,
|
|
1033
864
|
causeCode: selected.code,
|
|
1034
865
|
cause: selected.cause,
|
|
866
|
+
errorName: selected.errorName,
|
|
867
|
+
actual: selected.actual,
|
|
868
|
+
expected: selected.expected,
|
|
869
|
+
origin: selected.origin,
|
|
870
|
+
layer: selected.layer,
|
|
1035
871
|
related: selected.related,
|
|
1036
|
-
location: test2.location ? `${(0,
|
|
872
|
+
location: test2.location ? `${(0, import_node_path.relative)(projectRoot, module2.moduleId).replaceAll("\\", "/")}:${test2.location.line}:${test2.location.column}` : errorLocation(selected.rawCause),
|
|
1037
873
|
hint: failureHint(selected.rawCause),
|
|
1038
874
|
trace: failureTrace(test2)
|
|
1039
875
|
};
|
|
@@ -1043,13 +879,18 @@ function toModuleResult(module2, projectRoot) {
|
|
|
1043
879
|
test: "<collection>",
|
|
1044
880
|
causeCode: moduleFailure.code,
|
|
1045
881
|
cause: moduleFailure.cause,
|
|
882
|
+
errorName: moduleFailure.errorName,
|
|
883
|
+
actual: moduleFailure.actual,
|
|
884
|
+
expected: moduleFailure.expected,
|
|
885
|
+
origin: moduleFailure.origin,
|
|
886
|
+
layer: moduleFailure.layer,
|
|
1046
887
|
related: moduleFailure.related,
|
|
1047
888
|
location: errorLocation(moduleFailure.rawCause),
|
|
1048
889
|
hint: failureHint(moduleFailure.rawCause)
|
|
1049
890
|
});
|
|
1050
891
|
}
|
|
1051
892
|
return {
|
|
1052
|
-
file: (0,
|
|
893
|
+
file: (0, import_node_path.relative)(projectRoot, module2.moduleId).replaceAll("\\", "/"),
|
|
1053
894
|
state: module2.state(),
|
|
1054
895
|
errors,
|
|
1055
896
|
primaryError: moduleErrors[0] ?? failures[0]?.cause,
|
|
@@ -1059,6 +900,47 @@ function toModuleResult(module2, projectRoot) {
|
|
|
1059
900
|
failures
|
|
1060
901
|
};
|
|
1061
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.";
|
|
906
|
+
function formatPendingProductTestReport(pending) {
|
|
907
|
+
if (pending.length === 0) return void 0;
|
|
908
|
+
const lines = [
|
|
909
|
+
"REACT_FOCUSED_TESTS: FAILED",
|
|
910
|
+
"CAUSE_CODE: TODO_OR_SKIP_TESTS",
|
|
911
|
+
"CAUSE: Product tests still contain explicit todo/skip cases."
|
|
912
|
+
];
|
|
913
|
+
const reportedIncompleteFiles = /* @__PURE__ */ new Set();
|
|
914
|
+
for (const item of pending) {
|
|
915
|
+
if (item.fileOnlyContainsPendingTests && !reportedIncompleteFiles.has(item.file)) {
|
|
916
|
+
lines.push("FILE_CAUSE_CODE: PRODUCT_TEST_FILE_NOT_IMPLEMENTED");
|
|
917
|
+
lines.push(
|
|
918
|
+
`FILE_CAUSE: Every collected test in ${item.file} is marked todo/skip.`
|
|
919
|
+
);
|
|
920
|
+
reportedIncompleteFiles.add(item.file);
|
|
921
|
+
}
|
|
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
|
+
}
|
|
1062
944
|
function formatReactFailureSummary(modules) {
|
|
1063
945
|
const failures = modules.flatMap(
|
|
1064
946
|
(module2) => (module2.failures ?? []).map((failure) => ({
|
|
@@ -1073,11 +955,20 @@ function formatReactFailureSummary(modules) {
|
|
|
1073
955
|
lines.push(`TEST: ${failure.test}`);
|
|
1074
956
|
lines.push(`CAUSE_CODE: ${failure.causeCode ?? "TEST_FAILURE"}`);
|
|
1075
957
|
lines.push(`CAUSE: ${failure.cause}`);
|
|
958
|
+
appendFailureDetails(lines, failure);
|
|
1076
959
|
for (const [relatedIndex, related] of (failure.related ?? []).entries()) {
|
|
1077
960
|
lines.push(`RELATED_${relatedIndex + 1}_CODE: ${related.code}`);
|
|
1078
961
|
lines.push(
|
|
1079
|
-
`RELATED_${relatedIndex + 1}: ${
|
|
962
|
+
`RELATED_${relatedIndex + 1}: ${firstFailureLine(related.message) ?? related.message}`
|
|
1080
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
|
+
}
|
|
1081
972
|
}
|
|
1082
973
|
if (failure.trace) lines.push(`TRACE: ${failure.trace}`);
|
|
1083
974
|
if (failure.location) lines.push(`AT: ${failure.location}`);
|
|
@@ -1086,6 +977,282 @@ function formatReactFailureSummary(modules) {
|
|
|
1086
977
|
lines.push("TEST_RESULT: FAIL");
|
|
1087
978
|
return lines;
|
|
1088
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 import_oxc_parser = require("oxc-parser");
|
|
1051
|
+
var PRODUCTION_PLAYTHROUGH = "tests/production-playthrough.test.tsx";
|
|
1052
|
+
var PRODUCTION_APP = "src/App.tsx";
|
|
1053
|
+
var EXAMPLE_IMPORT_PREFIX = "@/game/example/";
|
|
1054
|
+
var REACT_RUNTIME_ENTRY = "miaoda-game-devkit/react";
|
|
1055
|
+
var REACT_TESTING_ENTRY = "miaoda-game-devkit/react/testing";
|
|
1056
|
+
var CLOCK_IMPORTS = /* @__PURE__ */ new Set(["browserGameClock"]);
|
|
1057
|
+
var CONTROLLER_IMPORTS = /* @__PURE__ */ new Set([
|
|
1058
|
+
"useGameController",
|
|
1059
|
+
"useOwnedGameController"
|
|
1060
|
+
]);
|
|
1061
|
+
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
|
|
1062
|
+
function runnableTestFiles(root, directory = (0, import_node_path2.join)(root, "tests")) {
|
|
1063
|
+
if (!(0, import_node_fs.existsSync)(directory)) return [];
|
|
1064
|
+
const files = [];
|
|
1065
|
+
for (const entry of (0, import_node_fs.readdirSync)(directory, { withFileTypes: true })) {
|
|
1066
|
+
const path = (0, import_node_path2.join)(directory, entry.name);
|
|
1067
|
+
if (entry.isDirectory()) {
|
|
1068
|
+
files.push(...runnableTestFiles(root, path));
|
|
1069
|
+
} else if (entry.isFile() && /\.test\.[cm]?[jt]sx?$/.test(entry.name)) {
|
|
1070
|
+
files.push(path);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
return files;
|
|
1074
|
+
}
|
|
1075
|
+
function extension(path) {
|
|
1076
|
+
const index = path.lastIndexOf(".");
|
|
1077
|
+
return index < 0 ? "" : path.slice(index);
|
|
1078
|
+
}
|
|
1079
|
+
function sourceFiles(root, directory = (0, import_node_path2.join)(root, "src")) {
|
|
1080
|
+
if (!(0, import_node_fs.existsSync)(directory)) return [];
|
|
1081
|
+
const files = [];
|
|
1082
|
+
for (const entry of (0, import_node_fs.readdirSync)(directory, { withFileTypes: true })) {
|
|
1083
|
+
const path = (0, import_node_path2.join)(directory, entry.name);
|
|
1084
|
+
const projectPath = (0, import_node_path2.relative)(root, path).replaceAll("\\", "/");
|
|
1085
|
+
if (entry.isDirectory()) {
|
|
1086
|
+
if (projectPath === "src/game/example") continue;
|
|
1087
|
+
files.push(...sourceFiles(root, path));
|
|
1088
|
+
} else if (entry.isFile() && SOURCE_EXTENSIONS.has(extension(entry.name))) {
|
|
1089
|
+
files.push(path);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
return files;
|
|
1093
|
+
}
|
|
1094
|
+
function parseSource(file, source) {
|
|
1095
|
+
return (0, import_oxc_parser.parseSync)(file, source, { sourceType: "module" });
|
|
1096
|
+
}
|
|
1097
|
+
function staticImports(file, source) {
|
|
1098
|
+
return parseSource(file, source).module.staticImports;
|
|
1099
|
+
}
|
|
1100
|
+
function collectDynamicImportSpecifiers(value, modules) {
|
|
1101
|
+
if (Array.isArray(value)) {
|
|
1102
|
+
for (const item of value) collectDynamicImportSpecifiers(item, modules);
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
if (!isRecord(value)) return;
|
|
1106
|
+
if (value.type === "ImportExpression" && isRecord(value.source) && value.source.type === "Literal" && typeof value.source.value === "string") {
|
|
1107
|
+
modules.push(value.source.value);
|
|
1108
|
+
}
|
|
1109
|
+
for (const child of Object.values(value)) {
|
|
1110
|
+
collectDynamicImportSpecifiers(child, modules);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
function importedModuleSpecifiers(file, source) {
|
|
1114
|
+
const parsed = parseSource(file, source);
|
|
1115
|
+
const modules = parsed.module.staticImports.map(
|
|
1116
|
+
({ moduleRequest }) => moduleRequest.value
|
|
1117
|
+
);
|
|
1118
|
+
collectDynamicImportSpecifiers(parsed.program, modules);
|
|
1119
|
+
return modules;
|
|
1120
|
+
}
|
|
1121
|
+
function productionFileImportsExample(file, projectRoot) {
|
|
1122
|
+
const exampleRoot = (0, import_node_path2.join)(projectRoot, "src/game/example");
|
|
1123
|
+
return importedModuleSpecifiers(file, (0, import_node_fs.readFileSync)(file, "utf8")).some(
|
|
1124
|
+
(moduleName) => {
|
|
1125
|
+
if (moduleName.startsWith(EXAMPLE_IMPORT_PREFIX)) return true;
|
|
1126
|
+
if (!moduleName.startsWith(".")) return false;
|
|
1127
|
+
const target = (0, import_node_path2.resolve)((0, import_node_path2.dirname)(file), moduleName);
|
|
1128
|
+
return target === exampleRoot || target.startsWith(`${exampleRoot}${import_node_path2.sep}`);
|
|
1129
|
+
}
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
function importsExampleAlias(file, source) {
|
|
1133
|
+
return importedModuleSpecifiers(file, source).some(
|
|
1134
|
+
(moduleName) => moduleName.startsWith(EXAMPLE_IMPORT_PREFIX)
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
function namedImports(file, source, moduleName) {
|
|
1138
|
+
const names = /* @__PURE__ */ new Set();
|
|
1139
|
+
for (const declaration of staticImports(file, source)) {
|
|
1140
|
+
if (declaration.moduleRequest.value !== moduleName) continue;
|
|
1141
|
+
for (const entry of declaration.entries) {
|
|
1142
|
+
if (entry.importName.kind === "Name" && entry.importName.name) {
|
|
1143
|
+
names.add(entry.importName.name);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return names;
|
|
1148
|
+
}
|
|
1149
|
+
function containsAny(values, expected) {
|
|
1150
|
+
return [...values].some((value) => expected.has(value));
|
|
1151
|
+
}
|
|
1152
|
+
function isRecord(value) {
|
|
1153
|
+
return typeof value === "object" && value !== null;
|
|
1154
|
+
}
|
|
1155
|
+
function containsObserveProperty(value) {
|
|
1156
|
+
if (Array.isArray(value)) return value.some(containsObserveProperty);
|
|
1157
|
+
if (!isRecord(value)) return false;
|
|
1158
|
+
if (value.type === "Property" && isRecord(value.key)) {
|
|
1159
|
+
if (value.key.type === "Identifier" && value.key.name === "observe") return true;
|
|
1160
|
+
if (value.key.type === "Literal" && value.key.value === "observe") return true;
|
|
1161
|
+
}
|
|
1162
|
+
return Object.values(value).some(containsObserveProperty);
|
|
1163
|
+
}
|
|
1164
|
+
function declaresObserve(file, source) {
|
|
1165
|
+
return containsObserveProperty(parseSource(file, source).program);
|
|
1166
|
+
}
|
|
1167
|
+
function auditReactAuthoritativePlaythrough(projectRoot) {
|
|
1168
|
+
const clockFiles = [];
|
|
1169
|
+
const controllerFiles = [];
|
|
1170
|
+
const productionFiles = sourceFiles(projectRoot);
|
|
1171
|
+
for (const file of productionFiles) {
|
|
1172
|
+
const imports = namedImports(
|
|
1173
|
+
file,
|
|
1174
|
+
(0, import_node_fs.readFileSync)(file, "utf8"),
|
|
1175
|
+
REACT_RUNTIME_ENTRY
|
|
1176
|
+
);
|
|
1177
|
+
const projectPath = (0, import_node_path2.relative)(projectRoot, file).replaceAll("\\", "/");
|
|
1178
|
+
if (containsAny(imports, CLOCK_IMPORTS)) clockFiles.push(projectPath);
|
|
1179
|
+
if (containsAny(imports, CONTROLLER_IMPORTS))
|
|
1180
|
+
controllerFiles.push(projectPath);
|
|
1181
|
+
}
|
|
1182
|
+
const appPath = (0, import_node_path2.join)(projectRoot, PRODUCTION_APP);
|
|
1183
|
+
const productionEntryExists = (0, import_node_fs.existsSync)(appPath);
|
|
1184
|
+
const productionUsesExample = productionFiles.some(
|
|
1185
|
+
(file) => productionFileImportsExample(file, projectRoot)
|
|
1186
|
+
);
|
|
1187
|
+
const staleExampleTestFiles = productionUsesExample ? [] : runnableTestFiles(projectRoot).filter(
|
|
1188
|
+
(file) => importsExampleAlias(file, (0, import_node_fs.readFileSync)(file, "utf8"))
|
|
1189
|
+
).map((file) => (0, import_node_path2.relative)(projectRoot, file).replaceAll("\\", "/"));
|
|
1190
|
+
const issues = [];
|
|
1191
|
+
if (staleExampleTestFiles.length > 0) {
|
|
1192
|
+
issues.push(
|
|
1193
|
+
`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.`
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
if (clockFiles.length === 0 && controllerFiles.length === 0) {
|
|
1197
|
+
return {
|
|
1198
|
+
ok: issues.length === 0,
|
|
1199
|
+
issues,
|
|
1200
|
+
clockFiles,
|
|
1201
|
+
controllerFiles,
|
|
1202
|
+
productionEntryExists,
|
|
1203
|
+
productionUsesExample,
|
|
1204
|
+
staleExampleTestFiles
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
const testPath = (0, import_node_path2.join)(projectRoot, PRODUCTION_PLAYTHROUGH);
|
|
1208
|
+
const testSource = (0, import_node_fs.existsSync)(testPath) ? (0, import_node_fs.readFileSync)(testPath, "utf8") : "";
|
|
1209
|
+
const testingImports = namedImports(
|
|
1210
|
+
testPath,
|
|
1211
|
+
testSource,
|
|
1212
|
+
REACT_TESTING_ENTRY
|
|
1213
|
+
);
|
|
1214
|
+
const hasObserve = declaresObserve(testPath, testSource);
|
|
1215
|
+
if (!hasObserve) {
|
|
1216
|
+
issues.push(
|
|
1217
|
+
`${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.`
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
if (clockFiles.length > 0 && !testingImports.has("ManualGameClock")) {
|
|
1221
|
+
issues.push(
|
|
1222
|
+
`${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().`
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
return {
|
|
1226
|
+
ok: issues.length === 0,
|
|
1227
|
+
issues,
|
|
1228
|
+
clockFiles,
|
|
1229
|
+
controllerFiles,
|
|
1230
|
+
productionEntryExists,
|
|
1231
|
+
productionUsesExample,
|
|
1232
|
+
staleExampleTestFiles
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// src/react/react-playthrough-policy.ts
|
|
1237
|
+
function assessProductTestAlignment(projectRoot) {
|
|
1238
|
+
const audit = auditReactAuthoritativePlaythrough(projectRoot);
|
|
1239
|
+
if (audit.staleExampleTestFiles.length > 0) {
|
|
1240
|
+
return { status: "FAILED", cause: audit.issues[0] };
|
|
1241
|
+
}
|
|
1242
|
+
if (!audit.productionEntryExists) {
|
|
1243
|
+
return {
|
|
1244
|
+
status: "NOT_VERIFIED",
|
|
1245
|
+
cause: "The production src/App.tsx entry does not exist, so product-test alignment could not be verified."
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
if (audit.productionUsesExample) {
|
|
1249
|
+
return {
|
|
1250
|
+
status: "NOT_VERIFIED",
|
|
1251
|
+
cause: "The production App still uses the replaceable src/game/example teaching game, so replacement-game test alignment is not applicable yet."
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
return { status: "PASS" };
|
|
1255
|
+
}
|
|
1089
1256
|
function repairGuidance(code) {
|
|
1090
1257
|
switch (code) {
|
|
1091
1258
|
case "GAME_SNAPSHOT_REFERENCE_REUSED":
|
|
@@ -1100,8 +1267,16 @@ function repairGuidance(code) {
|
|
|
1100
1267
|
return "The stage ran and asserted, but its observable state matched the previous milestone. Preserve the intended gameplay result and observe the same production Controller rendered by <App />. Do not substitute arbitrary labels or a weaker state change.";
|
|
1101
1268
|
case "PLAYTHROUGH_BOUND_EXHAUSTED":
|
|
1102
1269
|
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.";
|
|
1270
|
+
case "PLAYTHROUGH_OUTCOME_NOT_REACHED":
|
|
1271
|
+
return "The stage action completed, but its until condition never became true. Inspect TRACE and Last diagnostics, then confirm that the production DOM input reaches the rendered App, until describes the result caused by this stage, and observe reads the matching authoritative state when used. Add a deterministic step only if the gameplay is actually driven by time or frames; do not add a no-op step or weaken the intended outcome.";
|
|
1103
1272
|
case "PLAYTHROUGH_CLOCK_NOT_ADVANCED":
|
|
1104
1273
|
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.";
|
|
1274
|
+
case "EXPECTATION_MISMATCH":
|
|
1275
|
+
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.";
|
|
1276
|
+
case "PRODUCT_RUNTIME_TYPE_ERROR":
|
|
1277
|
+
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.";
|
|
1278
|
+
case "TEST_API_MISMATCH":
|
|
1279
|
+
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
1280
|
case "INVALID_STAGE_ORDER":
|
|
1106
1281
|
case "INCOMPLETE_PLAYTHROUGH_EVIDENCE":
|
|
1107
1282
|
case "INVALID_STAGE_NAME":
|
|
@@ -1187,36 +1362,14 @@ function assessReactPlaythroughReport(input) {
|
|
|
1187
1362
|
}
|
|
1188
1363
|
return { ...base, status: "PASS", failsRun: false };
|
|
1189
1364
|
}
|
|
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
|
-
}
|
|
1365
|
+
|
|
1366
|
+
// src/react/react-playthrough-reporter.ts
|
|
1367
|
+
var PRODUCTION_PLAYTHROUGH_FILE = "tests/production-playthrough.test.tsx";
|
|
1215
1368
|
var ReactPlaythroughReporter = class {
|
|
1216
1369
|
/** 绑定模板根目录,以稳定识别生产可玩性测试而不依赖测试名称。 */
|
|
1217
1370
|
constructor(projectRoot) {
|
|
1218
1371
|
this.projectRoot = projectRoot;
|
|
1219
|
-
this.expectedModuleId = (0,
|
|
1372
|
+
this.expectedModuleId = (0, import_node_path3.resolve)(projectRoot, this.expectedFile);
|
|
1220
1373
|
}
|
|
1221
1374
|
projectRoot;
|
|
1222
1375
|
expectedFile = PRODUCTION_PLAYTHROUGH_FILE;
|
|
@@ -1226,7 +1379,7 @@ var ReactPlaythroughReporter = class {
|
|
|
1226
1379
|
/** 记录本轮是否实际选择了生产流程文件,用于区分聚焦运行与门禁失败。 */
|
|
1227
1380
|
onTestRunStart(specifications) {
|
|
1228
1381
|
this.expectedFileScheduled = specifications.some(
|
|
1229
|
-
(specification) => (0,
|
|
1382
|
+
(specification) => (0, import_node_path3.resolve)(specification.moduleId) === this.expectedModuleId
|
|
1230
1383
|
);
|
|
1231
1384
|
this.focusedSelection = specifications.some(
|
|
1232
1385
|
(specification) => Boolean(specification.project.globalConfig.testNamePattern) || Boolean(specification.testNamePattern) || Boolean(specification.testLines?.length)
|
|
@@ -1238,16 +1391,17 @@ var ReactPlaythroughReporter = class {
|
|
|
1238
1391
|
testModules,
|
|
1239
1392
|
this.projectRoot
|
|
1240
1393
|
);
|
|
1394
|
+
const moduleResults = testModules.map(
|
|
1395
|
+
(module2) => toReactPlaythroughModuleResult(module2, this.projectRoot)
|
|
1396
|
+
);
|
|
1241
1397
|
const report = assessReactPlaythroughReport({
|
|
1242
1398
|
expectedFile: this.expectedFile,
|
|
1243
1399
|
expectedFileExists: (0, import_node_fs2.existsSync)(this.expectedModuleId),
|
|
1244
1400
|
expectedFileScheduled: this.expectedFileScheduled,
|
|
1245
1401
|
focusedSelection: this.focusedSelection,
|
|
1246
|
-
modules:
|
|
1247
|
-
(module2) => toModuleResult(module2, this.projectRoot)
|
|
1248
|
-
),
|
|
1402
|
+
modules: moduleResults,
|
|
1249
1403
|
unhandledErrors: extractFailureEntries(unhandledErrors).map(
|
|
1250
|
-
(entry) =>
|
|
1404
|
+
(entry) => firstFailureLine(entry.message) ?? entry.message
|
|
1251
1405
|
)
|
|
1252
1406
|
});
|
|
1253
1407
|
const alignment = assessProductTestAlignment(this.projectRoot);
|
|
@@ -1265,14 +1419,14 @@ var ReactPlaythroughReporter = class {
|
|
|
1265
1419
|
console.error(pendingOutput);
|
|
1266
1420
|
process.exitCode = 1;
|
|
1267
1421
|
}
|
|
1268
|
-
const summary = formatReactFailureSummary(
|
|
1269
|
-
testModules.map((module2) => toModuleResult(module2, this.projectRoot))
|
|
1270
|
-
);
|
|
1422
|
+
const summary = formatReactFailureSummary(moduleResults);
|
|
1271
1423
|
if ((report.failsRun || alignment.status === "FAILED" || pendingProductTests.length > 0) && summary[0] === "TEST_RESULT: PASS") {
|
|
1272
1424
|
summary[0] = "TEST_RESULT: FAIL";
|
|
1273
1425
|
}
|
|
1274
|
-
console.log(
|
|
1275
|
-
|
|
1426
|
+
console.log(
|
|
1427
|
+
`
|
|
1428
|
+
${[...formatReactTestCountSummary(moduleResults), ...summary].join("\n")}`
|
|
1429
|
+
);
|
|
1276
1430
|
}
|
|
1277
1431
|
};
|
|
1278
1432
|
|
|
@@ -1284,13 +1438,13 @@ function getJSDOMWorkerExecArgv() {
|
|
|
1284
1438
|
|
|
1285
1439
|
// src/react-vitest-config.ts
|
|
1286
1440
|
function resolvePhaser3BrowserEntry(projectRoot) {
|
|
1287
|
-
const manifestPath = (0,
|
|
1441
|
+
const manifestPath = (0, import_node_path4.resolve)(projectRoot, "node_modules/phaser/package.json");
|
|
1288
1442
|
if (!(0, import_node_fs3.existsSync)(manifestPath)) return void 0;
|
|
1289
1443
|
try {
|
|
1290
1444
|
const manifest = JSON.parse((0, import_node_fs3.readFileSync)(manifestPath, "utf8"));
|
|
1291
1445
|
if (!manifest.version?.startsWith("3.")) return void 0;
|
|
1292
|
-
const browserEntry = (0,
|
|
1293
|
-
(0,
|
|
1446
|
+
const browserEntry = (0, import_node_path4.resolve)(
|
|
1447
|
+
(0, import_node_path4.dirname)(manifestPath),
|
|
1294
1448
|
manifest.browser ?? "dist/phaser.js"
|
|
1295
1449
|
);
|
|
1296
1450
|
return (0, import_node_fs3.existsSync)(browserEntry) ? browserEntry : void 0;
|
|
@@ -1308,7 +1462,7 @@ function defineReactGameVitestConfig(options) {
|
|
|
1308
1462
|
alias: {
|
|
1309
1463
|
...phaser3BrowserEntry ? { phaser: phaser3BrowserEntry } : {},
|
|
1310
1464
|
...options.aliases,
|
|
1311
|
-
"@": (0,
|
|
1465
|
+
"@": (0, import_node_path4.resolve)(options.projectRoot, "src")
|
|
1312
1466
|
}
|
|
1313
1467
|
},
|
|
1314
1468
|
test: {
|
|
@@ -1332,8 +1486,11 @@ function defineReactGameVitestConfig(options) {
|
|
|
1332
1486
|
sequence: {
|
|
1333
1487
|
setupFiles: "list"
|
|
1334
1488
|
},
|
|
1335
|
-
|
|
1336
|
-
|
|
1489
|
+
...options.enablePlaythroughReporter ? {
|
|
1490
|
+
// The structured reporter remains available to existing Devkit consumers,
|
|
1491
|
+
// but is no longer a default requirement for generated React games.
|
|
1492
|
+
reporters: [new ReactPlaythroughReporter(options.projectRoot)]
|
|
1493
|
+
} : {},
|
|
1337
1494
|
restoreMocks: true,
|
|
1338
1495
|
clearMocks: true,
|
|
1339
1496
|
testTimeout: options.testTimeout,
|