miaoda-game-devkit 0.2.16 → 0.2.19
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 +22 -6
- package/dist/{gameplay-audit-DmbBA0M_.d.mts → gameplay-audit-CKgNYw2r.d.mts} +46 -13
- package/dist/{gameplay-audit-DmbBA0M_.d.ts → gameplay-audit-CKgNYw2r.d.ts} +46 -13
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +346 -48
- package/dist/index.mjs +346 -48
- package/dist/react/testing.d.mts +36 -2
- package/dist/react/testing.d.ts +36 -2
- package/dist/react/testing.js +141 -15
- package/dist/react/testing.mjs +130 -14
- package/dist/react/vitest-config.js +146 -18
- package/dist/react/vitest-config.mjs +135 -17
- package/dist/vitest-config.d.mts +1 -1
- package/dist/vitest-config.d.ts +1 -1
- package/dist/vitest-config.js +9 -9
- package/dist/vitest-config.mjs +9 -9
- package/package.json +5 -1
package/dist/react/testing.d.mts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { G as GameClock, a as GameFrameCallback } from '../game-clock-suUidZdT.mjs';
|
|
2
2
|
import { RenderResult } from '@testing-library/react';
|
|
3
|
+
import { UserEvent } from '@testing-library/user-event';
|
|
3
4
|
import { ReactNode } from 'react';
|
|
5
|
+
import { ExpectStatic } from 'vitest';
|
|
4
6
|
|
|
5
7
|
/** 测试用确定性时钟;应用测试推进它,不直接伪造游戏状态。 */
|
|
6
8
|
declare class ManualGameClock implements GameClock {
|
|
@@ -26,36 +28,68 @@ interface StepUntilOptions {
|
|
|
26
28
|
maxSteps?: number;
|
|
27
29
|
/** 每轮推进一单位游戏自有时间或工作;即时回合制结果可以省略。 */
|
|
28
30
|
step?: (step: number) => void | Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Optional read-only state used to make bounded failures actionable.
|
|
33
|
+
* The value is sampled only when the bound is exhausted.
|
|
34
|
+
*/
|
|
35
|
+
diagnostics?: () => unknown;
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
/** 单条 React 主流程测试留下的可序列化运行期证据。 */
|
|
32
39
|
interface ReactPlaythroughEvidence {
|
|
33
40
|
/** 捕获阶段观察到的键盘、指针或触摸类 DOM 输入总数。 */
|
|
34
41
|
domInputEvents: number;
|
|
42
|
+
/** 由 performInput 明确标记的入口输入次数。 */
|
|
43
|
+
entryInputs: number;
|
|
44
|
+
/** 由 performInput 明确标记的核心玩法输入次数。 */
|
|
45
|
+
primaryInputs: number;
|
|
35
46
|
/** 在步数上限内成功满足条件的 stepUntil 次数。 */
|
|
36
47
|
boundedRuns: number;
|
|
37
48
|
/** 最后一次 stepUntil 成功后新增的 Vitest 断言数。 */
|
|
38
49
|
assertionsAfterOutcome: number;
|
|
50
|
+
/** 已验证相对 entered snapshot 发生变化的进展或终局 checkpoint 次数。 */
|
|
51
|
+
changedOutcomeCheckpoints: number;
|
|
52
|
+
/** 已记录的 checkpoint 类型,供 reporter 给出精确提示。 */
|
|
53
|
+
checkpoints: ReactPlaythroughCheckpointKind[];
|
|
39
54
|
/** helper 是否已经完成全部证据校验。 */
|
|
40
55
|
verified: boolean;
|
|
41
56
|
}
|
|
42
57
|
/** 通过 Vitest task metadata 从 worker 传递给主线程 reporter 的数据。 */
|
|
43
58
|
interface ReactPlaythroughMetadata {
|
|
44
59
|
/** metadata 结构版本,用于拒绝无法识别的旧数据。 */
|
|
45
|
-
version:
|
|
60
|
+
version: 2;
|
|
46
61
|
/** 静态、非交互项目跳过主流程验证时必须提供的理由。 */
|
|
47
62
|
waiverReason?: string;
|
|
48
63
|
/** 测试执行期间持续更新的客观证据。 */
|
|
49
64
|
evidence: ReactPlaythroughEvidence;
|
|
50
65
|
}
|
|
51
66
|
|
|
67
|
+
/** 玩家输入在最低可玩流程中的语义阶段。 */
|
|
68
|
+
type ReactPlaythroughInputKind = "entry" | "primary";
|
|
69
|
+
/** 可审计状态证据;entered 是基线,progress/terminal 是有效结果。 */
|
|
70
|
+
type ReactPlaythroughCheckpointKind = "entered" | "progress" | "terminal";
|
|
52
71
|
/** 主流程回调唯一需要学习的测试工具。 */
|
|
53
72
|
interface ReactPlaythroughArguments {
|
|
54
73
|
/** Testing Library 对生产游戏入口的渲染结果。 */
|
|
55
74
|
view: RenderResult;
|
|
75
|
+
/** 已由 playthroughTest 创建的真实 DOM 用户输入驱动。 */
|
|
76
|
+
user: UserEvent;
|
|
77
|
+
/** Vitest 绑定到当前 test 的断言 API;结果断言必须使用这个实例。 */
|
|
78
|
+
expect: ExpectStatic;
|
|
79
|
+
/**
|
|
80
|
+
* 包裹一段真实 DOM 输入并声明用途。
|
|
81
|
+
* entry 用于进入合法游戏状态,primary 用于移动、挥拍、拖拽、落子等核心操作。
|
|
82
|
+
*/
|
|
83
|
+
performInput(kind: ReactPlaythroughInputKind, input: () => void | Promise<void>): Promise<void>;
|
|
84
|
+
/**
|
|
85
|
+
* 记录只读状态证据。先在 entry 后记录 entered,再在 primary 和 stepUntil
|
|
86
|
+
* 后记录 progress 或 terminal;Canvas 游戏应传 Telemetry snapshot。
|
|
87
|
+
*/
|
|
88
|
+
checkpoint(kind: ReactPlaythroughCheckpointKind, snapshot: unknown): void;
|
|
56
89
|
/**
|
|
57
90
|
* 在硬性步数上限内等待权威结果。
|
|
58
91
|
* 返回后必须再断言该结果,进入中间 active/running 状态不算完成。
|
|
92
|
+
* 即时结果不需要 step;时间、帧或自定义 scheduler 流程必须传入确定性 step。
|
|
59
93
|
*/
|
|
60
94
|
stepUntil(condition: () => boolean, options?: StepUntilOptions): Promise<number>;
|
|
61
95
|
}
|
|
@@ -102,4 +136,4 @@ declare module "vitest" {
|
|
|
102
136
|
}
|
|
103
137
|
}
|
|
104
138
|
|
|
105
|
-
export { ManualGameClock, type ReactPlaythroughArguments, type ReactPlaythroughAuditInput, type ReactPlaythroughAuditResult, type ReactPlaythroughEvidence, type ReactPlaythroughMetadata, type ReactPlaythroughTest, type StepUntilOptions, auditReactPlaythroughRun, playthroughTest };
|
|
139
|
+
export { ManualGameClock, type ReactPlaythroughArguments, type ReactPlaythroughAuditInput, type ReactPlaythroughAuditResult, type ReactPlaythroughCheckpointKind, type ReactPlaythroughEvidence, type ReactPlaythroughInputKind, type ReactPlaythroughMetadata, type ReactPlaythroughTest, type StepUntilOptions, auditReactPlaythroughRun, playthroughTest };
|
package/dist/react/testing.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { G as GameClock, a as GameFrameCallback } from '../game-clock-suUidZdT.js';
|
|
2
2
|
import { RenderResult } from '@testing-library/react';
|
|
3
|
+
import { UserEvent } from '@testing-library/user-event';
|
|
3
4
|
import { ReactNode } from 'react';
|
|
5
|
+
import { ExpectStatic } from 'vitest';
|
|
4
6
|
|
|
5
7
|
/** 测试用确定性时钟;应用测试推进它,不直接伪造游戏状态。 */
|
|
6
8
|
declare class ManualGameClock implements GameClock {
|
|
@@ -26,36 +28,68 @@ interface StepUntilOptions {
|
|
|
26
28
|
maxSteps?: number;
|
|
27
29
|
/** 每轮推进一单位游戏自有时间或工作;即时回合制结果可以省略。 */
|
|
28
30
|
step?: (step: number) => void | Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Optional read-only state used to make bounded failures actionable.
|
|
33
|
+
* The value is sampled only when the bound is exhausted.
|
|
34
|
+
*/
|
|
35
|
+
diagnostics?: () => unknown;
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
/** 单条 React 主流程测试留下的可序列化运行期证据。 */
|
|
32
39
|
interface ReactPlaythroughEvidence {
|
|
33
40
|
/** 捕获阶段观察到的键盘、指针或触摸类 DOM 输入总数。 */
|
|
34
41
|
domInputEvents: number;
|
|
42
|
+
/** 由 performInput 明确标记的入口输入次数。 */
|
|
43
|
+
entryInputs: number;
|
|
44
|
+
/** 由 performInput 明确标记的核心玩法输入次数。 */
|
|
45
|
+
primaryInputs: number;
|
|
35
46
|
/** 在步数上限内成功满足条件的 stepUntil 次数。 */
|
|
36
47
|
boundedRuns: number;
|
|
37
48
|
/** 最后一次 stepUntil 成功后新增的 Vitest 断言数。 */
|
|
38
49
|
assertionsAfterOutcome: number;
|
|
50
|
+
/** 已验证相对 entered snapshot 发生变化的进展或终局 checkpoint 次数。 */
|
|
51
|
+
changedOutcomeCheckpoints: number;
|
|
52
|
+
/** 已记录的 checkpoint 类型,供 reporter 给出精确提示。 */
|
|
53
|
+
checkpoints: ReactPlaythroughCheckpointKind[];
|
|
39
54
|
/** helper 是否已经完成全部证据校验。 */
|
|
40
55
|
verified: boolean;
|
|
41
56
|
}
|
|
42
57
|
/** 通过 Vitest task metadata 从 worker 传递给主线程 reporter 的数据。 */
|
|
43
58
|
interface ReactPlaythroughMetadata {
|
|
44
59
|
/** metadata 结构版本,用于拒绝无法识别的旧数据。 */
|
|
45
|
-
version:
|
|
60
|
+
version: 2;
|
|
46
61
|
/** 静态、非交互项目跳过主流程验证时必须提供的理由。 */
|
|
47
62
|
waiverReason?: string;
|
|
48
63
|
/** 测试执行期间持续更新的客观证据。 */
|
|
49
64
|
evidence: ReactPlaythroughEvidence;
|
|
50
65
|
}
|
|
51
66
|
|
|
67
|
+
/** 玩家输入在最低可玩流程中的语义阶段。 */
|
|
68
|
+
type ReactPlaythroughInputKind = "entry" | "primary";
|
|
69
|
+
/** 可审计状态证据;entered 是基线,progress/terminal 是有效结果。 */
|
|
70
|
+
type ReactPlaythroughCheckpointKind = "entered" | "progress" | "terminal";
|
|
52
71
|
/** 主流程回调唯一需要学习的测试工具。 */
|
|
53
72
|
interface ReactPlaythroughArguments {
|
|
54
73
|
/** Testing Library 对生产游戏入口的渲染结果。 */
|
|
55
74
|
view: RenderResult;
|
|
75
|
+
/** 已由 playthroughTest 创建的真实 DOM 用户输入驱动。 */
|
|
76
|
+
user: UserEvent;
|
|
77
|
+
/** Vitest 绑定到当前 test 的断言 API;结果断言必须使用这个实例。 */
|
|
78
|
+
expect: ExpectStatic;
|
|
79
|
+
/**
|
|
80
|
+
* 包裹一段真实 DOM 输入并声明用途。
|
|
81
|
+
* entry 用于进入合法游戏状态,primary 用于移动、挥拍、拖拽、落子等核心操作。
|
|
82
|
+
*/
|
|
83
|
+
performInput(kind: ReactPlaythroughInputKind, input: () => void | Promise<void>): Promise<void>;
|
|
84
|
+
/**
|
|
85
|
+
* 记录只读状态证据。先在 entry 后记录 entered,再在 primary 和 stepUntil
|
|
86
|
+
* 后记录 progress 或 terminal;Canvas 游戏应传 Telemetry snapshot。
|
|
87
|
+
*/
|
|
88
|
+
checkpoint(kind: ReactPlaythroughCheckpointKind, snapshot: unknown): void;
|
|
56
89
|
/**
|
|
57
90
|
* 在硬性步数上限内等待权威结果。
|
|
58
91
|
* 返回后必须再断言该结果,进入中间 active/running 状态不算完成。
|
|
92
|
+
* 即时结果不需要 step;时间、帧或自定义 scheduler 流程必须传入确定性 step。
|
|
59
93
|
*/
|
|
60
94
|
stepUntil(condition: () => boolean, options?: StepUntilOptions): Promise<number>;
|
|
61
95
|
}
|
|
@@ -102,4 +136,4 @@ declare module "vitest" {
|
|
|
102
136
|
}
|
|
103
137
|
}
|
|
104
138
|
|
|
105
|
-
export { ManualGameClock, type ReactPlaythroughArguments, type ReactPlaythroughAuditInput, type ReactPlaythroughAuditResult, type ReactPlaythroughEvidence, type ReactPlaythroughMetadata, type ReactPlaythroughTest, type StepUntilOptions, auditReactPlaythroughRun, playthroughTest };
|
|
139
|
+
export { ManualGameClock, type ReactPlaythroughArguments, type ReactPlaythroughAuditInput, type ReactPlaythroughAuditResult, type ReactPlaythroughCheckpointKind, type ReactPlaythroughEvidence, type ReactPlaythroughInputKind, type ReactPlaythroughMetadata, type ReactPlaythroughTest, type StepUntilOptions, auditReactPlaythroughRun, playthroughTest };
|
package/dist/react/testing.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/react/testing.ts
|
|
@@ -83,10 +93,21 @@ var ManualGameClock = class {
|
|
|
83
93
|
|
|
84
94
|
// src/react/react-playthrough.ts
|
|
85
95
|
var import_react2 = require("@testing-library/react");
|
|
96
|
+
var import_user_event = __toESM(require("@testing-library/user-event"));
|
|
86
97
|
var import_vitest = require("vitest");
|
|
87
98
|
|
|
88
99
|
// src/react/react-playthrough-core.ts
|
|
89
100
|
var import_react = require("@testing-library/react");
|
|
101
|
+
function formatDiagnostics(read) {
|
|
102
|
+
if (!read) return void 0;
|
|
103
|
+
try {
|
|
104
|
+
const value = read();
|
|
105
|
+
if (typeof value === "string") return value;
|
|
106
|
+
return JSON.stringify(value);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return `diagnostics() threw: ${String(error)}`;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
90
111
|
function normalizePlaythroughWaiverReason(waiverReason) {
|
|
91
112
|
if (waiverReason === void 0) return void 0;
|
|
92
113
|
const reason = waiverReason.trim();
|
|
@@ -112,8 +133,11 @@ async function runBoundedUntil(condition, options = {}) {
|
|
|
112
133
|
});
|
|
113
134
|
}
|
|
114
135
|
}
|
|
136
|
+
const diagnostics = formatDiagnostics(options.diagnostics);
|
|
137
|
+
const guidance = options.step ? "The step callback ran, but the authoritative outcome did not change." : "No step callback was provided, so time-driven gameplay was not advanced. Inject a ManualGameClock for this test and pass step: () => clock.stepFrame().";
|
|
138
|
+
const suffix = diagnostics ? ` Last diagnostics: ${diagnostics}` : "";
|
|
115
139
|
throw new Error(
|
|
116
|
-
`Playthrough outcome was not reached within ${maxSteps} steps
|
|
140
|
+
`Playthrough outcome was not reached within ${maxSteps} steps. ${guidance}${suffix}`
|
|
117
141
|
);
|
|
118
142
|
}
|
|
119
143
|
|
|
@@ -127,27 +151,54 @@ var INPUT_EVENTS = [
|
|
|
127
151
|
"touchstart",
|
|
128
152
|
"touchend"
|
|
129
153
|
];
|
|
154
|
+
function describeMissingEvidence(evidence) {
|
|
155
|
+
if (!evidence || evidence.entryInputs === 0) return "entry \u8F93\u5165";
|
|
156
|
+
if (evidence.primaryInputs === 0) return "primary \u8F93\u5165";
|
|
157
|
+
if (!evidence.checkpoints.includes("entered")) return "entered checkpoint";
|
|
158
|
+
if (evidence.boundedRuns === 0) return "\u6709\u754C stepUntil";
|
|
159
|
+
if (evidence.assertionsAfterOutcome === 0) return "stepUntil \u540E\u7684\u7ED3\u679C\u65AD\u8A00";
|
|
160
|
+
if (evidence.changedOutcomeCheckpoints === 0) {
|
|
161
|
+
return "\u53D1\u751F\u72B6\u6001\u53D8\u5316\u7684 progress/terminal checkpoint";
|
|
162
|
+
}
|
|
163
|
+
return "\u5B8C\u6574\u7684 playthrough \u6821\u9A8C\u6807\u8BB0";
|
|
164
|
+
}
|
|
130
165
|
function createMetadata(waiverReason) {
|
|
131
166
|
return {
|
|
132
|
-
version:
|
|
167
|
+
version: 2,
|
|
133
168
|
waiverReason,
|
|
134
169
|
evidence: {
|
|
135
170
|
domInputEvents: 0,
|
|
171
|
+
entryInputs: 0,
|
|
172
|
+
primaryInputs: 0,
|
|
136
173
|
boundedRuns: 0,
|
|
137
174
|
assertionsAfterOutcome: 0,
|
|
175
|
+
changedOutcomeCheckpoints: 0,
|
|
176
|
+
checkpoints: [],
|
|
138
177
|
verified: false
|
|
139
178
|
}
|
|
140
179
|
};
|
|
141
180
|
}
|
|
181
|
+
function snapshotFingerprint(snapshot) {
|
|
182
|
+
try {
|
|
183
|
+
const serialized = JSON.stringify(snapshot);
|
|
184
|
+
if (serialized === void 0) throw new Error("unsupported value");
|
|
185
|
+
return serialized;
|
|
186
|
+
} catch {
|
|
187
|
+
throw new Error(
|
|
188
|
+
"checkpoint snapshot must be JSON-serializable. Pass a read-only Telemetry snapshot or a small visible-state object."
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
142
192
|
function definePlaythrough(element, run, waiverReason) {
|
|
143
193
|
const reason = normalizePlaythroughWaiverReason(waiverReason);
|
|
144
194
|
const metadata = createMetadata(reason);
|
|
145
195
|
(0, import_vitest.test)("production game completes a bounded playthrough", {
|
|
146
196
|
skip: Boolean(reason),
|
|
147
197
|
meta: { reactPlaythrough: metadata }
|
|
148
|
-
}, async () => {
|
|
198
|
+
}, async ({ expect }) => {
|
|
149
199
|
const evidence = metadata.evidence;
|
|
150
200
|
let assertionsAtOutcome;
|
|
201
|
+
let enteredSnapshot;
|
|
151
202
|
const recordInput = () => {
|
|
152
203
|
evidence.domInputEvents += 1;
|
|
153
204
|
};
|
|
@@ -161,25 +212,94 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
161
212
|
"playthroughTest must render the production game entry."
|
|
162
213
|
);
|
|
163
214
|
}
|
|
215
|
+
const user = import_user_event.default.setup();
|
|
164
216
|
await run({
|
|
165
217
|
view,
|
|
218
|
+
user,
|
|
219
|
+
expect,
|
|
220
|
+
async performInput(kind, input) {
|
|
221
|
+
if (kind === "entry" && enteredSnapshot !== void 0) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
'performInput("entry") must run before checkpoint("entered"). Group multiple setup actions in the same callback.'
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
if (kind === "primary" && enteredSnapshot === void 0) {
|
|
227
|
+
throw new Error(
|
|
228
|
+
'Before performInput("primary"), run an entry input and checkpoint("entered", snapshot).'
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
const inputsBefore = evidence.domInputEvents;
|
|
232
|
+
await input();
|
|
233
|
+
if (evidence.domInputEvents === inputsBefore) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
`performInput("${kind}") did not dispatch a supported production DOM input. Use the provided user or dispatch a real keyboard, pointer, or touch event to the production target.`
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
if (kind === "entry") evidence.entryInputs += 1;
|
|
239
|
+
else evidence.primaryInputs += 1;
|
|
240
|
+
},
|
|
241
|
+
checkpoint(kind, snapshot) {
|
|
242
|
+
const fingerprint = snapshotFingerprint(snapshot);
|
|
243
|
+
if (kind === "entered") {
|
|
244
|
+
if (enteredSnapshot !== void 0) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
'checkpoint("entered") may only be recorded once, before the primary input.'
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
if (evidence.entryInputs === 0) {
|
|
250
|
+
throw new Error(
|
|
251
|
+
'checkpoint("entered") must follow performInput("entry", ...).'
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
enteredSnapshot = fingerprint;
|
|
255
|
+
evidence.checkpoints.push(kind);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (evidence.primaryInputs === 0) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
`checkpoint("${kind}") must follow performInput("primary", ...).`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
if (evidence.boundedRuns === 0) {
|
|
264
|
+
throw new Error(
|
|
265
|
+
`checkpoint("${kind}") must be recorded after stepUntil returns.`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
if (assertionsAtOutcome === void 0 || expect.getState().assertionCalls <= assertionsAtOutcome) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
`Use the expect provided by playthroughTest to assert the authoritative result after stepUntil, then record checkpoint("${kind}", snapshot).`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
if (fingerprint === enteredSnapshot) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
`checkpoint("${kind}") matches the entered snapshot. Assert a production state change caused by the primary input.`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
evidence.changedOutcomeCheckpoints += 1;
|
|
279
|
+
evidence.checkpoints.push(kind);
|
|
280
|
+
},
|
|
166
281
|
async stepUntil(condition, options = {}) {
|
|
167
282
|
const steps = await runBoundedUntil(condition, options);
|
|
168
|
-
if (evidence.
|
|
283
|
+
if (evidence.primaryInputs === 0) {
|
|
169
284
|
throw new Error(
|
|
170
|
-
|
|
285
|
+
'stepUntil must follow performInput("primary", ...). A menu/help click is not gameplay evidence.'
|
|
171
286
|
);
|
|
172
287
|
}
|
|
173
288
|
evidence.boundedRuns += 1;
|
|
174
|
-
assertionsAtOutcome =
|
|
289
|
+
assertionsAtOutcome = expect.getState().assertionCalls;
|
|
175
290
|
return steps;
|
|
176
291
|
}
|
|
177
292
|
});
|
|
178
|
-
const assertionCalls =
|
|
293
|
+
const assertionCalls = expect.getState().assertionCalls;
|
|
179
294
|
evidence.assertionsAfterOutcome = assertionsAtOutcome === void 0 ? 0 : assertionCalls - assertionsAtOutcome;
|
|
180
|
-
if (evidence.
|
|
295
|
+
if (evidence.entryInputs === 0) {
|
|
181
296
|
throw new Error(
|
|
182
|
-
|
|
297
|
+
'playthroughTest must perform an entry input with performInput("entry", ...).'
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
if (evidence.primaryInputs === 0) {
|
|
301
|
+
throw new Error(
|
|
302
|
+
'playthroughTest must perform a core game action with performInput("primary", ...).'
|
|
183
303
|
);
|
|
184
304
|
}
|
|
185
305
|
if (evidence.boundedRuns === 0) {
|
|
@@ -187,7 +307,12 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
187
307
|
}
|
|
188
308
|
if (evidence.assertionsAfterOutcome === 0) {
|
|
189
309
|
throw new Error(
|
|
190
|
-
"
|
|
310
|
+
"Use the expect provided by playthroughTest to assert an authoritative game outcome after stepUntil returns."
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
if (evidence.changedOutcomeCheckpoints === 0) {
|
|
314
|
+
throw new Error(
|
|
315
|
+
'After asserting the result, record checkpoint("progress", snapshot) or checkpoint("terminal", snapshot). The snapshot must differ from checkpoint("entered").'
|
|
191
316
|
);
|
|
192
317
|
}
|
|
193
318
|
evidence.verified = true;
|
|
@@ -208,7 +333,9 @@ function auditReactPlaythroughRun(tests) {
|
|
|
208
333
|
const declared = tests.filter((candidate) => candidate.metadata);
|
|
209
334
|
const valid = declared.filter(({ state, metadata }) => {
|
|
210
335
|
const evidence = metadata?.evidence;
|
|
211
|
-
return state === "passed" && evidence?.verified === true && evidence.domInputEvents > 0 && evidence.boundedRuns > 0 && evidence.assertionsAfterOutcome > 0
|
|
336
|
+
return state === "passed" && evidence?.verified === true && evidence.domInputEvents > 0 && evidence.entryInputs > 0 && evidence.primaryInputs > 0 && evidence.boundedRuns > 0 && evidence.assertionsAfterOutcome > 0 && evidence.changedOutcomeCheckpoints > 0 && evidence.checkpoints.includes("entered") && evidence.checkpoints.some(
|
|
337
|
+
(checkpoint) => checkpoint === "progress" || checkpoint === "terminal"
|
|
338
|
+
);
|
|
212
339
|
});
|
|
213
340
|
const waivers = declared.filter(
|
|
214
341
|
({ state, metadata }) => state === "skipped" && (metadata?.waiverReason?.trim().length ?? 0) >= 20
|
|
@@ -216,7 +343,7 @@ function auditReactPlaythroughRun(tests) {
|
|
|
216
343
|
const issues = [];
|
|
217
344
|
if (declared.length === 0) {
|
|
218
345
|
issues.push(
|
|
219
|
-
|
|
346
|
+
'\u7F3A\u5C11\u751F\u4EA7\u6E38\u620F\u53EF\u73A9\u6027\u9A8C\u8BC1\uFF1A\u4F7F\u7528 playthroughTest \u6E32\u67D3 <App />\uFF0C\u4F9D\u6B21\u6267\u884C performInput("entry")\u3001checkpoint("entered")\u3001performInput("primary")\u3001stepUntil\u3001\u7528 playthroughTest \u63D0\u4F9B\u7684 expect \u65AD\u8A00\u7ED3\u679C\uFF0C\u5E76\u8BB0\u5F55 progress/terminal checkpoint\u3002'
|
|
220
347
|
);
|
|
221
348
|
} else {
|
|
222
349
|
for (const candidate of declared) {
|
|
@@ -230,9 +357,8 @@ function auditReactPlaythroughRun(tests) {
|
|
|
230
357
|
} else if (candidate.state !== "passed") {
|
|
231
358
|
issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7684\u72B6\u6001\u4E3A ${candidate.state}\u3002`);
|
|
232
359
|
} else {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
);
|
|
360
|
+
const missing = describeMissingEvidence(candidate.metadata?.evidence);
|
|
361
|
+
issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7F3A\u5C11${missing}\u3002`);
|
|
236
362
|
}
|
|
237
363
|
}
|
|
238
364
|
}
|
package/dist/react/testing.mjs
CHANGED
|
@@ -55,10 +55,21 @@ var ManualGameClock = class {
|
|
|
55
55
|
|
|
56
56
|
// src/react/react-playthrough.ts
|
|
57
57
|
import { render } from "@testing-library/react";
|
|
58
|
-
import
|
|
58
|
+
import userEvent from "@testing-library/user-event";
|
|
59
|
+
import { test } from "vitest";
|
|
59
60
|
|
|
60
61
|
// src/react/react-playthrough-core.ts
|
|
61
62
|
import { act } from "@testing-library/react";
|
|
63
|
+
function formatDiagnostics(read) {
|
|
64
|
+
if (!read) return void 0;
|
|
65
|
+
try {
|
|
66
|
+
const value = read();
|
|
67
|
+
if (typeof value === "string") return value;
|
|
68
|
+
return JSON.stringify(value);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return `diagnostics() threw: ${String(error)}`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
62
73
|
function normalizePlaythroughWaiverReason(waiverReason) {
|
|
63
74
|
if (waiverReason === void 0) return void 0;
|
|
64
75
|
const reason = waiverReason.trim();
|
|
@@ -84,8 +95,11 @@ async function runBoundedUntil(condition, options = {}) {
|
|
|
84
95
|
});
|
|
85
96
|
}
|
|
86
97
|
}
|
|
98
|
+
const diagnostics = formatDiagnostics(options.diagnostics);
|
|
99
|
+
const guidance = options.step ? "The step callback ran, but the authoritative outcome did not change." : "No step callback was provided, so time-driven gameplay was not advanced. Inject a ManualGameClock for this test and pass step: () => clock.stepFrame().";
|
|
100
|
+
const suffix = diagnostics ? ` Last diagnostics: ${diagnostics}` : "";
|
|
87
101
|
throw new Error(
|
|
88
|
-
`Playthrough outcome was not reached within ${maxSteps} steps
|
|
102
|
+
`Playthrough outcome was not reached within ${maxSteps} steps. ${guidance}${suffix}`
|
|
89
103
|
);
|
|
90
104
|
}
|
|
91
105
|
|
|
@@ -99,27 +113,54 @@ var INPUT_EVENTS = [
|
|
|
99
113
|
"touchstart",
|
|
100
114
|
"touchend"
|
|
101
115
|
];
|
|
116
|
+
function describeMissingEvidence(evidence) {
|
|
117
|
+
if (!evidence || evidence.entryInputs === 0) return "entry \u8F93\u5165";
|
|
118
|
+
if (evidence.primaryInputs === 0) return "primary \u8F93\u5165";
|
|
119
|
+
if (!evidence.checkpoints.includes("entered")) return "entered checkpoint";
|
|
120
|
+
if (evidence.boundedRuns === 0) return "\u6709\u754C stepUntil";
|
|
121
|
+
if (evidence.assertionsAfterOutcome === 0) return "stepUntil \u540E\u7684\u7ED3\u679C\u65AD\u8A00";
|
|
122
|
+
if (evidence.changedOutcomeCheckpoints === 0) {
|
|
123
|
+
return "\u53D1\u751F\u72B6\u6001\u53D8\u5316\u7684 progress/terminal checkpoint";
|
|
124
|
+
}
|
|
125
|
+
return "\u5B8C\u6574\u7684 playthrough \u6821\u9A8C\u6807\u8BB0";
|
|
126
|
+
}
|
|
102
127
|
function createMetadata(waiverReason) {
|
|
103
128
|
return {
|
|
104
|
-
version:
|
|
129
|
+
version: 2,
|
|
105
130
|
waiverReason,
|
|
106
131
|
evidence: {
|
|
107
132
|
domInputEvents: 0,
|
|
133
|
+
entryInputs: 0,
|
|
134
|
+
primaryInputs: 0,
|
|
108
135
|
boundedRuns: 0,
|
|
109
136
|
assertionsAfterOutcome: 0,
|
|
137
|
+
changedOutcomeCheckpoints: 0,
|
|
138
|
+
checkpoints: [],
|
|
110
139
|
verified: false
|
|
111
140
|
}
|
|
112
141
|
};
|
|
113
142
|
}
|
|
143
|
+
function snapshotFingerprint(snapshot) {
|
|
144
|
+
try {
|
|
145
|
+
const serialized = JSON.stringify(snapshot);
|
|
146
|
+
if (serialized === void 0) throw new Error("unsupported value");
|
|
147
|
+
return serialized;
|
|
148
|
+
} catch {
|
|
149
|
+
throw new Error(
|
|
150
|
+
"checkpoint snapshot must be JSON-serializable. Pass a read-only Telemetry snapshot or a small visible-state object."
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
114
154
|
function definePlaythrough(element, run, waiverReason) {
|
|
115
155
|
const reason = normalizePlaythroughWaiverReason(waiverReason);
|
|
116
156
|
const metadata = createMetadata(reason);
|
|
117
157
|
test("production game completes a bounded playthrough", {
|
|
118
158
|
skip: Boolean(reason),
|
|
119
159
|
meta: { reactPlaythrough: metadata }
|
|
120
|
-
}, async () => {
|
|
160
|
+
}, async ({ expect }) => {
|
|
121
161
|
const evidence = metadata.evidence;
|
|
122
162
|
let assertionsAtOutcome;
|
|
163
|
+
let enteredSnapshot;
|
|
123
164
|
const recordInput = () => {
|
|
124
165
|
evidence.domInputEvents += 1;
|
|
125
166
|
};
|
|
@@ -133,13 +174,77 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
133
174
|
"playthroughTest must render the production game entry."
|
|
134
175
|
);
|
|
135
176
|
}
|
|
177
|
+
const user = userEvent.setup();
|
|
136
178
|
await run({
|
|
137
179
|
view,
|
|
180
|
+
user,
|
|
181
|
+
expect,
|
|
182
|
+
async performInput(kind, input) {
|
|
183
|
+
if (kind === "entry" && enteredSnapshot !== void 0) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
'performInput("entry") must run before checkpoint("entered"). Group multiple setup actions in the same callback.'
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
if (kind === "primary" && enteredSnapshot === void 0) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
'Before performInput("primary"), run an entry input and checkpoint("entered", snapshot).'
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
const inputsBefore = evidence.domInputEvents;
|
|
194
|
+
await input();
|
|
195
|
+
if (evidence.domInputEvents === inputsBefore) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`performInput("${kind}") did not dispatch a supported production DOM input. Use the provided user or dispatch a real keyboard, pointer, or touch event to the production target.`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
if (kind === "entry") evidence.entryInputs += 1;
|
|
201
|
+
else evidence.primaryInputs += 1;
|
|
202
|
+
},
|
|
203
|
+
checkpoint(kind, snapshot) {
|
|
204
|
+
const fingerprint = snapshotFingerprint(snapshot);
|
|
205
|
+
if (kind === "entered") {
|
|
206
|
+
if (enteredSnapshot !== void 0) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
'checkpoint("entered") may only be recorded once, before the primary input.'
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
if (evidence.entryInputs === 0) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
'checkpoint("entered") must follow performInput("entry", ...).'
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
enteredSnapshot = fingerprint;
|
|
217
|
+
evidence.checkpoints.push(kind);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (evidence.primaryInputs === 0) {
|
|
221
|
+
throw new Error(
|
|
222
|
+
`checkpoint("${kind}") must follow performInput("primary", ...).`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
if (evidence.boundedRuns === 0) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
`checkpoint("${kind}") must be recorded after stepUntil returns.`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
if (assertionsAtOutcome === void 0 || expect.getState().assertionCalls <= assertionsAtOutcome) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
`Use the expect provided by playthroughTest to assert the authoritative result after stepUntil, then record checkpoint("${kind}", snapshot).`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
if (fingerprint === enteredSnapshot) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
`checkpoint("${kind}") matches the entered snapshot. Assert a production state change caused by the primary input.`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
evidence.changedOutcomeCheckpoints += 1;
|
|
241
|
+
evidence.checkpoints.push(kind);
|
|
242
|
+
},
|
|
138
243
|
async stepUntil(condition, options = {}) {
|
|
139
244
|
const steps = await runBoundedUntil(condition, options);
|
|
140
|
-
if (evidence.
|
|
245
|
+
if (evidence.primaryInputs === 0) {
|
|
141
246
|
throw new Error(
|
|
142
|
-
|
|
247
|
+
'stepUntil must follow performInput("primary", ...). A menu/help click is not gameplay evidence.'
|
|
143
248
|
);
|
|
144
249
|
}
|
|
145
250
|
evidence.boundedRuns += 1;
|
|
@@ -149,9 +254,14 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
149
254
|
});
|
|
150
255
|
const assertionCalls = expect.getState().assertionCalls;
|
|
151
256
|
evidence.assertionsAfterOutcome = assertionsAtOutcome === void 0 ? 0 : assertionCalls - assertionsAtOutcome;
|
|
152
|
-
if (evidence.
|
|
257
|
+
if (evidence.entryInputs === 0) {
|
|
153
258
|
throw new Error(
|
|
154
|
-
|
|
259
|
+
'playthroughTest must perform an entry input with performInput("entry", ...).'
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
if (evidence.primaryInputs === 0) {
|
|
263
|
+
throw new Error(
|
|
264
|
+
'playthroughTest must perform a core game action with performInput("primary", ...).'
|
|
155
265
|
);
|
|
156
266
|
}
|
|
157
267
|
if (evidence.boundedRuns === 0) {
|
|
@@ -159,7 +269,12 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
159
269
|
}
|
|
160
270
|
if (evidence.assertionsAfterOutcome === 0) {
|
|
161
271
|
throw new Error(
|
|
162
|
-
"
|
|
272
|
+
"Use the expect provided by playthroughTest to assert an authoritative game outcome after stepUntil returns."
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
if (evidence.changedOutcomeCheckpoints === 0) {
|
|
276
|
+
throw new Error(
|
|
277
|
+
'After asserting the result, record checkpoint("progress", snapshot) or checkpoint("terminal", snapshot). The snapshot must differ from checkpoint("entered").'
|
|
163
278
|
);
|
|
164
279
|
}
|
|
165
280
|
evidence.verified = true;
|
|
@@ -180,7 +295,9 @@ function auditReactPlaythroughRun(tests) {
|
|
|
180
295
|
const declared = tests.filter((candidate) => candidate.metadata);
|
|
181
296
|
const valid = declared.filter(({ state, metadata }) => {
|
|
182
297
|
const evidence = metadata?.evidence;
|
|
183
|
-
return state === "passed" && evidence?.verified === true && evidence.domInputEvents > 0 && evidence.boundedRuns > 0 && evidence.assertionsAfterOutcome > 0
|
|
298
|
+
return state === "passed" && evidence?.verified === true && evidence.domInputEvents > 0 && evidence.entryInputs > 0 && evidence.primaryInputs > 0 && evidence.boundedRuns > 0 && evidence.assertionsAfterOutcome > 0 && evidence.changedOutcomeCheckpoints > 0 && evidence.checkpoints.includes("entered") && evidence.checkpoints.some(
|
|
299
|
+
(checkpoint) => checkpoint === "progress" || checkpoint === "terminal"
|
|
300
|
+
);
|
|
184
301
|
});
|
|
185
302
|
const waivers = declared.filter(
|
|
186
303
|
({ state, metadata }) => state === "skipped" && (metadata?.waiverReason?.trim().length ?? 0) >= 20
|
|
@@ -188,7 +305,7 @@ function auditReactPlaythroughRun(tests) {
|
|
|
188
305
|
const issues = [];
|
|
189
306
|
if (declared.length === 0) {
|
|
190
307
|
issues.push(
|
|
191
|
-
|
|
308
|
+
'\u7F3A\u5C11\u751F\u4EA7\u6E38\u620F\u53EF\u73A9\u6027\u9A8C\u8BC1\uFF1A\u4F7F\u7528 playthroughTest \u6E32\u67D3 <App />\uFF0C\u4F9D\u6B21\u6267\u884C performInput("entry")\u3001checkpoint("entered")\u3001performInput("primary")\u3001stepUntil\u3001\u7528 playthroughTest \u63D0\u4F9B\u7684 expect \u65AD\u8A00\u7ED3\u679C\uFF0C\u5E76\u8BB0\u5F55 progress/terminal checkpoint\u3002'
|
|
192
309
|
);
|
|
193
310
|
} else {
|
|
194
311
|
for (const candidate of declared) {
|
|
@@ -202,9 +319,8 @@ function auditReactPlaythroughRun(tests) {
|
|
|
202
319
|
} else if (candidate.state !== "passed") {
|
|
203
320
|
issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7684\u72B6\u6001\u4E3A ${candidate.state}\u3002`);
|
|
204
321
|
} else {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
);
|
|
322
|
+
const missing = describeMissingEvidence(candidate.metadata?.evidence);
|
|
323
|
+
issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7F3A\u5C11${missing}\u3002`);
|
|
208
324
|
}
|
|
209
325
|
}
|
|
210
326
|
}
|