miaoda-game-devkit 0.2.12 → 0.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -8
- package/bin/miaoda-phaser-game-lint.js +3 -0
- package/bin/miaoda-react-game-lint.js +3 -0
- package/dist/cli/phaser-lint.js +163 -0
- package/dist/cli/react-lint.js +163 -0
- package/dist/lint/setup.mjs +23 -41
- package/dist/react/vitest-config.js +34 -1
- package/dist/react/vitest-config.mjs +35 -2
- package/dist/react/vitest-setup.js +72 -1
- package/dist/react/vitest-setup.mjs +72 -1
- package/dist/vitest-config.js +16 -1
- package/dist/vitest-config.mjs +16 -1
- package/package.json +7 -4
- package/bin/miaoda-game-lint.js +0 -3
- package/dist/cli/lint.js +0 -127
- package/dist/lint/contracts.config.mjs +0 -38
- package/dist/lint/game-telemetry.contract.mjs +0 -1402
- package/dist/lint/gameplay-audit.contract.mjs +0 -909
- package/dist/lint/gameplay-contract.contract.mjs +0 -1408
- package/dist/lint/phaser-headless.contract.mjs +0 -1856
- package/dist/lint/phaser-text-assertions.contract.mjs +0 -32
- package/dist/lint/resource-import-plugin.contract.mjs +0 -97
- package/dist/lint/vitest-config.contract.mjs +0 -1028
|
@@ -1,1028 +0,0 @@
|
|
|
1
|
-
// src/lint/vitest-config.test.ts
|
|
2
|
-
import { resolve as resolve4 } from "path";
|
|
3
|
-
import { describe, expect, it } from "vitest";
|
|
4
|
-
|
|
5
|
-
// src/react/manual-game-clock.ts
|
|
6
|
-
var ManualGameClock = class {
|
|
7
|
-
time = 0;
|
|
8
|
-
nextId = 1;
|
|
9
|
-
frames = /* @__PURE__ */ new Map();
|
|
10
|
-
timers = /* @__PURE__ */ new Map();
|
|
11
|
-
now() {
|
|
12
|
-
return this.time;
|
|
13
|
-
}
|
|
14
|
-
requestFrame(callback) {
|
|
15
|
-
const id = this.nextId++;
|
|
16
|
-
this.frames.set(id, callback);
|
|
17
|
-
return id;
|
|
18
|
-
}
|
|
19
|
-
cancelFrame(id) {
|
|
20
|
-
this.frames.delete(id);
|
|
21
|
-
}
|
|
22
|
-
setTimeout(callback, delayMs) {
|
|
23
|
-
const id = this.nextId++;
|
|
24
|
-
this.timers.set(id, { callback, dueAt: this.time + delayMs });
|
|
25
|
-
return id;
|
|
26
|
-
}
|
|
27
|
-
clearTimeout(id) {
|
|
28
|
-
this.timers.delete(id);
|
|
29
|
-
}
|
|
30
|
-
advanceBy(deltaMs) {
|
|
31
|
-
if (!Number.isFinite(deltaMs) || deltaMs < 0) {
|
|
32
|
-
throw new RangeError("deltaMs must be a non-negative finite number");
|
|
33
|
-
}
|
|
34
|
-
this.time += deltaMs;
|
|
35
|
-
const dueTimers = [...this.timers.entries()].filter(([, timer]) => timer.dueAt <= this.time).sort((left, right) => left[1].dueAt - right[1].dueAt);
|
|
36
|
-
for (const [id, timer] of dueTimers) {
|
|
37
|
-
if (timer.dueAt <= this.time && this.timers.delete(id)) timer.callback();
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
stepFrame(deltaMs = 16) {
|
|
41
|
-
this.advanceBy(deltaMs);
|
|
42
|
-
const callbacks = [...this.frames.values()];
|
|
43
|
-
this.frames.clear();
|
|
44
|
-
for (const callback of callbacks) callback(this.time);
|
|
45
|
-
}
|
|
46
|
-
stepFrames(count, deltaMs = 16) {
|
|
47
|
-
if (!Number.isInteger(count) || count < 0) {
|
|
48
|
-
throw new RangeError("count must be a non-negative integer");
|
|
49
|
-
}
|
|
50
|
-
for (let index = 0; index < count; index += 1) this.stepFrame(deltaMs);
|
|
51
|
-
}
|
|
52
|
-
pendingFrameCount() {
|
|
53
|
-
return this.frames.size;
|
|
54
|
-
}
|
|
55
|
-
pendingTimerCount() {
|
|
56
|
-
return this.timers.size;
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
// src/react-vitest-config.ts
|
|
61
|
-
import { resolve } from "path";
|
|
62
|
-
import { defineConfig } from "vitest/config";
|
|
63
|
-
function defineReactGameVitestConfig(options) {
|
|
64
|
-
return defineConfig({
|
|
65
|
-
resolve: {
|
|
66
|
-
alias: { ...options.aliases, "@": resolve(options.projectRoot, "src") }
|
|
67
|
-
},
|
|
68
|
-
test: {
|
|
69
|
-
include: [
|
|
70
|
-
"src/**/*.{test,spec}.{ts,tsx}",
|
|
71
|
-
"tests/**/*.{test,spec}.{ts,tsx}"
|
|
72
|
-
],
|
|
73
|
-
environment: "jsdom",
|
|
74
|
-
environmentOptions: {
|
|
75
|
-
jsdom: { url: "http://localhost/", pretendToBeVisual: true }
|
|
76
|
-
},
|
|
77
|
-
setupFiles: [
|
|
78
|
-
"miaoda-game-devkit/react/vitest-setup",
|
|
79
|
-
...options.additionalSetupFiles ?? []
|
|
80
|
-
],
|
|
81
|
-
reporters: ["minimal"],
|
|
82
|
-
restoreMocks: true,
|
|
83
|
-
clearMocks: true,
|
|
84
|
-
testTimeout: options.testTimeout,
|
|
85
|
-
hookTimeout: options.hookTimeout
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// src/vite.ts
|
|
91
|
-
import { existsSync } from "fs";
|
|
92
|
-
import { createRequire } from "module";
|
|
93
|
-
import { dirname, join, normalize, resolve as resolve2, sep } from "path";
|
|
94
|
-
var PHASER_MODULE_ID = "phaser";
|
|
95
|
-
var REXUI_OPTIMIZE_ENTRIES = [
|
|
96
|
-
"phaser4-rex-plugins/templates/ui/ui-plugin.js"
|
|
97
|
-
];
|
|
98
|
-
function isPromiseLike(value) {
|
|
99
|
-
return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
|
|
100
|
-
}
|
|
101
|
-
function resolvePhaserFacade(projectRoot2) {
|
|
102
|
-
const requireFromProject = createRequire(join(projectRoot2, "package.json"));
|
|
103
|
-
const viteEntry = requireFromProject.resolve("miaoda-game-devkit/vite");
|
|
104
|
-
const facade = join(dirname(viteEntry), "phaser-facade.mjs");
|
|
105
|
-
if (!existsSync(facade)) {
|
|
106
|
-
throw new Error(
|
|
107
|
-
`miaoda-game-devkit \u7F3A\u5C11 Phaser facade\uFF1A${facade}`
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
return facade;
|
|
111
|
-
}
|
|
112
|
-
function isInsideDirectory(filePath, directory) {
|
|
113
|
-
const normalizedFile = normalize(filePath);
|
|
114
|
-
const normalizedDirectory = `${normalize(directory)}${sep}`;
|
|
115
|
-
return normalizedFile.startsWith(normalizedDirectory);
|
|
116
|
-
}
|
|
117
|
-
function phaserRexUIScenePlugin(projectRoot2) {
|
|
118
|
-
let scenesRoot = projectRoot2 ? resolve2(projectRoot2, "src/scenes") : void 0;
|
|
119
|
-
let phaserFacade = projectRoot2 ? resolvePhaserFacade(projectRoot2) : void 0;
|
|
120
|
-
return {
|
|
121
|
-
name: "miaoda-phaser-rexui-scene",
|
|
122
|
-
enforce: "pre",
|
|
123
|
-
configResolved(config) {
|
|
124
|
-
const resolvedProjectRoot = projectRoot2 ?? config.root;
|
|
125
|
-
scenesRoot ??= resolve2(resolvedProjectRoot, "src/scenes");
|
|
126
|
-
phaserFacade ??= resolvePhaserFacade(resolvedProjectRoot);
|
|
127
|
-
},
|
|
128
|
-
/** 查询参数属于构建工具元数据,去除后才能稳定判断真实源码位置。 */
|
|
129
|
-
resolveId(source, importer) {
|
|
130
|
-
if (source !== PHASER_MODULE_ID || !importer || !scenesRoot) {
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
const cleanImporter = importer.split("?", 1)[0] ?? importer;
|
|
134
|
-
return isInsideDirectory(cleanImporter, scenesRoot) ? phaserFacade ?? null : null;
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
function withGameDefaults(config) {
|
|
139
|
-
const optimizeDeps = config.optimizeDeps ?? {};
|
|
140
|
-
return {
|
|
141
|
-
...config,
|
|
142
|
-
optimizeDeps: {
|
|
143
|
-
...optimizeDeps,
|
|
144
|
-
include: [
|
|
145
|
-
.../* @__PURE__ */ new Set([
|
|
146
|
-
...optimizeDeps.include ?? [],
|
|
147
|
-
...REXUI_OPTIMIZE_ENTRIES
|
|
148
|
-
])
|
|
149
|
-
]
|
|
150
|
-
},
|
|
151
|
-
plugins: [
|
|
152
|
-
phaserRexUIScenePlugin(),
|
|
153
|
-
...config.plugins ?? []
|
|
154
|
-
]
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
function defineGameViteConfig(config) {
|
|
158
|
-
if (typeof config === "function") {
|
|
159
|
-
return (env) => {
|
|
160
|
-
const resolved = config(env);
|
|
161
|
-
return isPromiseLike(resolved) ? resolved.then(withGameDefaults) : withGameDefaults(resolved);
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
return isPromiseLike(config) ? config.then(withGameDefaults) : withGameDefaults(config);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// src/vitest-config.ts
|
|
168
|
-
import { resolve as resolve3 } from "path";
|
|
169
|
-
import { defineConfig as defineConfig2 } from "vitest/config";
|
|
170
|
-
|
|
171
|
-
// src/gameplay-audit.ts
|
|
172
|
-
import { readdirSync, readFileSync, statSync } from "fs";
|
|
173
|
-
import { join as join2 } from "path";
|
|
174
|
-
function normalizeList(value) {
|
|
175
|
-
if (value === void 0) return [];
|
|
176
|
-
const values = typeof value === "string" ? [value] : value;
|
|
177
|
-
return [...new Set(values.map((item) => item.trim()).filter(Boolean))];
|
|
178
|
-
}
|
|
179
|
-
function normalizeSet(values) {
|
|
180
|
-
return [
|
|
181
|
-
...new Set(values.map((value) => value.trim()).filter(Boolean))
|
|
182
|
-
].sort();
|
|
183
|
-
}
|
|
184
|
-
function sameSet(left, right) {
|
|
185
|
-
return JSON.stringify(normalizeSet(left)) === JSON.stringify(normalizeSet(right));
|
|
186
|
-
}
|
|
187
|
-
function normalizeGameplayRequirements(requirements, scenes) {
|
|
188
|
-
const requiredScenes = normalizeSet([
|
|
189
|
-
...scenes,
|
|
190
|
-
...normalizeList(requirements.requireScene)
|
|
191
|
-
]);
|
|
192
|
-
return {
|
|
193
|
-
requireInput: requirements.requireInput || void 0,
|
|
194
|
-
requireFrameAdvance: requirements.requireFrameAdvance || void 0,
|
|
195
|
-
requirePhysicsStep: requirements.requirePhysicsStep || void 0,
|
|
196
|
-
requireTransition: normalizeList(requirements.requireTransition),
|
|
197
|
-
requireScene: requiredScenes,
|
|
198
|
-
requireRestart: normalizeList(requirements.requireRestart),
|
|
199
|
-
requireCheckpoint: normalizeList(requirements.requireCheckpoint),
|
|
200
|
-
requireDestroy: true
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
function createGameplayContractMetadata(contract) {
|
|
204
|
-
const scenes = normalizeSet(contract.scenes);
|
|
205
|
-
return {
|
|
206
|
-
id: contract.id.trim(),
|
|
207
|
-
kind: contract.kind,
|
|
208
|
-
scenes,
|
|
209
|
-
requirements: normalizeGameplayRequirements(contract, scenes),
|
|
210
|
-
verified: false
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
function validateGameplayAuditOptions(options, projectRoot2) {
|
|
214
|
-
const issues = [];
|
|
215
|
-
const contracts = options.contracts;
|
|
216
|
-
const scenes = [...new Set(options.scenes.map((scene) => scene.trim()).filter(Boolean))];
|
|
217
|
-
const mode = options.mode ?? "interactive";
|
|
218
|
-
if (mode === "non-interactive") {
|
|
219
|
-
const justification = options.nonInteractiveJustification?.trim() ?? "";
|
|
220
|
-
if (justification.length < 20) {
|
|
221
|
-
issues.push(
|
|
222
|
-
"non-interactive \u6A21\u5F0F\u5FC5\u987B\u63D0\u4F9B\u81F3\u5C11 20 \u4E2A\u5B57\u7B26\u7684 nonInteractiveJustification\u3002"
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
if (mode !== "interactive" && mode !== "non-interactive") {
|
|
227
|
-
issues.push("gameplayAudit.mode \u5FC5\u987B\u662F interactive \u6216 non-interactive\u3002");
|
|
228
|
-
}
|
|
229
|
-
if (scenes.length === 0) issues.push("gameplayAudit.scenes \u4E0D\u80FD\u4E3A\u7A7A\u3002");
|
|
230
|
-
if (scenes.length !== options.scenes.length) {
|
|
231
|
-
issues.push("gameplayAudit.scenes \u4E0D\u80FD\u5305\u542B\u7A7A\u503C\u6216\u91CD\u590D Scene key\u3002");
|
|
232
|
-
}
|
|
233
|
-
const contractIds = /* @__PURE__ */ new Set();
|
|
234
|
-
const coveredScenes = /* @__PURE__ */ new Set();
|
|
235
|
-
const kinds = /* @__PURE__ */ new Map();
|
|
236
|
-
const invalidKindContracts = [];
|
|
237
|
-
for (const contract of contracts ?? []) {
|
|
238
|
-
const id = contract.id.trim();
|
|
239
|
-
if (!id) {
|
|
240
|
-
issues.push("gameplayAudit.contracts \u4E2D\u5B58\u5728\u7A7A contract id\u3002");
|
|
241
|
-
} else if (id !== contract.id) {
|
|
242
|
-
issues.push(`contract id "${contract.id}" \u4E0D\u80FD\u5305\u542B\u9996\u5C3E\u7A7A\u767D\u3002`);
|
|
243
|
-
} else if (contractIds.has(id)) {
|
|
244
|
-
issues.push(`gameplayAudit.contracts \u5305\u542B\u91CD\u590D contract id\uFF1A${id}\u3002`);
|
|
245
|
-
} else {
|
|
246
|
-
contractIds.add(id);
|
|
247
|
-
}
|
|
248
|
-
const testFile = contract.testFile.replaceAll("\\", "/");
|
|
249
|
-
if (!/^tests\/.+\.test\.ts$/.test(testFile) || testFile.split("/").includes("..")) {
|
|
250
|
-
issues.push(
|
|
251
|
-
`contract "${id || "<empty>"}" \u7684 testFile \u5FC5\u987B\u5339\u914D tests/**/*.test.ts\u3002`
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
const contractScenes = normalizeSet(contract.scenes);
|
|
255
|
-
if (!["boot", "playthrough", "transition", "recovery"].includes(contract.kind)) {
|
|
256
|
-
invalidKindContracts.push(id || "<empty>");
|
|
257
|
-
} else {
|
|
258
|
-
const list = kinds.get(contract.kind) ?? [];
|
|
259
|
-
list.push(contract);
|
|
260
|
-
kinds.set(contract.kind, list);
|
|
261
|
-
}
|
|
262
|
-
if (contract.kind === "playthrough") {
|
|
263
|
-
if (!contract.requireFrameAdvance) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
|
|
264
|
-
if (normalizeList(contract.requireCheckpoint).length === 0) issues.push(`playthrough contract "${id}" \u5FC5\u987B\u58F0\u660E requireCheckpoint\u3002`);
|
|
265
|
-
}
|
|
266
|
-
if (contract.kind === "transition" && normalizeList(contract.requireTransition).length === 0) {
|
|
267
|
-
issues.push(`transition contract "${id}" \u5FC5\u987B\u58F0\u660E requireTransition\u3002`);
|
|
268
|
-
}
|
|
269
|
-
if (contract.kind === "recovery") {
|
|
270
|
-
if (!contract.requireFrameAdvance) issues.push(`recovery contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
|
|
271
|
-
if (normalizeList(contract.requireRestart).length === 0) issues.push(`recovery contract "${id}" \u5FC5\u987B\u58F0\u660E requireRestart\u3002`);
|
|
272
|
-
if (normalizeList(contract.requireCheckpoint).length < 2) issues.push(`recovery contract "${id}" \u81F3\u5C11\u9700\u8981\u4E24\u4E2A checkpoint\uFF08\u91CD\u7F6E\u524D\u540E\uFF09\u3002`);
|
|
273
|
-
}
|
|
274
|
-
if (contractScenes.length === 0) {
|
|
275
|
-
issues.push(`contract "${id || "<empty>"}" \u5FC5\u987B\u58F0\u660E\u81F3\u5C11\u4E00\u4E2A Scene\u3002`);
|
|
276
|
-
}
|
|
277
|
-
for (const scene of contractScenes) {
|
|
278
|
-
coveredScenes.add(scene);
|
|
279
|
-
if (!scenes.includes(scene)) {
|
|
280
|
-
issues.push(
|
|
281
|
-
`contract "${id}" \u5F15\u7528\u4E86\u672A\u5217\u5165 gameplayAudit.scenes \u7684 Scene\uFF1A${scene}\u3002`
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
if (invalidKindContracts.length > 0) {
|
|
287
|
-
issues.push(
|
|
288
|
-
`\u4EE5\u4E0B contract \u7F3A\u5C11\u6709\u6548 kind\uFF1A${invalidKindContracts.map((id) => `"${id}"`).join("\u3001")}\u3002\u8BF7\u4E3A\u6BCF\u4E2A contract \u8BBE\u7F6E kind: "boot"\u3001"playthrough"\u3001"transition" \u6216 "recovery"\u3002`
|
|
289
|
-
);
|
|
290
|
-
}
|
|
291
|
-
if (contracts) {
|
|
292
|
-
for (const scene of scenes) {
|
|
293
|
-
if (!coveredScenes.has(scene)) {
|
|
294
|
-
issues.push(`Scene "${scene}" \u6CA1\u6709\u4EFB\u4F55 gameplay contract \u8D1F\u8D23\u9A8C\u8BC1\u3002`);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
if (contracts.length === 0)
|
|
298
|
-
issues.push("gameplayAudit.contracts \u5B58\u5728\u65F6\u4E0D\u80FD\u4E3A\u7A7A\uFF1B\u4E0D\u4F7F\u7528\u663E\u5F0F\u6E05\u5355\u8BF7\u7701\u7565\u8BE5\u5B57\u6BB5\u3002");
|
|
299
|
-
}
|
|
300
|
-
if (mode === "interactive" && contracts) {
|
|
301
|
-
const hasPlaythrough = (kinds.get("playthrough") ?? []).some(
|
|
302
|
-
(contract) => Boolean(contract.requireFrameAdvance) && normalizeList(contract.requireCheckpoint).length > 0
|
|
303
|
-
);
|
|
304
|
-
const hasInput = contracts.some((contract) => contract.requireInput);
|
|
305
|
-
if (!hasPlaythrough || !hasInput) {
|
|
306
|
-
issues.push(
|
|
307
|
-
"\u4EA4\u4E92\u9879\u76EE\u7F3A\u5C11\u6700\u4F4E\u73A9\u6CD5\u57FA\u7EBF\uFF1A\u81F3\u5C11\u4E00\u4E2A playthrough contract \u5FC5\u987B\u58F0\u660E requireFrameAdvance \u548C checkpoint\uFF0C\u4E14\u81F3\u5C11\u4E00\u4E2A contract \u5FC5\u987B\u58F0\u660E requireInput: true\u3002"
|
|
308
|
-
);
|
|
309
|
-
}
|
|
310
|
-
const restartSignal = projectRoot2 ? containsProductionRestart(projectRoot2) : false;
|
|
311
|
-
if (restartSignal && options.flows?.restart !== true) {
|
|
312
|
-
issues.push("\u68C0\u6D4B\u5230\u751F\u4EA7 Scene \u4F7F\u7528 scene.restart()\uFF1B\u5FC5\u987B\u58F0\u660E flows.restart: true \u5E76\u63D0\u4F9B kind: recovery contract\u3002");
|
|
313
|
-
} else if (restartSignal && (kinds.get("recovery") ?? []).length === 0) {
|
|
314
|
-
issues.push("\u68C0\u6D4B\u5230\u751F\u4EA7 Scene \u4F7F\u7528 scene.restart()\uFF0C\u4F46\u7F3A\u5C11 kind: recovery contract\u3002");
|
|
315
|
-
} else if ((options.flows?.restart || options.flows?.reset) && (kinds.get("recovery") ?? []).length === 0) {
|
|
316
|
-
issues.push("\u9879\u76EE\u58F0\u660E\u6216\u5B9E\u73B0\u4E86 restart/reset \u6D41\u7A0B\uFF0C\u4F46\u7F3A\u5C11 kind: recovery contract\u3002");
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
return issues;
|
|
320
|
-
}
|
|
321
|
-
function containsProductionRestart(projectRoot2) {
|
|
322
|
-
const sceneRoot = join2(projectRoot2, "src", "scenes");
|
|
323
|
-
const visit = (directory) => {
|
|
324
|
-
let entries;
|
|
325
|
-
try {
|
|
326
|
-
entries = readdirSync(directory);
|
|
327
|
-
} catch {
|
|
328
|
-
return false;
|
|
329
|
-
}
|
|
330
|
-
for (const entry of entries) {
|
|
331
|
-
const path = join2(directory, entry);
|
|
332
|
-
let stats;
|
|
333
|
-
try {
|
|
334
|
-
stats = statSync(path);
|
|
335
|
-
} catch {
|
|
336
|
-
continue;
|
|
337
|
-
}
|
|
338
|
-
if (stats.isDirectory() && visit(path)) return true;
|
|
339
|
-
if (!stats.isFile() || !/\.(ts|tsx)$/.test(entry)) continue;
|
|
340
|
-
try {
|
|
341
|
-
const source = readFileSync(path, "utf8").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
|
|
342
|
-
if (/\bscene\s*\.\s*restart\s*\(/.test(source)) return true;
|
|
343
|
-
} catch {
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
return false;
|
|
347
|
-
};
|
|
348
|
-
return visit(sceneRoot);
|
|
349
|
-
}
|
|
350
|
-
function getDefinitionIssues(expected, actual) {
|
|
351
|
-
const issues = [];
|
|
352
|
-
const expectedMetadata = createGameplayContractMetadata(expected);
|
|
353
|
-
if (expectedMetadata.kind !== actual.kind) {
|
|
354
|
-
issues.push(`contract "${expected.id}" \u7684 kind \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`);
|
|
355
|
-
}
|
|
356
|
-
if (!sameSet(expectedMetadata.scenes, actual.scenes)) {
|
|
357
|
-
issues.push(
|
|
358
|
-
`contract "${expected.id}" \u58F0\u660E\u7684 Scene \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`
|
|
359
|
-
);
|
|
360
|
-
}
|
|
361
|
-
if (JSON.stringify(expectedMetadata.requirements) !== JSON.stringify(actual.requirements)) {
|
|
362
|
-
issues.push(
|
|
363
|
-
`contract "${expected.id}" \u7684 evidence requirements \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`
|
|
364
|
-
);
|
|
365
|
-
}
|
|
366
|
-
return issues;
|
|
367
|
-
}
|
|
368
|
-
function auditGameplayCollection(options, file, tests) {
|
|
369
|
-
const normalizedFile = file.replaceAll("\\", "/");
|
|
370
|
-
const expected = (options.contracts ?? []).filter(
|
|
371
|
-
(contract) => contract.testFile.replaceAll("\\", "/") === normalizedFile
|
|
372
|
-
);
|
|
373
|
-
const actual = tests.filter((test) => test.metadata);
|
|
374
|
-
const issues = [];
|
|
375
|
-
if (!options.contracts) {
|
|
376
|
-
const actualIds = /* @__PURE__ */ new Set();
|
|
377
|
-
for (const test of actual) {
|
|
378
|
-
const id = test.metadata?.id;
|
|
379
|
-
if (!id) continue;
|
|
380
|
-
if (actualIds.has(id)) issues.push(`contract "${id}" \u5728 ${normalizedFile} \u4E2D\u91CD\u590D\u5B9A\u4E49\u3002`);
|
|
381
|
-
actualIds.add(id);
|
|
382
|
-
}
|
|
383
|
-
return { passed: issues.length === 0, issues };
|
|
384
|
-
}
|
|
385
|
-
for (const contract of expected) {
|
|
386
|
-
const matches = actual.filter((test) => test.metadata?.id === contract.id);
|
|
387
|
-
if (matches.length === 0) {
|
|
388
|
-
issues.push(
|
|
389
|
-
`\u6CA1\u6709\u627E\u5230\u68C0\u67E5\u9879 "${contract.id}" \u7684\u53EF\u6267\u884C\u6D4B\u8BD5\uFF08${normalizedFile}\uFF09\u3002`
|
|
390
|
-
);
|
|
391
|
-
} else if (matches.length > 1) {
|
|
392
|
-
issues.push(
|
|
393
|
-
`contract "${contract.id}" \u5728 ${normalizedFile} \u4E2D\u91CD\u590D\u5B9A\u4E49\u3002`
|
|
394
|
-
);
|
|
395
|
-
} else if (matches[0]?.metadata) {
|
|
396
|
-
issues.push(...getDefinitionIssues(contract, matches[0].metadata));
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
for (const test of actual) {
|
|
400
|
-
const id = test.metadata?.id;
|
|
401
|
-
if (!id) continue;
|
|
402
|
-
const expectedContract = options.contracts.find(
|
|
403
|
-
(contract) => contract.id === id
|
|
404
|
-
);
|
|
405
|
-
if (!expectedContract) {
|
|
406
|
-
issues.push(
|
|
407
|
-
`\u6D4B\u8BD5 "${test.name}" \u58F0\u660E\u4E86\u672A\u5217\u5165 gameplayAudit \u6E05\u5355\u7684 contract "${id}"\u3002`
|
|
408
|
-
);
|
|
409
|
-
} else if (expectedContract.testFile.replaceAll("\\", "/") !== normalizedFile) {
|
|
410
|
-
issues.push(
|
|
411
|
-
`contract "${id}" \u5FC5\u987B\u4F4D\u4E8E ${expectedContract.testFile}\uFF0C\u5B9E\u9645\u4F4D\u4E8E ${normalizedFile}\u3002`
|
|
412
|
-
);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
return { passed: issues.length === 0, issues };
|
|
416
|
-
}
|
|
417
|
-
function getGameplayEvidenceIssues(id, requirements, evidence) {
|
|
418
|
-
const issues = [];
|
|
419
|
-
const inputEvents = evidence.mouseEvents + evidence.keyboardEvents + evidence.touchEvents;
|
|
420
|
-
if (requirements.requireInput && inputEvents === 0) {
|
|
421
|
-
issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002`);
|
|
422
|
-
}
|
|
423
|
-
if (requirements.requireFrameAdvance && evidence.frames === 0) {
|
|
424
|
-
issues.push(`contract "${id}" \u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27\u3002`);
|
|
425
|
-
}
|
|
426
|
-
if (requirements.requirePhysicsStep && evidence.physicsSteps === 0) {
|
|
427
|
-
issues.push(`contract "${id}" \u672A\u63A8\u8FDB Arcade Physics\u3002`);
|
|
428
|
-
}
|
|
429
|
-
for (const target of normalizeList(requirements.requireTransition)) {
|
|
430
|
-
if (!evidence.transitions.some((transition) => transition.to === target)) {
|
|
431
|
-
issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u5230 ${target} \u7684 Scene \u8F6C\u573A\u3002`);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
for (const scene of normalizeList(requirements.requireScene)) {
|
|
435
|
-
if (!evidence.visitedScenes.includes(scene))
|
|
436
|
-
issues.push(`contract "${id}" \u672A\u8BBF\u95EE Scene "${scene}"\u3002`);
|
|
437
|
-
}
|
|
438
|
-
for (const scene of normalizeList(requirements.requireRestart)) {
|
|
439
|
-
if (!evidence.restartedScenes.includes(scene)) {
|
|
440
|
-
issues.push(`contract "${id}" \u672A\u91CD\u542F Scene "${scene}"\u3002`);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
for (const checkpoint of normalizeList(requirements.requireCheckpoint)) {
|
|
444
|
-
if (!evidence.checkpoints.includes(checkpoint)) {
|
|
445
|
-
issues.push(`contract "${id}" \u7F3A\u5C11 checkpoint "${checkpoint}"\u3002`);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
if (requirements.requireDestroy && !evidence.destroyed) {
|
|
449
|
-
issues.push(`contract "${id}" \u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1\u3002`);
|
|
450
|
-
}
|
|
451
|
-
return issues;
|
|
452
|
-
}
|
|
453
|
-
function auditGameplayRun(options, tests) {
|
|
454
|
-
const issues = [];
|
|
455
|
-
const actualContracts = tests.filter((test) => test.metadata);
|
|
456
|
-
const observedEvidence = [];
|
|
457
|
-
const discoveredContracts = actualContracts.flatMap((test) => {
|
|
458
|
-
const metadata = test.metadata;
|
|
459
|
-
if (!metadata) return [];
|
|
460
|
-
return [{
|
|
461
|
-
id: metadata.id,
|
|
462
|
-
kind: metadata.kind,
|
|
463
|
-
testFile: test.file,
|
|
464
|
-
scenes: metadata.scenes,
|
|
465
|
-
...metadata.requirements
|
|
466
|
-
}];
|
|
467
|
-
});
|
|
468
|
-
const expectedContracts = options.contracts ?? discoveredContracts;
|
|
469
|
-
if (!options.contracts) {
|
|
470
|
-
const discoveredIssues = validateGameplayAuditOptions(
|
|
471
|
-
{ ...options, contracts: discoveredContracts }
|
|
472
|
-
);
|
|
473
|
-
issues.push(...discoveredIssues);
|
|
474
|
-
}
|
|
475
|
-
for (const expected of expectedContracts) {
|
|
476
|
-
const matches = actualContracts.filter(
|
|
477
|
-
(test2) => test2.metadata?.id === expected.id
|
|
478
|
-
);
|
|
479
|
-
if (matches.length === 0) {
|
|
480
|
-
issues.push(`\u7F3A\u5C11 gameplay contract "${expected.id}"\u3002`);
|
|
481
|
-
continue;
|
|
482
|
-
}
|
|
483
|
-
if (matches.length > 1) {
|
|
484
|
-
issues.push(`gameplay contract "${expected.id}" \u88AB\u591A\u4E2A\u6D4B\u8BD5\u91CD\u590D\u58F0\u660E\u3002`);
|
|
485
|
-
continue;
|
|
486
|
-
}
|
|
487
|
-
const test = matches[0];
|
|
488
|
-
const metadata = test?.metadata;
|
|
489
|
-
if (!test || !metadata) continue;
|
|
490
|
-
if (metadata.evidence) observedEvidence.push(metadata.evidence);
|
|
491
|
-
if (test.file.replaceAll("\\", "/") !== expected.testFile.replaceAll("\\", "/")) {
|
|
492
|
-
issues.push(
|
|
493
|
-
`contract "${expected.id}" \u5FC5\u987B\u4F4D\u4E8E ${expected.testFile}\uFF0C\u5B9E\u9645\u4F4D\u4E8E ${test.file}\u3002`
|
|
494
|
-
);
|
|
495
|
-
}
|
|
496
|
-
issues.push(...getDefinitionIssues(expected, metadata));
|
|
497
|
-
if (test.state !== "passed") {
|
|
498
|
-
if (test.error) {
|
|
499
|
-
issues.push(`\u73A9\u6CD5\u6D4B\u8BD5 "${expected.id}" \u6D4B\u8BD5\u5931\u8D25\uFF1A${test.error}`);
|
|
500
|
-
} else {
|
|
501
|
-
issues.push(`\u73A9\u6CD5\u6D4B\u8BD5 "${expected.id}" \u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A ${test.state}\u3002`);
|
|
502
|
-
}
|
|
503
|
-
continue;
|
|
504
|
-
}
|
|
505
|
-
if (!metadata.verified || !metadata.evidence) {
|
|
506
|
-
issues.push(`contract "${expected.id}" \u672A\u5B8C\u6210 onTestFinished \u8BC1\u636E\u6821\u9A8C\u3002`);
|
|
507
|
-
continue;
|
|
508
|
-
}
|
|
509
|
-
const evidenceIssues = getGameplayEvidenceIssues(
|
|
510
|
-
expected.id,
|
|
511
|
-
metadata.requirements,
|
|
512
|
-
metadata.evidence
|
|
513
|
-
);
|
|
514
|
-
issues.push(...evidenceIssues);
|
|
515
|
-
}
|
|
516
|
-
for (const test of actualContracts) {
|
|
517
|
-
const id = test.metadata?.id;
|
|
518
|
-
if (id && options.contracts && !options.contracts.some((contract) => contract.id === id)) {
|
|
519
|
-
issues.push(
|
|
520
|
-
`\u53D1\u73B0\u672A\u5217\u5165 gameplayAudit \u6E05\u5355\u7684 contract "${id}"\uFF08${test.file}\uFF09\u3002`
|
|
521
|
-
);
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
const registeredScenes = normalizeSet(
|
|
525
|
-
observedEvidence.flatMap((evidence) => evidence.registeredScenes)
|
|
526
|
-
);
|
|
527
|
-
const expectedScenes = normalizeSet(options.scenes);
|
|
528
|
-
if (observedEvidence.length > 0) {
|
|
529
|
-
for (const scene of expectedScenes) {
|
|
530
|
-
if (!registeredScenes.includes(scene)) {
|
|
531
|
-
issues.push(
|
|
532
|
-
`\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002`
|
|
533
|
-
);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
for (const scene of registeredScenes) {
|
|
537
|
-
if (!expectedScenes.includes(scene)) {
|
|
538
|
-
issues.push(
|
|
539
|
-
`host registry \u51FA\u73B0\u672A\u5217\u5165 gameplayAudit.scenes \u7684\u751F\u4EA7 Scene "${scene}"\u3002`
|
|
540
|
-
);
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
return { passed: issues.length === 0, issues: [...new Set(issues)] };
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// src/gameplay-audit-reporter.ts
|
|
548
|
-
import { relative } from "path";
|
|
549
|
-
function normalizePath(path) {
|
|
550
|
-
return path.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
551
|
-
}
|
|
552
|
-
function isGameplayContractMetadata(value) {
|
|
553
|
-
if (!value || typeof value !== "object") return false;
|
|
554
|
-
const record = value;
|
|
555
|
-
return typeof record.id === "string" && Array.isArray(record.scenes) && Boolean(record.requirements) && typeof record.requirements === "object";
|
|
556
|
-
}
|
|
557
|
-
function toAuditTest(test) {
|
|
558
|
-
const metadata = test.meta().gameplayContract;
|
|
559
|
-
const firstError = test.result().errors?.[0];
|
|
560
|
-
return {
|
|
561
|
-
file: normalizePath(test.module.relativeModuleId),
|
|
562
|
-
name: test.fullName,
|
|
563
|
-
state: test.result().state,
|
|
564
|
-
metadata: isGameplayContractMetadata(metadata) ? metadata : void 0,
|
|
565
|
-
error: firstError?.message?.replaceAll(/\s+/g, " ").trim().slice(0, 360) || void 0
|
|
566
|
-
};
|
|
567
|
-
}
|
|
568
|
-
function formatGameplayIssues(issues) {
|
|
569
|
-
const groups = /* @__PURE__ */ new Map();
|
|
570
|
-
const repairKeys = /* @__PURE__ */ new Set();
|
|
571
|
-
const repairs = [];
|
|
572
|
-
const missingByFile = /* @__PURE__ */ new Map();
|
|
573
|
-
const missingIds = /* @__PURE__ */ new Set();
|
|
574
|
-
for (const issue of issues) {
|
|
575
|
-
const match = issue.match(
|
|
576
|
-
/没有找到检查项 "([^"]+)" 的可执行测试((.+))。/
|
|
577
|
-
);
|
|
578
|
-
if (!match) continue;
|
|
579
|
-
const [, id, file] = match;
|
|
580
|
-
missingIds.add(id);
|
|
581
|
-
const ids = missingByFile.get(file) ?? [];
|
|
582
|
-
if (!ids.includes(id)) ids.push(id);
|
|
583
|
-
missingByFile.set(file, ids);
|
|
584
|
-
}
|
|
585
|
-
const repairText = {
|
|
586
|
-
input: "\u8F93\u5165\uFF1A\u5148\u63A8\u8FDB\u4E00\u5E27\uFF0C\u518D\u901A\u8FC7 host.input.mouse\u3001host.input.keyboard \u6216 host.input.touch \u9A71\u52A8\u771F\u5B9E\u751F\u4EA7\u8F93\u5165\u3002",
|
|
587
|
-
frames: "\u5E27\u63A8\u8FDB\uFF1A\u5728\u8F93\u5165\u6216\u751F\u4EA7\u547D\u4EE4\u540E\u8C03\u7528 host.stepFrames()\uFF0C\u5F02\u6B65 update \u4F7F\u7528 host.stepFramesAsync()\u3002",
|
|
588
|
-
physics: "\u7269\u7406\uFF1A\u4EC5\u5728\u751F\u4EA7\u4F7F\u7528 Arcade Physics \u65F6\u8C03\u7528 host.stepPhysics()\uFF1B\u5176\u4ED6\u7269\u7406\u7CFB\u7EDF\u4F7F\u7528\u5B8C\u6574\u5E27\u3002",
|
|
589
|
-
transition: "\u573A\u666F\u5207\u6362\uFF1A\u542F\u52A8\u6E38\u620F\u524D\u6CE8\u518C\u76EE\u6807 Scene\uFF0C\u8C03\u7528\u6E38\u620F\u4E2D\u7684\u5207\u6362\u903B\u8F91\uFF0C\u518D\u7528 host.stepUntil() \u7B49\u5F85\u76EE\u6807\u6FC0\u6D3B\u5E76\u68C0\u67E5\u5207\u6362\u7ED3\u679C\u3002",
|
|
590
|
-
visit: "Scene\uFF1A\u786E\u8BA4\u6D4B\u8BD5\u901A\u8FC7\u771F\u5B9E\u5165\u53E3\u6216\u5408\u6CD5\u8F6C\u573A\u8BBF\u95EE\u8BE5 Scene\uFF0C\u5E76\u4F7F\u7528\u771F\u5B9E\u914D\u7F6E\u542F\u52A8\u3002",
|
|
591
|
-
restart: "\u6062\u590D\uFF1A\u901A\u8FC7\u771F\u5B9E\u8F93\u5165\u6216\u751F\u4EA7 controls \u89E6\u53D1 restart\uFF0C\u7B49\u5F85\u5B8C\u6574\u5E27\u5E76\u65AD\u8A00\u72B6\u6001\u5DF2\u6062\u590D\u3002",
|
|
592
|
-
checkpoint: "\u8FDB\u5EA6\u6807\u8BB0\uFF1A\u5148\u68C0\u67E5\u6E38\u620F\u81EA\u8EAB\u7684\u72B6\u6001\uFF08\u4F8B\u5982\u5206\u6570\u3001\u751F\u547D\u503C\u6216\u5F53\u524D\u5173\u5361\uFF09\uFF0C\u518D\u8C03\u7528 host.checkpoint()\u3002",
|
|
593
|
-
destroy: "\u6E05\u7406\uFF1A\u5728 afterEach \u4E2D\u8C03\u7528 host.destroy()\uFF0C\u5E76\u4FDD\u6301\u9500\u6BC1\u5E42\u7B49\u3002",
|
|
594
|
-
registry: "Scene \u6CE8\u518C\uFF1A\u786E\u8BA4\u6E38\u620F\u6E90\u7801\u6CE8\u518C\u4E86\u8BE5 Scene\uFF1B\u6D4B\u8BD5\u9700\u8981\u5207\u6362\u5230\u5B83\u65F6\uFF0C\u5728\u521B\u5EFA host \u65F6\u901A\u8FC7 additionalScenes \u63D0\u4F9B Scene \u7C7B\u3002",
|
|
595
|
-
missing: "\u6253\u5F00\u4E0A\u8FF0\u6587\u4EF6\uFF0C\u7528 gameplayTest \u6DFB\u52A0\u8FD9\u4E9B\u73A9\u6CD5\u6D4B\u8BD5\uFF1Bid \u5FC5\u987B\u4E0E vitest.config.ts \u4E2D\u7684\u914D\u7F6E\u5B8C\u5168\u4E00\u81F4\uFF0C\u521B\u5EFA host \u540E\u8C03\u7528 contract.attachHost(host)\u3002",
|
|
596
|
-
test: "\u5148\u4FEE\u590D\u5BF9\u5E94\u6D4B\u8BD5\u4E0A\u65B9\u663E\u793A\u7684\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF\uFF0C\u7136\u540E\u91CD\u65B0\u8FD0\u884C pnpm test\u3002",
|
|
597
|
-
unhandled: "\u5F02\u6B65\u9519\u8BEF\uFF1A\u5148\u4FEE\u590D\u4E0A\u65B9\u663E\u793A\u7684\u7B2C\u4E00\u6761\u9519\u8BEF\uFF1B\u8F93\u5165\u3001\u5E27\u63A8\u8FDB\u548C\u751F\u547D\u5468\u671F\u64CD\u4F5C\u90FD\u8981\u5728\u6B63\u786E\u7684 host \u751F\u547D\u5468\u671F\u5185\u5E76\u4F7F\u7528 await\u3002"
|
|
598
|
-
};
|
|
599
|
-
const getCategory = (issue) => {
|
|
600
|
-
if (issue.includes("\u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165")) return { category: "input", key: "input" };
|
|
601
|
-
if (issue.includes("\u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27")) return { category: "frames", key: "frames" };
|
|
602
|
-
if (issue.includes("\u672A\u63A8\u8FDB Arcade Physics")) return { category: "physics", key: "physics" };
|
|
603
|
-
if (issue.includes("\u672A\u8BB0\u5F55\u5230 ") && issue.includes("Scene \u8F6C\u573A")) {
|
|
604
|
-
const target = issue.match(/未记录到 (.+?) 的 Scene 转场/)?.[1] ?? "unknown";
|
|
605
|
-
return { category: "transition", key: `transition:${target}` };
|
|
606
|
-
}
|
|
607
|
-
if (issue.includes("\u672A\u8BBF\u95EE Scene")) {
|
|
608
|
-
const scene = issue.match(/未访问 Scene "([^"]+)"/)?.[1] ?? "unknown";
|
|
609
|
-
return { category: "visit", key: `visit:${scene}` };
|
|
610
|
-
}
|
|
611
|
-
if (issue.includes("\u672A\u91CD\u542F Scene")) {
|
|
612
|
-
const scene = issue.match(/未重启 Scene "([^"]+)"/)?.[1] ?? "unknown";
|
|
613
|
-
return { category: "restart", key: `restart:${scene}` };
|
|
614
|
-
}
|
|
615
|
-
if (issue.includes("\u7F3A\u5C11 checkpoint")) return { category: "checkpoint", key: `checkpoint:${issue.match(/checkpoint "([^"]+)"/)?.[1] ?? issue}` };
|
|
616
|
-
if (issue.includes("\u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1")) return { category: "destroy", key: "destroy" };
|
|
617
|
-
if (issue.includes("\u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry")) {
|
|
618
|
-
const scene = issue.match(/生产 Scene "([^"]+)"/)?.[1] ?? "unknown";
|
|
619
|
-
return { category: "registry", key: `registry:${scene}` };
|
|
620
|
-
}
|
|
621
|
-
if (issue.includes("\u7F3A\u5C11 gameplay contract")) return { category: "missing", key: "missing" };
|
|
622
|
-
if (issue.includes("\u6D4B\u8BD5\u5931\u8D25\uFF1A")) {
|
|
623
|
-
const message = issue.split("\u6D4B\u8BD5\u5931\u8D25\uFF1A")[1] ?? issue;
|
|
624
|
-
return { category: "test", key: `test:${message}` };
|
|
625
|
-
}
|
|
626
|
-
if (issue.includes("\u6D4B\u8BD5\u8FD0\u884C\u671F\u95F4\u53D1\u751F\u5F02\u6B65\u9519\u8BEF\uFF1A")) {
|
|
627
|
-
const message = issue.split("\u6D4B\u8BD5\u8FD0\u884C\u671F\u95F4\u53D1\u751F\u5F02\u6B65\u9519\u8BEF\uFF1A")[1] ?? issue;
|
|
628
|
-
return { category: "unhandled", key: `test:${message}` };
|
|
629
|
-
}
|
|
630
|
-
if (issue.includes("\u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A")) return { category: "test", key: "test" };
|
|
631
|
-
return { category: "other", key: `other:${issue}` };
|
|
632
|
-
};
|
|
633
|
-
for (const [file, ids] of missingByFile) {
|
|
634
|
-
groups.set(`missing:${file}`, {
|
|
635
|
-
text: `${file} \u4E2D\u6CA1\u6709\u627E\u5230\u914D\u7F6E\u8981\u6C42\u7684\u73A9\u6CD5\u6D4B\u8BD5\uFF1A${ids.join("\u3001")}\u3002`,
|
|
636
|
-
ids: [],
|
|
637
|
-
key: `missing:${file}`
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
if (missingByFile.size > 0) {
|
|
641
|
-
repairKeys.add("missing");
|
|
642
|
-
repairs.push(repairText.missing);
|
|
643
|
-
}
|
|
644
|
-
for (const rawIssue of [...new Set(issues)]) {
|
|
645
|
-
const compact = rawIssue.replaceAll(/\s+/g, " ").trim();
|
|
646
|
-
if (compact.startsWith("\u6CA1\u6709\u627E\u5230\u68C0\u67E5\u9879 ")) continue;
|
|
647
|
-
const missingContract = compact.match(/缺少 (?:gameplay )?contract "([^"]+)"/)?.[1];
|
|
648
|
-
if (missingContract && missingIds.has(missingContract)) continue;
|
|
649
|
-
if (missingIds.size > 0 && compact.includes("\u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry")) {
|
|
650
|
-
continue;
|
|
651
|
-
}
|
|
652
|
-
const readable = compact.replaceAll(/(?:gameplay )?contract "([^"]+)"/g, '\u73A9\u6CD5\u6D4B\u8BD5 "$1"').replaceAll("evidence requirements", "\u8981\u6C42\u9A8C\u8BC1\u7684\u6E38\u620F\u884C\u4E3A").replaceAll("metadata", "\u6D4B\u8BD5\u914D\u7F6E").replaceAll("host registry", "\u6D4B\u8BD5\u8BB0\u5F55\u7684 Scene \u5217\u8868");
|
|
653
|
-
const id = compact.match(/(?:contract|玩法测试) "([^"]+)"/)?.[1];
|
|
654
|
-
const { category, key } = getCategory(compact);
|
|
655
|
-
const group = groups.get(key);
|
|
656
|
-
if (group) {
|
|
657
|
-
if (id && !group.ids.includes(id)) group.ids.push(id);
|
|
658
|
-
} else {
|
|
659
|
-
groups.set(key, { text: readable, ids: id ? [id] : [], key });
|
|
660
|
-
}
|
|
661
|
-
if (repairText[category] && !repairKeys.has(category)) {
|
|
662
|
-
repairKeys.add(category);
|
|
663
|
-
repairs.push(repairText[category]);
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
const grouped = [...groups.values()];
|
|
667
|
-
const visible = grouped.slice(0, 8).map((group) => {
|
|
668
|
-
if (group.ids.length <= 1) return group.text;
|
|
669
|
-
return `${group.text}\uFF08\u540C\u7C7B\u95EE\u9898\u8FD8\u51FA\u73B0\u5728\uFF1A${group.ids.slice(1).join("\u3001")}\uFF09`;
|
|
670
|
-
});
|
|
671
|
-
return {
|
|
672
|
-
issues: visible.map(
|
|
673
|
-
(issue) => issue.length > 360 ? `${issue.slice(0, 357)}...` : issue
|
|
674
|
-
),
|
|
675
|
-
repairs: repairs.slice(0, 8),
|
|
676
|
-
omitted: Math.max(0, grouped.length - visible.length)
|
|
677
|
-
};
|
|
678
|
-
}
|
|
679
|
-
function printIssues(stage, issues) {
|
|
680
|
-
if (issues.length === 0) return;
|
|
681
|
-
console.error(`
|
|
682
|
-
GAMEPLAY_AUDIT: ${stage} FAILED`);
|
|
683
|
-
console.error("\u73A9\u6CD5\u6D4B\u8BD5\u672A\u901A\u8FC7\u3002\u8BF7\u5148\u5904\u7406\u7B2C\u4E00\u7C7B\u95EE\u9898\uFF0C\u91CD\u65B0\u8FD0\u884C\u540E\u518D\u5904\u7406\u540E\u7EED\u95EE\u9898\u3002");
|
|
684
|
-
const formatted = formatGameplayIssues(issues);
|
|
685
|
-
for (const issue of formatted.issues) console.error(`- ${issue}`);
|
|
686
|
-
if (formatted.repairs.length > 0) {
|
|
687
|
-
console.error("\u4FEE\u590D\u5EFA\u8BAE\uFF08\u540C\u7C7B\u95EE\u9898\u53EA\u5217\u4E00\u6B21\uFF09\uFF1A");
|
|
688
|
-
for (const repair of formatted.repairs) console.error(`- ${repair}`);
|
|
689
|
-
}
|
|
690
|
-
if (formatted.omitted > 0) {
|
|
691
|
-
console.error(
|
|
692
|
-
`- \u5176\u4F59 ${formatted.omitted} \u7C7B\u95EE\u9898\u5DF2\u7701\u7565\uFF1B\u5148\u4FEE\u590D\u4EE5\u4E0A\u95EE\u9898\u540E\u91CD\u65B0\u8FD0\u884C pnpm test\u3002`
|
|
693
|
-
);
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
var GameplayAuditReporter = class {
|
|
697
|
-
projectRoot;
|
|
698
|
-
options;
|
|
699
|
-
preflightIssues = /* @__PURE__ */ new Set();
|
|
700
|
-
/** 创建绑定到单个游戏项目根目录和权威清单的 reporter。 */
|
|
701
|
-
constructor(projectRoot2, options) {
|
|
702
|
-
this.projectRoot = projectRoot2;
|
|
703
|
-
this.options = options;
|
|
704
|
-
}
|
|
705
|
-
/** 每次 Vitest 运行开始时重置预检状态,并检查清单中的测试文件是否会执行。 */
|
|
706
|
-
onTestRunStart(specifications) {
|
|
707
|
-
this.preflightIssues.clear();
|
|
708
|
-
const scheduledFiles = new Set(
|
|
709
|
-
specifications.map(
|
|
710
|
-
(specification) => normalizePath(relative(this.projectRoot, specification.moduleId))
|
|
711
|
-
)
|
|
712
|
-
);
|
|
713
|
-
for (const contract of this.options.contracts ?? []) {
|
|
714
|
-
const testFile = normalizePath(contract.testFile);
|
|
715
|
-
if (!scheduledFiles.has(testFile)) {
|
|
716
|
-
this.reportPreflightIssue(
|
|
717
|
-
`contract "${contract.id}" \u7684\u6D4B\u8BD5\u6587\u4EF6\u672A\u8FDB\u5165\u672C\u6B21 Vitest \u8FD0\u884C\uFF1A${testFile}\u3002`
|
|
718
|
-
);
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
/** 在测试文件执行前检查静态 metadata 是否声明了该文件负责的全部 contract。 */
|
|
723
|
-
onTestModuleCollected(testModule) {
|
|
724
|
-
const file = normalizePath(testModule.relativeModuleId);
|
|
725
|
-
const tests = [...testModule.children.allTests()].map(toAuditTest);
|
|
726
|
-
const result = auditGameplayCollection(this.options, file, tests);
|
|
727
|
-
for (const issue of result.issues) this.reportPreflightIssue(issue);
|
|
728
|
-
}
|
|
729
|
-
/** 在测试运行结束后汇总全部 contract、Ledger 和 Scene registry,并决定命令退出码。 */
|
|
730
|
-
onTestRunEnd(testModules, unhandledErrors = [], reason = "passed") {
|
|
731
|
-
const collectionFailed = reason === "failed" && testModules.some(
|
|
732
|
-
(testModule) => testModule.state() === "failed" && [...testModule.children.allTests()].length === 0
|
|
733
|
-
);
|
|
734
|
-
if (collectionFailed) {
|
|
735
|
-
console.error(
|
|
736
|
-
"\nGAMEPLAY_AUDIT: NOT RUN\n\u73A9\u6CD5\u6D4B\u8BD5\u6587\u4EF6\u65E0\u6CD5\u52A0\u8F7D\uFF0C\u56E0\u6B64\u65E0\u6CD5\u68C0\u67E5\u6E38\u620F\u884C\u4E3A\u3002\u8BF7\u5148\u4FEE\u590D Vitest \u4E0A\u65B9\u663E\u793A\u7684\u8BED\u6CD5\u6216\u5BFC\u5165\u9519\u8BEF\uFF0C\u518D\u8FD0\u884C pnpm test\u3002"
|
|
737
|
-
);
|
|
738
|
-
process.exitCode = 1;
|
|
739
|
-
return;
|
|
740
|
-
}
|
|
741
|
-
const tests = testModules.flatMap(
|
|
742
|
-
(testModule) => [...testModule.children.allTests()].map(toAuditTest)
|
|
743
|
-
);
|
|
744
|
-
const result = auditGameplayRun(this.options, tests);
|
|
745
|
-
const runtimeIssues = unhandledErrors.map((error) => error.message?.replaceAll(/\s+/g, " ").trim()).filter(Boolean).map((message) => `\u6D4B\u8BD5\u8FD0\u884C\u671F\u95F4\u53D1\u751F\u5F02\u6B65\u9519\u8BEF\uFF1A${message}`);
|
|
746
|
-
const combinedIssues = [
|
|
747
|
-
...this.preflightIssues,
|
|
748
|
-
...result.issues,
|
|
749
|
-
...runtimeIssues
|
|
750
|
-
];
|
|
751
|
-
if (combinedIssues.length > 0) {
|
|
752
|
-
printIssues("FINAL", [...new Set(combinedIssues)]);
|
|
753
|
-
process.exitCode = 1;
|
|
754
|
-
} else {
|
|
755
|
-
console.log(
|
|
756
|
-
`
|
|
757
|
-
GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${tests.filter((test) => test.metadata).length} contracts, ${this.options.scenes.length} scenes)`
|
|
758
|
-
);
|
|
759
|
-
console.log(
|
|
760
|
-
"GAMEPLAY_AUDIT_NOTE: \u73A9\u6CD5\u68C0\u67E5\u5DF2\u901A\u8FC7\uFF1B\u4ECD\u9700\u4EE5\u6700\u540E\u7684 TEST_RESULT \u5224\u65AD\u6574\u4F53\u6210\u529F\u3002"
|
|
761
|
-
);
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
/** 立即输出一个此前未报告过的预检缺口,并让本次测试命令失败。 */
|
|
765
|
-
reportPreflightIssue(issue) {
|
|
766
|
-
if (this.preflightIssues.has(issue)) return;
|
|
767
|
-
this.preflightIssues.add(issue);
|
|
768
|
-
process.exitCode = 1;
|
|
769
|
-
}
|
|
770
|
-
};
|
|
771
|
-
|
|
772
|
-
// src/vitest-config.ts
|
|
773
|
-
var PHASER_FACADE_DEPENDENCY = /miaoda-game-devkit[\\/]dist[\\/]phaser-facade\.mjs$/;
|
|
774
|
-
var REXUI_DEPENDENCY = /phaser4-rex-plugins/;
|
|
775
|
-
function defineGameVitestConfig(options) {
|
|
776
|
-
const auditIssues = validateGameplayAuditOptions(options.gameplayAudit, options.projectRoot);
|
|
777
|
-
if (auditIssues.length > 0) {
|
|
778
|
-
throw new Error(
|
|
779
|
-
`\u65E0\u6548\u7684 gameplayAudit \u914D\u7F6E\uFF1A
|
|
780
|
-
- ${auditIssues.join("\n- ")}`
|
|
781
|
-
);
|
|
782
|
-
}
|
|
783
|
-
return defineConfig2({
|
|
784
|
-
plugins: [phaserRexUIScenePlugin(options.projectRoot)],
|
|
785
|
-
resolve: {
|
|
786
|
-
alias: {
|
|
787
|
-
...options.aliases,
|
|
788
|
-
"@": resolve3(options.projectRoot, "src")
|
|
789
|
-
}
|
|
790
|
-
},
|
|
791
|
-
// devkit 作为 external ESM 加载时,应用测试也必须 externalize Phaser,
|
|
792
|
-
// 否则 Vite alias 与 Node 条件导出会创建两个 Phaser 单例,Scene 无法启动。
|
|
793
|
-
ssr: {
|
|
794
|
-
external: ["phaser"]
|
|
795
|
-
},
|
|
796
|
-
test: {
|
|
797
|
-
include: ["tests/**/*.test.ts"],
|
|
798
|
-
testTimeout: options.testTimeout,
|
|
799
|
-
hookTimeout: options.hookTimeout,
|
|
800
|
-
server: {
|
|
801
|
-
deps: {
|
|
802
|
-
// devkit 模块需要在配置和审计阶段使用 Node 内置模块。
|
|
803
|
-
// 如果强制内联,Vite 会把这些模块按浏览器模块转换,
|
|
804
|
-
// 最终触发 "No such built-in module: node:"。
|
|
805
|
-
external: [/miaoda-game-devkit/],
|
|
806
|
-
inline: [
|
|
807
|
-
PHASER_FACADE_DEPENDENCY,
|
|
808
|
-
REXUI_DEPENDENCY,
|
|
809
|
-
...options.inlineDependencies ?? []
|
|
810
|
-
]
|
|
811
|
-
}
|
|
812
|
-
},
|
|
813
|
-
environment: "jsdom",
|
|
814
|
-
environmentOptions: {
|
|
815
|
-
jsdom: {
|
|
816
|
-
url: "http://localhost/"
|
|
817
|
-
}
|
|
818
|
-
},
|
|
819
|
-
setupFiles: [
|
|
820
|
-
"miaoda-game-devkit/vitest-setup",
|
|
821
|
-
...options.additionalSetupFiles ?? []
|
|
822
|
-
],
|
|
823
|
-
reporters: [
|
|
824
|
-
// Vitest's minimal reporter is optimized for AI output: passing tests
|
|
825
|
-
// stay quiet while failed tests retain their useful error message.
|
|
826
|
-
"minimal",
|
|
827
|
-
new GameplayAuditReporter(options.projectRoot, options.gameplayAudit)
|
|
828
|
-
],
|
|
829
|
-
restoreMocks: true,
|
|
830
|
-
clearMocks: true,
|
|
831
|
-
coverage: {
|
|
832
|
-
provider: "v8",
|
|
833
|
-
include: ["src/scenes/**/*.{ts,tsx}"],
|
|
834
|
-
exclude: [
|
|
835
|
-
"src/scenes/**/*.d.ts",
|
|
836
|
-
"src/scenes/**/*.{spec,test}.{ts,tsx}"
|
|
837
|
-
],
|
|
838
|
-
// Per-file threshold errors still identify omitted Scene files; the
|
|
839
|
-
// summary avoids printing a large coverage table on every successful run.
|
|
840
|
-
reporter: ["text-summary"],
|
|
841
|
-
thresholds: {
|
|
842
|
-
perFile: true,
|
|
843
|
-
lines: 1,
|
|
844
|
-
functions: 1,
|
|
845
|
-
statements: 1
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
});
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
// src/lint/vitest-config.test.ts
|
|
853
|
-
var projectRoot = resolve4(import.meta.dirname, "../..");
|
|
854
|
-
var gameplayAudit = {
|
|
855
|
-
mode: "non-interactive",
|
|
856
|
-
nonInteractiveJustification: "\u56FA\u5B9A\u5939\u5177\u53EA\u9A8C\u8BC1\u5171\u4EAB host \u57FA\u7EBF\uFF0C\u4E0D\u5305\u542B\u73A9\u6CD5\u8F93\u5165\u3002",
|
|
857
|
-
scenes: ["InputScene"],
|
|
858
|
-
contracts: [
|
|
859
|
-
{
|
|
860
|
-
id: "InputScene.boot",
|
|
861
|
-
kind: "boot",
|
|
862
|
-
testFile: "tests/input.test.ts",
|
|
863
|
-
scenes: ["InputScene"],
|
|
864
|
-
requireFrameAdvance: true
|
|
865
|
-
}
|
|
866
|
-
]
|
|
867
|
-
};
|
|
868
|
-
describe("defineGameVitestConfig", () => {
|
|
869
|
-
it("defineGameViteConfig \u4FDD\u6301 Vite defineConfig \u7684\u5E38\u7528\u8F93\u5165\u5F62\u5F0F", async () => {
|
|
870
|
-
const objectConfig = defineGameViteConfig({ plugins: [] });
|
|
871
|
-
expect(objectConfig.plugins?.[0]).toMatchObject({
|
|
872
|
-
name: "miaoda-phaser-rexui-scene"
|
|
873
|
-
});
|
|
874
|
-
const promiseConfig = await defineGameViteConfig(Promise.resolve({}));
|
|
875
|
-
expect(promiseConfig.plugins?.[0]).toMatchObject({
|
|
876
|
-
name: "miaoda-phaser-rexui-scene"
|
|
877
|
-
});
|
|
878
|
-
const functionConfig = defineGameViteConfig(() => ({ resolve: {} }));
|
|
879
|
-
const resolvedFunctionConfig = functionConfig({ command: "serve", mode: "test" });
|
|
880
|
-
expect(resolvedFunctionConfig.plugins?.[0]).toMatchObject({
|
|
881
|
-
name: "miaoda-phaser-rexui-scene"
|
|
882
|
-
});
|
|
883
|
-
});
|
|
884
|
-
it("defineGameViteConfig \u9884\u6784\u5EFA\u52A8\u6001 RexUI \u5165\u53E3\u5E76\u4FDD\u7559\u9879\u76EE\u914D\u7F6E", () => {
|
|
885
|
-
const config = defineGameViteConfig({
|
|
886
|
-
optimizeDeps: {
|
|
887
|
-
include: ["application-dependency"],
|
|
888
|
-
exclude: ["excluded-dependency"]
|
|
889
|
-
}
|
|
890
|
-
});
|
|
891
|
-
expect(config.optimizeDeps).toEqual({
|
|
892
|
-
include: [
|
|
893
|
-
"application-dependency",
|
|
894
|
-
"phaser4-rex-plugins/templates/ui/ui-plugin.js"
|
|
895
|
-
],
|
|
896
|
-
exclude: ["excluded-dependency"]
|
|
897
|
-
});
|
|
898
|
-
});
|
|
899
|
-
it("\u63D0\u4F9B Phaser \u8FD0\u884C\u65F6\u6D4B\u8BD5\u9700\u8981\u7684\u56FA\u5B9A\u57FA\u7EBF", () => {
|
|
900
|
-
const config = defineGameVitestConfig({ projectRoot, gameplayAudit });
|
|
901
|
-
expect(config.resolve?.alias).toMatchObject({
|
|
902
|
-
"@": resolve4(projectRoot, "src")
|
|
903
|
-
});
|
|
904
|
-
expect(config.plugins).toEqual([
|
|
905
|
-
expect.objectContaining({ name: "miaoda-phaser-rexui-scene" })
|
|
906
|
-
]);
|
|
907
|
-
expect(config.resolve?.alias).not.toHaveProperty("phaser");
|
|
908
|
-
expect(config.ssr).toMatchObject({ external: ["phaser"] });
|
|
909
|
-
expect(config.test).toMatchObject({
|
|
910
|
-
include: ["tests/**/*.test.ts"],
|
|
911
|
-
server: {
|
|
912
|
-
deps: {
|
|
913
|
-
external: [/miaoda-game-devkit/],
|
|
914
|
-
inline: [
|
|
915
|
-
/miaoda-game-devkit[\\/]dist[\\/]phaser-facade\.mjs$/,
|
|
916
|
-
/phaser4-rex-plugins/
|
|
917
|
-
]
|
|
918
|
-
}
|
|
919
|
-
},
|
|
920
|
-
environment: "jsdom",
|
|
921
|
-
environmentOptions: { jsdom: { url: "http://localhost/" } },
|
|
922
|
-
setupFiles: ["miaoda-game-devkit/vitest-setup"],
|
|
923
|
-
reporters: ["minimal", expect.any(Object)],
|
|
924
|
-
restoreMocks: true,
|
|
925
|
-
clearMocks: true,
|
|
926
|
-
coverage: {
|
|
927
|
-
provider: "v8",
|
|
928
|
-
include: ["src/scenes/**/*.{ts,tsx}"],
|
|
929
|
-
exclude: [
|
|
930
|
-
"src/scenes/**/*.d.ts",
|
|
931
|
-
"src/scenes/**/*.{spec,test}.{ts,tsx}"
|
|
932
|
-
],
|
|
933
|
-
reporter: ["text-summary"],
|
|
934
|
-
thresholds: {
|
|
935
|
-
perFile: true,
|
|
936
|
-
lines: 1,
|
|
937
|
-
functions: 1,
|
|
938
|
-
statements: 1
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
});
|
|
942
|
-
});
|
|
943
|
-
it("\u4FDD\u7559\u5B89\u5168\u6269\u5C55\u5E76\u5728\u57FA\u7840 setup \u4E4B\u540E\u8FFD\u52A0\u9879\u76EE setup", () => {
|
|
944
|
-
const config = defineGameVitestConfig({
|
|
945
|
-
projectRoot,
|
|
946
|
-
gameplayAudit,
|
|
947
|
-
inlineDependencies: [/project-specific-inline-dependency/],
|
|
948
|
-
additionalSetupFiles: ["tests/custom-setup.ts"],
|
|
949
|
-
testTimeout: 5e3,
|
|
950
|
-
hookTimeout: 2e3
|
|
951
|
-
});
|
|
952
|
-
expect(config.test).toMatchObject({
|
|
953
|
-
include: ["tests/**/*.test.ts"],
|
|
954
|
-
setupFiles: ["miaoda-game-devkit/vitest-setup", "tests/custom-setup.ts"],
|
|
955
|
-
server: {
|
|
956
|
-
deps: {
|
|
957
|
-
inline: [
|
|
958
|
-
/miaoda-game-devkit[\\/]dist[\\/]phaser-facade\.mjs$/,
|
|
959
|
-
/phaser4-rex-plugins/,
|
|
960
|
-
/project-specific-inline-dependency/
|
|
961
|
-
]
|
|
962
|
-
}
|
|
963
|
-
},
|
|
964
|
-
testTimeout: 5e3,
|
|
965
|
-
hookTimeout: 2e3
|
|
966
|
-
});
|
|
967
|
-
});
|
|
968
|
-
it("\u62D2\u7EDD\u7F3A\u5C11 Scene contract \u7684\u5BA1\u8BA1\u6E05\u5355", () => {
|
|
969
|
-
expect(
|
|
970
|
-
() => defineGameVitestConfig({
|
|
971
|
-
projectRoot,
|
|
972
|
-
gameplayAudit: {
|
|
973
|
-
scenes: ["InputScene", "ResultScene"],
|
|
974
|
-
contracts: gameplayAudit.contracts
|
|
975
|
-
}
|
|
976
|
-
})
|
|
977
|
-
).toThrow(/ResultScene.*没有任何 gameplay contract/s);
|
|
978
|
-
});
|
|
979
|
-
it("\u62D2\u7EDD\u4EA4\u4E92\u9879\u76EE\u53EA\u6709 boot smoke test", () => {
|
|
980
|
-
expect(
|
|
981
|
-
() => defineGameVitestConfig({
|
|
982
|
-
projectRoot,
|
|
983
|
-
gameplayAudit: {
|
|
984
|
-
mode: "interactive",
|
|
985
|
-
scenes: ["InputScene"],
|
|
986
|
-
contracts: gameplayAudit.contracts
|
|
987
|
-
}
|
|
988
|
-
})
|
|
989
|
-
).toThrow(/缺少最低玩法基线/);
|
|
990
|
-
});
|
|
991
|
-
});
|
|
992
|
-
describe("React game devkit", () => {
|
|
993
|
-
it("\u63D0\u4F9B\u9694\u79BB\u4E8E Phaser \u5BA1\u8BA1\u7684 JSDOM \u914D\u7F6E", () => {
|
|
994
|
-
const config = defineReactGameVitestConfig({ projectRoot });
|
|
995
|
-
expect(config.resolve?.alias).toMatchObject({
|
|
996
|
-
"@": resolve4(projectRoot, "src")
|
|
997
|
-
});
|
|
998
|
-
expect(config.ssr).toBeUndefined();
|
|
999
|
-
expect(config.test).toMatchObject({
|
|
1000
|
-
environment: "jsdom",
|
|
1001
|
-
environmentOptions: {
|
|
1002
|
-
jsdom: { url: "http://localhost/", pretendToBeVisual: true }
|
|
1003
|
-
},
|
|
1004
|
-
include: [
|
|
1005
|
-
"src/**/*.{test,spec}.{ts,tsx}",
|
|
1006
|
-
"tests/**/*.{test,spec}.{ts,tsx}"
|
|
1007
|
-
],
|
|
1008
|
-
setupFiles: ["miaoda-game-devkit/react/vitest-setup"],
|
|
1009
|
-
reporters: ["minimal"],
|
|
1010
|
-
restoreMocks: true,
|
|
1011
|
-
clearMocks: true
|
|
1012
|
-
});
|
|
1013
|
-
});
|
|
1014
|
-
it("\u624B\u52A8\u65F6\u949F\u786E\u5B9A\u6027\u63A8\u8FDB\u5E76\u6E05\u7406\u5E27\u548C\u5B9A\u65F6\u5668", () => {
|
|
1015
|
-
const clock = new ManualGameClock();
|
|
1016
|
-
let frames = 0;
|
|
1017
|
-
const timers = [];
|
|
1018
|
-
clock.requestFrame(() => frames++);
|
|
1019
|
-
clock.setTimeout(() => timers.push("late"), 20);
|
|
1020
|
-
clock.setTimeout(() => timers.push("early"), 10);
|
|
1021
|
-
clock.stepFrame(10);
|
|
1022
|
-
expect({ frames, timers }).toEqual({ frames: 1, timers: ["early"] });
|
|
1023
|
-
clock.advanceBy(10);
|
|
1024
|
-
expect({ frames, timers }).toEqual({ frames: 1, timers: ["early", "late"] });
|
|
1025
|
-
expect(clock.pendingFrameCount()).toBe(0);
|
|
1026
|
-
expect(clock.pendingTimerCount()).toBe(0);
|
|
1027
|
-
});
|
|
1028
|
-
});
|