miaoda-game-devkit 0.2.17 → 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.
@@ -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: 1;
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 };
@@ -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: 1;
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 };
@@ -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: 1,
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.domInputEvents === 0) {
283
+ if (evidence.primaryInputs === 0) {
195
284
  throw new Error(
196
- "stepUntil reached an outcome before any production DOM input."
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 = import_vitest.expect.getState().assertionCalls;
289
+ assertionsAtOutcome = expect.getState().assertionCalls;
201
290
  return steps;
202
291
  }
203
292
  });
204
- const assertionCalls = import_vitest.expect.getState().assertionCalls;
293
+ const assertionCalls = expect.getState().assertionCalls;
205
294
  evidence.assertionsAfterOutcome = assertionsAtOutcome === void 0 ? 0 : assertionCalls - assertionsAtOutcome;
206
- if (evidence.domInputEvents === 0) {
295
+ if (evidence.entryInputs === 0) {
207
296
  throw new Error(
208
- "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", ...).'
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
- "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").'
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
- "\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'
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
- issues.push(
260
- `\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`
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
  }
@@ -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 { expect, test } from "vitest";
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: 1,
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.domInputEvents === 0) {
245
+ if (evidence.primaryInputs === 0) {
157
246
  throw new Error(
158
- "stepUntil reached an outcome before any production DOM input."
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.domInputEvents === 0) {
257
+ if (evidence.entryInputs === 0) {
169
258
  throw new Error(
170
- "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", ...).'
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
- "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").'
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
- "\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'
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
- issues.push(
222
- `\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`
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
  }