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,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-vitest-config.ts
@@ -33,10 +43,21 @@ var import_node_path = require("path");
33
43
 
34
44
  // src/react/react-playthrough.ts
35
45
  var import_react2 = require("@testing-library/react");
46
+ var import_user_event = __toESM(require("@testing-library/user-event"));
36
47
  var import_vitest = require("vitest");
37
48
 
38
49
  // src/react/react-playthrough-core.ts
39
50
  var import_react = require("@testing-library/react");
51
+ function formatDiagnostics(read) {
52
+ if (!read) return void 0;
53
+ try {
54
+ const value = read();
55
+ if (typeof value === "string") return value;
56
+ return JSON.stringify(value);
57
+ } catch (error) {
58
+ return `diagnostics() threw: ${String(error)}`;
59
+ }
60
+ }
40
61
  function normalizePlaythroughWaiverReason(waiverReason) {
41
62
  if (waiverReason === void 0) return void 0;
42
63
  const reason = waiverReason.trim();
@@ -62,8 +83,11 @@ async function runBoundedUntil(condition, options = {}) {
62
83
  });
63
84
  }
64
85
  }
86
+ const diagnostics = formatDiagnostics(options.diagnostics);
87
+ 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().";
88
+ const suffix = diagnostics ? ` Last diagnostics: ${diagnostics}` : "";
65
89
  throw new Error(
66
- `Playthrough outcome was not reached within ${maxSteps} steps.`
90
+ `Playthrough outcome was not reached within ${maxSteps} steps. ${guidance}${suffix}`
67
91
  );
68
92
  }
69
93
 
@@ -77,27 +101,54 @@ var INPUT_EVENTS = [
77
101
  "touchstart",
78
102
  "touchend"
79
103
  ];
