miaoda-game-devkit 0.3.0 → 0.5.0

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.
@@ -114,47 +114,123 @@ var INPUT_EVENTS = [
114
114
  "touchend"
115
115
  ];
116
116
  var MIN_CHECKPOINTS = 2;
117
+ var REACT_PLAYTHROUGH_TRACE_ANNOTATION = "miaoda:react-playthrough-trace";
118
+ var MAX_TRACE_VALUE_LENGTH = 180;
119
+ var MAX_TRACE_LENGTH = 720;
120
+ function truncateTraceValue(value, limit) {
121
+ const compact = value.replace(/\s+/g, " ").trim();
122
+ if (compact.length <= limit) return compact;
123
+ return `${compact.slice(0, Math.max(0, limit - 1))}\u2026`;
124
+ }
125
+ function formatReactPlaythroughFailureTrace(trace) {
126
+ const stages = [
127
+ ["entered", trace.entered],
128
+ ["after-primary", trace.afterPrimary],
129
+ ["last", trace.last]
130
+ ].filter((stage) => stage[1] !== void 0).map(
131
+ ([stage, value]) => `${stage}=${truncateTraceValue(value, MAX_TRACE_VALUE_LENGTH)}`
132
+ );
133
+ const details = [
134
+ trace.checkpoints.length > 0 ? `checkpoints=${trace.checkpoints.join(",")}` : "checkpoints=none",
135
+ trace.step ? `stepUntil=${trace.step.completed ?? "failed"}/${trace.step.bound}` : void 0
136
+ ].filter((detail) => Boolean(detail));
137
+ const formatted = `${stages.join(" -> ")}${stages.length ? "; " : ""}${details.join("; ")}`;
138
+ return truncateTraceValue(formatted, MAX_TRACE_LENGTH);
139
+ }
140
+ var MAX_FORMATTED_OBSERVATION_LENGTH = 500;
141
+ function formatObservation(fingerprint) {
142
+ if (fingerprint.length <= MAX_FORMATTED_OBSERVATION_LENGTH) return fingerprint;
143
+ return `${fingerprint.slice(0, MAX_FORMATTED_OBSERVATION_LENGTH)}\u2026 (${fingerprint.length} chars)`;
144
+ }
145
+ function sampleObservation(observe, stage) {
146
+ let value;
147
+ try {
148
+ value = observe();
149
+ } catch (error) {
150
+ throw new Error(`observe() threw at ${stage}: ${String(error)}`);
151
+ }
152
+ try {
153
+ const fingerprint = JSON.stringify(value);
154
+ if (fingerprint === void 0) throw new Error("unsupported value");
155
+ return { fingerprint, formatted: formatObservation(fingerprint) };
156
+ } catch {
157
+ throw new Error(
158
+ `observe() must return JSON-serializable read-only state; sampling failed at ${stage}.`
159
+ );
160
+ }
161
+ }
162
+ function formatObservationTimeline(entered, afterPrimary, outcome) {
163
+ return [
164
+ `entered=${entered.formatted}`,
165
+ `after-primary=${afterPrimary?.formatted ?? "<not sampled>"}`,
166
+ `outcome=${outcome.formatted}`
167
+ ].join(", ");
168
+ }
117
169
  function describeMissingEvidence(evidence) {
118
- if (!evidence || evidence.entryInputs === 0) return "entry \u8F93\u5165";
119
- if (evidence.primaryInputs === 0) return "primary \u8F93\u5165";
170
+ if (!evidence || evidence.entryInputs === 0) return "an entry input";
171
+ if (evidence.primaryInputs === 0) return "a primary gameplay input";
120
172
  if (!evidence.checkpoints.includes("entered")) return "entered checkpoint";
121
- if (evidence.boundedRuns === 0) return "\u6709\u754C stepUntil";
122
- if (evidence.assertionsAfterOutcome === 0) return "stepUntil \u540E\u7684\u7ED3\u679C\u65AD\u8A00";
173
+ if (evidence.boundedRuns === 0) return "a bounded stepUntil call";
174
+ if (evidence.assertionsAfterOutcome === 0)
175
+ return "an outcome assertion after stepUntil";
123
176
  if (evidence.checkpoints.length < MIN_CHECKPOINTS)
124
- return `\u81F3\u5C11 ${MIN_CHECKPOINTS} \u4E2A checkpoint`;
177
+ return `at least ${MIN_CHECKPOINTS} checkpoints`;
125
178
  if (!evidence.checkpoints.some(
126
179
  (checkpoint) => checkpoint === "progress" || checkpoint === "terminal"
127
180
  )) {
128
181
  return "progress/terminal checkpoint";
129
182
  }
130
- return "\u5B8C\u6574\u7684 playthrough \u6821\u9A8C\u6807\u8BB0";
183
+ return "a complete playthrough verification marker";
131
184
  }
