miaoda-game-devkit 0.2.21 → 0.3.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/README.md +2 -2
- package/dist/react/index.d.mts +19 -1
- package/dist/react/index.d.ts +19 -1
- package/dist/react/index.js +50 -0
- package/dist/react/index.mjs +49 -0
- package/dist/react/testing.d.mts +10 -0
- package/dist/react/testing.d.ts +10 -0
- package/dist/react/testing.js +7 -0
- package/dist/react/testing.mjs +7 -0
- package/dist/react/vitest-config.js +70 -5
- package/dist/react/vitest-config.mjs +70 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -154,8 +154,8 @@ DOM 到 Phaser 的输入、命中测试和游戏状态变化,不验证 Canvas/
|
|
|
154
154
|
给出直接修复步骤;测试文件无法加载或配置要求的测试尚未建立时,不再继续报告 Scene
|
|
155
155
|
注册等派生问题。先处理第一类错误,再重新运行 `pnpm test`。
|
|
156
156
|
|
|
157
|
-
|
|
158
|
-
|
|
157
|
+
Phaser Vitest 配置使用 `minimal` reporter;React Vitest 配置使用无 ANSI 的精简 reporter。
|
|
158
|
+
两者都保留失败定位、coverage 摘要和最终机器标识,同时避免输出完整 DOM 快照、测试列表和逐文件噪声。
|
|
159
159
|
|
|
160
160
|
## 发布顺序
|
|
161
161
|
|
package/dist/react/index.d.mts
CHANGED
|
@@ -10,4 +10,22 @@ interface DisposableGameController {
|
|
|
10
10
|
*/
|
|
11
11
|
declare function useOwnedGameController<TController extends DisposableGameController>(createController: () => TController): TController;
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
/** 同时提供订阅与权威快照读取的游戏 Controller,是最小可渲染运行时。 */
|
|
14
|
+
interface SubscribableGameController<TState> extends DisposableGameController {
|
|
15
|
+
subscribe(listener: () => void): () => void;
|
|
16
|
+
snapshot(): TState;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 一次调用接管游戏 Controller 的创建、销毁与状态订阅:
|
|
20
|
+
* useOwnedGameController 的 ownership,加上 useSyncExternalStore 的渲染桥。
|
|
21
|
+
*
|
|
22
|
+
* 附带一道开发期检查:状态内容变了、但 snapshot() 返回的还是原来那个
|
|
23
|
+
* 对象时,React 会认为"什么都没变"而不刷新界面——游戏悄悄卡死,没有
|
|
24
|
+
* 任何报错。检测到这种情况就立刻抛错,并直接告诉你怎么修。
|
|
25
|
+
*/
|
|
26
|
+
declare function useGameController<TController extends SubscribableGameController<unknown>>(createController: () => TController): {
|
|
27
|
+
game: TController;
|
|
28
|
+
state: ReturnType<TController["snapshot"]>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export { type DisposableGameController, type SubscribableGameController, useGameController, useOwnedGameController };
|
package/dist/react/index.d.ts
CHANGED
|
@@ -10,4 +10,22 @@ interface DisposableGameController {
|
|
|
10
10
|
*/
|
|
11
11
|
declare function useOwnedGameController<TController extends DisposableGameController>(createController: () => TController): TController;
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
/** 同时提供订阅与权威快照读取的游戏 Controller,是最小可渲染运行时。 */
|
|
14
|
+
interface SubscribableGameController<TState> extends DisposableGameController {
|
|
15
|
+
subscribe(listener: () => void): () => void;
|
|
16
|
+
snapshot(): TState;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 一次调用接管游戏 Controller 的创建、销毁与状态订阅:
|
|
20
|
+
* useOwnedGameController 的 ownership,加上 useSyncExternalStore 的渲染桥。
|
|
21
|
+
*
|
|
22
|
+
* 附带一道开发期检查:状态内容变了、但 snapshot() 返回的还是原来那个
|
|
23
|
+
* 对象时,React 会认为"什么都没变"而不刷新界面——游戏悄悄卡死,没有
|
|
24
|
+
* 任何报错。检测到这种情况就立刻抛错,并直接告诉你怎么修。
|
|
25
|
+
*/
|
|
26
|
+
declare function useGameController<TController extends SubscribableGameController<unknown>>(createController: () => TController): {
|
|
27
|
+
game: TController;
|
|
28
|
+
state: ReturnType<TController["snapshot"]>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export { type DisposableGameController, type SubscribableGameController, useGameController, useOwnedGameController };
|
package/dist/react/index.js
CHANGED
|
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var react_exports = {};
|
|
22
22
|
__export(react_exports, {
|
|
23
23
|
browserGameClock: () => browserGameClock,
|
|
24
|
+
useGameController: () => useGameController,
|
|
24
25
|
useOwnedGameController: () => useOwnedGameController
|
|
25
26
|
});
|
|
26
27
|
module.exports = __toCommonJS(react_exports);
|
|
@@ -50,8 +51,57 @@ function useOwnedGameController(createController) {
|
|
|
50
51
|
}, [controller]);
|
|
51
52
|
return controller;
|
|
52
53
|
}
|
|
54
|
+
|
|
55
|
+
// src/react/use-game-controller.ts
|
|
56
|
+
var import_react2 = require("react");
|
|
57
|
+
function useGameController(createController) {
|
|
58
|
+
const game = useOwnedGameController(createController);
|
|
59
|
+
useSnapshotIntegrityCheck(game);
|
|
60
|
+
const state = (0, import_react2.useSyncExternalStore)(
|
|
61
|
+
game.subscribe,
|
|
62
|
+
game.snapshot,
|
|
63
|
+
game.snapshot
|
|
64
|
+
);
|
|
65
|
+
return { game, state };
|
|
66
|
+
}
|
|
67
|
+
function useSnapshotIntegrityCheck(game) {
|
|
68
|
+
const reported = (0, import_react2.useRef)(false);
|
|
69
|
+
(0, import_react2.useEffect)(() => {
|
|
70
|
+
if (process.env.NODE_ENV === "production") return void 0;
|
|
71
|
+
let previous = game.snapshot();
|
|
72
|
+
let previousJson;
|
|
73
|
+
try {
|
|
74
|
+
previousJson = JSON.stringify(previous);
|
|
75
|
+
} catch {
|
|
76
|
+
previousJson = void 0;
|
|
77
|
+
}
|
|
78
|
+
return game.subscribe(() => {
|
|
79
|
+
if (reported.current) return;
|
|
80
|
+
const next = game.snapshot();
|
|
81
|
+
if (!Object.is(previous, next)) {
|
|
82
|
+
previous = next;
|
|
83
|
+
previousJson = void 0;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
let nextJson;
|
|
87
|
+
try {
|
|
88
|
+
nextJson = JSON.stringify(next);
|
|
89
|
+
} catch {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (previousJson !== void 0 && nextJson !== previousJson) {
|
|
93
|
+
reported.current = true;
|
|
94
|
+
throw new Error(
|
|
95
|
+
"Game state changed while snapshot() returned the same reference \u2014 React compares snapshots by reference and skips the re-render, so the UI freezes. \u539F\u56E0\uFF1AReact \u9760\u6BD4\u8F83\u5F15\u7528\u5224\u65AD\u72B6\u6001\u53D8\u6CA1\u53D8\uFF0C\u5F15\u7528\u76F8\u540C\u5C31\u5F53\u4F5C\u6CA1\u53D8\u5316\u3002\u4FEE\u590D\uFF1A\u6BCF\u6B21 notify \u524D\u6362\u4E00\u4E2A\u65B0\u5BF9\u8C61\uFF0C\u4F8B\u5982\u5728 controller \u91CC cachedSnapshot = { ...state }\uFF1B\u4E0D\u8981\u628A\u88AB\u539F\u5730\u4FEE\u6539\u7684\u5185\u90E8\u5BF9\u8C61\u76F4\u63A5\u7ED9 React\u3002"
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
previousJson = nextJson;
|
|
99
|
+
});
|
|
100
|
+
}, [game]);
|
|
101
|
+
}
|
|
53
102
|
// Annotate the CommonJS export names for ESM import in node:
|
|
54
103
|
0 && (module.exports = {
|
|
55
104
|
browserGameClock,
|
|
105
|
+
useGameController,
|
|
56
106
|
useOwnedGameController
|
|
57
107
|
});
|
package/dist/react/index.mjs
CHANGED
|
@@ -23,7 +23,56 @@ function useOwnedGameController(createController) {
|
|
|
23
23
|
}, [controller]);
|
|
24
24
|
return controller;
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
// src/react/use-game-controller.ts
|
|
28
|
+
import { useEffect as useEffect2, useRef as useRef2, useSyncExternalStore } from "react";
|
|
29
|
+
function useGameController(createController) {
|
|
30
|
+
const game = useOwnedGameController(createController);
|
|
31
|
+
useSnapshotIntegrityCheck(game);
|
|
32
|
+
const state = useSyncExternalStore(
|
|
33
|
+
game.subscribe,
|
|
34
|
+
game.snapshot,
|
|
35
|
+
game.snapshot
|
|
36
|
+
);
|
|
37
|
+
return { game, state };
|
|
38
|
+
}
|
|
39
|
+
function useSnapshotIntegrityCheck(game) {
|
|
40
|
+
const reported = useRef2(false);
|
|
41
|
+
useEffect2(() => {
|
|
42
|
+
if (process.env.NODE_ENV === "production") return void 0;
|
|
43
|
+
let previous = game.snapshot();
|
|
44
|
+
let previousJson;
|
|
45
|
+
try {
|
|
46
|
+
previousJson = JSON.stringify(previous);
|
|
47
|
+
} catch {
|
|
48
|
+
previousJson = void 0;
|
|
49
|
+
}
|
|
50
|
+
return game.subscribe(() => {
|
|
51
|
+
if (reported.current) return;
|
|
52
|
+
const next = game.snapshot();
|
|
53
|
+
if (!Object.is(previous, next)) {
|
|
54
|
+
previous = next;
|
|
55
|
+
previousJson = void 0;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
let nextJson;
|
|
59
|
+
try {
|
|
60
|
+
nextJson = JSON.stringify(next);
|
|
61
|
+
} catch {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (previousJson !== void 0 && nextJson !== previousJson) {
|
|
65
|
+
reported.current = true;
|
|
66
|
+
throw new Error(
|
|
67
|
+
"Game state changed while snapshot() returned the same reference \u2014 React compares snapshots by reference and skips the re-render, so the UI freezes. \u539F\u56E0\uFF1AReact \u9760\u6BD4\u8F83\u5F15\u7528\u5224\u65AD\u72B6\u6001\u53D8\u6CA1\u53D8\uFF0C\u5F15\u7528\u76F8\u540C\u5C31\u5F53\u4F5C\u6CA1\u53D8\u5316\u3002\u4FEE\u590D\uFF1A\u6BCF\u6B21 notify \u524D\u6362\u4E00\u4E2A\u65B0\u5BF9\u8C61\uFF0C\u4F8B\u5982\u5728 controller \u91CC cachedSnapshot = { ...state }\uFF1B\u4E0D\u8981\u628A\u88AB\u539F\u5730\u4FEE\u6539\u7684\u5185\u90E8\u5BF9\u8C61\u76F4\u63A5\u7ED9 React\u3002"
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
previousJson = nextJson;
|
|
71
|
+
});
|
|
72
|
+
}, [game]);
|
|
73
|
+
}
|
|
26
74
|
export {
|
|
27
75
|
browserGameClock,
|
|
76
|
+
useGameController,
|
|
28
77
|
useOwnedGameController
|
|
29
78
|
};
|
package/dist/react/testing.d.mts
CHANGED
|
@@ -33,6 +33,16 @@ interface StepUntilOptions {
|
|
|
33
33
|
* The value is sampled only when the bound is exhausted.
|
|
34
34
|
*/
|
|
35
35
|
diagnostics?: () => unknown;
|
|
36
|
+
/**
|
|
37
|
+
* 默认有一道检查:如果等待的结果"不用玩就已经成立"(第 0 步即成立,
|
|
38
|
+
* 且页面文本从进入游戏起一个字都没变),说明这个结果跟游戏过程无关,
|
|
39
|
+
* 判为失败——它能抓住输入没接上、界面卡死、断言了静态标题这类假通过。
|
|
40
|
+
*
|
|
41
|
+
* 例外:Canvas 游戏的结果画在画布上,页面文本本来就不会变。这类游戏
|
|
42
|
+
* 观察的是 Telemetry 等页面外的权威状态,此时置 true 跳过该检查。
|
|
43
|
+
* 结果通过 DOM 呈现的游戏不要传。
|
|
44
|
+
*/
|
|
45
|
+
allowStaticDom?: boolean;
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
/** 单条 React 主流程测试留下的可序列化运行期证据。 */
|
package/dist/react/testing.d.ts
CHANGED
|
@@ -33,6 +33,16 @@ interface StepUntilOptions {
|
|
|
33
33
|
* The value is sampled only when the bound is exhausted.
|
|
34
34
|
*/
|
|
35
35
|
diagnostics?: () => unknown;
|
|
36
|
+
/**
|
|
37
|
+
* 默认有一道检查:如果等待的结果"不用玩就已经成立"(第 0 步即成立,
|
|
38
|
+
* 且页面文本从进入游戏起一个字都没变),说明这个结果跟游戏过程无关,
|
|
39
|
+
* 判为失败——它能抓住输入没接上、界面卡死、断言了静态标题这类假通过。
|
|
40
|
+
*
|
|
41
|
+
* 例外:Canvas 游戏的结果画在画布上,页面文本本来就不会变。这类游戏
|
|
42
|
+
* 观察的是 Telemetry 等页面外的权威状态,此时置 true 跳过该检查。
|
|
43
|
+
* 结果通过 DOM 呈现的游戏不要传。
|
|
44
|
+
*/
|
|
45
|
+
allowStaticDom?: boolean;
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
/** 单条 React 主流程测试留下的可序列化运行期证据。 */
|
package/dist/react/testing.js
CHANGED
|
@@ -192,6 +192,7 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
192
192
|
const evidence = metadata.evidence;
|
|
193
193
|
let assertionsAtOutcome;
|
|
194
194
|
let enteredRecorded = false;
|
|
195
|
+
let domTextAtEntered;
|
|
195
196
|
const recordInput = () => {
|
|
196
197
|
evidence.domInputEvents += 1;
|
|
197
198
|
};
|
|
@@ -244,6 +245,7 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
244
245
|
);
|
|
245
246
|
}
|
|
246
247
|
enteredRecorded = true;
|
|
248
|
+
domTextAtEntered = document.body.textContent ?? "";
|
|
247
249
|
evidence.checkpoints.push(kind);
|
|
248
250
|
return;
|
|
249
251
|
}
|
|
@@ -271,6 +273,11 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
271
273
|
'stepUntil must follow performInput("primary", ...). A menu/help click is not gameplay evidence.'
|
|
272
274
|
);
|
|
273
275
|
}
|
|
276
|
+
if (steps === 0 && options.allowStaticDom !== true && domTextAtEntered !== void 0 && (document.body.textContent ?? "") === domTextAtEntered) {
|
|
277
|
+
throw new Error(
|
|
278
|
+
'stepUntil outcome was already true at step 0 and the DOM has not changed since checkpoint("entered"), so the flow cannot prove that gameplay changed anything. \u610F\u601D\uFF1A\u6E38\u620F\u4E00\u6B65\u90FD\u6CA1\u73A9\uFF0C\u7B49\u5F85\u7684"\u7ED3\u679C"\u5C31\u5DF2\u7ECF\u6210\u7ACB\uFF0C\u9875\u9762\u4E5F\u4E00\u4E2A\u5B57\u6CA1\u53D8\u2014\u2014\u8FD9\u4E2A\u7ED3\u679C\u8BC1\u660E\u4E0D\u4E86\u4EFB\u4F55\u4E8B\u3002\u5E38\u89C1\u539F\u56E0\uFF1A\u2460 \u8F93\u5165\u6CA1\u6709\u63A5\u5230\u6E38\u620F\u4E0A\uFF1B\u2461 \u754C\u9762\u5361\u6B7B\uFF08\u72B6\u6001\u6539\u4E86\u4F46 snapshot \u5F15\u7528\u6CA1\u6362\uFF0CReact \u6CA1\u6709\u5237\u65B0\uFF09\uFF1B\u2462 \u65AD\u8A00\u4E86\u5F00\u5C40\u524D\u5C31\u5B58\u5728\u7684\u9759\u6001\u6587\u672C\u3002\u4FEE\u590D\uFF1A\u7B49\u5F85\u5E76\u65AD\u8A00\u53EA\u6709\u73A9\u8D77\u6765\u4E4B\u540E\u624D\u4F1A\u51FA\u73B0\u7684\u4E1C\u897F\uFF08\u5F00\u59CB\u906E\u7F69\u6D88\u5931\u3001\u6BD4\u5206\u53D8\u5316\u3001\u7ED3\u7B97\u753B\u9762\u51FA\u73B0\uFF09\u3002\u4F8B\u5916\uFF1A\u7ED3\u679C\u753B\u5728 Canvas \u4E0A\u3001\u7ECF Telemetry \u7B49\u9875\u9762\u5916\u72B6\u6001\u89C2\u5BDF\u7684\u6E38\u620F\uFF0C\u663E\u5F0F\u4F20 { allowStaticDom: true }\u3002'
|
|
279
|
+
);
|
|
280
|
+
}
|
|
274
281
|
evidence.boundedRuns += 1;
|
|
275
282
|
assertionsAtOutcome = expect.getState().assertionCalls;
|
|
276
283
|
return steps;
|
package/dist/react/testing.mjs
CHANGED
|
@@ -154,6 +154,7 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
154
154
|
const evidence = metadata.evidence;
|
|
155
155
|
let assertionsAtOutcome;
|
|
156
156
|
let enteredRecorded = false;
|
|
157
|
+
let domTextAtEntered;
|
|
157
158
|
const recordInput = () => {
|
|
158
159
|
evidence.domInputEvents += 1;
|
|
159
160
|
};
|
|
@@ -206,6 +207,7 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
206
207
|
);
|
|
207
208
|
}
|
|
208
209
|
enteredRecorded = true;
|
|
210
|
+
domTextAtEntered = document.body.textContent ?? "";
|
|
209
211
|
evidence.checkpoints.push(kind);
|
|
210
212
|
return;
|
|
211
213
|
}
|
|
@@ -233,6 +235,11 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
233
235
|
'stepUntil must follow performInput("primary", ...). A menu/help click is not gameplay evidence.'
|
|
234
236
|
);
|
|
235
237
|
}
|
|
238
|
+
if (steps === 0 && options.allowStaticDom !== true && domTextAtEntered !== void 0 && (document.body.textContent ?? "") === domTextAtEntered) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
'stepUntil outcome was already true at step 0 and the DOM has not changed since checkpoint("entered"), so the flow cannot prove that gameplay changed anything. \u610F\u601D\uFF1A\u6E38\u620F\u4E00\u6B65\u90FD\u6CA1\u73A9\uFF0C\u7B49\u5F85\u7684"\u7ED3\u679C"\u5C31\u5DF2\u7ECF\u6210\u7ACB\uFF0C\u9875\u9762\u4E5F\u4E00\u4E2A\u5B57\u6CA1\u53D8\u2014\u2014\u8FD9\u4E2A\u7ED3\u679C\u8BC1\u660E\u4E0D\u4E86\u4EFB\u4F55\u4E8B\u3002\u5E38\u89C1\u539F\u56E0\uFF1A\u2460 \u8F93\u5165\u6CA1\u6709\u63A5\u5230\u6E38\u620F\u4E0A\uFF1B\u2461 \u754C\u9762\u5361\u6B7B\uFF08\u72B6\u6001\u6539\u4E86\u4F46 snapshot \u5F15\u7528\u6CA1\u6362\uFF0CReact \u6CA1\u6709\u5237\u65B0\uFF09\uFF1B\u2462 \u65AD\u8A00\u4E86\u5F00\u5C40\u524D\u5C31\u5B58\u5728\u7684\u9759\u6001\u6587\u672C\u3002\u4FEE\u590D\uFF1A\u7B49\u5F85\u5E76\u65AD\u8A00\u53EA\u6709\u73A9\u8D77\u6765\u4E4B\u540E\u624D\u4F1A\u51FA\u73B0\u7684\u4E1C\u897F\uFF08\u5F00\u59CB\u906E\u7F69\u6D88\u5931\u3001\u6BD4\u5206\u53D8\u5316\u3001\u7ED3\u7B97\u753B\u9762\u51FA\u73B0\uFF09\u3002\u4F8B\u5916\uFF1A\u7ED3\u679C\u753B\u5728 Canvas \u4E0A\u3001\u7ECF Telemetry \u7B49\u9875\u9762\u5916\u72B6\u6001\u89C2\u5BDF\u7684\u6E38\u620F\uFF0C\u663E\u5F0F\u4F20 { allowStaticDom: true }\u3002'
|
|
241
|
+
);
|
|
242
|
+
}
|
|
236
243
|
evidence.boundedRuns += 1;
|
|
237
244
|
assertionsAtOutcome = expect.getState().assertionCalls;
|
|
238
245
|
return steps;
|
|
@@ -40,6 +40,7 @@ var import_config = require("vitest/config");
|
|
|
40
40
|
// src/react/react-playthrough-reporter.ts
|
|
41
41
|
var import_node_fs = require("fs");
|
|
42
42
|
var import_node_path = require("path");
|
|
43
|
+
var import_node_util = require("util");
|
|
43
44
|
|
|
44
45
|
// src/react/react-playthrough.ts
|
|
45
46
|
var import_react2 = require("@testing-library/react");
|
|
@@ -142,6 +143,7 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
142
143
|
const evidence = metadata.evidence;
|
|
143
144
|
let assertionsAtOutcome;
|
|
144
145
|
let enteredRecorded = false;
|
|
146
|
+
let domTextAtEntered;
|
|
145
147
|
const recordInput = () => {
|
|
146
148
|
evidence.domInputEvents += 1;
|
|
147
149
|
};
|
|
@@ -194,6 +196,7 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
194
196
|
);
|
|
195
197
|
}
|
|
196
198
|
enteredRecorded = true;
|
|
199
|
+
domTextAtEntered = document.body.textContent ?? "";
|
|
197
200
|
evidence.checkpoints.push(kind);
|
|
198
201
|
return;
|
|
199
202
|
}
|
|
@@ -221,6 +224,11 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
221
224
|
'stepUntil must follow performInput("primary", ...). A menu/help click is not gameplay evidence.'
|
|
222
225
|
);
|
|
223
226
|
}
|
|
227
|
+
if (steps === 0 && options.allowStaticDom !== true && domTextAtEntered !== void 0 && (document.body.textContent ?? "") === domTextAtEntered) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
'stepUntil outcome was already true at step 0 and the DOM has not changed since checkpoint("entered"), so the flow cannot prove that gameplay changed anything. \u610F\u601D\uFF1A\u6E38\u620F\u4E00\u6B65\u90FD\u6CA1\u73A9\uFF0C\u7B49\u5F85\u7684"\u7ED3\u679C"\u5C31\u5DF2\u7ECF\u6210\u7ACB\uFF0C\u9875\u9762\u4E5F\u4E00\u4E2A\u5B57\u6CA1\u53D8\u2014\u2014\u8FD9\u4E2A\u7ED3\u679C\u8BC1\u660E\u4E0D\u4E86\u4EFB\u4F55\u4E8B\u3002\u5E38\u89C1\u539F\u56E0\uFF1A\u2460 \u8F93\u5165\u6CA1\u6709\u63A5\u5230\u6E38\u620F\u4E0A\uFF1B\u2461 \u754C\u9762\u5361\u6B7B\uFF08\u72B6\u6001\u6539\u4E86\u4F46 snapshot \u5F15\u7528\u6CA1\u6362\uFF0CReact \u6CA1\u6709\u5237\u65B0\uFF09\uFF1B\u2462 \u65AD\u8A00\u4E86\u5F00\u5C40\u524D\u5C31\u5B58\u5728\u7684\u9759\u6001\u6587\u672C\u3002\u4FEE\u590D\uFF1A\u7B49\u5F85\u5E76\u65AD\u8A00\u53EA\u6709\u73A9\u8D77\u6765\u4E4B\u540E\u624D\u4F1A\u51FA\u73B0\u7684\u4E1C\u897F\uFF08\u5F00\u59CB\u906E\u7F69\u6D88\u5931\u3001\u6BD4\u5206\u53D8\u5316\u3001\u7ED3\u7B97\u753B\u9762\u51FA\u73B0\uFF09\u3002\u4F8B\u5916\uFF1A\u7ED3\u679C\u753B\u5728 Canvas \u4E0A\u3001\u7ECF Telemetry \u7B49\u9875\u9762\u5916\u72B6\u6001\u89C2\u5BDF\u7684\u6E38\u620F\uFF0C\u663E\u5F0F\u4F20 { allowStaticDom: true }\u3002'
|
|
230
|
+
);
|
|
231
|
+
}
|
|
224
232
|
evidence.boundedRuns += 1;
|
|
225
233
|
assertionsAtOutcome = expect.getState().assertionCalls;
|
|
226
234
|
return steps;
|
|
@@ -323,7 +331,18 @@ function toAuditInput(test2) {
|
|
|
323
331
|
}
|
|
324
332
|
function firstLine(value) {
|
|
325
333
|
if (typeof value !== "string") return void 0;
|
|
326
|
-
return value.split("\n").map((line) => line.trim()).find(Boolean);
|
|
334
|
+
return (0, import_node_util.stripVTControlCharacters)(value).split("\n").map((line) => line.trim()).find(Boolean);
|
|
335
|
+
}
|
|
336
|
+
function failureHint(value) {
|
|
337
|
+
const names = [...value.matchAll(/button\s*\n\s*Name "([^"]+)"/g)].map(
|
|
338
|
+
(match) => match[1]
|
|
339
|
+
);
|
|
340
|
+
if (names.length === 0) return void 0;
|
|
341
|
+
return `Available button names: ${names.map((name) => JSON.stringify(name)).join(", ")}`;
|
|
342
|
+
}
|
|
343
|
+
function errorLocation(value) {
|
|
344
|
+
const match = value.match(/(?:^|\n)\s*at\s+(.*?\.(?:test|spec)\.[^\n]+:\d+:\d+)/);
|
|
345
|
+
return match?.[1];
|
|
327
346
|
}
|
|
328
347
|
function toModuleResult(module2, projectRoot) {
|
|
329
348
|
const tests = [...module2.children.allTests()];
|
|
@@ -333,13 +352,48 @@ function toModuleResult(module2, projectRoot) {
|
|
|
333
352
|
(test2) => (test2.result().errors ?? []).map((error) => firstLine(error.message))
|
|
334
353
|
)
|
|
335
354
|
].filter((message) => Boolean(message));
|
|
355
|
+
const failures = tests.filter((test2) => test2.result().state === "failed").map((test2) => {
|
|
356
|
+
const raw = test2.result().errors?.[0]?.message ?? module2.errors()[0]?.message ?? "Unknown failure";
|
|
357
|
+
return {
|
|
358
|
+
test: test2.fullName,
|
|
359
|
+
cause: firstLine(raw) ?? "Unknown failure",
|
|
360
|
+
location: test2.location ? `${(0, import_node_path.relative)(projectRoot, module2.moduleId).replaceAll("\\", "/")}:${test2.location.line}:${test2.location.column}` : errorLocation(raw),
|
|
361
|
+
hint: failureHint(raw)
|
|
362
|
+
};
|
|
363
|
+
});
|
|
364
|
+
if (failures.length === 0 && tests.length === 0 && module2.errors().length > 0) {
|
|
365
|
+
const raw = module2.errors()[0]?.message ?? "Module failed to load";
|
|
366
|
+
failures.push({
|
|
367
|
+
test: "<collection>",
|
|
368
|
+
cause: firstLine(raw) ?? "Module failed to load",
|
|
369
|
+
location: errorLocation(raw),
|
|
370
|
+
hint: failureHint(raw)
|
|
371
|
+
});
|
|
372
|
+
}
|
|
336
373
|
return {
|
|
337
374
|
file: (0, import_node_path.relative)(projectRoot, module2.moduleId).replaceAll("\\", "/"),
|
|
338
375
|
state: module2.state(),
|
|
339
376
|
errors,
|
|
340
|
-
tests: tests.map(toAuditInput)
|
|
377
|
+
tests: tests.map(toAuditInput),
|
|
378
|
+
failures
|
|
341
379
|
};
|
|
342
380
|
}
|
|
381
|
+
function formatFailureSummary(modules) {
|
|
382
|
+
const failures = modules.flatMap(
|
|
383
|
+
(module2) => (module2.failures ?? []).map((failure) => ({ ...failure, file: module2.file }))
|
|
384
|
+
);
|
|
385
|
+
if (failures.length === 0) return ["TEST_RESULT: PASS"];
|
|
386
|
+
const lines = [`FAILED_TESTS: ${failures.length}`];
|
|
387
|
+
for (const [index, failure] of failures.entries()) {
|
|
388
|
+
lines.push(`FAILURE_${index + 1}: ${failure.file}`);
|
|
389
|
+
lines.push(`TEST: ${failure.test}`);
|
|
390
|
+
lines.push(`CAUSE: ${failure.cause}`);
|
|
391
|
+
if (failure.location) lines.push(`AT: ${failure.location}`);
|
|
392
|
+
if (failure.hint) lines.push(`HINT: ${failure.hint}`);
|
|
393
|
+
}
|
|
394
|
+
lines.push("TEST_RESULT: FAIL");
|
|
395
|
+
return lines;
|
|
396
|
+
}
|
|
343
397
|
function assessReactPlaythroughReport(input) {
|
|
344
398
|
const base = { file: input.expectedFile };
|
|
345
399
|
if (!input.expectedFileScheduled) {
|
|
@@ -388,12 +442,14 @@ function assessReactPlaythroughReport(input) {
|
|
|
388
442
|
const cause = productionModule.errors[0] ?? input.unhandledErrors?.[0] ?? audit.issues[0] ?? "\u751F\u4EA7\u53EF\u73A9\u6D41\u7A0B\u6CA1\u6709\u7559\u4E0B\u5B8C\u6574\u8BC1\u636E\u3002";
|
|
389
443
|
const timedOut = /outcome was not reached within \d+ steps/i.test(cause);
|
|
390
444
|
const missingStep = /No step callback was provided/i.test(cause);
|
|
445
|
+
const staticOutcome = /already true at step 0 and the DOM has not changed/i.test(cause);
|
|
446
|
+
const staleSnapshot = /snapshot\(\) returned the same reference/i.test(cause);
|
|
391
447
|
const missingStructuredEvidence = /performInput|checkpoint/.test(cause);
|
|
392
448
|
return {
|
|
393
449
|
...base,
|
|
394
450
|
status: "FAILED",
|
|
395
451
|
cause,
|
|
396
|
-
next: missingStep ? "\u8BE5\u6D41\u7A0B\u662F\u65F6\u95F4\u6216\u5E27\u9A71\u52A8\u7684\uFF0C\u4F46 stepUntil \u6CA1\u6709\u63A8\u8FDB\u6E38\u620F\u65F6\u95F4\uFF1B\u4E3A\u751F\u4EA7\u6E38\u620F\u6CE8\u5165 devkit \u7684 GameClock\uFF0C\u5E76\u4F20\u5165 step: () => clock.stepFrame()\u3002\u4E0D\u8981\u7528\u771F\u5B9E setTimeout\u3002" : timedOut ? "\u771F\u5B9E\u8F93\u5165\u5DF2\u6267\u884C\uFF0C\u4F46\u73A9\u6CD5\u6CA1\u6709\u5728\u4E0A\u9650\u5185\u4EA7\u751F\u7ED3\u679C\uFF1B\u68C0\u67E5\u751F\u4EA7\u63A7\u5236\u662F\u5426\u6536\u5230\u8F93\u5165\uFF0C\u518D\u67E5\u770B\u8D85\u65F6\u9519\u8BEF\u4E2D\u7684 Last diagnostics \u5224\u65AD\u662F\u6E38\u620F\u5FAA\u73AF\u3001\u89C4\u5219\u72B6\u6001\u8FD8\u662F UI \u540C\u6B65\u672A\u63A8\u8FDB\u3002" : missingStructuredEvidence ? '\u6309\u987A\u5E8F\u8865\u9F50\u81F3\u5C11\u4E24\u6B21\u7B7E\u5230\uFF1AperformInput("entry") \u540E\u8C03\u7528 checkpoint("entered")\uFF1B\u518D\u7528 performInput("primary") \u6267\u884C\u6838\u5FC3\u64CD\u4F5C\uFF0CstepUntil \u7B49\u5F85\u7ED3\u679C\uFF0C\u7528\u56DE\u8C03\u63D0\u4F9B\u7684 expect \u65AD\u8A00\u540E\u8C03\u7528 checkpoint("progress") \u6216 checkpoint("terminal")\u3002' : "\u4ECE\u751F\u4EA7\u5165\u53E3\u6267\u884C\u771F\u5B9E DOM \u8F93\u5165\uFF0C\u7528 stepUntil \u6709\u754C\u63A8\u8FDB\u5230\u73A9\u5BB6\u53EF\u89C1\u7ED3\u679C\u6216\u6E38\u620F\u6743\u5A01\u72B6\u6001\uFF0C\u5E76\u5728\u5176\u540E\u65AD\u8A00\uFF1B\u4E0D\u8981\u76F4\u8FBE\u5185\u90E8\u5173\u5361\u6216\u4FEE\u6539\u73A9\u6CD5\u72B6\u6001\u3002",
|
|
452
|
+
next: staleSnapshot ? "\u6E38\u620F\u539F\u5730\u4FEE\u6539\u72B6\u6001\u540E\u6CA1\u6709\u53D1\u5E03\u65B0\u7684\u5FEB\u7167\u5F15\u7528\uFF0CReact \u56E0 Object.is \u6BD4\u8F83\u76F8\u540C\u800C\u8DF3\u8FC7\u91CD\u6E32\u3002\u5728 controller \u7684 notify \u8DEF\u5F84\u4E0A\u53D1\u5E03\u65B0\u9876\u5C42\u5BF9\u8C61\uFF08cachedSnapshot = { ...state }\uFF09\uFF0C\u4E0D\u8981\u628A\u53EF\u53D8\u7684\u5185\u90E8\u5BF9\u8C61\u76F4\u63A5\u4F5C\u4E3A\u5FEB\u7167\u66B4\u9732\u3002" : staticOutcome ? "\u7ED3\u679C\u5728\u63A8\u8FDB\u524D\u5DF2\u6210\u7ACB\u4E14 DOM \u81EA entered \u4EE5\u6765\u65E0\u53D8\u5316\uFF1A\u8F93\u5165\u53EF\u80FD\u672A\u63A5\u5230\u751F\u4EA7\u63A7\u5236\uFF0CUI \u53EF\u80FD\u51BB\u7ED3\uFF0C\u4E5F\u53EF\u80FD\u65AD\u8A00\u4E86\u6E38\u620F\u5F00\u59CB\u524D\u5C31\u5B58\u5728\u7684\u9759\u6001\u6587\u672C\u3002\u6539\u4E3A\u7B49\u5F85\u5E76\u65AD\u8A00\u53EA\u6709\u73A9\u6CD5\u63A8\u8FDB\u540E\u624D\u51FA\u73B0\u7684\u72B6\u6001\uFF08\u5F00\u59CB\u906E\u7F69\u6D88\u5931\u3001\u6BD4\u5206\u53D8\u5316\u3001\u7ED3\u7B97\u51FA\u73B0\uFF09\uFF1B\u7ECF Canvas/Telemetry \u7B49 DOM \u5916\u6743\u5A01\u72B6\u6001\u89C2\u5BDF\u7684\u6D41\u7A0B\u4F20 { allowStaticDom: true }\u3002" : missingStep ? "\u8BE5\u6D41\u7A0B\u662F\u65F6\u95F4\u6216\u5E27\u9A71\u52A8\u7684\uFF0C\u4F46 stepUntil \u6CA1\u6709\u63A8\u8FDB\u6E38\u620F\u65F6\u95F4\uFF1B\u4E3A\u751F\u4EA7\u6E38\u620F\u6CE8\u5165 devkit \u7684 GameClock\uFF0C\u5E76\u4F20\u5165 step: () => clock.stepFrame()\u3002\u4E0D\u8981\u7528\u771F\u5B9E setTimeout\u3002" : timedOut ? "\u771F\u5B9E\u8F93\u5165\u5DF2\u6267\u884C\uFF0C\u4F46\u73A9\u6CD5\u6CA1\u6709\u5728\u4E0A\u9650\u5185\u4EA7\u751F\u7ED3\u679C\uFF1B\u68C0\u67E5\u751F\u4EA7\u63A7\u5236\u662F\u5426\u6536\u5230\u8F93\u5165\uFF0C\u518D\u67E5\u770B\u8D85\u65F6\u9519\u8BEF\u4E2D\u7684 Last diagnostics \u5224\u65AD\u662F\u6E38\u620F\u5FAA\u73AF\u3001\u89C4\u5219\u72B6\u6001\u8FD8\u662F UI \u540C\u6B65\u672A\u63A8\u8FDB\u3002" : missingStructuredEvidence ? '\u6309\u987A\u5E8F\u8865\u9F50\u81F3\u5C11\u4E24\u6B21\u7B7E\u5230\uFF1AperformInput("entry") \u540E\u8C03\u7528 checkpoint("entered")\uFF1B\u518D\u7528 performInput("primary") \u6267\u884C\u6838\u5FC3\u64CD\u4F5C\uFF0CstepUntil \u7B49\u5F85\u7ED3\u679C\uFF0C\u7528\u56DE\u8C03\u63D0\u4F9B\u7684 expect \u65AD\u8A00\u540E\u8C03\u7528 checkpoint("progress") \u6216 checkpoint("terminal")\u3002' : "\u4ECE\u751F\u4EA7\u5165\u53E3\u6267\u884C\u771F\u5B9E DOM \u8F93\u5165\uFF0C\u7528 stepUntil \u6709\u754C\u63A8\u8FDB\u5230\u73A9\u5BB6\u53EF\u89C1\u7ED3\u679C\u6216\u6E38\u620F\u6743\u5A01\u72B6\u6001\uFF0C\u5E76\u5728\u5176\u540E\u65AD\u8A00\uFF1B\u4E0D\u8981\u76F4\u8FBE\u5185\u90E8\u5173\u5361\u6216\u4FEE\u6539\u73A9\u6CD5\u72B6\u6001\u3002",
|
|
397
453
|
failsRun: true
|
|
398
454
|
};
|
|
399
455
|
}
|
|
@@ -458,6 +514,14 @@ var ReactPlaythroughReporter = class {
|
|
|
458
514
|
} else {
|
|
459
515
|
console.log(output);
|
|
460
516
|
}
|
|
517
|
+
const summary = formatFailureSummary(
|
|
518
|
+
testModules.map((module2) => toModuleResult(module2, this.projectRoot))
|
|
519
|
+
);
|
|
520
|
+
if (report.failsRun && summary[0] === "TEST_RESULT: PASS") {
|
|
521
|
+
summary[0] = "TEST_RESULT: FAIL";
|
|
522
|
+
}
|
|
523
|
+
console.log(`
|
|
524
|
+
${summary.join("\n")}`);
|
|
461
525
|
}
|
|
462
526
|
};
|
|
463
527
|
|
|
@@ -506,6 +570,7 @@ function defineReactGameVitestConfig(options) {
|
|
|
506
570
|
environmentOptions: {
|
|
507
571
|
jsdom: { url: "http://localhost/", pretendToBeVisual: true }
|
|
508
572
|
},
|
|
573
|
+
includeTaskLocation: true,
|
|
509
574
|
setupFiles: [
|
|
510
575
|
"miaoda-game-devkit/react/vitest-setup",
|
|
511
576
|
...options.additionalSetupFiles ?? []
|
|
@@ -513,8 +578,8 @@ function defineReactGameVitestConfig(options) {
|
|
|
513
578
|
sequence: {
|
|
514
579
|
setupFiles: "list"
|
|
515
580
|
},
|
|
516
|
-
//
|
|
517
|
-
reporters: [
|
|
581
|
+
// 单一 reporter 输出无 ANSI 的紧凑失败摘要和项目级可玩性门禁。
|
|
582
|
+
reporters: [new ReactPlaythroughReporter(options.projectRoot)],
|
|
518
583
|
restoreMocks: true,
|
|
519
584
|
clearMocks: true,
|
|
520
585
|
testTimeout: options.testTimeout,
|
|
@@ -6,6 +6,7 @@ import { defineConfig } from "vitest/config";
|
|
|
6
6
|
// src/react/react-playthrough-reporter.ts
|
|
7
7
|
import { existsSync } from "fs";
|
|
8
8
|
import { relative, resolve } from "path";
|
|
9
|
+
import { stripVTControlCharacters } from "util";
|
|
9
10
|
|
|
10
11
|
// src/react/react-playthrough.ts
|
|
11
12
|
import { render } from "@testing-library/react";
|
|
@@ -108,6 +109,7 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
108
109
|
const evidence = metadata.evidence;
|
|
109
110
|
let assertionsAtOutcome;
|
|
110
111
|
let enteredRecorded = false;
|
|
112
|
+
let domTextAtEntered;
|
|
111
113
|
const recordInput = () => {
|
|
112
114
|
evidence.domInputEvents += 1;
|
|
113
115
|
};
|
|
@@ -160,6 +162,7 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
160
162
|
);
|
|
161
163
|
}
|
|
162
164
|
enteredRecorded = true;
|
|
165
|
+
domTextAtEntered = document.body.textContent ?? "";
|
|
163
166
|
evidence.checkpoints.push(kind);
|
|
164
167
|
return;
|
|
165
168
|
}
|
|
@@ -187,6 +190,11 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
187
190
|
'stepUntil must follow performInput("primary", ...). A menu/help click is not gameplay evidence.'
|
|
188
191
|
);
|
|
189
192
|
}
|
|
193
|
+
if (steps === 0 && options.allowStaticDom !== true && domTextAtEntered !== void 0 && (document.body.textContent ?? "") === domTextAtEntered) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
'stepUntil outcome was already true at step 0 and the DOM has not changed since checkpoint("entered"), so the flow cannot prove that gameplay changed anything. \u610F\u601D\uFF1A\u6E38\u620F\u4E00\u6B65\u90FD\u6CA1\u73A9\uFF0C\u7B49\u5F85\u7684"\u7ED3\u679C"\u5C31\u5DF2\u7ECF\u6210\u7ACB\uFF0C\u9875\u9762\u4E5F\u4E00\u4E2A\u5B57\u6CA1\u53D8\u2014\u2014\u8FD9\u4E2A\u7ED3\u679C\u8BC1\u660E\u4E0D\u4E86\u4EFB\u4F55\u4E8B\u3002\u5E38\u89C1\u539F\u56E0\uFF1A\u2460 \u8F93\u5165\u6CA1\u6709\u63A5\u5230\u6E38\u620F\u4E0A\uFF1B\u2461 \u754C\u9762\u5361\u6B7B\uFF08\u72B6\u6001\u6539\u4E86\u4F46 snapshot \u5F15\u7528\u6CA1\u6362\uFF0CReact \u6CA1\u6709\u5237\u65B0\uFF09\uFF1B\u2462 \u65AD\u8A00\u4E86\u5F00\u5C40\u524D\u5C31\u5B58\u5728\u7684\u9759\u6001\u6587\u672C\u3002\u4FEE\u590D\uFF1A\u7B49\u5F85\u5E76\u65AD\u8A00\u53EA\u6709\u73A9\u8D77\u6765\u4E4B\u540E\u624D\u4F1A\u51FA\u73B0\u7684\u4E1C\u897F\uFF08\u5F00\u59CB\u906E\u7F69\u6D88\u5931\u3001\u6BD4\u5206\u53D8\u5316\u3001\u7ED3\u7B97\u753B\u9762\u51FA\u73B0\uFF09\u3002\u4F8B\u5916\uFF1A\u7ED3\u679C\u753B\u5728 Canvas \u4E0A\u3001\u7ECF Telemetry \u7B49\u9875\u9762\u5916\u72B6\u6001\u89C2\u5BDF\u7684\u6E38\u620F\uFF0C\u663E\u5F0F\u4F20 { allowStaticDom: true }\u3002'
|
|
196
|
+
);
|
|
197
|
+
}
|
|
190
198
|
evidence.boundedRuns += 1;
|
|
191
199
|
assertionsAtOutcome = expect.getState().assertionCalls;
|
|
192
200
|
return steps;
|
|
@@ -289,7 +297,18 @@ function toAuditInput(test2) {
|
|
|
289
297
|
}
|
|
290
298
|
function firstLine(value) {
|
|
291
299
|
if (typeof value !== "string") return void 0;
|
|
292
|
-
return value.split("\n").map((line) => line.trim()).find(Boolean);
|
|
300
|
+
return stripVTControlCharacters(value).split("\n").map((line) => line.trim()).find(Boolean);
|
|
301
|
+
}
|
|
302
|
+
function failureHint(value) {
|
|
303
|
+
const names = [...value.matchAll(/button\s*\n\s*Name "([^"]+)"/g)].map(
|
|
304
|
+
(match) => match[1]
|
|
305
|
+
);
|
|
306
|
+
if (names.length === 0) return void 0;
|
|
307
|
+
return `Available button names: ${names.map((name) => JSON.stringify(name)).join(", ")}`;
|
|
308
|
+
}
|
|
309
|
+
function errorLocation(value) {
|
|
310
|
+
const match = value.match(/(?:^|\n)\s*at\s+(.*?\.(?:test|spec)\.[^\n]+:\d+:\d+)/);
|
|
311
|
+
return match?.[1];
|
|
293
312
|
}
|
|
294
313
|
function toModuleResult(module, projectRoot) {
|
|
295
314
|
const tests = [...module.children.allTests()];
|
|
@@ -299,13 +318,48 @@ function toModuleResult(module, projectRoot) {
|
|
|
299
318
|
(test2) => (test2.result().errors ?? []).map((error) => firstLine(error.message))
|
|
300
319
|
)
|
|
301
320
|
].filter((message) => Boolean(message));
|
|
321
|
+
const failures = tests.filter((test2) => test2.result().state === "failed").map((test2) => {
|
|
322
|
+
const raw = test2.result().errors?.[0]?.message ?? module.errors()[0]?.message ?? "Unknown failure";
|
|
323
|
+
return {
|
|
324
|
+
test: test2.fullName,
|
|
325
|
+
cause: firstLine(raw) ?? "Unknown failure",
|
|
326
|
+
location: test2.location ? `${relative(projectRoot, module.moduleId).replaceAll("\\", "/")}:${test2.location.line}:${test2.location.column}` : errorLocation(raw),
|
|
327
|
+
hint: failureHint(raw)
|
|
328
|
+
};
|
|
329
|
+
});
|
|
330
|
+
if (failures.length === 0 && tests.length === 0 && module.errors().length > 0) {
|
|
331
|
+
const raw = module.errors()[0]?.message ?? "Module failed to load";
|
|
332
|
+
failures.push({
|
|
333
|
+
test: "<collection>",
|
|
334
|
+
cause: firstLine(raw) ?? "Module failed to load",
|
|
335
|
+
location: errorLocation(raw),
|
|
336
|
+
hint: failureHint(raw)
|
|
337
|
+
});
|
|
338
|
+
}
|
|
302
339
|
return {
|
|
303
340
|
file: relative(projectRoot, module.moduleId).replaceAll("\\", "/"),
|
|
304
341
|
state: module.state(),
|
|
305
342
|
errors,
|
|
306
|
-
tests: tests.map(toAuditInput)
|
|
343
|
+
tests: tests.map(toAuditInput),
|
|
344
|
+
failures
|
|
307
345
|
};
|
|
308
346
|
}
|
|
347
|
+
function formatFailureSummary(modules) {
|
|
348
|
+
const failures = modules.flatMap(
|
|
349
|
+
(module) => (module.failures ?? []).map((failure) => ({ ...failure, file: module.file }))
|
|
350
|
+
);
|
|
351
|
+
if (failures.length === 0) return ["TEST_RESULT: PASS"];
|
|
352
|
+
const lines = [`FAILED_TESTS: ${failures.length}`];
|
|
353
|
+
for (const [index, failure] of failures.entries()) {
|
|
354
|
+
lines.push(`FAILURE_${index + 1}: ${failure.file}`);
|
|
355
|
+
lines.push(`TEST: ${failure.test}`);
|
|
356
|
+
lines.push(`CAUSE: ${failure.cause}`);
|
|
357
|
+
if (failure.location) lines.push(`AT: ${failure.location}`);
|
|
358
|
+
if (failure.hint) lines.push(`HINT: ${failure.hint}`);
|
|
359
|
+
}
|
|
360
|
+
lines.push("TEST_RESULT: FAIL");
|
|
361
|
+
return lines;
|
|
362
|
+
}
|
|
309
363
|
function assessReactPlaythroughReport(input) {
|
|
310
364
|
const base = { file: input.expectedFile };
|
|
311
365
|
if (!input.expectedFileScheduled) {
|
|
@@ -354,12 +408,14 @@ function assessReactPlaythroughReport(input) {
|
|
|
354
408
|
const cause = productionModule.errors[0] ?? input.unhandledErrors?.[0] ?? audit.issues[0] ?? "\u751F\u4EA7\u53EF\u73A9\u6D41\u7A0B\u6CA1\u6709\u7559\u4E0B\u5B8C\u6574\u8BC1\u636E\u3002";
|
|
355
409
|
const timedOut = /outcome was not reached within \d+ steps/i.test(cause);
|
|
356
410
|
const missingStep = /No step callback was provided/i.test(cause);
|
|
411
|
+
const staticOutcome = /already true at step 0 and the DOM has not changed/i.test(cause);
|
|
412
|
+
const staleSnapshot = /snapshot\(\) returned the same reference/i.test(cause);
|
|
357
413
|
const missingStructuredEvidence = /performInput|checkpoint/.test(cause);
|
|
358
414
|
return {
|
|
359
415
|
...base,
|
|
360
416
|
status: "FAILED",
|
|
361
417
|
cause,
|
|
362
|
-
next: missingStep ? "\u8BE5\u6D41\u7A0B\u662F\u65F6\u95F4\u6216\u5E27\u9A71\u52A8\u7684\uFF0C\u4F46 stepUntil \u6CA1\u6709\u63A8\u8FDB\u6E38\u620F\u65F6\u95F4\uFF1B\u4E3A\u751F\u4EA7\u6E38\u620F\u6CE8\u5165 devkit \u7684 GameClock\uFF0C\u5E76\u4F20\u5165 step: () => clock.stepFrame()\u3002\u4E0D\u8981\u7528\u771F\u5B9E setTimeout\u3002" : timedOut ? "\u771F\u5B9E\u8F93\u5165\u5DF2\u6267\u884C\uFF0C\u4F46\u73A9\u6CD5\u6CA1\u6709\u5728\u4E0A\u9650\u5185\u4EA7\u751F\u7ED3\u679C\uFF1B\u68C0\u67E5\u751F\u4EA7\u63A7\u5236\u662F\u5426\u6536\u5230\u8F93\u5165\uFF0C\u518D\u67E5\u770B\u8D85\u65F6\u9519\u8BEF\u4E2D\u7684 Last diagnostics \u5224\u65AD\u662F\u6E38\u620F\u5FAA\u73AF\u3001\u89C4\u5219\u72B6\u6001\u8FD8\u662F UI \u540C\u6B65\u672A\u63A8\u8FDB\u3002" : missingStructuredEvidence ? '\u6309\u987A\u5E8F\u8865\u9F50\u81F3\u5C11\u4E24\u6B21\u7B7E\u5230\uFF1AperformInput("entry") \u540E\u8C03\u7528 checkpoint("entered")\uFF1B\u518D\u7528 performInput("primary") \u6267\u884C\u6838\u5FC3\u64CD\u4F5C\uFF0CstepUntil \u7B49\u5F85\u7ED3\u679C\uFF0C\u7528\u56DE\u8C03\u63D0\u4F9B\u7684 expect \u65AD\u8A00\u540E\u8C03\u7528 checkpoint("progress") \u6216 checkpoint("terminal")\u3002' : "\u4ECE\u751F\u4EA7\u5165\u53E3\u6267\u884C\u771F\u5B9E DOM \u8F93\u5165\uFF0C\u7528 stepUntil \u6709\u754C\u63A8\u8FDB\u5230\u73A9\u5BB6\u53EF\u89C1\u7ED3\u679C\u6216\u6E38\u620F\u6743\u5A01\u72B6\u6001\uFF0C\u5E76\u5728\u5176\u540E\u65AD\u8A00\uFF1B\u4E0D\u8981\u76F4\u8FBE\u5185\u90E8\u5173\u5361\u6216\u4FEE\u6539\u73A9\u6CD5\u72B6\u6001\u3002",
|
|
418
|
+
next: staleSnapshot ? "\u6E38\u620F\u539F\u5730\u4FEE\u6539\u72B6\u6001\u540E\u6CA1\u6709\u53D1\u5E03\u65B0\u7684\u5FEB\u7167\u5F15\u7528\uFF0CReact \u56E0 Object.is \u6BD4\u8F83\u76F8\u540C\u800C\u8DF3\u8FC7\u91CD\u6E32\u3002\u5728 controller \u7684 notify \u8DEF\u5F84\u4E0A\u53D1\u5E03\u65B0\u9876\u5C42\u5BF9\u8C61\uFF08cachedSnapshot = { ...state }\uFF09\uFF0C\u4E0D\u8981\u628A\u53EF\u53D8\u7684\u5185\u90E8\u5BF9\u8C61\u76F4\u63A5\u4F5C\u4E3A\u5FEB\u7167\u66B4\u9732\u3002" : staticOutcome ? "\u7ED3\u679C\u5728\u63A8\u8FDB\u524D\u5DF2\u6210\u7ACB\u4E14 DOM \u81EA entered \u4EE5\u6765\u65E0\u53D8\u5316\uFF1A\u8F93\u5165\u53EF\u80FD\u672A\u63A5\u5230\u751F\u4EA7\u63A7\u5236\uFF0CUI \u53EF\u80FD\u51BB\u7ED3\uFF0C\u4E5F\u53EF\u80FD\u65AD\u8A00\u4E86\u6E38\u620F\u5F00\u59CB\u524D\u5C31\u5B58\u5728\u7684\u9759\u6001\u6587\u672C\u3002\u6539\u4E3A\u7B49\u5F85\u5E76\u65AD\u8A00\u53EA\u6709\u73A9\u6CD5\u63A8\u8FDB\u540E\u624D\u51FA\u73B0\u7684\u72B6\u6001\uFF08\u5F00\u59CB\u906E\u7F69\u6D88\u5931\u3001\u6BD4\u5206\u53D8\u5316\u3001\u7ED3\u7B97\u51FA\u73B0\uFF09\uFF1B\u7ECF Canvas/Telemetry \u7B49 DOM \u5916\u6743\u5A01\u72B6\u6001\u89C2\u5BDF\u7684\u6D41\u7A0B\u4F20 { allowStaticDom: true }\u3002" : missingStep ? "\u8BE5\u6D41\u7A0B\u662F\u65F6\u95F4\u6216\u5E27\u9A71\u52A8\u7684\uFF0C\u4F46 stepUntil \u6CA1\u6709\u63A8\u8FDB\u6E38\u620F\u65F6\u95F4\uFF1B\u4E3A\u751F\u4EA7\u6E38\u620F\u6CE8\u5165 devkit \u7684 GameClock\uFF0C\u5E76\u4F20\u5165 step: () => clock.stepFrame()\u3002\u4E0D\u8981\u7528\u771F\u5B9E setTimeout\u3002" : timedOut ? "\u771F\u5B9E\u8F93\u5165\u5DF2\u6267\u884C\uFF0C\u4F46\u73A9\u6CD5\u6CA1\u6709\u5728\u4E0A\u9650\u5185\u4EA7\u751F\u7ED3\u679C\uFF1B\u68C0\u67E5\u751F\u4EA7\u63A7\u5236\u662F\u5426\u6536\u5230\u8F93\u5165\uFF0C\u518D\u67E5\u770B\u8D85\u65F6\u9519\u8BEF\u4E2D\u7684 Last diagnostics \u5224\u65AD\u662F\u6E38\u620F\u5FAA\u73AF\u3001\u89C4\u5219\u72B6\u6001\u8FD8\u662F UI \u540C\u6B65\u672A\u63A8\u8FDB\u3002" : missingStructuredEvidence ? '\u6309\u987A\u5E8F\u8865\u9F50\u81F3\u5C11\u4E24\u6B21\u7B7E\u5230\uFF1AperformInput("entry") \u540E\u8C03\u7528 checkpoint("entered")\uFF1B\u518D\u7528 performInput("primary") \u6267\u884C\u6838\u5FC3\u64CD\u4F5C\uFF0CstepUntil \u7B49\u5F85\u7ED3\u679C\uFF0C\u7528\u56DE\u8C03\u63D0\u4F9B\u7684 expect \u65AD\u8A00\u540E\u8C03\u7528 checkpoint("progress") \u6216 checkpoint("terminal")\u3002' : "\u4ECE\u751F\u4EA7\u5165\u53E3\u6267\u884C\u771F\u5B9E DOM \u8F93\u5165\uFF0C\u7528 stepUntil \u6709\u754C\u63A8\u8FDB\u5230\u73A9\u5BB6\u53EF\u89C1\u7ED3\u679C\u6216\u6E38\u620F\u6743\u5A01\u72B6\u6001\uFF0C\u5E76\u5728\u5176\u540E\u65AD\u8A00\uFF1B\u4E0D\u8981\u76F4\u8FBE\u5185\u90E8\u5173\u5361\u6216\u4FEE\u6539\u73A9\u6CD5\u72B6\u6001\u3002",
|
|
363
419
|
failsRun: true
|
|
364
420
|
};
|
|
365
421
|
}
|
|
@@ -424,6 +480,14 @@ var ReactPlaythroughReporter = class {
|
|
|
424
480
|
} else {
|
|
425
481
|
console.log(output);
|
|
426
482
|
}
|
|
483
|
+
const summary = formatFailureSummary(
|
|
484
|
+
testModules.map((module) => toModuleResult(module, this.projectRoot))
|
|
485
|
+
);
|
|
486
|
+
if (report.failsRun && summary[0] === "TEST_RESULT: PASS") {
|
|
487
|
+
summary[0] = "TEST_RESULT: FAIL";
|
|
488
|
+
}
|
|
489
|
+
console.log(`
|
|
490
|
+
${summary.join("\n")}`);
|
|
427
491
|
}
|
|
428
492
|
};
|
|
429
493
|
|
|
@@ -472,6 +536,7 @@ function defineReactGameVitestConfig(options) {
|
|
|
472
536
|
environmentOptions: {
|
|
473
537
|
jsdom: { url: "http://localhost/", pretendToBeVisual: true }
|
|
474
538
|
},
|
|
539
|
+
includeTaskLocation: true,
|
|
475
540
|
setupFiles: [
|
|
476
541
|
"miaoda-game-devkit/react/vitest-setup",
|
|
477
542
|
...options.additionalSetupFiles ?? []
|
|
@@ -479,8 +544,8 @@ function defineReactGameVitestConfig(options) {
|
|
|
479
544
|
sequence: {
|
|
480
545
|
setupFiles: "list"
|
|
481
546
|
},
|
|
482
|
-
//
|
|
483
|
-
reporters: [
|
|
547
|
+
// 单一 reporter 输出无 ANSI 的紧凑失败摘要和项目级可玩性门禁。
|
|
548
|
+
reporters: [new ReactPlaythroughReporter(options.projectRoot)],
|
|
484
549
|
restoreMocks: true,
|
|
485
550
|
clearMocks: true,
|
|
486
551
|
testTimeout: options.testTimeout,
|