miaoda-game-devkit 0.1.1 → 0.2.1

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.
@@ -12,7 +12,9 @@ function collectMissingBitmapGlyphs(text, chars) {
12
12
  const code = text.charCodeAt(index);
13
13
  if (seen.has(code) || chars[code] !== void 0) continue;
14
14
  seen.add(code);
15
- missing.push(`${JSON.stringify(character)} (U+${code.toString(16).toUpperCase().padStart(4, "0")})`);
15
+ missing.push(
16
+ `${JSON.stringify(character)} (U+${code.toString(16).toUpperCase().padStart(4, "0")})`
17
+ );
16
18
  }
17
19
  return missing;
18
20
  }
@@ -12,43 +12,40 @@ function createCanvasContext(canvas) {
12
12
  canvas,
13
13
  font: "16px sans-serif"
14
14
  };
15
- const context = new Proxy(
16
- state,
17
- {
18
- get(target, property) {
19
- if (property in target) {
20
- return target[property];
21
- }
22
- if (property === "createImageData" || property === "getImageData") {
23
- return (...args) => imageData(args.at(-2), args.at(-1));
24
- }
25
- if (property === "measureText") {
26
- return (value) => {
27
- const font = String(target.font ?? "16px sans-serif");
28
- const size = Number.parseFloat(font.match(/([\d.]+)px/)?.[1] ?? "16");
29
- const width = [...String(value)].reduce((total, character) => {
30
- if (/\s/.test(character)) return total + size * 0.33;
31
- if (/[^\u0000-\u00ff]/.test(character)) return total + size;
32
- if (/[MW@#%&]/.test(character)) return total + size * 0.9;
33
- if (/[ilI1.,'`]/.test(character)) return total + size * 0.32;
34
- if (/[A-Z0-9]/.test(character)) return total + size * 0.64;
35
- return total + size * 0.56;
36
- }, 0);
37
- return {
38
- width,
39
- actualBoundingBoxAscent: size * 0.8,
40
- actualBoundingBoxDescent: size * 0.2
41
- };
15
+ const context = new Proxy(state, {
16
+ get(target, property) {
17
+ if (property in target) {
18
+ return target[property];
19
+ }
20
+ if (property === "createImageData" || property === "getImageData") {
21
+ return (...args) => imageData(args.at(-2), args.at(-1));
22
+ }
23
+ if (property === "measureText") {
24
+ return (value) => {
25
+ const font = String(target.font ?? "16px sans-serif");
26
+ const size = Number.parseFloat(font.match(/([\d.]+)px/)?.[1] ?? "16");
27
+ const width = [...String(value)].reduce((total, character) => {
28
+ if (/\s/.test(character)) return total + size * 0.33;
29
+ if (/[^\u0000-\u00ff]/.test(character)) return total + size;
30
+ if (/[MW@#%&]/.test(character)) return total + size * 0.9;
31
+ if (/[ilI1.,'`]/.test(character)) return total + size * 0.32;
32
+ if (/[A-Z0-9]/.test(character)) return total + size * 0.64;
33
+ return total + size * 0.56;
34
+ }, 0);
35
+ return {
36
+ width,
37
+ actualBoundingBoxAscent: size * 0.8,
38
+ actualBoundingBoxDescent: size * 0.2
42
39
  };
43
- }
44
- if (property === "createLinearGradient" || property === "createRadialGradient") {
45
- return () => ({ addColorStop() {
46
- } });
47
- }
48
- return () => void 0;
40
+ };
49
41
  }
42
+ if (property === "createLinearGradient" || property === "createRadialGradient") {
43
+ return () => ({ addColorStop() {
44
+ } });
45
+ }
46
+ return () => void 0;
50
47
  }
51
- );
48
+ });
52
49
  return context;
53
50
  }
54
51
  HTMLCanvasElement.prototype.getContext = function getContext(contextId) {
@@ -5,12 +5,345 @@ import { describe, expect, it } from "vitest";
5
5
  // src/vitest-config.ts
6
6
  import { createRequire } from "module";
7
7
  import { dirname, resolve } from "path";
8
- import {
9
- defineConfig
10
- } from "vitest/config";
8
+ import { defineConfig } from "vitest/config";
9
+
10
+ // src/gameplay-audit.ts
11
+ function normalizeList(value) {
12
+ if (value === void 0) return [];
13
+ const values = typeof value === "string" ? [value] : value;
14
+ return [...new Set(values.map((item) => item.trim()).filter(Boolean))];
15
+ }
16
+ function normalizeSet(values) {
17
+ return [
18
+ ...new Set(values.map((value) => value.trim()).filter(Boolean))
19
+ ].sort();
20
+ }
21
+ function sameSet(left, right) {
22
+ return JSON.stringify(normalizeSet(left)) === JSON.stringify(normalizeSet(right));
23
+ }
24
+ function normalizeGameplayRequirements(requirements, scenes) {
25
+ const requiredScenes = normalizeSet([
26
+ ...scenes,
27
+ ...normalizeList(requirements.requireScene)
28
+ ]);
29
+ return {
30
+ requireInput: requirements.requireInput || void 0,
31
+ requireFrameAdvance: requirements.requireFrameAdvance || void 0,
32
+ requirePhysicsStep: requirements.requirePhysicsStep || void 0,
33
+ requireTransition: normalizeList(requirements.requireTransition),
34
+ requireScene: requiredScenes,
35
+ requireRestart: normalizeList(requirements.requireRestart),
36
+ requireCheckpoint: normalizeList(requirements.requireCheckpoint),
37
+ requireDestroy: true
38
+ };
39
+ }
40
+ function createGameplayContractMetadata(contract) {
41
+ const scenes = normalizeSet(contract.scenes);
42
+ return {
43
+ id: contract.id.trim(),
44
+ scenes,
45
+ requirements: normalizeGameplayRequirements(contract, scenes),
46
+ verified: false
47
+ };
48
+ }
49
+ function validateGameplayAuditOptions(options) {
50
+ const issues = [];
51
+ const scenes = normalizeSet(options.scenes);
52
+ if (scenes.length === 0) issues.push("gameplayAudit.scenes \u4E0D\u80FD\u4E3A\u7A7A\u3002");
53
+ if (scenes.length !== options.scenes.length) {
54
+ issues.push("gameplayAudit.scenes \u4E0D\u80FD\u5305\u542B\u7A7A\u503C\u6216\u91CD\u590D Scene key\u3002");
55
+ }
56
+ const contractIds = /* @__PURE__ */ new Set();
57
+ const coveredScenes = /* @__PURE__ */ new Set();
58
+ for (const contract of options.contracts) {
59
+ const id = contract.id.trim();
60
+ if (!id) {
61
+ issues.push("gameplayAudit.contracts \u4E2D\u5B58\u5728\u7A7A contract id\u3002");
62
+ } else if (id !== contract.id) {
63
+ issues.push(`contract id "${contract.id}" \u4E0D\u80FD\u5305\u542B\u9996\u5C3E\u7A7A\u767D\u3002`);
64
+ } else if (contractIds.has(id)) {
65
+ issues.push(`gameplayAudit.contracts \u5305\u542B\u91CD\u590D contract id\uFF1A${id}\u3002`);
66
+ } else {
67
+ contractIds.add(id);
68
+ }
69
+ const testFile = contract.testFile.replaceAll("\\", "/");
70
+ if (!/^tests\/.+\.test\.ts$/.test(testFile) || testFile.split("/").includes("..")) {
71
+ issues.push(
72
+ `contract "${id || "<empty>"}" \u7684 testFile \u5FC5\u987B\u5339\u914D tests/**/*.test.ts\u3002`
73
+ );
74
+ }
75
+ const contractScenes = normalizeSet(contract.scenes);
76
+ if (contractScenes.length === 0) {
77
+ issues.push(`contract "${id || "<empty>"}" \u5FC5\u987B\u58F0\u660E\u81F3\u5C11\u4E00\u4E2A Scene\u3002`);
78
+ }
79
+ for (const scene of contractScenes) {
80
+ coveredScenes.add(scene);
81
+ if (!scenes.includes(scene)) {
82
+ issues.push(
83
+ `contract "${id}" \u5F15\u7528\u4E86\u672A\u5217\u5165 gameplayAudit.scenes \u7684 Scene\uFF1A${scene}\u3002`
84
+ );
85
+ }
86
+ }
87
+ }
88
+ for (const scene of scenes) {
89
+ if (!coveredScenes.has(scene)) {
90
+ issues.push(`Scene "${scene}" \u6CA1\u6709\u4EFB\u4F55 gameplay contract \u8D1F\u8D23\u9A8C\u8BC1\u3002`);
91
+ }
92
+ }
93
+ if (options.contracts.length === 0)
94
+ issues.push("gameplayAudit.contracts \u4E0D\u80FD\u4E3A\u7A7A\u3002");
95
+ return issues;
96
+ }
97
+ function getDefinitionIssues(expected, actual) {
98
+ const issues = [];
99
+ const expectedMetadata = createGameplayContractMetadata(expected);
100
+ if (!sameSet(expectedMetadata.scenes, actual.scenes)) {
101
+ issues.push(
102
+ `contract "${expected.id}" \u58F0\u660E\u7684 Scene \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`
103
+ );
104
+ }
105
+ if (JSON.stringify(expectedMetadata.requirements) !== JSON.stringify(actual.requirements)) {
106
+ issues.push(
107
+ `contract "${expected.id}" \u7684 evidence requirements \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`
108
+ );
109
+ }
110
+ return issues;
111
+ }
112
+ function auditGameplayCollection(options, file, tests) {
113
+ const normalizedFile = file.replaceAll("\\", "/");
114
+ const expected = options.contracts.filter(
115
+ (contract) => contract.testFile.replaceAll("\\", "/") === normalizedFile
116
+ );
117
+ const actual = tests.filter((test) => test.metadata);
118
+ const issues = [];
119
+ for (const contract of expected) {
120
+ const matches = actual.filter((test) => test.metadata?.id === contract.id);
121
+ if (matches.length === 0) {
122
+ issues.push(
123
+ `\u6D4B\u8BD5\u6267\u884C\u524D\u672A\u6536\u96C6\u5230 contract "${contract.id}"\uFF08${normalizedFile}\uFF09\u3002`
124
+ );
125
+ } else if (matches.length > 1) {
126
+ issues.push(
127
+ `contract "${contract.id}" \u5728 ${normalizedFile} \u4E2D\u91CD\u590D\u5B9A\u4E49\u3002`
128
+ );
129
+ } else if (matches[0]?.metadata) {
130
+ issues.push(...getDefinitionIssues(contract, matches[0].metadata));
131
+ }
132
+ }
133
+ for (const test of actual) {
134
+ const id = test.metadata?.id;
135
+ if (!id) continue;
136
+ const expectedContract = options.contracts.find(
137
+ (contract) => contract.id === id
138
+ );
139
+ if (!expectedContract) {
140
+ issues.push(
141
+ `\u6D4B\u8BD5 "${test.name}" \u58F0\u660E\u4E86\u672A\u5217\u5165 gameplayAudit \u6E05\u5355\u7684 contract "${id}"\u3002`
142
+ );
143
+ } else if (expectedContract.testFile.replaceAll("\\", "/") !== normalizedFile) {
144
+ issues.push(
145
+ `contract "${id}" \u5FC5\u987B\u4F4D\u4E8E ${expectedContract.testFile}\uFF0C\u5B9E\u9645\u4F4D\u4E8E ${normalizedFile}\u3002`
146
+ );
147
+ }
148
+ }
149
+ return { passed: issues.length === 0, issues };
150
+ }
151
+ function getGameplayEvidenceIssues(id, requirements, evidence) {
152
+ const issues = [];
153
+ const inputEvents = evidence.mouseEvents + evidence.keyboardEvents + evidence.touchEvents;
154
+ if (requirements.requireInput && inputEvents === 0)
155
+ issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002`);
156
+ if (requirements.requireFrameAdvance && evidence.frames === 0)
157
+ issues.push(`contract "${id}" \u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27\u3002`);
158
+ if (requirements.requirePhysicsStep && evidence.physicsSteps === 0)
159
+ issues.push(`contract "${id}" \u672A\u63A8\u8FDB Arcade Physics\u3002`);
160
+ for (const target of normalizeList(requirements.requireTransition)) {
161
+ if (!evidence.transitions.some((transition) => transition.to === target)) {
162
+ issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u5230 ${target} \u7684 Scene \u8F6C\u573A\u3002`);
163
+ }
164
+ }
165
+ for (const scene of normalizeList(requirements.requireScene)) {
166
+ if (!evidence.visitedScenes.includes(scene))
167
+ issues.push(`contract "${id}" \u672A\u8BBF\u95EE Scene "${scene}"\u3002`);
168
+ }
169
+ for (const scene of normalizeList(requirements.requireRestart)) {
170
+ if (!evidence.restartedScenes.includes(scene))
171
+ issues.push(`contract "${id}" \u672A\u91CD\u542F Scene "${scene}"\u3002`);
172
+ }
173
+ for (const checkpoint of normalizeList(requirements.requireCheckpoint)) {
174
+ if (!evidence.checkpoints.includes(checkpoint))
175
+ issues.push(`contract "${id}" \u7F3A\u5C11 checkpoint "${checkpoint}"\u3002`);
176
+ }
177
+ if (requirements.requireDestroy && !evidence.destroyed)
178
+ issues.push(`contract "${id}" \u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1\u3002`);
179
+ return issues;
180
+ }
181
+ function auditGameplayRun(options, tests) {
182
+ const issues = [];
183
+ const actualContracts = tests.filter((test) => test.metadata);
184
+ const acceptedEvidence = [];
185
+ for (const expected of options.contracts) {
186
+ const matches = actualContracts.filter(
187
+ (test2) => test2.metadata?.id === expected.id
188
+ );
189
+ if (matches.length === 0) {
190
+ issues.push(`\u7F3A\u5C11 gameplay contract "${expected.id}"\u3002`);
191
+ continue;
192
+ }
193
+ if (matches.length > 1) {
194
+ issues.push(`gameplay contract "${expected.id}" \u88AB\u591A\u4E2A\u6D4B\u8BD5\u91CD\u590D\u58F0\u660E\u3002`);
195
+ continue;
196
+ }
197
+ const test = matches[0];
198
+ const metadata = test?.metadata;
199
+ if (!test || !metadata) continue;
200
+ if (test.file.replaceAll("\\", "/") !== expected.testFile.replaceAll("\\", "/")) {
201
+ issues.push(
202
+ `contract "${expected.id}" \u5FC5\u987B\u4F4D\u4E8E ${expected.testFile}\uFF0C\u5B9E\u9645\u4F4D\u4E8E ${test.file}\u3002`
203
+ );
204
+ }
205
+ issues.push(...getDefinitionIssues(expected, metadata));
206
+ if (test.state !== "passed") {
207
+ issues.push(`contract "${expected.id}" \u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A ${test.state}\u3002`);
208
+ continue;
209
+ }
210
+ if (!metadata.verified || !metadata.evidence) {
211
+ issues.push(`contract "${expected.id}" \u672A\u5B8C\u6210 onTestFinished \u8BC1\u636E\u6821\u9A8C\u3002`);
212
+ continue;
213
+ }
214
+ const evidenceIssues = getGameplayEvidenceIssues(
215
+ expected.id,
216
+ metadata.requirements,
217
+ metadata.evidence
218
+ );
219
+ issues.push(...evidenceIssues);
220
+ if (evidenceIssues.length === 0) acceptedEvidence.push(metadata.evidence);
221
+ }
222
+ for (const test of actualContracts) {
223
+ const id = test.metadata?.id;
224
+ if (id && !options.contracts.some((contract) => contract.id === id)) {
225
+ issues.push(
226
+ `\u53D1\u73B0\u672A\u5217\u5165 gameplayAudit \u6E05\u5355\u7684 contract "${id}"\uFF08${test.file}\uFF09\u3002`
227
+ );
228
+ }
229
+ }
230
+ const registeredScenes = normalizeSet(
231
+ acceptedEvidence.flatMap((evidence) => evidence.registeredScenes)
232
+ );
233
+ const expectedScenes = normalizeSet(options.scenes);
234
+ for (const scene of expectedScenes) {
235
+ if (!registeredScenes.includes(scene))
236
+ issues.push(
237
+ `\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u4EFB\u4F55\u901A\u8FC7\u5BA1\u8BA1\u7684 host registry \u4E2D\u3002`
238
+ );
239
+ }
240
+ for (const scene of registeredScenes) {
241
+ if (!expectedScenes.includes(scene))
242
+ issues.push(
243
+ `host registry \u51FA\u73B0\u672A\u5217\u5165 gameplayAudit.scenes \u7684\u751F\u4EA7 Scene "${scene}"\u3002`
244
+ );
245
+ }
246
+ return { passed: issues.length === 0, issues: [...new Set(issues)] };
247
+ }
248
+
249
+ // src/gameplay-audit-reporter.ts
250
+ import { relative } from "path";
251
+ function normalizePath(path) {
252
+ return path.replaceAll("\\", "/").replace(/^\.\//, "");
253
+ }
254
+ function isGameplayContractMetadata(value) {
255
+ if (!value || typeof value !== "object") return false;
256
+ const record = value;
257
+ return typeof record.id === "string" && Array.isArray(record.scenes) && Boolean(record.requirements) && typeof record.requirements === "object";
258
+ }
259
+ function toAuditTest(test) {
260
+ const metadata = test.meta().gameplayContract;
261
+ return {
262
+ file: normalizePath(test.module.relativeModuleId),
263
+ name: test.fullName,
264
+ state: test.result().state,
265
+ metadata: isGameplayContractMetadata(metadata) ? metadata : void 0
266
+ };
267
+ }
268
+ function printIssues(stage, issues) {
269
+ if (issues.length === 0) return;
270
+ console.error(`
271
+ GAMEPLAY_AUDIT: ${stage} FAILED`);
272
+ for (const issue of issues) console.error(`- ${issue}`);
273
+ }
274
+ var GameplayAuditReporter = class {
275
+ projectRoot;
276
+ options;
277
+ preflightIssues = /* @__PURE__ */ new Set();
278
+ /** 创建绑定到单个游戏项目根目录和权威清单的 reporter。 */
279
+ constructor(projectRoot2, options) {
280
+ this.projectRoot = projectRoot2;
281
+ this.options = options;
282
+ }
283
+ /** 每次 Vitest 运行开始时重置预检状态,并检查清单中的测试文件是否会执行。 */
284
+ onTestRunStart(specifications) {
285
+ this.preflightIssues.clear();
286
+ const scheduledFiles = new Set(
287
+ specifications.map(
288
+ (specification) => normalizePath(relative(this.projectRoot, specification.moduleId))
289
+ )
290
+ );
291
+ for (const contract of this.options.contracts) {
292
+ const testFile = normalizePath(contract.testFile);
293
+ if (!scheduledFiles.has(testFile)) {
294
+ this.reportPreflightIssue(
295
+ `contract "${contract.id}" \u7684\u6D4B\u8BD5\u6587\u4EF6\u672A\u8FDB\u5165\u672C\u6B21 Vitest \u8FD0\u884C\uFF1A${testFile}\u3002`
296
+ );
297
+ }
298
+ }
299
+ }
300
+ /** 在测试文件执行前检查静态 metadata 是否声明了该文件负责的全部 contract。 */
301
+ onTestModuleCollected(testModule) {
302
+ const file = normalizePath(testModule.relativeModuleId);
303
+ const tests = [...testModule.children.allTests()].map(toAuditTest);
304
+ const result = auditGameplayCollection(this.options, file, tests);
305
+ for (const issue of result.issues) this.reportPreflightIssue(issue);
306
+ }
307
+ /** 在测试运行结束后汇总全部 contract、Ledger 和 Scene registry,并决定命令退出码。 */
308
+ onTestRunEnd(testModules) {
309
+ const tests = testModules.flatMap(
310
+ (testModule) => [...testModule.children.allTests()].map(toAuditTest)
311
+ );
312
+ const result = auditGameplayRun(this.options, tests);
313
+ if (!result.passed) {
314
+ printIssues("FINAL", result.issues);
315
+ process.exitCode = 1;
316
+ } else {
317
+ console.log(
318
+ `
319
+ GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${this.options.contracts.length} contracts, ${this.options.scenes.length} scenes)`
320
+ );
321
+ }
322
+ }
323
+ /** 立即输出一个此前未报告过的预检缺口,并让本次测试命令失败。 */
324
+ reportPreflightIssue(issue) {
325
+ if (this.preflightIssues.has(issue)) return;
326
+ this.preflightIssues.add(issue);
327
+ printIssues("PREFLIGHT", [issue]);
328
+ process.exitCode = 1;
329
+ }
330
+ };
331
+
332
+ // src/vitest-config.ts
11
333
  function defineGameVitestConfig(options) {
12
- const projectRequire = createRequire(resolve(options.projectRoot, "package.json"));
13
- const phaserPackageRoot = dirname(projectRequire.resolve("phaser/package.json"));
334
+ const auditIssues = validateGameplayAuditOptions(options.gameplayAudit);
335
+ if (auditIssues.length > 0) {
336
+ throw new Error(
337
+ `\u65E0\u6548\u7684 gameplayAudit \u914D\u7F6E\uFF1A
338
+ - ${auditIssues.join("\n- ")}`
339
+ );
340
+ }
341
+ const projectRequire = createRequire(
342
+ resolve(options.projectRoot, "package.json")
343
+ );
344
+ const phaserPackageRoot = dirname(
345
+ projectRequire.resolve("phaser/package.json")
346
+ );
14
347
  const phaserBrowserBuild = resolve(phaserPackageRoot, "dist/phaser.js");
15
348
  return defineConfig({
16
349
  resolve: {
@@ -26,7 +359,10 @@ function defineGameVitestConfig(options) {
26
359
  hookTimeout: options.hookTimeout,
27
360
  server: {
28
361
  deps: {
29
- inline: [/miaoda-game-/]
362
+ // devkit 模块需要在配置和审计阶段使用 Node 内置模块。
363
+ // 如果强制内联,Vite 会把这些模块按浏览器模块转换,
364
+ // 最终触发 "No such built-in module: node:"。
365
+ external: [/miaoda-game-devkit/]
30
366
  }
31
367
  },
32
368
  environment: "jsdom",
@@ -39,6 +375,10 @@ function defineGameVitestConfig(options) {
39
375
  "miaoda-game-devkit/vitest-setup",
40
376
  ...options.additionalSetupFiles ?? []
41
377
  ],
378
+ reporters: [
379
+ "default",
380
+ new GameplayAuditReporter(options.projectRoot, options.gameplayAudit)
381
+ ],
42
382
  restoreMocks: true,
43
383
  clearMocks: true,
44
384
  coverage: {
@@ -62,18 +402,35 @@ function defineGameVitestConfig(options) {
62
402
 
63
403
  // src/lint/vitest-config.test.ts
64
404
  var projectRoot = resolve2(import.meta.dirname, "../..");
405
+ var gameplayAudit = {
406
+ scenes: ["InputScene"],
407
+ contracts: [
408
+ {
409
+ id: "InputScene.boot",
410
+ testFile: "tests/input.test.ts",
411
+ scenes: ["InputScene"],
412
+ requireFrameAdvance: true
413
+ }
414
+ ]
415
+ };
65
416
  describe("defineGameVitestConfig", () => {
66
417
  it("\u63D0\u4F9B Phaser \u8FD0\u884C\u65F6\u6D4B\u8BD5\u9700\u8981\u7684\u56FA\u5B9A\u57FA\u7EBF", () => {
67
- const config = defineGameVitestConfig({ projectRoot });
418
+ const config = defineGameVitestConfig({ projectRoot, gameplayAudit });
68
419
  expect(config.resolve?.alias).toMatchObject({
69
420
  "@": resolve2(projectRoot, "src"),
70
421
  phaser: expect.stringContaining("phaser/dist/phaser.js")
71
422
  });
72
423
  expect(config.test).toMatchObject({
73
424
  include: ["tests/**/*.test.ts"],
425
+ server: {
426
+ deps: {
427
+ external: [/miaoda-game-devkit/]
428
+ }
429
+ },
74
430
  environment: "jsdom",
75
431
  environmentOptions: { jsdom: { url: "http://localhost/" } },
76
432
  setupFiles: ["miaoda-game-devkit/vitest-setup"],
433
+ reporters: ["default", expect.any(Object)],
77
434
  restoreMocks: true,
78
435
  clearMocks: true,
79
436
  coverage: {
@@ -96,18 +453,27 @@ describe("defineGameVitestConfig", () => {
96
453
  it("\u4FDD\u7559\u5B89\u5168\u6269\u5C55\u5E76\u5728\u57FA\u7840 setup \u4E4B\u540E\u8FFD\u52A0\u9879\u76EE setup", () => {
97
454
  const config = defineGameVitestConfig({
98
455
  projectRoot,
456
+ gameplayAudit,
99
457
  additionalSetupFiles: ["tests/custom-setup.ts"],
100
458
  testTimeout: 5e3,
101
459
  hookTimeout: 2e3
102
460
  });
103
461
  expect(config.test).toMatchObject({
104
462
  include: ["tests/**/*.test.ts"],
105
- setupFiles: [
106
- "miaoda-game-devkit/vitest-setup",
107
- "tests/custom-setup.ts"
108
- ],
463
+ setupFiles: ["miaoda-game-devkit/vitest-setup", "tests/custom-setup.ts"],
109
464
  testTimeout: 5e3,
110
465
  hookTimeout: 2e3
111
466
  });
112
467
  });
468
+ it("\u62D2\u7EDD\u7F3A\u5C11 Scene contract \u7684\u5BA1\u8BA1\u6E05\u5355", () => {
469
+ expect(
470
+ () => defineGameVitestConfig({
471
+ projectRoot,
472
+ gameplayAudit: {
473
+ scenes: ["InputScene", "ResultScene"],
474
+ contracts: gameplayAudit.contracts
475
+ }
476
+ })
477
+ ).toThrow(/ResultScene.*没有任何 gameplay contract/s);
478
+ });
113
479
  });
@@ -1,8 +1,12 @@
1
1
  import { ViteUserConfig } from 'vitest/config';
2
+ import { c as GameplayAuditOptions } from './gameplay-audit-CYqLL8gY.mjs';
3
+ import 'phaser';
2
4
 
3
5
  interface GameVitestConfigOptions {
4
6
  /** 模板项目的绝对根目录,通常传入 `import.meta.dirname`。 */
5
7
  projectRoot: string;
8
+ /** 测试前后都必须审计的生产 Scene 和玩法契约清单。 */
9
+ gameplayAudit: GameplayAuditOptions;
6
10
  /** 项目需要的额外路径别名;`@` 和 `phaser` 由 devkit 统一维护。 */
7
11
  aliases?: Record<string, string>;
8
12
  /** 在 devkit 基础 setup 之后执行的项目级 setup 文件。 */
@@ -15,8 +19,8 @@ interface GameVitestConfigOptions {
15
19
  /**
16
20
  * 创建妙搭 Phaser 游戏的 Vitest 配置。
17
21
  *
18
- * HEADLESS 环境、游戏测试范围、Scene coverage mock 隔离均由 devkit
19
- * 固定;模板只传入项目根目录及少量安全扩展。
22
+ * HEADLESS 环境、游戏测试范围、Scene coverage、玩法审计和 mock 隔离均由 devkit
23
+ * 固定;模板传入项目根目录、玩法清单及少量安全扩展。
20
24
  */
21
25
  declare function defineGameVitestConfig(options: GameVitestConfigOptions): ViteUserConfig;
22
26
 
@@ -1,8 +1,12 @@
1
1
  import { ViteUserConfig } from 'vitest/config';
2
+ import { c as GameplayAuditOptions } from './gameplay-audit-CYqLL8gY.js';
3
+ import 'phaser';
2
4
 
3
5
  interface GameVitestConfigOptions {
4
6
  /** 模板项目的绝对根目录,通常传入 `import.meta.dirname`。 */
5
7
  projectRoot: string;
8
+ /** 测试前后都必须审计的生产 Scene 和玩法契约清单。 */
9
+ gameplayAudit: GameplayAuditOptions;
6
10
  /** 项目需要的额外路径别名;`@` 和 `phaser` 由 devkit 统一维护。 */
7
11
  aliases?: Record<string, string>;
8
12
  /** 在 devkit 基础 setup 之后执行的项目级 setup 文件。 */
@@ -15,8 +19,8 @@ interface GameVitestConfigOptions {
15
19
  /**
16
20
  * 创建妙搭 Phaser 游戏的 Vitest 配置。
17
21
  *
18
- * HEADLESS 环境、游戏测试范围、Scene coverage mock 隔离均由 devkit
19
- * 固定;模板只传入项目根目录及少量安全扩展。
22
+ * HEADLESS 环境、游戏测试范围、Scene coverage、玩法审计和 mock 隔离均由 devkit
23
+ * 固定;模板传入项目根目录、玩法清单及少量安全扩展。
20
24
  */
21
25
  declare function defineGameVitestConfig(options: GameVitestConfigOptions): ViteUserConfig;
22
26