132
- function createMetadata(waiverReason) {
185
+ function createEvidence() {
133
186
  return {
134
- version: 3,
135
- waiverReason,
136
- evidence: {
137
- domInputEvents: 0,
138
- entryInputs: 0,
139
- primaryInputs: 0,
140
- boundedRuns: 0,
141
- assertionsAfterOutcome: 0,
142
- checkpoints: [],
143
- verified: false
144
- }
187
+ domInputEvents: 0,
188
+ entryInputs: 0,
189
+ primaryInputs: 0,
190
+ boundedRuns: 0,
191
+ assertionsAfterOutcome: 0,
192
+ checkpoints: [],
193
+ verified: false
145
194
  };
146
195
  }
147
- function definePlaythrough(element, run, waiverReason) {
196
+ function createMetadata(waiverReason) {
197
+ return { version: 3, waiverReason, evidence: createEvidence() };
198
+ }
199
+ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
148
200
  const reason = normalizePlaythroughWaiverReason(waiverReason);
149
201
  const metadata = createMetadata(reason);
150
202
  test("production game completes a bounded playthrough", {
151
203
  skip: Boolean(reason),
152
204
  meta: { reactPlaythrough: metadata }
153
- }, async ({ expect }) => {
205
+ }, async ({ annotate, expect }) => {
206
+ metadata.evidence = createEvidence();
207
+ metadata.trace = void 0;
154
208
  const evidence = metadata.evidence;
155
209
  let assertionsAtOutcome;
156
210
  let enteredRecorded = false;
157
211
  let domTextAtEntered;
212
+ let enteredObservation;
213
+ let afterPrimaryObservation;
214
+ let enteredTrace;
215
+ let afterPrimaryTrace;
216
+ let outcomeTrace;
217
+ let stepTrace;
218
+ const sampleDomTrace = () => JSON.stringify((document.body.textContent ?? "").replace(/\s+/g, " ").trim());
219
+ const sampleLastTrace = () => {
220
+ if (!playthroughOptions?.observe) return sampleDomTrace();
221
+ try {
222
+ return sampleObservation(playthroughOptions.observe, "outcome").formatted;
223
+ } catch (error) {
224
+ return `<observe unavailable: ${String(error)}>`;
225
+ }
226
+ };
227
+ const createFailureTrace = () => formatReactPlaythroughFailureTrace({
228
+ entered: enteredTrace,
229
+ afterPrimary: afterPrimaryTrace,
230
+ last: outcomeTrace ?? sampleLastTrace(),
231
+ checkpoints: [...evidence.checkpoints],
232
+ step: stepTrace
233
+ });
158
234
  const recordInput = () => {
159
235
  evidence.domInputEvents += 1;
160
236
  };
@@ -192,7 +268,18 @@ function definePlaythrough(element, run, waiverReason) {
192
268
  );
193
269
  }
194
270
  if (kind === "entry") evidence.entryInputs += 1;
195
- else evidence.primaryInputs += 1;
271
+ else {
272
+ evidence.primaryInputs += 1;
273
+ if (playthroughOptions?.observe) {
274
+ afterPrimaryObservation = sampleObservation(
275
+ playthroughOptions.observe,
276
+ "after-primary"
277
+ );
278
+ afterPrimaryTrace = afterPrimaryObservation.formatted;
279
+ } else {
280
+ afterPrimaryTrace = sampleDomTrace();
281
+ }
282
+ }
196
283
  },
197
284
  checkpoint(kind) {
198
285
  if (kind === "entered") {
@@ -207,7 +294,16 @@ function definePlaythrough(element, run, waiverReason) {
207
294
  );
208
295
  }
209
296
  enteredRecorded = true;
210
- domTextAtEntered = document.body.textContent ?? "";
297
+ if (playthroughOptions?.observe) {
298
+ enteredObservation = sampleObservation(
299
+ playthroughOptions.observe,
300
+ "entered"
301
+ );
302
+ enteredTrace = enteredObservation.formatted;
303
+ } else {
304
+ domTextAtEntered = document.body.textContent ?? "";
305
+ enteredTrace = sampleDomTrace();
306
+ }
211
307
  evidence.checkpoints.push(kind);
212
308
  return;
213
309
  }
@@ -228,17 +324,43 @@ function definePlaythrough(element, run, waiverReason) {
228
324
  }
229
325
  evidence.checkpoints.push(kind);
230
326
  },
231
- async stepUntil(condition, options = {}) {
232
- const steps = await runBoundedUntil(condition, options);
327
+ async stepUntil(condition, stepOptions = {}) {
328
+ const stepBound = stepOptions.maxSteps ?? 120;
329
+ stepTrace = { bound: stepBound };
330
+ const boundedOptions = stepOptions.diagnostics || !playthroughOptions?.observe ? stepOptions : {
331
+ ...stepOptions,
332
+ diagnostics: playthroughOptions.observe
333
+ };
334
+ const steps = await runBoundedUntil(condition, boundedOptions);
335
+ stepTrace = { bound: stepBound, completed: steps };
233
336
  if (evidence.primaryInputs === 0) {
234
337
  throw new Error(
235
338
  'stepUntil must follow performInput("primary", ...). A menu/help click is not gameplay evidence.'
236
339
  );
237
340
  }
238
- if (steps === 0 && options.allowStaticDom !== true && domTextAtEntered !== void 0 && (document.body.textContent ?? "") === domTextAtEntered) {
239
- throw new Error(
240
- 'stepUntil outcome was already true at step 0 and the DOM has not changed since checkpoint("entered"), so the flow cannot prove that gameplay changed anything. \u610F\u601D\uFF1A\u6E38\u620F\u4E00\u6B65\u90FD\u6CA1\u73A9\uFF0C\u7B49\u5F85\u7684"\u7ED3\u679C"\u5C31\u5DF2\u7ECF\u6210\u7ACB\uFF0C\u9875\u9762\u4E5F\u4E00\u4E2A\u5B57\u6CA1\u53D8\u2014\u2014\u8FD9\u4E2A\u7ED3\u679C\u8BC1\u660E\u4E0D\u4E86\u4EFB\u4F55\u4E8B\u3002\u5E38\u89C1\u539F\u56E0\uFF1A\u2460 \u8F93\u5165\u6CA1\u6709\u63A5\u5230\u6E38\u620F\u4E0A\uFF1B\u2461 \u754C\u9762\u5361\u6B7B\uFF08\u72B6\u6001\u6539\u4E86\u4F46 snapshot \u5F15\u7528\u6CA1\u6362\uFF0CReact \u6CA1\u6709\u5237\u65B0\uFF09\uFF1B\u2462 \u65AD\u8A00\u4E86\u5F00\u5C40\u524D\u5C31\u5B58\u5728\u7684\u9759\u6001\u6587\u672C\u3002\u4FEE\u590D\uFF1A\u7B49\u5F85\u5E76\u65AD\u8A00\u53EA\u6709\u73A9\u8D77\u6765\u4E4B\u540E\u624D\u4F1A\u51FA\u73B0\u7684\u4E1C\u897F\uFF08\u5F00\u59CB\u906E\u7F69\u6D88\u5931\u3001\u6BD4\u5206\u53D8\u5316\u3001\u7ED3\u7B97\u753B\u9762\u51FA\u73B0\uFF09\u3002\u4F8B\u5916\uFF1A\u7ED3\u679C\u753B\u5728 Canvas \u4E0A\u3001\u7ECF Telemetry \u7B49\u9875\u9762\u5916\u72B6\u6001\u89C2\u5BDF\u7684\u6E38\u620F\uFF0C\u663E\u5F0F\u4F20 { allowStaticDom: true }\u3002'
341
+ if (playthroughOptions?.observe) {
342
+ if (!enteredObservation) {
343
+ throw new Error(
344
+ 'observe requires checkpoint("entered") before primary gameplay input.'
345
+ );
346
+ }
347
+ const outcomeObservation = sampleObservation(
348
+ playthroughOptions.observe,
349
+ "outcome"
241
350
  );
351
+ outcomeTrace = outcomeObservation.formatted;
352
+ if (outcomeObservation.fingerprint === enteredObservation.fingerprint) {
353
+ throw new Error(
354
+ `The authoritative observation did not change from checkpoint("entered") to the outcome. Timeline: ${formatObservationTimeline(enteredObservation, afterPrimaryObservation, outcomeObservation)}`
355
+ );
356
+ }
357
+ } else {
358
+ outcomeTrace = sampleDomTrace();
359
+ if (steps === 0 && domTextAtEntered !== void 0 && (document.body.textContent ?? "") === domTextAtEntered) {
360
+ throw new Error(
361
+ 'stepUntil found the outcome at step 0, and the DOM has not changed since checkpoint("entered"). The flow therefore provides no evidence that the primary gameplay input produced a result. For Canvas or Controller state outside the DOM, declare one playthrough observe callback.'
362
+ );
363
+ }
242
364
  }
243
365
  evidence.boundedRuns += 1;
244
366
  assertionsAtOutcome = expect.getState().assertionCalls;
@@ -273,7 +395,15 @@ function definePlaythrough(element, run, waiverReason) {
273
395
  );
274
396
  }
275
397
  evidence.verified = true;
398
+ } catch (error) {
399
+ metadata.trace = createFailureTrace();
400
+ try {
401
+ await annotate(metadata.trace, REACT_PLAYTHROUGH_TRACE_ANNOTATION);
402
+ } catch {
403
+ }
404
+ throw error;
276
405
  } finally {
406
+ metadata.trace ??= createFailureTrace();
277
407
  for (const event of INPUT_EVENTS) {
278
408
  document.removeEventListener(event, recordInput, true);
279
409
  }
@@ -281,9 +411,16 @@ function definePlaythrough(element, run, waiverReason) {
281
411
  });
282
412
  }
283
413
  var playthroughTest = Object.assign(
284
- (element, run) => definePlaythrough(element, run),
414
+ (element, optionsOrRun, maybeRun) => {
415
+ if (typeof optionsOrRun === "function") {
416
+ definePlaythrough(element, optionsOrRun);
417
+ return;
418
+ }
419
+ if (!maybeRun) throw new TypeError("playthroughTest requires a run callback.");
420
+ definePlaythrough(element, maybeRun, optionsOrRun);
421
+ },
285
422
  {
286
- skip: (reason, element, run) => definePlaythrough(element, run, reason)
423
+ skip: (reason, element, run) => definePlaythrough(element, run, void 0, reason)
287
424
  }
288
425
  );
289
426
  function auditReactPlaythroughRun(tests) {
@@ -300,7 +437,7 @@ function auditReactPlaythroughRun(tests) {
300
437
  const issues = [];
301
438
  if (declared.length === 0) {
302
439
  issues.push(
303
- '\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'
440
+ 'No production gameplay verification was declared. Use playthroughTest to render <App />, then run performInput("entry"), checkpoint("entered"), performInput("primary"), and a bounded stepUntil. Assert the authoritative outcome with the expect provided by playthroughTest, then record checkpoint("progress") or checkpoint("terminal").'
304
441
  );
305
442
  } else {
306
443
  for (const candidate of declared) {
@@ -309,13 +446,17 @@ function auditReactPlaythroughRun(tests) {
309
446
  if (isValid || isWaived) continue;
310
447
  if (candidate.state === "skipped") {
311
448
  issues.push(
312
- `\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u88AB\u8DF3\u8FC7\uFF0C\u4F46\u6CA1\u6709\u81F3\u5C11 20 \u4E2A\u5B57\u7B26\u7684\u660E\u786E\u7406\u7531\u3002`
449
+ `Playthrough ${JSON.stringify(candidate.name)} was skipped without an explicit reason of at least 20 characters.`
313
450
  );
314
451
  } else if (candidate.state !== "passed") {
315
- issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7684\u72B6\u6001\u4E3A ${candidate.state}\u3002`);
452
+ issues.push(
453
+ `Playthrough ${JSON.stringify(candidate.name)} finished with state ${candidate.state}.`
454
+ );
316
455
  } else {
317
456
  const missing = describeMissingEvidence(candidate.metadata?.evidence);
318
- issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7F3A\u5C11${missing}\u3002`);
457
+ issues.push(
458
+ `Playthrough ${JSON.stringify(candidate.name)} is missing ${missing}.`
459
+ );
319
460
  }
320
461
  }
321
462
  }