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.
@@ -101,27 +101,54 @@ var INPUT_EVENTS = [
101
101
  "touchstart",
102
102
  "touchend"
103
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
+ }
104
115
  function createMetadata(waiverReason) {
105
116
  return {
106
- version: 1,
117
+ version: 2,
107
118
  waiverReason,
108
119
  evidence: {
109
120
  domInputEvents: 0,
121
+ entryInputs: 0,
122
+ primaryInputs: 0,
110
123
  boundedRuns: 0,
111
124
  assertionsAfterOutcome: 0,
125
+ changedOutcomeCheckpoints: 0,
126
+ checkpoints: [],
112
127
  verified: false
113
128
  }
114
129
  };
115
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
+ }
116
142
  function definePlaythrough(element, run, waiverReason) {
117
143
  const reason = normalizePlaythroughWaiverReason(waiverReason);
118
144
  const metadata = createMetadata(reason);
119
145
  (0, import_vitest.test)("production game completes a bounded playthrough", {
120
146
  skip: Boolean(reason),
121
147
  meta: { reactPlaythrough: metadata }
122
- }, async () => {
148
+ }, async ({ expect }) => {
123
149
  const evidence = metadata.evidence;
124
150
  let assertionsAtOutcome;
151
+ let enteredSnapshot;
125
152
  const recordInput = () => {
126
153
  evidence.domInputEvents += 1;
127
154
  };
@@ -139,23 +166,90 @@ function definePlaythrough(element, run, waiverReason) {
139
166
  await run({
140
167
  view,
141
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
+ },
142
231
  async stepUntil(condition, options = {}) {
143
232
  const steps = await runBoundedUntil(condition, options);
144
- if (evidence.domInputEvents === 0) {
233
+ if (evidence.primaryInputs === 0) {
145
234
  throw new Error(
146
- "stepUntil reached an outcome before any production DOM input."
235
+ 'stepUntil must follow performInput("primary", ...). A menu/help click is not gameplay evidence.'
147
236
  );
148
237
  }
149
238
  evidence.boundedRuns += 1;
150
- assertionsAtOutcome = import_vitest.expect.getState().assertionCalls;
239
+ assertionsAtOutcome = expect.getState().assertionCalls;
151
240
  return steps;
152
241
  }
153
242
  });
154
- const assertionCalls = import_vitest.expect.getState().assertionCalls;
243
+ const assertionCalls = expect.getState().assertionCalls;
155
244
  evidence.assertionsAfterOutcome = assertionsAtOutcome === void 0 ? 0 : assertionCalls - assertionsAtOutcome;
156
- if (evidence.domInputEvents === 0) {
245
+ if (evidence.entryInputs === 0) {
157
246
  throw new Error(
158
- "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", ...).'
159
253
  );
160
254
  }
161
255
  if (evidence.boundedRuns === 0) {
@@ -163,7 +257,12 @@ function definePlaythrough(element, run, waiverReason) {
163
257
  }
164
258
  if (evidence.assertionsAfterOutcome === 0) {
165
259
  throw new Error(
166
- "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").'
167
266
  );
168
267
  }
169
268
  evidence.verified = true;
@@ -184,7 +283,9 @@ function auditReactPlaythroughRun(tests) {
184
283
  const declared = tests.filter((candidate) => candidate.metadata);
185
284
  const valid = declared.filter(({ state, metadata }) => {
186
285
  const evidence = metadata?.evidence;
187
- 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
+ );
188
289
  });
189
290
  const waivers = declared.filter(
190
291
  ({ state, metadata }) => state === "skipped" && (metadata?.waiverReason?.trim().length ?? 0) >= 20
@@ -192,7 +293,7 @@ function auditReactPlaythroughRun(tests) {
192
293
  const issues = [];
193
294
  if (declared.length === 0) {
194
295
  issues.push(
195
- "\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'
196
297
  );
197
298
  } else {
198
299
  for (const candidate of declared) {
@@ -206,9 +307,8 @@ function auditReactPlaythroughRun(tests) {
206
307
  } else if (candidate.state !== "passed") {
207
308
  issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7684\u72B6\u6001\u4E3A ${candidate.state}\u3002`);
208
309
  } else {
209
- issues.push(
210
- `\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`
211
- );
310
+ const missing = describeMissingEvidence(candidate.metadata?.evidence);
311
+ issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7F3A\u5C11${missing}\u3002`);
212
312
  }
213
313
  }
214
314
  }
@@ -223,7 +323,7 @@ var PRODUCTION_PLAYTHROUGH_FILE = "tests/production-playthrough.test.tsx";
223
323
  function isMetadata(value) {
224
324
  if (!value || typeof value !== "object") return false;
225
325
  const metadata = value;
226
- return metadata.version === 1 && Boolean(metadata.evidence);
326
+ return metadata.version === 2 && Boolean(metadata.evidence);
227
327
  }
228
328
  function toAuditInput(test2) {
229
329
  const metadata = test2.meta().reactPlaythrough;
@@ -267,7 +367,7 @@ function assessReactPlaythroughReport(input) {
267
367
  ...base,
268
368
  status: "FAILED",
269
369
  cause: "\u751F\u4EA7\u53EF\u73A9\u6027\u6D4B\u8BD5\u6587\u4EF6\u4E0D\u5B58\u5728\u3002",
270
- 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',
271
371
  failsRun: true
272
372
  };
273
373
  }
@@ -300,11 +400,12 @@ function assessReactPlaythroughReport(input) {
300
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";
301
401
  const timedOut = /outcome was not reached within \d+ steps/i.test(cause);
302
402
  const missingStep = /No step callback was provided/i.test(cause);
403
+ const missingStructuredEvidence = /performInput|checkpoint/.test(cause);
303
404
  return {
304
405
  ...base,
305
406
  status: "FAILED",
306
407
  cause,
307
- 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" : "\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",
308
409
  failsRun: true
309
410
  };
310
411
  }
@@ -10,7 +10,7 @@ import { relative, resolve } from "path";
10
10
  // src/react/react-playthrough.ts
11
11
  import { render } from "@testing-library/react";
12
12
  import userEvent from "@testing-library/user-event";
13
- import { expect, test } from "vitest";
13
+ import { test } from "vitest";
14
14
 
15
15
  // src/react/react-playthrough-core.ts
16
16
  import { act } from "@testing-library/react";
@@ -67,27 +67,54 @@ var INPUT_EVENTS = [
67
67
  "touchstart",
68
68
  "touchend"
69
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
+ }
70
81
  function createMetadata(waiverReason) {
71
82
  return {
72
- version: 1,
83
+ version: 2,
73
84
  waiverReason,
74
85
  evidence: {
75
86
  domInputEvents: 0,
87
+ entryInputs: 0,
88
+ primaryInputs: 0,
76
89
  boundedRuns: 0,
77
90
  assertionsAfterOutcome: 0,
91
+ changedOutcomeCheckpoints: 0,
92
+ checkpoints: [],
78
93
  verified: false
79
94
  }
80
95
  };
81
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
+ }
82
108
  function definePlaythrough(element, run, waiverReason) {
83
109
  const reason = normalizePlaythroughWaiverReason(waiverReason);
84
110
  const metadata = createMetadata(reason);
85
111
  test("production game completes a bounded playthrough", {
86
112
  skip: Boolean(reason),
87
113
  meta: { reactPlaythrough: metadata }
88
- }, async () => {
114
+ }, async ({ expect }) => {
89
115
  const evidence = metadata.evidence;
90
116
  let assertionsAtOutcome;
117
+ let enteredSnapshot;
91
118
  const recordInput = () => {
92
119
  evidence.domInputEvents += 1;
93
120
  };
@@ -105,11 +132,73 @@ function definePlaythrough(element, run, waiverReason) {
105
132
  await run({
106
133
  view,
107
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
+ },
108
197
  async stepUntil(condition, options = {}) {
109
198
  const steps = await runBoundedUntil(condition, options);
110
- if (evidence.domInputEvents === 0) {
199
+ if (evidence.primaryInputs === 0) {
111
200
  throw new Error(
112
- "stepUntil reached an outcome before any production DOM input."
201
+ 'stepUntil must follow performInput("primary", ...). A menu/help click is not gameplay evidence.'
113
202
  );
114
203
  }
115
204
  evidence.boundedRuns += 1;
@@ -119,9 +208,14 @@ function definePlaythrough(element, run, waiverReason) {
119
208
  });
120
209
  const assertionCalls = expect.getState().assertionCalls;
121
210
  evidence.assertionsAfterOutcome = assertionsAtOutcome === void 0 ? 0 : assertionCalls - assertionsAtOutcome;
122
- if (evidence.domInputEvents === 0) {
211
+ if (evidence.entryInputs === 0) {
123
212
  throw new Error(
124
- "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", ...).'
125
219
  );
126
220
  }
127
221
  if (evidence.boundedRuns === 0) {
@@ -129,7 +223,12 @@ function definePlaythrough(element, run, waiverReason) {
129
223
  }
130
224
  if (evidence.assertionsAfterOutcome === 0) {
131
225
  throw new Error(
132
- "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").'
133
232
  );
134
233
  }
135
234
  evidence.verified = true;
@@ -150,7 +249,9 @@ function auditReactPlaythroughRun(tests) {
150
249
  const declared = tests.filter((candidate) => candidate.metadata);
151
250
  const valid = declared.filter(({ state, metadata }) => {
152
251
  const evidence = metadata?.evidence;
153
- 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
+ );
154
255
  });
155
256
  const waivers = declared.filter(
156
257
  ({ state, metadata }) => state === "skipped" && (metadata?.waiverReason?.trim().length ?? 0) >= 20
@@ -158,7 +259,7 @@ function auditReactPlaythroughRun(tests) {
158
259
  const issues = [];
159
260
  if (declared.length === 0) {
160
261
  issues.push(
161
- "\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'
162
263
  );
163
264
  } else {
164
265
  for (const candidate of declared) {
@@ -172,9 +273,8 @@ function auditReactPlaythroughRun(tests) {
172
273
  } else if (candidate.state !== "passed") {
173
274
  issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7684\u72B6\u6001\u4E3A ${candidate.state}\u3002`);
174
275
  } else {
175
- issues.push(
176
- `\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`
177
- );
276
+ const missing = describeMissingEvidence(candidate.metadata?.evidence);
277
+ issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7F3A\u5C11${missing}\u3002`);
178
278
  }
179
279
  }
180
280
  }
@@ -189,7 +289,7 @@ var PRODUCTION_PLAYTHROUGH_FILE = "tests/production-playthrough.test.tsx";
189
289
  function isMetadata(value) {
190
290
  if (!value || typeof value !== "object") return false;
191
291
  const metadata = value;
192
- return metadata.version === 1 && Boolean(metadata.evidence);
292
+ return metadata.version === 2 && Boolean(metadata.evidence);
193
293
  }
194
294
  function toAuditInput(test2) {
195
295
  const metadata = test2.meta().reactPlaythrough;
@@ -233,7 +333,7 @@ function assessReactPlaythroughReport(input) {
233
333
  ...base,
234
334
  status: "FAILED",
235
335
  cause: "\u751F\u4EA7\u53EF\u73A9\u6027\u6D4B\u8BD5\u6587\u4EF6\u4E0D\u5B58\u5728\u3002",
236
- 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',
237
337
  failsRun: true
238
338
  };
239
339
  }
@@ -266,11 +366,12 @@ function assessReactPlaythroughReport(input) {
266
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";
267
367
  const timedOut = /outcome was not reached within \d+ steps/i.test(cause);
268
368
  const missingStep = /No step callback was provided/i.test(cause);
369
+ const missingStructuredEvidence = /performInput|checkpoint/.test(cause);
269
370
  return {
270
371
  ...base,
271
372
  status: "FAILED",
272
373
  cause,
273
- 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" : "\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",
274
375
  failsRun: true
275
376
  };
276
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}` };
@@ -251,14 +251,14 @@ function auditGameplayCollection(options, file, tests) {
251
251
  function getGameplayEvidenceIssues(id, requirements, evidence) {
252
252
  const issues = [];
253
253
  const inputEvents = evidence.mouseEvents + evidence.keyboardEvents + evidence.touchEvents;
254
- if (requirements.requireInput && inputEvents === 0) {
255
- issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002`);
254
+ if (requirements.requireInput && (inputEvents === 0 || evidence.consumedInputEvents === 0)) {
255
+ issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u88AB Phaser InputPlugin \u6D88\u8D39\u7684 DOM \u8F93\u5165\u3002`);
256
256
  }
257
- if (requirements.requireFrameAdvance && evidence.frames === 0) {
258
- issues.push(`contract "${id}" \u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27\u3002`);
257
+ if (requirements.requireFrameAdvance && (evidence.frames === 0 || evidence.observedFrames === 0)) {
258
+ issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u7531\u6D3B\u52A8 Scene \u5B9E\u9645\u6267\u884C\u7684\u5B8C\u6574 Phaser \u5E27\u3002`);
259
259
  }
260
- if (requirements.requirePhysicsStep && evidence.physicsSteps === 0) {
261
- issues.push(`contract "${id}" \u672A\u63A8\u8FDB Arcade Physics\u3002`);
260
+ if (requirements.requirePhysicsStep && (evidence.physicsSteps === 0 || evidence.observedPhysicsSteps === 0)) {
261
+ issues.push(`contract "${id}" \u672A\u8BB0\u5F55 Arcade WORLD_STEP\u3002`);
262
262
  }
263
263
  for (const target of normalizeList(requirements.requireTransition)) {
264
264
  if (!evidence.transitions.some((transition) => transition.to === target)) {
@@ -431,9 +431,9 @@ function formatGameplayIssues(issues) {
431
431
  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"
432
432
  };
433
433
  const getCategory = (issue) => {
434
- if (issue.includes("\u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165")) return { category: "input", key: "input" };
435
- if (issue.includes("\u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27")) return { category: "frames", key: "frames" };
436
- if (issue.includes("\u672A\u63A8\u8FDB Arcade Physics")) return { category: "physics", key: "physics" };
434
+ 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" };
435
+ 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" };
436
+ if (issue.includes("\u672A\u63A8\u8FDB Arcade Physics") || issue.includes("\u672A\u8BB0\u5F55 Arcade WORLD_STEP")) return { category: "physics", key: "physics" };
437
437
  if (issue.includes("\u672A\u8BB0\u5F55\u5230 ") && issue.includes("Scene \u8F6C\u573A")) {
438
438
  const target = issue.match(/未记录到 (.+?) 的 Scene 转场/)?.[1] ?? "unknown";
439
439
  return { category: "transition", key: `transition:${target}` };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miaoda-game-devkit",
3
- "version": "0.2.17",
3
+ "version": "0.2.19",
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",