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.
@@ -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: 1;
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 };
@@ -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: 1;
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 };
@@ -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: 1,
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.domInputEvents === 0) {
283
+ if (evidence.primaryInputs === 0) {
169
284
  throw new Error(
170
- "stepUntil reached an outcome before any production DOM input."
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 = import_vitest.expect.getState().assertionCalls;
289
+ assertionsAtOutcome = expect.getState().assertionCalls;
175
290
  return steps;
176
291
  }
177
292
  });
178
- const assertionCalls = import_vitest.expect.getState().assertionCalls;
293
+ const assertionCalls = expect.getState().assertionCalls;
179
294
  evidence.assertionsAfterOutcome = assertionsAtOutcome === void 0 ? 0 : assertionCalls - assertionsAtOutcome;
180
- if (evidence.domInputEvents === 0) {
295
+ if (evidence.entryInputs === 0) {
181
296
  throw new Error(
182
- "playthroughTest did not observe production DOM input."
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
- "Assert an authoritative game outcome after stepUntil returns."
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
- "\u7F3A\u5C11\u751F\u4EA7\u6E38\u620F\u53EF\u73A9\u6027\u9A8C\u8BC1\uFF1A\u4F7F\u7528 playthroughTest \u6E32\u67D3 <App />\uFF0C\u901A\u8FC7\u771F\u5B9E DOM \u8F93\u5165\u5B8C\u6210\u4E00\u4E2A\u6709\u6B65\u6570\u4E0A\u9650\u7684\u5173\u952E\u6D41\u7A0B\u3002"
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
- issues.push(
234
- `\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u6CA1\u6709\u7559\u4E0B\u5B8C\u6574\u7684\u8F93\u5165\u3001\u6709\u9650\u63A8\u8FDB\u548C\u7ED3\u679C\u65AD\u8A00\u8BC1\u636E\u3002`
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
  }
@@ -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 { expect, test } from "vitest";
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: 1,
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.domInputEvents === 0) {
245
+ if (evidence.primaryInputs === 0) {
141
246
  throw new Error(
142
- "stepUntil reached an outcome before any production DOM input."
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.domInputEvents === 0) {
257
+ if (evidence.entryInputs === 0) {
153
258
  throw new Error(
154
- "playthroughTest did not observe production DOM input."
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
- "Assert an authoritative game outcome after stepUntil returns."
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
- "\u7F3A\u5C11\u751F\u4EA7\u6E38\u620F\u53EF\u73A9\u6027\u9A8C\u8BC1\uFF1A\u4F7F\u7528 playthroughTest \u6E32\u67D3 <App />\uFF0C\u901A\u8FC7\u771F\u5B9E DOM \u8F93\u5165\u5B8C\u6210\u4E00\u4E2A\u6709\u6B65\u6570\u4E0A\u9650\u7684\u5173\u952E\u6D41\u7A0B\u3002"
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
- issues.push(
206
- `\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u6CA1\u6709\u7559\u4E0B\u5B8C\u6574\u7684\u8F93\u5165\u3001\u6709\u9650\u63A8\u8FDB\u548C\u7ED3\u679C\u65AD\u8A00\u8BC1\u636E\u3002`
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
  }