miaoda-game-devkit 0.2.17 → 0.2.20
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 +14 -9
- 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 +27 -2
- package/dist/react/testing.d.ts +27 -2
- package/dist/react/testing.js +114 -14
- package/dist/react/testing.mjs +113 -13
- package/dist/react/vitest-config.js +118 -17
- package/dist/react/vitest-config.mjs +117 -16
- 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 +1 -1
package/dist/react/testing.d.mts
CHANGED
|
@@ -2,6 +2,7 @@ import { G as GameClock, a as GameFrameCallback } from '../game-clock-suUidZdT.m
|
|
|
2
2
|
import { RenderResult } from '@testing-library/react';
|
|
3
3
|
import { UserEvent } from '@testing-library/user-event';
|
|
4
4
|
import { ReactNode } from 'react';
|
|
5
|
+
import { ExpectStatic } from 'vitest';
|
|
5
6
|
|
|
6
7
|
/** 测试用确定性时钟;应用测试推进它,不直接伪造游戏状态。 */
|
|
7
8
|
declare class ManualGameClock implements GameClock {
|
|
@@ -38,29 +39,53 @@ interface StepUntilOptions {
|
|
|
38
39
|
interface ReactPlaythroughEvidence {
|
|
39
40
|
/** 捕获阶段观察到的键盘、指针或触摸类 DOM 输入总数。 */
|
|
40
41
|
domInputEvents: number;
|
|
42
|
+
/** 由 performInput 明确标记的入口输入次数。 */
|
|
43
|
+
entryInputs: number;
|
|
44
|
+
/** 由 performInput 明确标记的核心玩法输入次数。 */
|
|
45
|
+
primaryInputs: number;
|
|
41
46
|
/** 在步数上限内成功满足条件的 stepUntil 次数。 */
|
|
42
47
|
boundedRuns: number;
|
|
43
48
|
/** 最后一次 stepUntil 成功后新增的 Vitest 断言数。 */
|
|
44
49
|
assertionsAfterOutcome: number;
|
|
50
|
+
/** 已验证相对 entered snapshot 发生变化的进展或终局 checkpoint 次数。 */
|
|
51
|
+
changedOutcomeCheckpoints: number;
|
|
52
|
+
/** 已记录的 checkpoint 类型,供 reporter 给出精确提示。 */
|
|
53
|
+
checkpoints: ReactPlaythroughCheckpointKind[];
|
|
45
54
|
/** helper 是否已经完成全部证据校验。 */
|
|
46
55
|
verified: boolean;
|
|
47
56
|
}
|
|
48
57
|
/** 通过 Vitest task metadata 从 worker 传递给主线程 reporter 的数据。 */
|
|
49
58
|
interface ReactPlaythroughMetadata {
|
|
50
59
|
/** metadata 结构版本,用于拒绝无法识别的旧数据。 */
|
|
51
|
-
version:
|
|
60
|
+
version: 2;
|
|
52
61
|
/** 静态、非交互项目跳过主流程验证时必须提供的理由。 */
|
|
53
62
|
waiverReason?: string;
|
|
54
63
|
/** 测试执行期间持续更新的客观证据。 */
|
|
55
64
|
evidence: ReactPlaythroughEvidence;
|
|
56
65
|
}
|
|
57
66
|
|
|
67
|
+
/** 玩家输入在最低可玩流程中的语义阶段。 */
|
|
68
|
+
type ReactPlaythroughInputKind = "entry" | "primary";
|
|
69
|
+
/** 可审计状态证据;entered 是基线,progress/terminal 是有效结果。 */
|
|
70
|
+
type ReactPlaythroughCheckpointKind = "entered" | "progress" | "terminal";
|
|
58
71
|
/** 主流程回调唯一需要学习的测试工具。 */
|
|
59
72
|
interface ReactPlaythroughArguments {
|
|
60
73
|
/** Testing Library 对生产游戏入口的渲染结果。 */
|
|
61
74
|
view: RenderResult;
|
|
62
75
|
/** 已由 playthroughTest 创建的真实 DOM 用户输入驱动。 */
|
|
63
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;
|
|
64
89
|
/**
|
|
65
90
|
* 在硬性步数上限内等待权威结果。
|
|
66
91
|
* 返回后必须再断言该结果,进入中间 active/running 状态不算完成。
|
|
@@ -111,4 +136,4 @@ declare module "vitest" {
|
|
|
111
136
|
}
|
|
112
137
|
}
|
|
113
138
|
|
|
114
|
-
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
|
@@ -2,6 +2,7 @@ import { G as GameClock, a as GameFrameCallback } from '../game-clock-suUidZdT.j
|
|
|
2
2
|
import { RenderResult } from '@testing-library/react';
|
|
3
3
|
import { UserEvent } from '@testing-library/user-event';
|
|
4
4
|
import { ReactNode } from 'react';
|
|
5
|
+
import { ExpectStatic } from 'vitest';
|
|
5
6
|
|
|
6
7
|
/** 测试用确定性时钟;应用测试推进它,不直接伪造游戏状态。 */
|
|
7
8
|
declare class ManualGameClock implements GameClock {
|
|
@@ -38,29 +39,53 @@ interface StepUntilOptions {
|
|
|
38
39
|
interface ReactPlaythroughEvidence {
|
|
39
40
|
/** 捕获阶段观察到的键盘、指针或触摸类 DOM 输入总数。 */
|
|
40
41
|
domInputEvents: number;
|
|
42
|
+
/** 由 performInput 明确标记的入口输入次数。 */
|
|
43
|
+
entryInputs: number;
|
|
44
|
+
/** 由 performInput 明确标记的核心玩法输入次数。 */
|
|
45
|
+
primaryInputs: number;
|
|
41
46
|
/** 在步数上限内成功满足条件的 stepUntil 次数。 */
|
|
42
47
|
boundedRuns: number;
|
|
43
48
|
/** 最后一次 stepUntil 成功后新增的 Vitest 断言数。 */
|
|
44
49
|
assertionsAfterOutcome: number;
|
|
50
|
+
/** 已验证相对 entered snapshot 发生变化的进展或终局 checkpoint 次数。 */
|
|
51
|
+
changedOutcomeCheckpoints: number;
|
|
52
|
+
/** 已记录的 checkpoint 类型,供 reporter 给出精确提示。 */
|
|
53
|
+
checkpoints: ReactPlaythroughCheckpointKind[];
|
|
45
54
|
/** helper 是否已经完成全部证据校验。 */
|
|
46
55
|
verified: boolean;
|
|
47
56
|
}
|
|
48
57
|
/** 通过 Vitest task metadata 从 worker 传递给主线程 reporter 的数据。 */
|
|
49
58
|
interface ReactPlaythroughMetadata {
|
|
50
59
|
/** metadata 结构版本,用于拒绝无法识别的旧数据。 */
|
|
51
|
-
version:
|
|
60
|
+
version: 2;
|
|
52
61
|
/** 静态、非交互项目跳过主流程验证时必须提供的理由。 */
|
|
53
62
|
waiverReason?: string;
|
|
54
63
|
/** 测试执行期间持续更新的客观证据。 */
|
|
55
64
|
evidence: ReactPlaythroughEvidence;
|
|
56
65
|
}
|
|
57
66
|
|
|
67
|
+
/** 玩家输入在最低可玩流程中的语义阶段。 */
|
|
68
|
+
type ReactPlaythroughInputKind = "entry" | "primary";
|
|
69
|
+
/** 可审计状态证据;entered 是基线,progress/terminal 是有效结果。 */
|
|
70
|
+
type ReactPlaythroughCheckpointKind = "entered" | "progress" | "terminal";
|
|
58
71
|
/** 主流程回调唯一需要学习的测试工具。 */
|
|
59
72
|
interface ReactPlaythroughArguments {
|
|
60
73
|
/** Testing Library 对生产游戏入口的渲染结果。 */
|
|
61
74
|
view: RenderResult;
|
|
62
75
|
/** 已由 playthroughTest 创建的真实 DOM 用户输入驱动。 */
|
|
63
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;
|
|
64
89
|
/**
|
|
65
90
|
* 在硬性步数上限内等待权威结果。
|
|
66
91
|
* 返回后必须再断言该结果,进入中间 active/running 状态不算完成。
|
|
@@ -111,4 +136,4 @@ declare module "vitest" {
|
|
|
111
136
|
}
|
|
112
137
|
}
|
|
113
138
|
|
|
114
|
-
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
|
@@ -151,27 +151,54 @@ var INPUT_EVENTS = [
|
|
|
151
151
|
"touchstart",
|
|
152
152
|
"touchend"
|
|
153
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
|
+
}
|
|
154
165
|
function createMetadata(waiverReason) {
|
|
155
166
|
return {
|
|
156
|
-
version:
|
|
167
|
+
version: 2,
|
|
157
168
|
waiverReason,
|
|
158
169
|
evidence: {
|
|
159
170
|
domInputEvents: 0,
|
|
171
|
+
entryInputs: 0,
|
|
172
|
+
primaryInputs: 0,
|
|
160
173
|
boundedRuns: 0,
|
|
161
174
|
assertionsAfterOutcome: 0,
|
|
175
|
+
changedOutcomeCheckpoints: 0,
|
|
176
|
+
checkpoints: [],
|
|
162
177
|
verified: false
|
|
163
178
|
}
|
|
164
179
|
};
|
|
165
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
|
+
}
|
|
166
192
|
function definePlaythrough(element, run, waiverReason) {
|
|
167
193
|
const reason = normalizePlaythroughWaiverReason(waiverReason);
|
|
168
194
|
const metadata = createMetadata(reason);
|
|
169
195
|
(0, import_vitest.test)("production game completes a bounded playthrough", {
|
|
170
196
|
skip: Boolean(reason),
|
|
171
197
|
meta: { reactPlaythrough: metadata }
|
|
172
|
-
}, async () => {
|
|
198
|
+
}, async ({ expect }) => {
|
|
173
199
|
const evidence = metadata.evidence;
|
|
174
200
|
let assertionsAtOutcome;
|
|
201
|
+
let enteredSnapshot;
|
|
175
202
|
const recordInput = () => {
|
|
176
203
|
evidence.domInputEvents += 1;
|
|
177
204
|
};
|
|
@@ -189,23 +216,90 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
189
216
|
await run({
|
|
190
217
|
view,
|
|
191
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
|
+
},
|
|
192
281
|
async stepUntil(condition, options = {}) {
|
|
193
282
|
const steps = await runBoundedUntil(condition, options);
|
|
194
|
-
if (evidence.
|
|
283
|
+
if (evidence.primaryInputs === 0) {
|
|
195
284
|
throw new Error(
|
|
196
|
-
|
|
285
|
+
'stepUntil must follow performInput("primary", ...). A menu/help click is not gameplay evidence.'
|
|
197
286
|
);
|
|
198
287
|
}
|
|
199
288
|
evidence.boundedRuns += 1;
|
|
200
|
-
assertionsAtOutcome =
|
|
289
|
+
assertionsAtOutcome = expect.getState().assertionCalls;
|
|
201
290
|
return steps;
|
|
202
291
|
}
|
|
203
292
|
});
|
|
204
|
-
const assertionCalls =
|
|
293
|
+
const assertionCalls = expect.getState().assertionCalls;
|
|
205
294
|
evidence.assertionsAfterOutcome = assertionsAtOutcome === void 0 ? 0 : assertionCalls - assertionsAtOutcome;
|
|
206
|
-
if (evidence.
|
|
295
|
+
if (evidence.entryInputs === 0) {
|
|
207
296
|
throw new Error(
|
|
208
|
-
|
|
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", ...).'
|
|
209
303
|
);
|
|
210
304
|
}
|
|
211
305
|
if (evidence.boundedRuns === 0) {
|
|
@@ -213,7 +307,12 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
213
307
|
}
|
|
214
308
|
if (evidence.assertionsAfterOutcome === 0) {
|
|
215
309
|
throw new Error(
|
|
216
|
-
"
|
|
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").'
|
|
217
316
|
);
|
|
218
317
|
}
|
|
219
318
|
evidence.verified = true;
|
|
@@ -234,7 +333,9 @@ function auditReactPlaythroughRun(tests) {
|
|
|
234
333
|
const declared = tests.filter((candidate) => candidate.metadata);
|
|
235
334
|
const valid = declared.filter(({ state, metadata }) => {
|
|
236
335
|
const evidence = metadata?.evidence;
|
|
237
|
-
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
|
+
);
|
|
238
339
|
});
|
|
239
340
|
const waivers = declared.filter(
|
|
240
341
|
({ state, metadata }) => state === "skipped" && (metadata?.waiverReason?.trim().length ?? 0) >= 20
|
|
@@ -242,7 +343,7 @@ function auditReactPlaythroughRun(tests) {
|
|
|
242
343
|
const issues = [];
|
|
243
344
|
if (declared.length === 0) {
|
|
244
345
|
issues.push(
|
|
245
|
-
|
|
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'
|
|
246
347
|
);
|
|
247
348
|
} else {
|
|
248
349
|
for (const candidate of declared) {
|
|
@@ -256,9 +357,8 @@ function auditReactPlaythroughRun(tests) {
|
|
|
256
357
|
} else if (candidate.state !== "passed") {
|
|
257
358
|
issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7684\u72B6\u6001\u4E3A ${candidate.state}\u3002`);
|
|
258
359
|
} else {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
);
|
|
360
|
+
const missing = describeMissingEvidence(candidate.metadata?.evidence);
|
|
361
|
+
issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7F3A\u5C11${missing}\u3002`);
|
|
262
362
|
}
|
|
263
363
|
}
|
|
264
364
|
}
|
package/dist/react/testing.mjs
CHANGED
|
@@ -56,7 +56,7 @@ var ManualGameClock = class {
|
|
|
56
56
|
// src/react/react-playthrough.ts
|
|
57
57
|
import { render } from "@testing-library/react";
|
|
58
58
|
import userEvent from "@testing-library/user-event";
|
|
59
|
-
import {
|
|
59
|
+
import { test } from "vitest";
|
|
60
60
|
|
|
61
61
|
// src/react/react-playthrough-core.ts
|
|
62
62
|
import { act } from "@testing-library/react";
|
|
@@ -113,27 +113,54 @@ var INPUT_EVENTS = [
|
|
|
113
113
|
"touchstart",
|
|
114
114
|
"touchend"
|
|
115
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
|
+
}
|
|
116
127
|
function createMetadata(waiverReason) {
|
|
117
128
|
return {
|
|
118
|
-
version:
|
|
129
|
+
version: 2,
|
|
119
130
|
waiverReason,
|
|
120
131
|
evidence: {
|
|
121
132
|
domInputEvents: 0,
|
|
133
|
+
entryInputs: 0,
|
|
134
|
+
primaryInputs: 0,
|
|
122
135
|
boundedRuns: 0,
|
|
123
136
|
assertionsAfterOutcome: 0,
|
|
137
|
+
changedOutcomeCheckpoints: 0,
|
|
138
|
+
checkpoints: [],
|
|
124
139
|
verified: false
|
|
125
140
|
}
|
|
126
141
|
};
|
|
127
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
|
+
}
|
|
128
154
|
function definePlaythrough(element, run, waiverReason) {
|
|
129
155
|
const reason = normalizePlaythroughWaiverReason(waiverReason);
|
|
130
156
|
const metadata = createMetadata(reason);
|
|
131
157
|
test("production game completes a bounded playthrough", {
|
|
132
158
|
skip: Boolean(reason),
|
|
133
159
|
meta: { reactPlaythrough: metadata }
|
|
134
|
-
}, async () => {
|
|
160
|
+
}, async ({ expect }) => {
|
|
135
161
|
const evidence = metadata.evidence;
|
|
136
162
|
let assertionsAtOutcome;
|
|
163
|
+
let enteredSnapshot;
|
|
137
164
|
const recordInput = () => {
|
|
138
165
|
evidence.domInputEvents += 1;
|
|
139
166
|
};
|
|
@@ -151,11 +178,73 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
151
178
|
await run({
|
|
152
179
|
view,
|
|
153
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
|
+
},
|
|
154
243
|
async stepUntil(condition, options = {}) {
|
|
155
244
|
const steps = await runBoundedUntil(condition, options);
|
|
156
|
-
if (evidence.
|
|
245
|
+
if (evidence.primaryInputs === 0) {
|
|
157
246
|
throw new Error(
|
|
158
|
-
|
|
247
|
+
'stepUntil must follow performInput("primary", ...). A menu/help click is not gameplay evidence.'
|
|
159
248
|
);
|
|
160
249
|
}
|
|
161
250
|
evidence.boundedRuns += 1;
|
|
@@ -165,9 +254,14 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
165
254
|
});
|
|
166
255
|
const assertionCalls = expect.getState().assertionCalls;
|
|
167
256
|
evidence.assertionsAfterOutcome = assertionsAtOutcome === void 0 ? 0 : assertionCalls - assertionsAtOutcome;
|
|
168
|
-
if (evidence.
|
|
257
|
+
if (evidence.entryInputs === 0) {
|
|
169
258
|
throw new Error(
|
|
170
|
-
|
|
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", ...).'
|
|
171
265
|
);
|
|
172
266
|
}
|
|
173
267
|
if (evidence.boundedRuns === 0) {
|
|
@@ -175,7 +269,12 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
175
269
|
}
|
|
176
270
|
if (evidence.assertionsAfterOutcome === 0) {
|
|
177
271
|
throw new Error(
|
|
178
|
-
"
|
|
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").'
|
|
179
278
|
);
|
|
180
279
|
}
|
|
181
280
|
evidence.verified = true;
|
|
@@ -196,7 +295,9 @@ function auditReactPlaythroughRun(tests) {
|
|
|
196
295
|
const declared = tests.filter((candidate) => candidate.metadata);
|
|
197
296
|
const valid = declared.filter(({ state, metadata }) => {
|
|
198
297
|
const evidence = metadata?.evidence;
|
|
199
|
-
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
|
+
);
|
|
200
301
|
});
|
|
201
302
|
const waivers = declared.filter(
|
|
202
303
|
({ state, metadata }) => state === "skipped" && (metadata?.waiverReason?.trim().length ?? 0) >= 20
|
|
@@ -204,7 +305,7 @@ function auditReactPlaythroughRun(tests) {
|
|
|
204
305
|
const issues = [];
|
|
205
306
|
if (declared.length === 0) {
|
|
206
307
|
issues.push(
|
|
207
|
-
|
|
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'
|
|
208
309
|
);
|
|
209
310
|
} else {
|
|
210
311
|
for (const candidate of declared) {
|
|
@@ -218,9 +319,8 @@ function auditReactPlaythroughRun(tests) {
|
|
|
218
319
|
} else if (candidate.state !== "passed") {
|
|
219
320
|
issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7684\u72B6\u6001\u4E3A ${candidate.state}\u3002`);
|
|
220
321
|
} else {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
);
|
|
322
|
+
const missing = describeMissingEvidence(candidate.metadata?.evidence);
|
|
323
|
+
issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7F3A\u5C11${missing}\u3002`);
|
|
224
324
|
}
|
|
225
325
|
}
|
|
226
326
|
}
|