miaoda-game-devkit 0.2.20 → 0.2.21

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 CHANGED
@@ -8,13 +8,14 @@
8
8
  Vitest 配置和自动收集运行错误的 Testing Library setup,分别从 `miaoda-game-devkit/react`、
9
9
  `miaoda-game-devkit/react/testing` 和 `miaoda-game-devkit/react/vitest-config` 导入;
10
10
  - React 的 `playthroughTest` 与项目级 reporter 自动验证独立的 entry/primary DOM
11
- 输入、有界推进、结果断言及前后权威 snapshot 变化;聚焦运行只提示未检查,
11
+ 输入、有界推进、结果断言及至少两次有序 checkpoint 签到;聚焦运行只提示未检查,
12
12
  完整运行缺少有效流程时失败;
13
13
 
14
14
  `playthroughTest` 的主流程回调从参数接收 `user`、`performInput`、`checkpoint`、
15
- `stepUntil` 和绑定当前测试的 `expect`。按 entry 输入、entered snapshot、primary 输入、
16
- 有界等待、使用回调提供的 `expect` 断言结果、
17
- progress/terminal snapshot 的顺序记录最低证据。大型游戏仍只保留一条最短关键流程;
15
+ `stepUntil` 和绑定当前测试的 `expect`。按 entry 输入、`checkpoint("entered")`、primary 输入、
16
+ 有界等待、使用回调提供的 `expect` 断言权威结果、
17
+ `checkpoint("progress")` 或 `checkpoint("terminal")` 的顺序记录最低证据。每条流程至少需要
18
+ 这两次 checkpoint;checkpoint 只负责签到,不携带或验证状态。大型游戏仍只保留一条最短关键流程;
18
19
  分支、关卡规则和恢复清理由专门测试覆盖。不要再次调用 `userEvent.setup()`。
19
20
  时间、帧或自定义 scheduler 流程必须显式传入确定性 `step`;精确物理时间、暂停恢复
20
21
  或调度清理测试可使用 `ManualGameClock`。