104
+ function describeMissingEvidence(evidence) {
105
+ if (!evidence || evidence.entryInputs === 0) return "entry \u8F93\u5165";
106
+ if (evidence.primaryInputs === 0) return "primary \u8F93\u5165";
107
+ if (!evidence.checkpoints.includes("entered")) return "entered checkpoint";
108
+ if (evidence.boundedRuns === 0) return "\u6709\u754C stepUntil";
109
+ 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";
112
+ }
113
+ return "\u5B8C\u6574\u7684 playthrough \u6821\u9A8C\u6807\u8BB0";
114
+ }
80
115
  function createMetadata(waiverReason) {
81
116
  return {
82
- version: 1,
117
+ version: 2,
83
118
  waiverReason,
84
119
  evidence: {
85
120
  domInputEvents: 0,
121
+ entryInputs: 0,
122
+ primaryInputs: 0,
86
123
  boundedRuns: 0,
87
124
  assertionsAfterOutcome: 0,
125
+ changedOutcomeCheckpoints: 0,
126
+ checkpoints: [],
88
127
  verified: false
89
128
  }
90
129
  };
91
130
  }
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
+ }
92
142
  function definePlaythrough(element, run, waiverReason) {
93
143
  const reason = normalizePlaythroughWaiverReason(waiverReason);
94
144
  const metadata = createMetadata(reason);
95
145
  (0, import_vitest.test)("production game completes a bounded playthrough", {
96
146
  skip: Boolean(reason),
97
147
  meta: { reactPlaythrough: metadata }
98
- }, async () => {
148
+ }, async ({ expect }) => {
99
149
  const evidence = metadata.evidence;
100
150
  let assertionsAtOutcome;
151
+ let enteredSnapshot;
101
152
  const recordInput = () => {
102
153
  evidence.domInputEvents += 1;
103
154
  };
@@ -111,25 +162,94 @@ function definePlaythrough(element, run, waiverReason) {
111
162
  "playthroughTest must render the production game entry."
112
163
  );
113
164
  }
165
+ const user = import_user_event.default.setup();
114
166
  await run({
115
167
  view,
168
+ user,
169
+ expect,
170
+ async performInput(kind, input) {
171
+ if (kind === "entry" && enteredSnapshot !== void 0) {
172
+ throw new Error(
173
+ 'performInput("entry") must run before checkpoint("entered"). Group multiple setup actions in the same callback.'
174
+ );
175
+ }
176
+ if (kind === "primary" && enteredSnapshot === void 0) {
177
+ throw new Error(
178
+ 'Before performInput("primary"), run an entry input and checkpoint("entered", snapshot).'
179
+ );
180
+ }
181
+ const inputsBefore = evidence.domInputEvents;
182
+ await input();
183
+ if (evidence.domInputEvents === inputsBefore) {
184
+ throw new Error(
185
+ `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.`
186
+ );
187
+ }
188
+ if (kind === "entry") evidence.entryInputs += 1;
189
+ else evidence.primaryInputs += 1;
190
+ },
191
+ checkpoint(kind, snapshot) {
192
+ const fingerprint = snapshotFingerprint(snapshot);
193
+ if (kind === "entered") {
194
+ if (enteredSnapshot !== void 0) {
195
+ throw new Error(
196
+ 'checkpoint("entered") may only be recorded once, before the primary input.'
197
+ );
198
+ }
199
+ if (evidence.entryInputs === 0) {
200
+ throw new Error(
201
+ 'checkpoint("entered") must follow performInput("entry", ...).'
202
+ );
203
+ }
204
+ enteredSnapshot = fingerprint;
205
+ evidence.checkpoints.push(kind);
206
+ return;
207
+ }
208
+ if (evidence.primaryInputs === 0) {
209
+ throw new Error(
210
+ `checkpoint("${kind}") must follow performInput("primary", ...).`
211
+ );
212
+ }
213
+ if (evidence.boundedRuns === 0) {
214
+ throw new Error(
215
+ `checkpoint("${kind}") must be recorded after stepUntil returns.`
216
+ );
217
+ }
218
+ if (assertionsAtOutcome === void 0 || expect.getState().assertionCalls <= assertionsAtOutcome) {
219
+ 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.`
226
+ );
227
+ }
228
+ evidence.changedOutcomeCheckpoints += 1;
229
+ evidence.checkpoints.push(kind);
230
+ },
116
231
  async stepUntil(condition, options = {}) {
117
232
  const steps = await runBoundedUntil(condition, options);
118
- if (evidence.domInputEvents === 0) {
233
+ if (evidence.primaryInputs === 0) {
119
234
  throw new Error(
120
- "stepUntil reached an outcome before any production DOM input."
235
+ 'stepUntil must follow performInput("primary", ...). A menu/help click is not gameplay evidence.'
121
236
  );
122
237
  }
123
238
  evidence.boundedRuns += 1;
124
- assertionsAtOutcome = import_vitest.expect.getState().assertionCalls;
239
+ assertionsAtOutcome = expect.getState().assertionCalls;
125
240
  return steps;
126
241
  }
127
242
  });
128
- const assertionCalls = import_vitest.expect.getState().assertionCalls;
243
+ const assertionCalls = expect.getState().assertionCalls;
129
244
  evidence.assertionsAfterOutcome = assertionsAtOutcome === void 0 ? 0 : assertionCalls - assertionsAtOutcome;
130
- if (evidence.domInputEvents === 0) {
245
+ if (evidence.entryInputs === 0) {
131
246
  throw new Error(
132
- "playthroughTest did not observe production DOM input."
247
+ 'playthroughTest must perform an entry input with performInput("entry", ...).'
248
+ );
249
+ }
250
+ if (evidence.primaryInputs === 0) {
251
+ throw new Error(
252
+ 'playthroughTest must perform a core game action with performInput("primary", ...).'
133
253
  );
134
254
  }
135
255
  if (evidence.boundedRuns === 0) {
@@ -137,7 +257,12 @@ function definePlaythrough(element, run, waiverReason) {
137
257
  }
138
258
  if (evidence.assertionsAfterOutcome === 0) {
139
259
  throw new Error(
140
- "Assert an authoritative game outcome after stepUntil returns."
260
+ "Use the expect provided by playthroughTest to assert an authoritative game outcome after stepUntil returns."
261
+ );
262
+ }
263
+ if (evidence.changedOutcomeCheckpoints === 0) {
264
+ throw new Error(
265
+ 'After asserting the result, record checkpoint("progress", snapshot) or checkpoint("terminal", snapshot). The snapshot must differ from checkpoint("entered").'
141
266
  );
142
267
  }
143
268
  evidence.verified = true;
@@ -158,7 +283,9 @@ function auditReactPlaythroughRun(tests) {
158
283
  const declared = tests.filter((candidate) => candidate.metadata);
159
284
  const valid = declared.filter(({ state, metadata }) => {
160
285
  const evidence = metadata?.evidence;
161
- return state === "passed" && evidence?.verified === true && evidence.domInputEvents > 0 && evidence.boundedRuns > 0 && evidence.assertionsAfterOutcome > 0;
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(
287
+ (checkpoint) => checkpoint === "progress" || checkpoint === "terminal"
288
+ );
162
289
  });
163
290
  const waivers = declared.filter(
164
291
  ({ state, metadata }) => state === "skipped" && (metadata?.waiverReason?.trim().length ?? 0) >= 20
@@ -166,7 +293,7 @@ function auditReactPlaythroughRun(tests) {
166
293
  const issues = [];
167
294
  if (declared.length === 0) {
168
295
  issues.push(
169
- "\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"
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'
170
297
  );
171
298
  } else {
172
299
  for (const candidate of declared) {
@@ -180,9 +307,8 @@ function auditReactPlaythroughRun(tests) {
180
307
  } else if (candidate.state !== "passed") {
181
308
  issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7684\u72B6\u6001\u4E3A ${candidate.state}\u3002`);
182
309
  } else {
183
- issues.push(
184
- `\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`
185
- );
310
+ const missing = describeMissingEvidence(candidate.metadata?.evidence);
311
+ issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7F3A\u5C11${missing}\u3002`);
186
312
  }
187
313
  }
188
314
  }
@@ -197,7 +323,7 @@ var PRODUCTION_PLAYTHROUGH_FILE = "tests/production-playthrough.test.tsx";
197
323
  function isMetadata(value) {
198
324
  if (!value || typeof value !== "object") return false;
199
325
  const metadata = value;
200
- return metadata.version === 1 && Boolean(metadata.evidence);
326
+ return metadata.version === 2 && Boolean(metadata.evidence);
201
327
  }
202
328
  function toAuditInput(test2) {
203
329
  const metadata = test2.meta().reactPlaythrough;
@@ -241,7 +367,7 @@ function assessReactPlaythroughReport(input) {
241
367
  ...base,
242
368
  status: "FAILED",
243
369
  cause: "\u751F\u4EA7\u53EF\u73A9\u6027\u6D4B\u8BD5\u6587\u4EF6\u4E0D\u5B58\u5728\u3002",
244
- next: "\u521B\u5EFA\u8BE5\u6587\u4EF6\uFF1A\u4ECE <App /> \u89E6\u53D1\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\u518D\u65AD\u8A00\u7ED3\u679C\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',
245
371
  failsRun: true
246
372
  };
247
373
  }
@@ -273,11 +399,13 @@ function assessReactPlaythroughReport(input) {
273
399
  if (!audit.passed) {
274
400
  const cause = productionModule.errors[0] ?? input.unhandledErrors?.[0] ?? audit.issues[0] ?? "\u751F\u4EA7\u53EF\u73A9\u6D41\u7A0B\u6CA1\u6709\u7559\u4E0B\u5B8C\u6574\u8BC1\u636E\u3002";
275
401
  const timedOut = /outcome was not reached within \d+ steps/i.test(cause);
402
+ const missingStep = /No step callback was provided/i.test(cause);
403
+ const missingStructuredEvidence = /performInput|checkpoint/.test(cause);
276
404
  return {
277
405
  ...base,
278
406
  status: "FAILED",
279
407
  cause,
280
- next: 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\u70B9\u51FB\u6216\u6309\u952E\u662F\u5426\u771F\u7684\u8C03\u7528\u6E38\u620F\u7684\u751F\u4EA7\u63A7\u5236\u65B9\u6CD5\uFF0C\u4EE5\u53CA\u6E38\u620F\u771F\u5B9E\u72B6\u6001\u662F\u5426\u5230\u8FBE\u5E76\u663E\u793A\u7ED3\u679C\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",
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",
281
409
  failsRun: true
282
410
  };
283
411
  }
@@ -9,10 +9,21 @@ import { relative, resolve } from "path";
9
9
 
10
10
  // src/react/react-playthrough.ts
11
11
  import { render } from "@testing-library/react";
12
- import { expect, test } from "vitest";
12
+ import userEvent from "@testing-library/user-event";
13
+ import { test } from "vitest";
13
14
 
14
15
  // src/react/react-playthrough-core.ts
15
16
  import { act } from "@testing-library/react";
17
+ function formatDiagnostics(read) {
18
+ if (!read) return void 0;
19
+ try {
20
+ const value = read();
21
+ if (typeof value === "string") return value;
22
+ return JSON.stringify(value);
23
+ } catch (error) {
24
+ return `diagnostics() threw: ${String(error)}`;
25
+ }
26
+ }
16
27
  function normalizePlaythroughWaiverReason(waiverReason) {
17
28
  if (waiverReason === void 0) return void 0;
18
29
  const reason = waiverReason.trim();
@@ -38,8 +49,11 @@ async function runBoundedUntil(condition, options = {}) {
38
49
  });
39
50
  }
40
51
  }
52
+ const diagnostics = formatDiagnostics(options.diagnostics);
53
+ 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().";
54
+ const suffix = diagnostics ? ` Last diagnostics: ${diagnostics}` : "";
41
55
  throw new Error(
42
- `Playthrough outcome was not reached within ${maxSteps} steps.`
56
+ `Playthrough outcome was not reached within ${maxSteps} steps. ${guidance}${suffix}`
43
57
  );
44
58
  }
45
59
 
@@ -53,27 +67,54 @@ var INPUT_EVENTS = [
53
67
  "touchstart",
54
68
  "touchend"
55
69
  ];
70
+ function describeMissingEvidence(evidence) {
71
+ if (!evidence || evidence.entryInputs === 0) return "entry \u8F93\u5165";
72
+ if (evidence.primaryInputs === 0) return "primary \u8F93\u5165";
73
+ if (!evidence.checkpoints.includes("entered")) return "entered checkpoint";
74
+ if (evidence.boundedRuns === 0) return "\u6709\u754C stepUntil";
75
+ 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";
78
+ }
79
+ return "\u5B8C\u6574\u7684 playthrough \u6821\u9A8C\u6807\u8BB0";
80
+ }
56
81
  function createMetadata(waiverReason) {
57
82
  return {
58
- version: 1,
83
+ version: 2,
59
84
  waiverReason,
60
85
  evidence: {
61
86
  domInputEvents: 0,
87
+ entryInputs: 0,
88
+ primaryInputs: 0,
62
89
  boundedRuns: 0,
63
90
  assertionsAfterOutcome: 0,
91
+ changedOutcomeCheckpoints: 0,
92
+ checkpoints: [],
64
93
  verified: false
65
94
  }
66
95
  };
67
96
  }
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
+ }
68
108
  function definePlaythrough(element, run, waiverReason) {
69
109
  const reason = normalizePlaythroughWaiverReason(waiverReason);
70
110
  const metadata = createMetadata(reason);
71
111
  test("production game completes a bounded playthrough", {
72
112
  skip: Boolean(reason),
73
113
  meta: { reactPlaythrough: metadata }
74
- }, async () => {
114
+ }, async ({ expect }) => {
75
115
  const evidence = metadata.evidence;
76
116
  let assertionsAtOutcome;
117
+ let enteredSnapshot;
77
118
  const recordInput = () => {
78
119
  evidence.domInputEvents += 1;
79
120
  };
@@ -87,13 +128,77 @@ function definePlaythrough(element, run, waiverReason) {
87
128
  "playthroughTest must render the production game entry."
88
129
  );
89
130
  }
131
+ const user = userEvent.setup();
90
132
  await run({
91
133
  view,
134
+ user,
135
+ expect,
136
+ async performInput(kind, input) {
137
+ if (kind === "entry" && enteredSnapshot !== void 0) {
138
+ throw new Error(
139
+ 'performInput("entry") must run before checkpoint("entered"). Group multiple setup actions in the same callback.'
140
+ );
141
+ }
142
+ if (kind === "primary" && enteredSnapshot === void 0) {
143
+ throw new Error(
144
+ 'Before performInput("primary"), run an entry input and checkpoint("entered", snapshot).'
145
+ );
146
+ }
147
+ const inputsBefore = evidence.domInputEvents;
148
+ await input();
149
+ if (evidence.domInputEvents === inputsBefore) {
150
+ throw new Error(
151
+ `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.`
152
+ );
153
+ }
154
+ if (kind === "entry") evidence.entryInputs += 1;
155
+ else evidence.primaryInputs += 1;
156
+ },
157
+ checkpoint(kind, snapshot) {
158
+ const fingerprint = snapshotFingerprint(snapshot);
159
+ if (kind === "entered") {
160
+ if (enteredSnapshot !== void 0) {
161
+ throw new Error(
162
+ 'checkpoint("entered") may only be recorded once, before the primary input.'
163
+ );
164
+ }
165
+ if (evidence.entryInputs === 0) {
166
+ throw new Error(
167
+ 'checkpoint("entered") must follow performInput("entry", ...).'
168
+ );
169
+ }
170
+ enteredSnapshot = fingerprint;
171
+ evidence.checkpoints.push(kind);
172
+ return;
173
+ }
174
+ if (evidence.primaryInputs === 0) {
175
+ throw new Error(
176
+ `checkpoint("${kind}") must follow performInput("primary", ...).`
177
+ );
178
+ }
179
+ if (evidence.boundedRuns === 0) {
180
+ throw new Error(
181
+ `checkpoint("${kind}") must be recorded after stepUntil returns.`
182
+ );
183
+ }
184
+ if (assertionsAtOutcome === void 0 || expect.getState().assertionCalls <= assertionsAtOutcome) {
185
+ 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.`
192
+ );
193
+ }
194
+ evidence.changedOutcomeCheckpoints += 1;
195
+ evidence.checkpoints.push(kind);
196
+ },
92
197
  async stepUntil(condition, options = {}) {
93
198
  const steps = await runBoundedUntil(condition, options);
94
- if (evidence.domInputEvents === 0) {
199
+ if (evidence.primaryInputs === 0) {
95
200
  throw new Error(
96
- "stepUntil reached an outcome before any production DOM input."
201
+ 'stepUntil must follow performInput("primary", ...). A menu/help click is not gameplay evidence.'
97
202
  );
98
203
  }
99
204
  evidence.boundedRuns += 1;
@@ -103,9 +208,14 @@ function definePlaythrough(element, run, waiverReason) {
103
208
  });
104
209
  const assertionCalls = expect.getState().assertionCalls;
105
210
  evidence.assertionsAfterOutcome = assertionsAtOutcome === void 0 ? 0 : assertionCalls - assertionsAtOutcome;
106
- if (evidence.domInputEvents === 0) {
211
+ if (evidence.entryInputs === 0) {
107
212
  throw new Error(
108
- "playthroughTest did not observe production DOM input."
213
+ 'playthroughTest must perform an entry input with performInput("entry", ...).'
214
+ );
215
+ }
216
+ if (evidence.primaryInputs === 0) {
217
+ throw new Error(
218
+ 'playthroughTest must perform a core game action with performInput("primary", ...).'
109
219
  );
110
220
  }
111
221
  if (evidence.boundedRuns === 0) {
@@ -113,7 +223,12 @@ function definePlaythrough(element, run, waiverReason) {
113
223
  }
114
224
  if (evidence.assertionsAfterOutcome === 0) {
115
225
  throw new Error(
116
- "Assert an authoritative game outcome after stepUntil returns."
226
+ "Use the expect provided by playthroughTest to assert an authoritative game outcome after stepUntil returns."
227
+ );
228
+ }
229
+ if (evidence.changedOutcomeCheckpoints === 0) {
230
+ throw new Error(
231
+ 'After asserting the result, record checkpoint("progress", snapshot) or checkpoint("terminal", snapshot). The snapshot must differ from checkpoint("entered").'
117
232
  );
118
233
  }
119
234
  evidence.verified = true;
@@ -134,7 +249,9 @@ function auditReactPlaythroughRun(tests) {
134
249
  const declared = tests.filter((candidate) => candidate.metadata);
135
250
  const valid = declared.filter(({ state, metadata }) => {
136
251
  const evidence = metadata?.evidence;
137
- return state === "passed" && evidence?.verified === true && evidence.domInputEvents > 0 && evidence.boundedRuns > 0 && evidence.assertionsAfterOutcome > 0;
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(
253
+ (checkpoint) => checkpoint === "progress" || checkpoint === "terminal"
254
+ );
138
255
  });
139
256
  const waivers = declared.filter(
140
257
  ({ state, metadata }) => state === "skipped" && (metadata?.waiverReason?.trim().length ?? 0) >= 20
@@ -142,7 +259,7 @@ function auditReactPlaythroughRun(tests) {
142
259
  const issues = [];
143
260
  if (declared.length === 0) {
144
261
  issues.push(
145
- "\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"
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'
146
263
  );
147
264
  } else {
148
265
  for (const candidate of declared) {
@@ -156,9 +273,8 @@ function auditReactPlaythroughRun(tests) {
156
273
  } else if (candidate.state !== "passed") {
157
274
  issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7684\u72B6\u6001\u4E3A ${candidate.state}\u3002`);
158
275
  } else {
159
- issues.push(
160
- `\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`
161
- );
276
+ const missing = describeMissingEvidence(candidate.metadata?.evidence);
277
+ issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7F3A\u5C11${missing}\u3002`);
162
278
  }
163
279
  }
164
280
  }
@@ -173,7 +289,7 @@ var PRODUCTION_PLAYTHROUGH_FILE = "tests/production-playthrough.test.tsx";
173
289
  function isMetadata(value) {
174
290
  if (!value || typeof value !== "object") return false;
175
291
  const metadata = value;
176
- return metadata.version === 1 && Boolean(metadata.evidence);
292
+ return metadata.version === 2 && Boolean(metadata.evidence);
177
293
  }
178
294
  function toAuditInput(test2) {
179
295
  const metadata = test2.meta().reactPlaythrough;
@@ -217,7 +333,7 @@ function assessReactPlaythroughReport(input) {
217
333
  ...base,
218
334
  status: "FAILED",
219
335
  cause: "\u751F\u4EA7\u53EF\u73A9\u6027\u6D4B\u8BD5\u6587\u4EF6\u4E0D\u5B58\u5728\u3002",
220
- next: "\u521B\u5EFA\u8BE5\u6587\u4EF6\uFF1A\u4ECE <App /> \u89E6\u53D1\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\u518D\u65AD\u8A00\u7ED3\u679C\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',
221
337
  failsRun: true
222
338
  };
223
339
  }
@@ -249,11 +365,13 @@ function assessReactPlaythroughReport(input) {
249
365
  if (!audit.passed) {
250
366
  const cause = productionModule.errors[0] ?? input.unhandledErrors?.[0] ?? audit.issues[0] ?? "\u751F\u4EA7\u53EF\u73A9\u6D41\u7A0B\u6CA1\u6709\u7559\u4E0B\u5B8C\u6574\u8BC1\u636E\u3002";
251
367
  const timedOut = /outcome was not reached within \d+ steps/i.test(cause);
368
+ const missingStep = /No step callback was provided/i.test(cause);
369
+ const missingStructuredEvidence = /performInput|checkpoint/.test(cause);
252
370
  return {
253
371
  ...base,
254
372
  status: "FAILED",
255
373
  cause,
256
- next: 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\u70B9\u51FB\u6216\u6309\u952E\u662F\u5426\u771F\u7684\u8C03\u7528\u6E38\u620F\u7684\u751F\u4EA7\u63A7\u5236\u65B9\u6CD5\uFF0C\u4EE5\u53CA\u6E38\u620F\u771F\u5B9E\u72B6\u6001\u662F\u5426\u5230\u8FBE\u5E76\u663E\u793A\u7ED3\u679C\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",
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",
257
375
  failsRun: true
258
376
  };
259
377
  }
@@ -1,5 +1,5 @@
1
1
  import { ViteUserConfig } from 'vitest/config';
2
- import { G as GameplayAuditOptions } from './gameplay-audit-DmbBA0M_.mjs';
2
+ import { G as GameplayAuditOptions } from './gameplay-audit-CKgNYw2r.mjs';
3
3
  import 'phaser';
4
4
 
5
5
  interface GameVitestConfigOptions {
@@ -1,5 +1,5 @@
1
1
  import { ViteUserConfig } from 'vitest/config';
2
- import { G as GameplayAuditOptions } from './gameplay-audit-DmbBA0M_.js';
2
+ import { G as GameplayAuditOptions } from './gameplay-audit-CKgNYw2r.js';
3
3
  import 'phaser';
4
4
 
5
5
  interface GameVitestConfigOptions {
@@ -275,14 +275,14 @@ function auditGameplayCollection(options, file, tests) {
275
275
  function getGameplayEvidenceIssues(id, requirements, evidence) {
276
276
  const issues = [];
277
277
  const inputEvents = evidence.mouseEvents + evidence.keyboardEvents + evidence.touchEvents;
278
- if (requirements.requireInput && inputEvents === 0) {
279
- issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002`);
278
+ if (requirements.requireInput && (inputEvents === 0 || evidence.consumedInputEvents === 0)) {
279
+ issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u88AB Phaser InputPlugin \u6D88\u8D39\u7684 DOM \u8F93\u5165\u3002`);
280
280
  }
281
- if (requirements.requireFrameAdvance && evidence.frames === 0) {
282
- issues.push(`contract "${id}" \u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27\u3002`);
281
+ if (requirements.requireFrameAdvance && (evidence.frames === 0 || evidence.observedFrames === 0)) {
282
+ issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u7531\u6D3B\u52A8 Scene \u5B9E\u9645\u6267\u884C\u7684\u5B8C\u6574 Phaser \u5E27\u3002`);
283
283
  }
284
- if (requirements.requirePhysicsStep && evidence.physicsSteps === 0) {
285
- issues.push(`contract "${id}" \u672A\u63A8\u8FDB Arcade Physics\u3002`);
284
+ if (requirements.requirePhysicsStep && (evidence.physicsSteps === 0 || evidence.observedPhysicsSteps === 0)) {
285
+ issues.push(`contract "${id}" \u672A\u8BB0\u5F55 Arcade WORLD_STEP\u3002`);
286
286
  }
287
287
  for (const target of normalizeList(requirements.requireTransition)) {
288
288
  if (!evidence.transitions.some((transition) => transition.to === target)) {
@@ -455,9 +455,9 @@ function formatGameplayIssues(issues) {
455
455
  unhandled: "\u5F02\u6B65\u9519\u8BEF\uFF1A\u5148\u4FEE\u590D\u4E0A\u65B9\u663E\u793A\u7684\u7B2C\u4E00\u6761\u9519\u8BEF\uFF1B\u8F93\u5165\u3001\u5E27\u63A8\u8FDB\u548C\u751F\u547D\u5468\u671F\u64CD\u4F5C\u90FD\u8981\u5728\u6B63\u786E\u7684 host \u751F\u547D\u5468\u671F\u5185\u5E76\u4F7F\u7528 await\u3002"
456
456
  };
457
457
  const getCategory = (issue) => {
458
- if (issue.includes("\u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165")) return { category: "input", key: "input" };
459
- if (issue.includes("\u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27")) return { category: "frames", key: "frames" };
460
- if (issue.includes("\u672A\u63A8\u8FDB Arcade Physics")) return { category: "physics", key: "physics" };
458
+ if (issue.includes("\u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165") || issue.includes("Phaser InputPlugin \u6D88\u8D39\u7684 DOM \u8F93\u5165")) return { category: "input", key: "input" };
459
+ if (issue.includes("\u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27") || issue.includes("\u6D3B\u52A8 Scene \u5B9E\u9645\u6267\u884C\u7684\u5B8C\u6574 Phaser \u5E27")) return { category: "frames", key: "frames" };
460
+ if (issue.includes("\u672A\u63A8\u8FDB Arcade Physics") || issue.includes("\u672A\u8BB0\u5F55 Arcade WORLD_STEP")) return { category: "physics", key: "physics" };
461
461
  if (issue.includes("\u672A\u8BB0\u5F55\u5230 ") && issue.includes("Scene \u8F6C\u573A")) {
462
462
  const target = issue.match(/未记录到 (.+?) 的 Scene 转场/)?.[1] ?? "unknown";
463
463
  return { category: "transition", key: `transition:${target}` };