@@ -47,8 +47,6 @@ interface ReactPlaythroughEvidence {
47
47
  boundedRuns: number;
48
48
  /** 最后一次 stepUntil 成功后新增的 Vitest 断言数。 */
49
49
  assertionsAfterOutcome: number;
50
- /** 已验证相对 entered snapshot 发生变化的进展或终局 checkpoint 次数。 */
51
- changedOutcomeCheckpoints: number;
52
50
  /** 已记录的 checkpoint 类型,供 reporter 给出精确提示。 */
53
51
  checkpoints: ReactPlaythroughCheckpointKind[];
54
52
  /** helper 是否已经完成全部证据校验。 */
@@ -57,7 +55,7 @@ interface ReactPlaythroughEvidence {
57
55
  /** 通过 Vitest task metadata 从 worker 传递给主线程 reporter 的数据。 */
58
56
  interface ReactPlaythroughMetadata {
59
57
  /** metadata 结构版本,用于拒绝无法识别的旧数据。 */
60
- version: 2;
58
+ version: 3;
61
59
  /** 静态、非交互项目跳过主流程验证时必须提供的理由。 */
62
60
  waiverReason?: string;
63
61
  /** 测试执行期间持续更新的客观证据。 */
@@ -66,7 +64,7 @@ interface ReactPlaythroughMetadata {
66
64
 
67
65
  /** 玩家输入在最低可玩流程中的语义阶段。 */
68
66
  type ReactPlaythroughInputKind = "entry" | "primary";
69
- /** 可审计状态证据;entered 是基线,progress/terminal 是有效结果。 */
67
+ /** 可审计流程签到;entered 是入口,progress/terminal 是有效结果。 */
70
68
  type ReactPlaythroughCheckpointKind = "entered" | "progress" | "terminal";
71
69
  /** 主流程回调唯一需要学习的测试工具。 */
72
70
  interface ReactPlaythroughArguments {
@@ -82,10 +80,10 @@ interface ReactPlaythroughArguments {
82
80
  */
83
81
  performInput(kind: ReactPlaythroughInputKind, input: () => void | Promise<void>): Promise<void>;
84
82
  /**
85
- * 记录只读状态证据。先在 entry 后记录 entered,再在 primary 和 stepUntil
86
- * 后记录 progress 或 terminal;Canvas 游戏应传 Telemetry snapshot
83
+ * 记录流程签到。先在 entry 后记录 entered,再在 primary、stepUntil
84
+ * 结果断言后记录 progress 或 terminal。
87
85
  */
88
- checkpoint(kind: ReactPlaythroughCheckpointKind, snapshot: unknown): void;
86
+ checkpoint(kind: ReactPlaythroughCheckpointKind): void;
89
87
  /**
90
88
  * 在硬性步数上限内等待权威结果。
91
89
  * 返回后必须再断言该结果,进入中间 active/running 状态不算完成。
@@ -47,8 +47,6 @@ interface ReactPlaythroughEvidence {
47
47
  boundedRuns: number;
48
48
  /** 最后一次 stepUntil 成功后新增的 Vitest 断言数。 */
49
49
  assertionsAfterOutcome: number;
50
- /** 已验证相对 entered snapshot 发生变化的进展或终局 checkpoint 次数。 */
51
- changedOutcomeCheckpoints: number;
52
50
  /** 已记录的 checkpoint 类型,供 reporter 给出精确提示。 */
53
51
  checkpoints: ReactPlaythroughCheckpointKind[];
54
52
  /** helper 是否已经完成全部证据校验。 */
@@ -57,7 +55,7 @@ interface ReactPlaythroughEvidence {
57
55
  /** 通过 Vitest task metadata 从 worker 传递给主线程 reporter 的数据。 */
58
56
  interface ReactPlaythroughMetadata {
59
57
  /** metadata 结构版本,用于拒绝无法识别的旧数据。 */
60
- version: 2;
58
+ version: 3;
61
59
  /** 静态、非交互项目跳过主流程验证时必须提供的理由。 */
62
60
  waiverReason?: string;
63
61
  /** 测试执行期间持续更新的客观证据。 */
@@ -66,7 +64,7 @@ interface ReactPlaythroughMetadata {
66
64
 
67
65
  /** 玩家输入在最低可玩流程中的语义阶段。 */
68
66
  type ReactPlaythroughInputKind = "entry" | "primary";
69
- /** 可审计状态证据;entered 是基线,progress/terminal 是有效结果。 */
67
+ /** 可审计流程签到;entered 是入口,progress/terminal 是有效结果。 */
70
68
  type ReactPlaythroughCheckpointKind = "entered" | "progress" | "terminal";
71
69
  /** 主流程回调唯一需要学习的测试工具。 */
72
70
  interface ReactPlaythroughArguments {
@@ -82,10 +80,10 @@ interface ReactPlaythroughArguments {
82
80
  */
83
81
  performInput(kind: ReactPlaythroughInputKind, input: () => void | Promise<void>): Promise<void>;
84
82
  /**
85
- * 记录只读状态证据。先在 entry 后记录 entered,再在 primary 和 stepUntil
86
- * 后记录 progress 或 terminal;Canvas 游戏应传 Telemetry snapshot
83
+ * 记录流程签到。先在 entry 后记录 entered,再在 primary、stepUntil
84
+ * 结果断言后记录 progress 或 terminal。
87
85
  */
88
- checkpoint(kind: ReactPlaythroughCheckpointKind, snapshot: unknown): void;
86
+ checkpoint(kind: ReactPlaythroughCheckpointKind): void;
89
87
  /**
90
88
  * 在硬性步数上限内等待权威结果。
91
89
  * 返回后必须再断言该结果,进入中间 active/running 状态不算完成。
@@ -151,20 +151,25 @@ var INPUT_EVENTS = [
151
151
  "touchstart",
152
152
  "touchend"
153
153
  ];
154
+ var MIN_CHECKPOINTS = 2;
154
155
  function describeMissingEvidence(evidence) {
155
156
  if (!evidence || evidence.entryInputs === 0) return "entry \u8F93\u5165";
156
157
  if (evidence.primaryInputs === 0) return "primary \u8F93\u5165";
157
158
  if (!evidence.checkpoints.includes("entered")) return "entered checkpoint";
158
159
  if (evidence.boundedRuns === 0) return "\u6709\u754C stepUntil";
159
160
  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";
161
+ if (evidence.checkpoints.length < MIN_CHECKPOINTS)
162
+ return `\u81F3\u5C11 ${MIN_CHECKPOINTS} \u4E2A checkpoint`;
163
+ if (!evidence.checkpoints.some(
164
+ (checkpoint) => checkpoint === "progress" || checkpoint === "terminal"
165
+ )) {
166
+ return "progress/terminal checkpoint";
162
167
  }
163
168
  return "\u5B8C\u6574\u7684 playthrough \u6821\u9A8C\u6807\u8BB0";
164
169
  }
165
170
  function createMetadata(waiverReason) {
166
171
  return {
167
- version: 2,
172
+ version: 3,
168
173
  waiverReason,
169
174
  evidence: {
170
175
  domInputEvents: 0,
@@ -172,23 +177,11 @@ function createMetadata(waiverReason) {
172
177
  primaryInputs: 0,
173
178
  boundedRuns: 0,
174
179
  assertionsAfterOutcome: 0,
175
- changedOutcomeCheckpoints: 0,
176
180
  checkpoints: [],
177
181
  verified: false
178
182
  }
179
183
  };
180
184
  }
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
- }
192
185
  function definePlaythrough(element, run, waiverReason) {
193
186
  const reason = normalizePlaythroughWaiverReason(waiverReason);
194
187
  const metadata = createMetadata(reason);
@@ -198,7 +191,7 @@ function definePlaythrough(element, run, waiverReason) {
198
191
  }, async ({ expect }) => {
199
192
  const evidence = metadata.evidence;
200
193
  let assertionsAtOutcome;
201
- let enteredSnapshot;
194
+ let enteredRecorded = false;
202
195
  const recordInput = () => {
203
196
  evidence.domInputEvents += 1;
204
197
  };
@@ -218,14 +211,14 @@ function definePlaythrough(element, run, waiverReason) {
218
211
  user,
219
212
  expect,
220
213
  async performInput(kind, input) {
221
- if (kind === "entry" && enteredSnapshot !== void 0) {
214
+ if (kind === "entry" && enteredRecorded) {
222
215
  throw new Error(
223
216
  'performInput("entry") must run before checkpoint("entered"). Group multiple setup actions in the same callback.'
224
217
  );
225
218
  }
226
- if (kind === "primary" && enteredSnapshot === void 0) {
219
+ if (kind === "primary" && !enteredRecorded) {
227
220
  throw new Error(
228
- 'Before performInput("primary"), run an entry input and checkpoint("entered", snapshot).'
221
+ 'Before performInput("primary"), run an entry input and checkpoint("entered").'
229
222
  );
230
223
  }
231
224
  const inputsBefore = evidence.domInputEvents;
@@ -238,10 +231,9 @@ function definePlaythrough(element, run, waiverReason) {
238
231
  if (kind === "entry") evidence.entryInputs += 1;
239
232
  else evidence.primaryInputs += 1;
240
233
  },
241
- checkpoint(kind, snapshot) {
242
- const fingerprint = snapshotFingerprint(snapshot);
234
+ checkpoint(kind) {
243
235
  if (kind === "entered") {
244
- if (enteredSnapshot !== void 0) {
236
+ if (enteredRecorded) {
245
237
  throw new Error(
246
238
  'checkpoint("entered") may only be recorded once, before the primary input.'
247
239
  );
@@ -251,7 +243,7 @@ function definePlaythrough(element, run, waiverReason) {
251
243
  'checkpoint("entered") must follow performInput("entry", ...).'
252
244
  );
253
245
  }
254
- enteredSnapshot = fingerprint;
246
+ enteredRecorded = true;
255
247
  evidence.checkpoints.push(kind);
256
248
  return;
257
249
  }
@@ -267,15 +259,9 @@ function definePlaythrough(element, run, waiverReason) {
267
259
  }
268
260
  if (assertionsAtOutcome === void 0 || expect.getState().assertionCalls <= assertionsAtOutcome) {
269
261
  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.`
262
+ `Use the expect provided by playthroughTest to assert the authoritative result after stepUntil, then record checkpoint("${kind}").`
276
263
  );
277
264
  }
278
- evidence.changedOutcomeCheckpoints += 1;
279
265
  evidence.checkpoints.push(kind);
280
266
  },
281
267
  async stepUntil(condition, options = {}) {
@@ -310,9 +296,11 @@ function definePlaythrough(element, run, waiverReason) {
310
296
  "Use the expect provided by playthroughTest to assert an authoritative game outcome after stepUntil returns."
311
297
  );
312
298
  }
313
- if (evidence.changedOutcomeCheckpoints === 0) {
299
+ if (evidence.checkpoints.length < MIN_CHECKPOINTS || !evidence.checkpoints.some(
300
+ (checkpoint) => checkpoint === "progress" || checkpoint === "terminal"
301
+ )) {
314
302
  throw new Error(
315
- 'After asserting the result, record checkpoint("progress", snapshot) or checkpoint("terminal", snapshot). The snapshot must differ from checkpoint("entered").'
303
+ `playthroughTest requires at least ${MIN_CHECKPOINTS} checkpoints: checkpoint("entered") and, after asserting the result, checkpoint("progress") or checkpoint("terminal").`
316
304
  );
317
305
  }
318
306
  evidence.verified = true;
@@ -333,7 +321,7 @@ function auditReactPlaythroughRun(tests) {
333
321
  const declared = tests.filter((candidate) => candidate.metadata);
334
322
  const valid = declared.filter(({ state, metadata }) => {
335
323
  const evidence = metadata?.evidence;
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(
324
+ return state === "passed" && evidence?.verified === true && evidence.domInputEvents > 0 && evidence.entryInputs > 0 && evidence.primaryInputs > 0 && evidence.boundedRuns > 0 && evidence.assertionsAfterOutcome > 0 && evidence.checkpoints.length >= MIN_CHECKPOINTS && evidence.checkpoints.includes("entered") && evidence.checkpoints.some(
337
325
  (checkpoint) => checkpoint === "progress" || checkpoint === "terminal"
338
326
  );
339
327
  });
@@ -343,7 +331,7 @@ function auditReactPlaythroughRun(tests) {
343
331
  const issues = [];
344
332
  if (declared.length === 0) {
345
333
  issues.push(
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'
334
+ '\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\u6743\u5A01\u7ED3\u679C\uFF0C\u5E76\u8BB0\u5F55 checkpoint("progress") \u6216 checkpoint("terminal")\uFF1B\u81F3\u5C11\u5B8C\u6210\u8FD9\u4E24\u6B21 checkpoint \u7B7E\u5230\u3002'
347
335
  );
348
336
  } else {
349
337
  for (const candidate of declared) {
@@ -113,20 +113,25 @@ var INPUT_EVENTS = [
113
113
  "touchstart",
114
114
  "touchend"
115
115
  ];
116
+ var MIN_CHECKPOINTS = 2;
116
117
  function describeMissingEvidence(evidence) {
117
118
  if (!evidence || evidence.entryInputs === 0) return "entry \u8F93\u5165";
118
119
  if (evidence.primaryInputs === 0) return "primary \u8F93\u5165";
119
120
  if (!evidence.checkpoints.includes("entered")) return "entered checkpoint";
120
121
  if (evidence.boundedRuns === 0) return "\u6709\u754C stepUntil";
121
122
  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";
123
+ if (evidence.checkpoints.length < MIN_CHECKPOINTS)
124
+ return `\u81F3\u5C11 ${MIN_CHECKPOINTS} \u4E2A checkpoint`;
125
+ if (!evidence.checkpoints.some(
126
+ (checkpoint) => checkpoint === "progress" || checkpoint === "terminal"
127
+ )) {
128
+ return "progress/terminal checkpoint";
124
129
  }
125
130
  return "\u5B8C\u6574\u7684 playthrough \u6821\u9A8C\u6807\u8BB0";
126
131
  }
127
132
  function createMetadata(waiverReason) {
128
133
  return {
129
- version: 2,
134
+ version: 3,
130
135
  waiverReason,
131
136
  evidence: {
132
137
  domInputEvents: 0,
@@ -134,23 +139,11 @@ function createMetadata(waiverReason) {
134
139
  primaryInputs: 0,
135
140
  boundedRuns: 0,
136
141
  assertionsAfterOutcome: 0,
137
- changedOutcomeCheckpoints: 0,
138
142
  checkpoints: [],
139
143
  verified: false
140
144
  }
141
145
  };
142
146
  }
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
- }
154
147
  function definePlaythrough(element, run, waiverReason) {
155
148
  const reason = normalizePlaythroughWaiverReason(waiverReason);
156
149
  const metadata = createMetadata(reason);
@@ -160,7 +153,7 @@ function definePlaythrough(element, run, waiverReason) {
160
153
  }, async ({ expect }) => {
161
154
  const evidence = metadata.evidence;
162
155
  let assertionsAtOutcome;
163
- let enteredSnapshot;
156
+ let enteredRecorded = false;
164
157
  const recordInput = () => {
165
158
  evidence.domInputEvents += 1;
166
159
  };
@@ -180,14 +173,14 @@ function definePlaythrough(element, run, waiverReason) {
180
173
  user,
181
174
  expect,
182
175
  async performInput(kind, input) {
183
- if (kind === "entry" && enteredSnapshot !== void 0) {
176
+ if (kind === "entry" && enteredRecorded) {
184
177
  throw new Error(
185
178
  'performInput("entry") must run before checkpoint("entered"). Group multiple setup actions in the same callback.'
186
179
  );
187
180
  }
188
- if (kind === "primary" && enteredSnapshot === void 0) {
181
+ if (kind === "primary" && !enteredRecorded) {
189
182
  throw new Error(
190
- 'Before performInput("primary"), run an entry input and checkpoint("entered", snapshot).'
183
+ 'Before performInput("primary"), run an entry input and checkpoint("entered").'
191
184
  );
192
185
  }
193
186
  const inputsBefore = evidence.domInputEvents;
@@ -200,10 +193,9 @@ function definePlaythrough(element, run, waiverReason) {
200
193
  if (kind === "entry") evidence.entryInputs += 1;
201
194
  else evidence.primaryInputs += 1;
202
195
  },
203
- checkpoint(kind, snapshot) {
204
- const fingerprint = snapshotFingerprint(snapshot);
196
+ checkpoint(kind) {
205
197
  if (kind === "entered") {
206
- if (enteredSnapshot !== void 0) {
198
+ if (enteredRecorded) {
207
199
  throw new Error(
208
200
  'checkpoint("entered") may only be recorded once, before the primary input.'
209
201
  );
@@ -213,7 +205,7 @@ function definePlaythrough(element, run, waiverReason) {
213
205
  'checkpoint("entered") must follow performInput("entry", ...).'
214
206
  );
215
207
  }
216
- enteredSnapshot = fingerprint;
208
+ enteredRecorded = true;
217
209
  evidence.checkpoints.push(kind);
218
210
  return;
219
211
  }
@@ -229,15 +221,9 @@ function definePlaythrough(element, run, waiverReason) {
229
221
  }
230
222
  if (assertionsAtOutcome === void 0 || expect.getState().assertionCalls <= assertionsAtOutcome) {
231
223
  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.`
224
+ `Use the expect provided by playthroughTest to assert the authoritative result after stepUntil, then record checkpoint("${kind}").`
238
225
  );
239
226
  }
240
- evidence.changedOutcomeCheckpoints += 1;
241
227
  evidence.checkpoints.push(kind);
242
228
  },
243
229
  async stepUntil(condition, options = {}) {
@@ -272,9 +258,11 @@ function definePlaythrough(element, run, waiverReason) {
272
258
  "Use the expect provided by playthroughTest to assert an authoritative game outcome after stepUntil returns."
273
259
  );
274
260
  }
275
- if (evidence.changedOutcomeCheckpoints === 0) {
261
+ if (evidence.checkpoints.length < MIN_CHECKPOINTS || !evidence.checkpoints.some(
262
+ (checkpoint) => checkpoint === "progress" || checkpoint === "terminal"
263
+ )) {
276
264
  throw new Error(
277
- 'After asserting the result, record checkpoint("progress", snapshot) or checkpoint("terminal", snapshot). The snapshot must differ from checkpoint("entered").'
265
+ `playthroughTest requires at least ${MIN_CHECKPOINTS} checkpoints: checkpoint("entered") and, after asserting the result, checkpoint("progress") or checkpoint("terminal").`
278
266
  );
279
267
  }
280
268
  evidence.verified = true;
@@ -295,7 +283,7 @@ function auditReactPlaythroughRun(tests) {
295
283
  const declared = tests.filter((candidate) => candidate.metadata);
296
284
  const valid = declared.filter(({ state, metadata }) => {
297
285
  const evidence = metadata?.evidence;
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(
286
+ return state === "passed" && evidence?.verified === true && evidence.domInputEvents > 0 && evidence.entryInputs > 0 && evidence.primaryInputs > 0 && evidence.boundedRuns > 0 && evidence.assertionsAfterOutcome > 0 && evidence.checkpoints.length >= MIN_CHECKPOINTS && evidence.checkpoints.includes("entered") && evidence.checkpoints.some(
299
287
  (checkpoint) => checkpoint === "progress" || checkpoint === "terminal"
300
288
  );
301
289
  });
@@ -305,7 +293,7 @@ function auditReactPlaythroughRun(tests) {
305
293
  const issues = [];
306
294
  if (declared.length === 0) {
307
295
  issues.push(
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'
296
+ '\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\u6743\u5A01\u7ED3\u679C\uFF0C\u5E76\u8BB0\u5F55 checkpoint("progress") \u6216 checkpoint("terminal")\uFF1B\u81F3\u5C11\u5B8C\u6210\u8FD9\u4E24\u6B21 checkpoint \u7B7E\u5230\u3002'
309
297
  );
310
298
  } else {
311
299
  for (const candidate of declared) {
@@ -101,20 +101,25 @@ var INPUT_EVENTS = [
101
101
  "touchstart",
102
102
  "touchend"
103
103
  ];
104
+ var MIN_CHECKPOINTS = 2;
104
105
  function describeMissingEvidence(evidence) {
105
106
  if (!evidence || evidence.entryInputs === 0) return "entry \u8F93\u5165";
106
107
  if (evidence.primaryInputs === 0) return "primary \u8F93\u5165";
107
108
  if (!evidence.checkpoints.includes("entered")) return "entered checkpoint";
108
109
  if (evidence.boundedRuns === 0) return "\u6709\u754C stepUntil";
109
110
  if (evidence.assertionsAfterOutcome === 0) return "stepUntil \u540E\u7684\u7ED3\u679C\u65AD\u8A00";
110
- if (evidence.changedOutcomeCheckpoints === 0) {
111
- return "\u53D1\u751F\u72B6\u6001\u53D8\u5316\u7684 progress/terminal checkpoint";
111
+ if (evidence.checkpoints.length < MIN_CHECKPOINTS)
112
+ return `\u81F3\u5C11 ${MIN_CHECKPOINTS} \u4E2A checkpoint`;
113
+ if (!evidence.checkpoints.some(
114
+ (checkpoint) => checkpoint === "progress" || checkpoint === "terminal"
115
+ )) {
116
+ return "progress/terminal checkpoint";
112
117
  }
113
118
  return "\u5B8C\u6574\u7684 playthrough \u6821\u9A8C\u6807\u8BB0";
114
119
  }
115
120
  function createMetadata(waiverReason) {
116
121
  return {
117
- version: 2,
122
+ version: 3,
118
123
  waiverReason,
119
124
  evidence: {
120
125
  domInputEvents: 0,
@@ -122,23 +127,11 @@ function createMetadata(waiverReason) {
122
127
  primaryInputs: 0,
123
128
  boundedRuns: 0,
124
129
  assertionsAfterOutcome: 0,
125
- changedOutcomeCheckpoints: 0,
126
130
  checkpoints: [],
127
131
  verified: false
128
132
  }
129
133
  };
130
134
  }
131
- function snapshotFingerprint(snapshot) {
132
- try {
133
- const serialized = JSON.stringify(snapshot);
134
- if (serialized === void 0) throw new Error("unsupported value");
135
- return serialized;
136
- } catch {
137
- throw new Error(
138
- "checkpoint snapshot must be JSON-serializable. Pass a read-only Telemetry snapshot or a small visible-state object."
139
- );
140
- }
141
- }
142
135
  function definePlaythrough(element, run, waiverReason) {
143
136
  const reason = normalizePlaythroughWaiverReason(waiverReason);
144
137
  const metadata = createMetadata(reason);
@@ -148,7 +141,7 @@ function definePlaythrough(element, run, waiverReason) {
148
141
  }, async ({ expect }) => {
149
142
  const evidence = metadata.evidence;
150
143
  let assertionsAtOutcome;
151
- let enteredSnapshot;
144
+ let enteredRecorded = false;
152
145
  const recordInput = () => {
153
146
  evidence.domInputEvents += 1;
154
147
  };
@@ -168,14 +161,14 @@ function definePlaythrough(element, run, waiverReason) {
168
161
  user,
169
162
  expect,
170
163
  async performInput(kind, input) {
171
- if (kind === "entry" && enteredSnapshot !== void 0) {
164
+ if (kind === "entry" && enteredRecorded) {
172
165
  throw new Error(
173
166
  'performInput("entry") must run before checkpoint("entered"). Group multiple setup actions in the same callback.'
174
167
  );
175
168
  }
176
- if (kind === "primary" && enteredSnapshot === void 0) {
169
+ if (kind === "primary" && !enteredRecorded) {
177
170
  throw new Error(
178
- 'Before performInput("primary"), run an entry input and checkpoint("entered", snapshot).'
171
+ 'Before performInput("primary"), run an entry input and checkpoint("entered").'
179
172
  );
180
173
  }
181
174
  const inputsBefore = evidence.domInputEvents;
@@ -188,10 +181,9 @@ function definePlaythrough(element, run, waiverReason) {
188
181
  if (kind === "entry") evidence.entryInputs += 1;
189
182
  else evidence.primaryInputs += 1;
190
183
  },
191
- checkpoint(kind, snapshot) {
192
- const fingerprint = snapshotFingerprint(snapshot);
184
+ checkpoint(kind) {
193
185
  if (kind === "entered") {
194
- if (enteredSnapshot !== void 0) {
186
+ if (enteredRecorded) {
195
187
  throw new Error(
196
188
  'checkpoint("entered") may only be recorded once, before the primary input.'
197
189
  );
@@ -201,7 +193,7 @@ function definePlaythrough(element, run, waiverReason) {
201
193
  'checkpoint("entered") must follow performInput("entry", ...).'
202
194
  );
203
195
  }
204
- enteredSnapshot = fingerprint;
196
+ enteredRecorded = true;
205
197
  evidence.checkpoints.push(kind);
206
198
  return;
207
199
  }
@@ -217,15 +209,9 @@ function definePlaythrough(element, run, waiverReason) {
217
209
  }
218
210
  if (assertionsAtOutcome === void 0 || expect.getState().assertionCalls <= assertionsAtOutcome) {
219
211
  throw new Error(
220
- `Use the expect provided by playthroughTest to assert the authoritative result after stepUntil, then record checkpoint("${kind}", snapshot).`
221
- );
222
- }
223
- if (fingerprint === enteredSnapshot) {
224
- throw new Error(
225
- `checkpoint("${kind}") matches the entered snapshot. Assert a production state change caused by the primary input.`
212
+ `Use the expect provided by playthroughTest to assert the authoritative result after stepUntil, then record checkpoint("${kind}").`
226
213
  );
227
214
  }
228
- evidence.changedOutcomeCheckpoints += 1;
229
215
  evidence.checkpoints.push(kind);
230
216
  },
231
217
  async stepUntil(condition, options = {}) {
@@ -260,9 +246,11 @@ function definePlaythrough(element, run, waiverReason) {
260
246
  "Use the expect provided by playthroughTest to assert an authoritative game outcome after stepUntil returns."
261
247
  );
262
248
  }
263
- if (evidence.changedOutcomeCheckpoints === 0) {
249
+ if (evidence.checkpoints.length < MIN_CHECKPOINTS || !evidence.checkpoints.some(
250
+ (checkpoint) => checkpoint === "progress" || checkpoint === "terminal"
251
+ )) {
264
252
  throw new Error(
265
- 'After asserting the result, record checkpoint("progress", snapshot) or checkpoint("terminal", snapshot). The snapshot must differ from checkpoint("entered").'
253
+ `playthroughTest requires at least ${MIN_CHECKPOINTS} checkpoints: checkpoint("entered") and, after asserting the result, checkpoint("progress") or checkpoint("terminal").`
266
254
  );
267
255
  }
268
256
  evidence.verified = true;
@@ -283,7 +271,7 @@ function auditReactPlaythroughRun(tests) {
283
271
  const declared = tests.filter((candidate) => candidate.metadata);
284
272
  const valid = declared.filter(({ state, metadata }) => {
285
273
  const evidence = metadata?.evidence;
286
- 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(
274
+ return state === "passed" && evidence?.verified === true && evidence.domInputEvents > 0 && evidence.entryInputs > 0 && evidence.primaryInputs > 0 && evidence.boundedRuns > 0 && evidence.assertionsAfterOutcome > 0 && evidence.checkpoints.length >= MIN_CHECKPOINTS && evidence.checkpoints.includes("entered") && evidence.checkpoints.some(
287
275
  (checkpoint) => checkpoint === "progress" || checkpoint === "terminal"
288
276
  );
289
277
  });
@@ -293,7 +281,7 @@ function auditReactPlaythroughRun(tests) {
293
281
  const issues = [];
294
282
  if (declared.length === 0) {
295
283
  issues.push(
296
- '\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'
284
+ '\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\u6743\u5A01\u7ED3\u679C\uFF0C\u5E76\u8BB0\u5F55 checkpoint("progress") \u6216 checkpoint("terminal")\uFF1B\u81F3\u5C11\u5B8C\u6210\u8FD9\u4E24\u6B21 checkpoint \u7B7E\u5230\u3002'
297
285
  );
298
286
  } else {
299
287
  for (const candidate of declared) {
@@ -323,7 +311,7 @@ var PRODUCTION_PLAYTHROUGH_FILE = "tests/production-playthrough.test.tsx";
323
311
  function isMetadata(value) {
324
312
  if (!value || typeof value !== "object") return false;
325
313
  const metadata = value;
326
- return metadata.version === 2 && Boolean(metadata.evidence);
314
+ return metadata.version === 3 && Boolean(metadata.evidence);
327
315
  }
328
316
  function toAuditInput(test2) {
329
317
  const metadata = test2.meta().reactPlaythrough;
@@ -367,7 +355,7 @@ function assessReactPlaythroughReport(input) {
367
355
  ...base,
368
356
  status: "FAILED",
369
357
  cause: "\u751F\u4EA7\u53EF\u73A9\u6027\u6D4B\u8BD5\u6587\u4EF6\u4E0D\u5B58\u5728\u3002",
370
- next: '\u521B\u5EFA\u8BE5\u6587\u4EF6\uFF1A\u4ECE <App /> \u901A\u8FC7\u771F\u5B9E DOM \u8F93\u5165\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\u3002Canvas \u7528\u771F\u5B9E DOM \u4E8B\u4EF6\u8F93\u5165\u5E76\u8BFB\u53D6 Telemetry snapshot\u3002',
358
+ next: '\u521B\u5EFA\u8BE5\u6587\u4EF6\uFF1A\u4ECE <App /> \u901A\u8FC7\u771F\u5B9E DOM \u8F93\u5165\u4F9D\u6B21\u6267\u884C performInput("entry")\u3001checkpoint("entered")\u3001performInput("primary")\u3001stepUntil\u3001\u7528 playthroughTest \u63D0\u4F9B\u7684 expect \u65AD\u8A00\u6743\u5A01\u7ED3\u679C\uFF0C\u5E76\u8BB0\u5F55 checkpoint("progress") \u6216 checkpoint("terminal")\u3002\u6BCF\u6761\u4E3B\u6D41\u7A0B\u81F3\u5C11\u9700\u8981\u8FD9\u4E24\u6B21 checkpoint \u7B7E\u5230\u3002',
371
359
  failsRun: true
372
360
  };
373
361
  }
@@ -405,7 +393,7 @@ function assessReactPlaythroughReport(input) {
405
393
  ...base,
406
394
  status: "FAILED",
407
395
  cause,
408
- next: missingStep ? "\u8BE5\u6D41\u7A0B\u662F\u65F6\u95F4\u6216\u5E27\u9A71\u52A8\u7684\uFF0C\u4F46 stepUntil \u6CA1\u6709\u63A8\u8FDB\u6E38\u620F\u65F6\u95F4\uFF1B\u4E3A\u751F\u4EA7\u6E38\u620F\u6CE8\u5165 devkit \u7684 GameClock\uFF0C\u5E76\u4F20\u5165 step: () => clock.stepFrame()\u3002\u4E0D\u8981\u7528\u771F\u5B9E setTimeout\u3002" : timedOut ? "\u771F\u5B9E\u8F93\u5165\u5DF2\u6267\u884C\uFF0C\u4F46\u73A9\u6CD5\u6CA1\u6709\u5728\u4E0A\u9650\u5185\u4EA7\u751F\u7ED3\u679C\uFF1B\u68C0\u67E5\u751F\u4EA7\u63A7\u5236\u662F\u5426\u6536\u5230\u8F93\u5165\uFF0C\u518D\u67E5\u770B\u8D85\u65F6\u9519\u8BEF\u4E2D\u7684 Last diagnostics \u5224\u65AD\u662F\u6E38\u620F\u5FAA\u73AF\u3001\u89C4\u5219\u72B6\u6001\u8FD8\u662F UI \u540C\u6B65\u672A\u63A8\u8FDB\u3002" : missingStructuredEvidence ? '\u6309\u987A\u5E8F\u8865\u9F50\uFF1AperformInput("entry") \u540E\u8BB0\u5F55 entered snapshot\uFF1B\u518D\u7528 performInput("primary") \u6267\u884C\u6838\u5FC3\u64CD\u4F5C\uFF0CstepUntil \u7B49\u5F85\u53D8\u5316\uFF0C\u65AD\u8A00\u540E\u8BB0\u5F55 progress \u6216 terminal snapshot\u3002' : "\u4ECE\u751F\u4EA7\u5165\u53E3\u6267\u884C\u771F\u5B9E DOM \u8F93\u5165\uFF0C\u7528 stepUntil \u6709\u754C\u63A8\u8FDB\u5230\u73A9\u5BB6\u53EF\u89C1\u7ED3\u679C\u6216\u6E38\u620F\u771F\u5B9E\u72B6\u6001\u7684\u53EA\u8BFB snapshot\uFF0C\u5E76\u5728\u5176\u540E\u65AD\u8A00\uFF1B\u4E0D\u8981\u76F4\u8FBE\u5185\u90E8\u5173\u5361\u6216\u4FEE\u6539\u73A9\u6CD5\u72B6\u6001\u3002",
396
+ next: missingStep ? "\u8BE5\u6D41\u7A0B\u662F\u65F6\u95F4\u6216\u5E27\u9A71\u52A8\u7684\uFF0C\u4F46 stepUntil \u6CA1\u6709\u63A8\u8FDB\u6E38\u620F\u65F6\u95F4\uFF1B\u4E3A\u751F\u4EA7\u6E38\u620F\u6CE8\u5165 devkit \u7684 GameClock\uFF0C\u5E76\u4F20\u5165 step: () => clock.stepFrame()\u3002\u4E0D\u8981\u7528\u771F\u5B9E setTimeout\u3002" : timedOut ? "\u771F\u5B9E\u8F93\u5165\u5DF2\u6267\u884C\uFF0C\u4F46\u73A9\u6CD5\u6CA1\u6709\u5728\u4E0A\u9650\u5185\u4EA7\u751F\u7ED3\u679C\uFF1B\u68C0\u67E5\u751F\u4EA7\u63A7\u5236\u662F\u5426\u6536\u5230\u8F93\u5165\uFF0C\u518D\u67E5\u770B\u8D85\u65F6\u9519\u8BEF\u4E2D\u7684 Last diagnostics \u5224\u65AD\u662F\u6E38\u620F\u5FAA\u73AF\u3001\u89C4\u5219\u72B6\u6001\u8FD8\u662F UI \u540C\u6B65\u672A\u63A8\u8FDB\u3002" : missingStructuredEvidence ? '\u6309\u987A\u5E8F\u8865\u9F50\u81F3\u5C11\u4E24\u6B21\u7B7E\u5230\uFF1AperformInput("entry") \u540E\u8C03\u7528 checkpoint("entered")\uFF1B\u518D\u7528 performInput("primary") \u6267\u884C\u6838\u5FC3\u64CD\u4F5C\uFF0CstepUntil \u7B49\u5F85\u7ED3\u679C\uFF0C\u7528\u56DE\u8C03\u63D0\u4F9B\u7684 expect \u65AD\u8A00\u540E\u8C03\u7528 checkpoint("progress") \u6216 checkpoint("terminal")\u3002' : "\u4ECE\u751F\u4EA7\u5165\u53E3\u6267\u884C\u771F\u5B9E DOM \u8F93\u5165\uFF0C\u7528 stepUntil \u6709\u754C\u63A8\u8FDB\u5230\u73A9\u5BB6\u53EF\u89C1\u7ED3\u679C\u6216\u6E38\u620F\u6743\u5A01\u72B6\u6001\uFF0C\u5E76\u5728\u5176\u540E\u65AD\u8A00\uFF1B\u4E0D\u8981\u76F4\u8FBE\u5185\u90E8\u5173\u5361\u6216\u4FEE\u6539\u73A9\u6CD5\u72B6\u6001\u3002",
409
397
  failsRun: true
410
398
  };
411
399
  }
@@ -67,20 +67,25 @@ var INPUT_EVENTS = [
67
67
  "touchstart",
68
68
  "touchend"
69
69
  ];
70
+ var MIN_CHECKPOINTS = 2;
70
71
  function describeMissingEvidence(evidence) {
71
72
  if (!evidence || evidence.entryInputs === 0) return "entry \u8F93\u5165";
72
73
  if (evidence.primaryInputs === 0) return "primary \u8F93\u5165";
73
74
  if (!evidence.checkpoints.includes("entered")) return "entered checkpoint";
74
75
  if (evidence.boundedRuns === 0) return "\u6709\u754C stepUntil";
75
76
  if (evidence.assertionsAfterOutcome === 0) return "stepUntil \u540E\u7684\u7ED3\u679C\u65AD\u8A00";
76
- if (evidence.changedOutcomeCheckpoints === 0) {
77
- return "\u53D1\u751F\u72B6\u6001\u53D8\u5316\u7684 progress/terminal checkpoint";
77
+ if (evidence.checkpoints.length < MIN_CHECKPOINTS)
78
+ return `\u81F3\u5C11 ${MIN_CHECKPOINTS} \u4E2A checkpoint`;
79
+ if (!evidence.checkpoints.some(
80
+ (checkpoint) => checkpoint === "progress" || checkpoint === "terminal"
81
+ )) {
82
+ return "progress/terminal checkpoint";
78
83
  }
79
84
  return "\u5B8C\u6574\u7684 playthrough \u6821\u9A8C\u6807\u8BB0";
80
85
  }
81
86
  function createMetadata(waiverReason) {
82
87
  return {
83
- version: 2,
88
+ version: 3,
84
89
  waiverReason,
85
90
  evidence: {
86
91
  domInputEvents: 0,
@@ -88,23 +93,11 @@ function createMetadata(waiverReason) {
88
93
  primaryInputs: 0,
89
94
  boundedRuns: 0,
90
95
  assertionsAfterOutcome: 0,
91
- changedOutcomeCheckpoints: 0,
92
96
  checkpoints: [],
93
97
  verified: false
94
98
  }
95
99
  };
96
100
  }
97
- function snapshotFingerprint(snapshot) {
98
- try {
99
- const serialized = JSON.stringify(snapshot);
100
- if (serialized === void 0) throw new Error("unsupported value");
101
- return serialized;
102
- } catch {
103
- throw new Error(
104
- "checkpoint snapshot must be JSON-serializable. Pass a read-only Telemetry snapshot or a small visible-state object."
105
- );
106
- }
107
- }
108
101
  function definePlaythrough(element, run, waiverReason) {
109
102
  const reason = normalizePlaythroughWaiverReason(waiverReason);
110
103
  const metadata = createMetadata(reason);
@@ -114,7 +107,7 @@ function definePlaythrough(element, run, waiverReason) {
114
107
  }, async ({ expect }) => {
115
108
  const evidence = metadata.evidence;
116
109
  let assertionsAtOutcome;
117
- let enteredSnapshot;
110
+ let enteredRecorded = false;
118
111
  const recordInput = () => {
119
112
  evidence.domInputEvents += 1;
120
113
  };
@@ -134,14 +127,14 @@ function definePlaythrough(element, run, waiverReason) {
134
127
  user,
135
128
  expect,
136
129
  async performInput(kind, input) {
137
- if (kind === "entry" && enteredSnapshot !== void 0) {
130
+ if (kind === "entry" && enteredRecorded) {
138
131
  throw new Error(
139
132
  'performInput("entry") must run before checkpoint("entered"). Group multiple setup actions in the same callback.'
140
133
  );
141
134
  }
142
- if (kind === "primary" && enteredSnapshot === void 0) {
135
+ if (kind === "primary" && !enteredRecorded) {
143
136
  throw new Error(
144
- 'Before performInput("primary"), run an entry input and checkpoint("entered", snapshot).'
137
+ 'Before performInput("primary"), run an entry input and checkpoint("entered").'
145
138
  );
146
139
  }
147
140
  const inputsBefore = evidence.domInputEvents;
@@ -154,10 +147,9 @@ function definePlaythrough(element, run, waiverReason) {
154
147
  if (kind === "entry") evidence.entryInputs += 1;
155
148
  else evidence.primaryInputs += 1;
156
149
  },
157
- checkpoint(kind, snapshot) {
158
- const fingerprint = snapshotFingerprint(snapshot);
150
+ checkpoint(kind) {
159
151
  if (kind === "entered") {
160
- if (enteredSnapshot !== void 0) {
152
+ if (enteredRecorded) {
161
153
  throw new Error(
162
154
  'checkpoint("entered") may only be recorded once, before the primary input.'
163
155
  );
@@ -167,7 +159,7 @@ function definePlaythrough(element, run, waiverReason) {
167
159
  'checkpoint("entered") must follow performInput("entry", ...).'
168
160
  );
169
161
  }
170
- enteredSnapshot = fingerprint;
162
+ enteredRecorded = true;
171
163
  evidence.checkpoints.push(kind);
172
164
  return;
173
165
  }
@@ -183,15 +175,9 @@ function definePlaythrough(element, run, waiverReason) {
183
175
  }
184
176
  if (assertionsAtOutcome === void 0 || expect.getState().assertionCalls <= assertionsAtOutcome) {
185
177
  throw new Error(
186
- `Use the expect provided by playthroughTest to assert the authoritative result after stepUntil, then record checkpoint("${kind}", snapshot).`
187
- );
188
- }
189
- if (fingerprint === enteredSnapshot) {
190
- throw new Error(
191
- `checkpoint("${kind}") matches the entered snapshot. Assert a production state change caused by the primary input.`
178
+ `Use the expect provided by playthroughTest to assert the authoritative result after stepUntil, then record checkpoint("${kind}").`
192
179
  );
193
180
  }
194
- evidence.changedOutcomeCheckpoints += 1;
195
181
  evidence.checkpoints.push(kind);
196
182
  },
197
183
  async stepUntil(condition, options = {}) {
@@ -226,9 +212,11 @@ function definePlaythrough(element, run, waiverReason) {
226
212
  "Use the expect provided by playthroughTest to assert an authoritative game outcome after stepUntil returns."
227
213
  );
228
214
  }
229
- if (evidence.changedOutcomeCheckpoints === 0) {
215
+ if (evidence.checkpoints.length < MIN_CHECKPOINTS || !evidence.checkpoints.some(
216
+ (checkpoint) => checkpoint === "progress" || checkpoint === "terminal"
217
+ )) {
230
218
  throw new Error(
231
- 'After asserting the result, record checkpoint("progress", snapshot) or checkpoint("terminal", snapshot). The snapshot must differ from checkpoint("entered").'
219
+ `playthroughTest requires at least ${MIN_CHECKPOINTS} checkpoints: checkpoint("entered") and, after asserting the result, checkpoint("progress") or checkpoint("terminal").`
232
220
  );
233
221
  }
234
222
  evidence.verified = true;
@@ -249,7 +237,7 @@ function auditReactPlaythroughRun(tests) {
249
237
  const declared = tests.filter((candidate) => candidate.metadata);
250
238
  const valid = declared.filter(({ state, metadata }) => {
251
239
  const evidence = metadata?.evidence;
252
- 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(
240
+ return state === "passed" && evidence?.verified === true && evidence.domInputEvents > 0 && evidence.entryInputs > 0 && evidence.primaryInputs > 0 && evidence.boundedRuns > 0 && evidence.assertionsAfterOutcome > 0 && evidence.checkpoints.length >= MIN_CHECKPOINTS && evidence.checkpoints.includes("entered") && evidence.checkpoints.some(
253
241
  (checkpoint) => checkpoint === "progress" || checkpoint === "terminal"
254
242
  );
255
243
  });
@@ -259,7 +247,7 @@ function auditReactPlaythroughRun(tests) {
259
247
  const issues = [];
260
248
  if (declared.length === 0) {
261
249
  issues.push(
262
- '\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'
250
+ '\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\u6743\u5A01\u7ED3\u679C\uFF0C\u5E76\u8BB0\u5F55 checkpoint("progress") \u6216 checkpoint("terminal")\uFF1B\u81F3\u5C11\u5B8C\u6210\u8FD9\u4E24\u6B21 checkpoint \u7B7E\u5230\u3002'
263
251
  );
264
252
  } else {
265
253
  for (const candidate of declared) {
@@ -289,7 +277,7 @@ var PRODUCTION_PLAYTHROUGH_FILE = "tests/production-playthrough.test.tsx";
289
277
  function isMetadata(value) {
290
278
  if (!value || typeof value !== "object") return false;
291
279
  const metadata = value;
292
- return metadata.version === 2 && Boolean(metadata.evidence);
280
+ return metadata.version === 3 && Boolean(metadata.evidence);
293
281
  }
294
282
  function toAuditInput(test2) {
295
283
  const metadata = test2.meta().reactPlaythrough;
@@ -333,7 +321,7 @@ function assessReactPlaythroughReport(input) {
333
321
  ...base,
334
322
  status: "FAILED",
335
323
  cause: "\u751F\u4EA7\u53EF\u73A9\u6027\u6D4B\u8BD5\u6587\u4EF6\u4E0D\u5B58\u5728\u3002",
336
- next: '\u521B\u5EFA\u8BE5\u6587\u4EF6\uFF1A\u4ECE <App /> \u901A\u8FC7\u771F\u5B9E DOM \u8F93\u5165\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\u3002Canvas \u7528\u771F\u5B9E DOM \u4E8B\u4EF6\u8F93\u5165\u5E76\u8BFB\u53D6 Telemetry snapshot\u3002',
324
+ next: '\u521B\u5EFA\u8BE5\u6587\u4EF6\uFF1A\u4ECE <App /> \u901A\u8FC7\u771F\u5B9E DOM \u8F93\u5165\u4F9D\u6B21\u6267\u884C performInput("entry")\u3001checkpoint("entered")\u3001performInput("primary")\u3001stepUntil\u3001\u7528 playthroughTest \u63D0\u4F9B\u7684 expect \u65AD\u8A00\u6743\u5A01\u7ED3\u679C\uFF0C\u5E76\u8BB0\u5F55 checkpoint("progress") \u6216 checkpoint("terminal")\u3002\u6BCF\u6761\u4E3B\u6D41\u7A0B\u81F3\u5C11\u9700\u8981\u8FD9\u4E24\u6B21 checkpoint \u7B7E\u5230\u3002',
337
325
  failsRun: true
338
326
  };
339
327
  }
@@ -371,7 +359,7 @@ function assessReactPlaythroughReport(input) {
371
359
  ...base,
372
360
  status: "FAILED",
373
361
  cause,
374
- next: missingStep ? "\u8BE5\u6D41\u7A0B\u662F\u65F6\u95F4\u6216\u5E27\u9A71\u52A8\u7684\uFF0C\u4F46 stepUntil \u6CA1\u6709\u63A8\u8FDB\u6E38\u620F\u65F6\u95F4\uFF1B\u4E3A\u751F\u4EA7\u6E38\u620F\u6CE8\u5165 devkit \u7684 GameClock\uFF0C\u5E76\u4F20\u5165 step: () => clock.stepFrame()\u3002\u4E0D\u8981\u7528\u771F\u5B9E setTimeout\u3002" : timedOut ? "\u771F\u5B9E\u8F93\u5165\u5DF2\u6267\u884C\uFF0C\u4F46\u73A9\u6CD5\u6CA1\u6709\u5728\u4E0A\u9650\u5185\u4EA7\u751F\u7ED3\u679C\uFF1B\u68C0\u67E5\u751F\u4EA7\u63A7\u5236\u662F\u5426\u6536\u5230\u8F93\u5165\uFF0C\u518D\u67E5\u770B\u8D85\u65F6\u9519\u8BEF\u4E2D\u7684 Last diagnostics \u5224\u65AD\u662F\u6E38\u620F\u5FAA\u73AF\u3001\u89C4\u5219\u72B6\u6001\u8FD8\u662F UI \u540C\u6B65\u672A\u63A8\u8FDB\u3002" : missingStructuredEvidence ? '\u6309\u987A\u5E8F\u8865\u9F50\uFF1AperformInput("entry") \u540E\u8BB0\u5F55 entered snapshot\uFF1B\u518D\u7528 performInput("primary") \u6267\u884C\u6838\u5FC3\u64CD\u4F5C\uFF0CstepUntil \u7B49\u5F85\u53D8\u5316\uFF0C\u65AD\u8A00\u540E\u8BB0\u5F55 progress \u6216 terminal snapshot\u3002' : "\u4ECE\u751F\u4EA7\u5165\u53E3\u6267\u884C\u771F\u5B9E DOM \u8F93\u5165\uFF0C\u7528 stepUntil \u6709\u754C\u63A8\u8FDB\u5230\u73A9\u5BB6\u53EF\u89C1\u7ED3\u679C\u6216\u6E38\u620F\u771F\u5B9E\u72B6\u6001\u7684\u53EA\u8BFB snapshot\uFF0C\u5E76\u5728\u5176\u540E\u65AD\u8A00\uFF1B\u4E0D\u8981\u76F4\u8FBE\u5185\u90E8\u5173\u5361\u6216\u4FEE\u6539\u73A9\u6CD5\u72B6\u6001\u3002",
362
+ next: missingStep ? "\u8BE5\u6D41\u7A0B\u662F\u65F6\u95F4\u6216\u5E27\u9A71\u52A8\u7684\uFF0C\u4F46 stepUntil \u6CA1\u6709\u63A8\u8FDB\u6E38\u620F\u65F6\u95F4\uFF1B\u4E3A\u751F\u4EA7\u6E38\u620F\u6CE8\u5165 devkit \u7684 GameClock\uFF0C\u5E76\u4F20\u5165 step: () => clock.stepFrame()\u3002\u4E0D\u8981\u7528\u771F\u5B9E setTimeout\u3002" : timedOut ? "\u771F\u5B9E\u8F93\u5165\u5DF2\u6267\u884C\uFF0C\u4F46\u73A9\u6CD5\u6CA1\u6709\u5728\u4E0A\u9650\u5185\u4EA7\u751F\u7ED3\u679C\uFF1B\u68C0\u67E5\u751F\u4EA7\u63A7\u5236\u662F\u5426\u6536\u5230\u8F93\u5165\uFF0C\u518D\u67E5\u770B\u8D85\u65F6\u9519\u8BEF\u4E2D\u7684 Last diagnostics \u5224\u65AD\u662F\u6E38\u620F\u5FAA\u73AF\u3001\u89C4\u5219\u72B6\u6001\u8FD8\u662F UI \u540C\u6B65\u672A\u63A8\u8FDB\u3002" : missingStructuredEvidence ? '\u6309\u987A\u5E8F\u8865\u9F50\u81F3\u5C11\u4E24\u6B21\u7B7E\u5230\uFF1AperformInput("entry") \u540E\u8C03\u7528 checkpoint("entered")\uFF1B\u518D\u7528 performInput("primary") \u6267\u884C\u6838\u5FC3\u64CD\u4F5C\uFF0CstepUntil \u7B49\u5F85\u7ED3\u679C\uFF0C\u7528\u56DE\u8C03\u63D0\u4F9B\u7684 expect \u65AD\u8A00\u540E\u8C03\u7528 checkpoint("progress") \u6216 checkpoint("terminal")\u3002' : "\u4ECE\u751F\u4EA7\u5165\u53E3\u6267\u884C\u771F\u5B9E DOM \u8F93\u5165\uFF0C\u7528 stepUntil \u6709\u754C\u63A8\u8FDB\u5230\u73A9\u5BB6\u53EF\u89C1\u7ED3\u679C\u6216\u6E38\u620F\u6743\u5A01\u72B6\u6001\uFF0C\u5E76\u5728\u5176\u540E\u65AD\u8A00\uFF1B\u4E0D\u8981\u76F4\u8FBE\u5185\u90E8\u5173\u5361\u6216\u4FEE\u6539\u73A9\u6CD5\u72B6\u6001\u3002",
375
363
  failsRun: true
376
364
  };
377
365
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miaoda-game-devkit",
3
- "version": "0.2.20",
3
+ "version": "0.2.21",
4
4
  "description": "Shared React and Phaser game lint plus deterministic testing tools for Miaoda games",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",