miaoda-game-devkit 0.4.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.
- package/README.md +10 -2
- package/dist/react/testing.d.mts +2 -0
- package/dist/react/testing.d.ts +2 -0
- package/dist/react/testing.js +81 -17
- package/dist/react/testing.mjs +81 -17
- package/dist/react/vitest-config.js +109 -23
- package/dist/react/vitest-config.mjs +109 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,6 +21,13 @@
|
|
|
21
21
|
或调度清理测试可使用 `ManualGameClock`。Canvas/Controller 流程在 `playthroughTest` 上声明一次
|
|
22
22
|
`observe`,返回 JSON 可序列化的权威 Telemetry 状态;框架自动比较流程状态并复用为超时诊断。
|
|
23
23
|
|
|
24
|
+
失败的 production playthrough 会额外输出一条有界 `TRACE:`:记录 entry 后、primary 后和
|
|
25
|
+
最后一次可观测状态,以及已完成 checkpoint 与推进步数。通过流程不输出 TRACE,避免增加
|
|
26
|
+
常规 CI 噪声。显式启用 Vitest retry 时,每次尝试会重置证据,最终报告只展示最后一次尝试的
|
|
27
|
+
CAUSE 与 TRACE;若测试主体已完成、随后由 `afterEach` 或运行时错误判失败,reporter 仍会从
|
|
28
|
+
task metadata 恢复这条流程证据。状态优先来自 `observe`,没有 Telemetry 时才使用有界 DOM
|
|
29
|
+
摘要;不要把 TRACE 当作新的断言或第二份业务状态。
|
|
30
|
+
|
|
24
31
|
Vitest 的 fake timers(包括 `vi.advanceTimersToNextFrame()`)适合在 devkit 自身的
|
|
25
32
|
专门 scheduler 合约中使用,但不在所有 production playthrough 中全局启用;React
|
|
26
33
|
Testing Library 的 `asyncWrapper` 和 user-event 内部 timer 会因此互相影响。使用自定义
|
|
@@ -85,8 +92,9 @@ Phaser 4 模板对应使用 `miaoda-phaser-game-lint`。命令类型不通过依
|
|
|
85
92
|
开发工具包自行维护并精确固定 Vitest 等工具版本,从自身依赖中解析可执行文件,使用方不需要重复声明
|
|
86
93
|
Biome、Oxlint 或 tsgo。仓库中保留稳定的命令入口,因此 `dist/` 尚未生成时,pnpm
|
|
87
94
|
也能正确建立命令链接;工作区开发状态下由 Nx 在模板检查前构建开发工具包,发布包则
|
|
88
|
-
直接携带运行时 `dist/`。打包消费者契约会验证固定 Vitest 版本以及 reporter
|
|
89
|
-
|
|
95
|
+
直接携带运行时 `dist/`。打包消费者契约会验证固定 Vitest 版本以及 reporter 的通过、聚焦、
|
|
96
|
+
缺失流程、retry 失败诊断和 hook 失败诊断行为。开发工具包内部的 `*.test.*` 与构建后的
|
|
97
|
+
`*.contract.mjs` 都不会
|
|
90
98
|
进入 npm 包;这些契约只由 devkit 自己的测试目标执行。
|
|
91
99
|
|
|
92
100
|
monorepo 中的模板可以放置只包含 `extends: ["miaoda-game-devkit/biome"]` 的薄配置,
|
package/dist/react/testing.d.mts
CHANGED
package/dist/react/testing.d.ts
CHANGED
package/dist/react/testing.js
CHANGED
|
@@ -152,6 +152,29 @@ var INPUT_EVENTS = [
|
|
|
152
152
|
"touchend"
|
|
153
153
|
];
|
|
154
154
|
var MIN_CHECKPOINTS = 2;
|
|
155
|
+
var REACT_PLAYTHROUGH_TRACE_ANNOTATION = "miaoda:react-playthrough-trace";
|
|
156
|
+
var MAX_TRACE_VALUE_LENGTH = 180;
|
|
157
|
+
var MAX_TRACE_LENGTH = 720;
|
|
158
|
+
function truncateTraceValue(value, limit) {
|
|
159
|
+
const compact = value.replace(/\s+/g, " ").trim();
|
|
160
|
+
if (compact.length <= limit) return compact;
|
|
161
|
+
return `${compact.slice(0, Math.max(0, limit - 1))}\u2026`;
|
|
162
|
+
}
|
|
163
|
+
function formatReactPlaythroughFailureTrace(trace) {
|
|
164
|
+
const stages = [
|
|
165
|
+
["entered", trace.entered],
|
|
166
|
+
["after-primary", trace.afterPrimary],
|
|
167
|
+
["last", trace.last]
|
|
168
|
+
].filter((stage) => stage[1] !== void 0).map(
|
|
169
|
+
([stage, value]) => `${stage}=${truncateTraceValue(value, MAX_TRACE_VALUE_LENGTH)}`
|
|
170
|
+
);
|
|
171
|
+
const details = [
|
|
172
|
+
trace.checkpoints.length > 0 ? `checkpoints=${trace.checkpoints.join(",")}` : "checkpoints=none",
|
|
173
|
+
trace.step ? `stepUntil=${trace.step.completed ?? "failed"}/${trace.step.bound}` : void 0
|
|
174
|
+
].filter((detail) => Boolean(detail));
|
|
175
|
+
const formatted = `${stages.join(" -> ")}${stages.length ? "; " : ""}${details.join("; ")}`;
|
|
176
|
+
return truncateTraceValue(formatted, MAX_TRACE_LENGTH);
|
|
177
|
+
}
|
|
155
178
|
var MAX_FORMATTED_OBSERVATION_LENGTH = 500;
|
|
156
179
|
function formatObservation(fingerprint) {
|
|
157
180
|
if (fingerprint.length <= MAX_FORMATTED_OBSERVATION_LENGTH) return fingerprint;
|
|
@@ -197,34 +220,55 @@ function describeMissingEvidence(evidence) {
|
|
|
197
220
|
}
|
|
198
221
|
return "a complete playthrough verification marker";
|
|
199
222
|
}
|
|
200
|
-
function
|
|
223
|
+
function createEvidence() {
|
|
201
224
|
return {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
assertionsAfterOutcome: 0,
|
|
210
|
-
checkpoints: [],
|
|
211
|
-
verified: false
|
|
212
|
-
}
|
|
225
|
+
domInputEvents: 0,
|
|
226
|
+
entryInputs: 0,
|
|
227
|
+
primaryInputs: 0,
|
|
228
|
+
boundedRuns: 0,
|
|
229
|
+
assertionsAfterOutcome: 0,
|
|
230
|
+
checkpoints: [],
|
|
231
|
+
verified: false
|
|
213
232
|
};
|
|
214
233
|
}
|
|
234
|
+
function createMetadata(waiverReason) {
|
|
235
|
+
return { version: 3, waiverReason, evidence: createEvidence() };
|
|
236
|
+
}
|
|
215
237
|
function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
216
238
|
const reason = normalizePlaythroughWaiverReason(waiverReason);
|
|
217
239
|
const metadata = createMetadata(reason);
|
|
218
240
|
(0, import_vitest.test)("production game completes a bounded playthrough", {
|
|
219
241
|
skip: Boolean(reason),
|
|
220
242
|
meta: { reactPlaythrough: metadata }
|
|
221
|
-
}, async ({ expect }) => {
|
|
243
|
+
}, async ({ annotate, expect }) => {
|
|
244
|
+
metadata.evidence = createEvidence();
|
|
245
|
+
metadata.trace = void 0;
|
|
222
246
|
const evidence = metadata.evidence;
|
|
223
247
|
let assertionsAtOutcome;
|
|
224
248
|
let enteredRecorded = false;
|
|
225
249
|
let domTextAtEntered;
|
|
226
250
|
let enteredObservation;
|
|
227
251
|
let afterPrimaryObservation;
|
|
252
|
+
let enteredTrace;
|
|
253
|
+
let afterPrimaryTrace;
|
|
254
|
+
let outcomeTrace;
|
|
255
|
+
let stepTrace;
|
|
256
|
+
const sampleDomTrace = () => JSON.stringify((document.body.textContent ?? "").replace(/\s+/g, " ").trim());
|
|
257
|
+
const sampleLastTrace = () => {
|
|
258
|
+
if (!playthroughOptions?.observe) return sampleDomTrace();
|
|
259
|
+
try {
|
|
260
|
+
return sampleObservation(playthroughOptions.observe, "outcome").formatted;
|
|
261
|
+
} catch (error) {
|
|
262
|
+
return `<observe unavailable: ${String(error)}>`;
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
const createFailureTrace = () => formatReactPlaythroughFailureTrace({
|
|
266
|
+
entered: enteredTrace,
|
|
267
|
+
afterPrimary: afterPrimaryTrace,
|
|
268
|
+
last: outcomeTrace ?? sampleLastTrace(),
|
|
269
|
+
checkpoints: [...evidence.checkpoints],
|
|
270
|
+
step: stepTrace
|
|
271
|
+
});
|
|
228
272
|
const recordInput = () => {
|
|
229
273
|
evidence.domInputEvents += 1;
|
|
230
274
|
};
|
|
@@ -269,6 +313,9 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
269
313
|
playthroughOptions.observe,
|
|
270
314
|
"after-primary"
|
|
271
315
|
);
|
|
316
|
+
afterPrimaryTrace = afterPrimaryObservation.formatted;
|
|
317
|
+
} else {
|
|
318
|
+
afterPrimaryTrace = sampleDomTrace();
|
|
272
319
|
}
|
|
273
320
|
}
|
|
274
321
|
},
|
|
@@ -290,8 +337,10 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
290
337
|
playthroughOptions.observe,
|
|
291
338
|
"entered"
|
|
292
339
|
);
|
|
340
|
+
enteredTrace = enteredObservation.formatted;
|
|
293
341
|
} else {
|
|
294
342
|
domTextAtEntered = document.body.textContent ?? "";
|
|
343
|
+
enteredTrace = sampleDomTrace();
|
|
295
344
|
}
|
|
296
345
|
evidence.checkpoints.push(kind);
|
|
297
346
|
return;
|
|
@@ -314,11 +363,14 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
314
363
|
evidence.checkpoints.push(kind);
|
|
315
364
|
},
|
|
316
365
|
async stepUntil(condition, stepOptions = {}) {
|
|
366
|
+
const stepBound = stepOptions.maxSteps ?? 120;
|
|
367
|
+
stepTrace = { bound: stepBound };
|
|
317
368
|
const boundedOptions = stepOptions.diagnostics || !playthroughOptions?.observe ? stepOptions : {
|
|
318
369
|
...stepOptions,
|
|
319
370
|
diagnostics: playthroughOptions.observe
|
|
320
371
|
};
|
|
321
372
|
const steps = await runBoundedUntil(condition, boundedOptions);
|
|
373
|
+
stepTrace = { bound: stepBound, completed: steps };
|
|
322
374
|
if (evidence.primaryInputs === 0) {
|
|
323
375
|
throw new Error(
|
|
324
376
|
'stepUntil must follow performInput("primary", ...). A menu/help click is not gameplay evidence.'
|
|
@@ -334,15 +386,19 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
334
386
|
playthroughOptions.observe,
|
|
335
387
|
"outcome"
|
|
336
388
|
);
|
|
389
|
+
outcomeTrace = outcomeObservation.formatted;
|
|
337
390
|
if (outcomeObservation.fingerprint === enteredObservation.fingerprint) {
|
|
338
391
|
throw new Error(
|
|
339
392
|
`The authoritative observation did not change from checkpoint("entered") to the outcome. Timeline: ${formatObservationTimeline(enteredObservation, afterPrimaryObservation, outcomeObservation)}`
|
|
340
393
|
);
|
|
341
394
|
}
|
|
342
|
-
} else
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
395
|
+
} else {
|
|
396
|
+
outcomeTrace = sampleDomTrace();
|
|
397
|
+
if (steps === 0 && domTextAtEntered !== void 0 && (document.body.textContent ?? "") === domTextAtEntered) {
|
|
398
|
+
throw new Error(
|
|
399
|
+
'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.'
|
|
400
|
+
);
|
|
401
|
+
}
|
|
346
402
|
}
|
|
347
403
|
evidence.boundedRuns += 1;
|
|
348
404
|
assertionsAtOutcome = expect.getState().assertionCalls;
|
|
@@ -377,7 +433,15 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
377
433
|
);
|
|
378
434
|
}
|
|
379
435
|
evidence.verified = true;
|
|
436
|
+
} catch (error) {
|
|
437
|
+
metadata.trace = createFailureTrace();
|
|
438
|
+
try {
|
|
439
|
+
await annotate(metadata.trace, REACT_PLAYTHROUGH_TRACE_ANNOTATION);
|
|
440
|
+
} catch {
|
|
441
|
+
}
|
|
442
|
+
throw error;
|
|
380
443
|
} finally {
|
|
444
|
+
metadata.trace ??= createFailureTrace();
|
|
381
445
|
for (const event of INPUT_EVENTS) {
|
|
382
446
|
document.removeEventListener(event, recordInput, true);
|
|
383
447
|
}
|
package/dist/react/testing.mjs
CHANGED
|
@@ -114,6 +114,29 @@ 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
|
+
}
|
|
117
140
|
var MAX_FORMATTED_OBSERVATION_LENGTH = 500;
|
|
118
141
|
function formatObservation(fingerprint) {
|
|
119
142
|
if (fingerprint.length <= MAX_FORMATTED_OBSERVATION_LENGTH) return fingerprint;
|
|
@@ -159,34 +182,55 @@ function describeMissingEvidence(evidence) {
|
|
|
159
182
|
}
|
|
160
183
|
return "a complete playthrough verification marker";
|
|
161
184
|
}
|
|
162
|
-
function
|
|
185
|
+
function createEvidence() {
|
|
163
186
|
return {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
assertionsAfterOutcome: 0,
|
|
172
|
-
checkpoints: [],
|
|
173
|
-
verified: false
|
|
174
|
-
}
|
|
187
|
+
domInputEvents: 0,
|
|
188
|
+
entryInputs: 0,
|
|
189
|
+
primaryInputs: 0,
|
|
190
|
+
boundedRuns: 0,
|
|
191
|
+
assertionsAfterOutcome: 0,
|
|
192
|
+
checkpoints: [],
|
|
193
|
+
verified: false
|
|
175
194
|
};
|
|
176
195
|
}
|
|
196
|
+
function createMetadata(waiverReason) {
|
|
197
|
+
return { version: 3, waiverReason, evidence: createEvidence() };
|
|
198
|
+
}
|
|
177
199
|
function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
178
200
|
const reason = normalizePlaythroughWaiverReason(waiverReason);
|
|
179
201
|
const metadata = createMetadata(reason);
|
|
180
202
|
test("production game completes a bounded playthrough", {
|
|
181
203
|
skip: Boolean(reason),
|
|
182
204
|
meta: { reactPlaythrough: metadata }
|
|
183
|
-
}, async ({ expect }) => {
|
|
205
|
+
}, async ({ annotate, expect }) => {
|
|
206
|
+
metadata.evidence = createEvidence();
|
|
207
|
+
metadata.trace = void 0;
|
|
184
208
|
const evidence = metadata.evidence;
|
|
185
209
|
let assertionsAtOutcome;
|
|
186
210
|
let enteredRecorded = false;
|
|
187
211
|
let domTextAtEntered;
|
|
188
212
|
let enteredObservation;
|
|
189
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
|
+
});
|
|
190
234
|
const recordInput = () => {
|
|
191
235
|
evidence.domInputEvents += 1;
|
|
192
236
|
};
|
|
@@ -231,6 +275,9 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
231
275
|
playthroughOptions.observe,
|
|
232
276
|
"after-primary"
|
|
233
277
|
);
|
|
278
|
+
afterPrimaryTrace = afterPrimaryObservation.formatted;
|
|
279
|
+
} else {
|
|
280
|
+
afterPrimaryTrace = sampleDomTrace();
|
|
234
281
|
}
|
|
235
282
|
}
|
|
236
283
|
},
|
|
@@ -252,8 +299,10 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
252
299
|
playthroughOptions.observe,
|
|
253
300
|
"entered"
|
|
254
301
|
);
|
|
302
|
+
enteredTrace = enteredObservation.formatted;
|
|
255
303
|
} else {
|
|
256
304
|
domTextAtEntered = document.body.textContent ?? "";
|
|
305
|
+
enteredTrace = sampleDomTrace();
|
|
257
306
|
}
|
|
258
307
|
evidence.checkpoints.push(kind);
|
|
259
308
|
return;
|
|
@@ -276,11 +325,14 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
276
325
|
evidence.checkpoints.push(kind);
|
|
277
326
|
},
|
|
278
327
|
async stepUntil(condition, stepOptions = {}) {
|
|
328
|
+
const stepBound = stepOptions.maxSteps ?? 120;
|
|
329
|
+
stepTrace = { bound: stepBound };
|
|
279
330
|
const boundedOptions = stepOptions.diagnostics || !playthroughOptions?.observe ? stepOptions : {
|
|
280
331
|
...stepOptions,
|
|
281
332
|
diagnostics: playthroughOptions.observe
|
|
282
333
|
};
|
|
283
334
|
const steps = await runBoundedUntil(condition, boundedOptions);
|
|
335
|
+
stepTrace = { bound: stepBound, completed: steps };
|
|
284
336
|
if (evidence.primaryInputs === 0) {
|
|
285
337
|
throw new Error(
|
|
286
338
|
'stepUntil must follow performInput("primary", ...). A menu/help click is not gameplay evidence.'
|
|
@@ -296,15 +348,19 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
296
348
|
playthroughOptions.observe,
|
|
297
349
|
"outcome"
|
|
298
350
|
);
|
|
351
|
+
outcomeTrace = outcomeObservation.formatted;
|
|
299
352
|
if (outcomeObservation.fingerprint === enteredObservation.fingerprint) {
|
|
300
353
|
throw new Error(
|
|
301
354
|
`The authoritative observation did not change from checkpoint("entered") to the outcome. Timeline: ${formatObservationTimeline(enteredObservation, afterPrimaryObservation, outcomeObservation)}`
|
|
302
355
|
);
|
|
303
356
|
}
|
|
304
|
-
} else
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
+
}
|
|
308
364
|
}
|
|
309
365
|
evidence.boundedRuns += 1;
|
|
310
366
|
assertionsAtOutcome = expect.getState().assertionCalls;
|
|
@@ -339,7 +395,15 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
339
395
|
);
|
|
340
396
|
}
|
|
341
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;
|
|
342
405
|
} finally {
|
|
406
|
+
metadata.trace ??= createFailureTrace();
|
|
343
407
|
for (const event of INPUT_EVENTS) {
|
|
344
408
|
document.removeEventListener(event, recordInput, true);
|
|
345
409
|
}
|
|
@@ -103,6 +103,29 @@ var INPUT_EVENTS = [
|
|
|
103
103
|
"touchend"
|
|
104
104
|
];
|
|
105
105
|
var MIN_CHECKPOINTS = 2;
|
|
106
|
+
var REACT_PLAYTHROUGH_TRACE_ANNOTATION = "miaoda:react-playthrough-trace";
|
|
107
|
+
var MAX_TRACE_VALUE_LENGTH = 180;
|
|
108
|
+
var MAX_TRACE_LENGTH = 720;
|
|
109
|
+
function truncateTraceValue(value, limit) {
|
|
110
|
+
const compact = value.replace(/\s+/g, " ").trim();
|
|
111
|
+
if (compact.length <= limit) return compact;
|
|
112
|
+
return `${compact.slice(0, Math.max(0, limit - 1))}\u2026`;
|
|
113
|
+
}
|
|
114
|
+
function formatReactPlaythroughFailureTrace(trace) {
|
|
115
|
+
const stages = [
|
|
116
|
+
["entered", trace.entered],
|
|
117
|
+
["after-primary", trace.afterPrimary],
|
|
118
|
+
["last", trace.last]
|
|
119
|
+
].filter((stage) => stage[1] !== void 0).map(
|
|
120
|
+
([stage, value]) => `${stage}=${truncateTraceValue(value, MAX_TRACE_VALUE_LENGTH)}`
|
|
121
|
+
);
|
|
122
|
+
const details = [
|
|
123
|
+
trace.checkpoints.length > 0 ? `checkpoints=${trace.checkpoints.join(",")}` : "checkpoints=none",
|
|
124
|
+
trace.step ? `stepUntil=${trace.step.completed ?? "failed"}/${trace.step.bound}` : void 0
|
|
125
|
+
].filter((detail) => Boolean(detail));
|
|
126
|
+
const formatted = `${stages.join(" -> ")}${stages.length ? "; " : ""}${details.join("; ")}`;
|
|
127
|
+
return truncateTraceValue(formatted, MAX_TRACE_LENGTH);
|
|
128
|
+
}
|
|
106
129
|
var MAX_FORMATTED_OBSERVATION_LENGTH = 500;
|
|
107
130
|
function formatObservation(fingerprint) {
|
|
108
131
|
if (fingerprint.length <= MAX_FORMATTED_OBSERVATION_LENGTH) return fingerprint;
|
|
@@ -148,34 +171,55 @@ function describeMissingEvidence(evidence) {
|
|
|
148
171
|
}
|
|
149
172
|
return "a complete playthrough verification marker";
|
|
150
173
|
}
|
|
151
|
-
function
|
|
174
|
+
function createEvidence() {
|
|
152
175
|
return {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
assertionsAfterOutcome: 0,
|
|
161
|
-
checkpoints: [],
|
|
162
|
-
verified: false
|
|
163
|
-
}
|
|
176
|
+
domInputEvents: 0,
|
|
177
|
+
entryInputs: 0,
|
|
178
|
+
primaryInputs: 0,
|
|
179
|
+
boundedRuns: 0,
|
|
180
|
+
assertionsAfterOutcome: 0,
|
|
181
|
+
checkpoints: [],
|
|
182
|
+
verified: false
|
|
164
183
|
};
|
|
165
184
|
}
|
|
185
|
+
function createMetadata(waiverReason) {
|
|
186
|
+
return { version: 3, waiverReason, evidence: createEvidence() };
|
|
187
|
+
}
|
|
166
188
|
function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
167
189
|
const reason = normalizePlaythroughWaiverReason(waiverReason);
|
|
168
190
|
const metadata = createMetadata(reason);
|
|
169
191
|
(0, import_vitest.test)("production game completes a bounded playthrough", {
|
|
170
192
|
skip: Boolean(reason),
|
|
171
193
|
meta: { reactPlaythrough: metadata }
|
|
172
|
-
}, async ({ expect }) => {
|
|
194
|
+
}, async ({ annotate, expect }) => {
|
|
195
|
+
metadata.evidence = createEvidence();
|
|
196
|
+
metadata.trace = void 0;
|
|
173
197
|
const evidence = metadata.evidence;
|
|
174
198
|
let assertionsAtOutcome;
|
|
175
199
|
let enteredRecorded = false;
|
|
176
200
|
let domTextAtEntered;
|
|
177
201
|
let enteredObservation;
|
|
178
202
|
let afterPrimaryObservation;
|
|
203
|
+
let enteredTrace;
|
|
204
|
+
let afterPrimaryTrace;
|
|
205
|
+
let outcomeTrace;
|
|
206
|
+
let stepTrace;
|
|
207
|
+
const sampleDomTrace = () => JSON.stringify((document.body.textContent ?? "").replace(/\s+/g, " ").trim());
|
|
208
|
+
const sampleLastTrace = () => {
|
|
209
|
+
if (!playthroughOptions?.observe) return sampleDomTrace();
|
|
210
|
+
try {
|
|
211
|
+
return sampleObservation(playthroughOptions.observe, "outcome").formatted;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
return `<observe unavailable: ${String(error)}>`;
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
const createFailureTrace = () => formatReactPlaythroughFailureTrace({
|
|
217
|
+
entered: enteredTrace,
|
|
218
|
+
afterPrimary: afterPrimaryTrace,
|
|
219
|
+
last: outcomeTrace ?? sampleLastTrace(),
|
|
220
|
+
checkpoints: [...evidence.checkpoints],
|
|
221
|
+
step: stepTrace
|
|
222
|
+
});
|
|
179
223
|
const recordInput = () => {
|
|
180
224
|
evidence.domInputEvents += 1;
|
|
181
225
|
};
|
|
@@ -220,6 +264,9 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
220
264
|
playthroughOptions.observe,
|
|
221
265
|
"after-primary"
|
|
222
266
|
);
|
|
267
|
+
afterPrimaryTrace = afterPrimaryObservation.formatted;
|
|
268
|
+
} else {
|
|
269
|
+
afterPrimaryTrace = sampleDomTrace();
|
|
223
270
|
}
|
|
224
271
|
}
|
|
225
272
|
},
|
|
@@ -241,8 +288,10 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
241
288
|
playthroughOptions.observe,
|
|
242
289
|
"entered"
|
|
243
290
|
);
|
|
291
|
+
enteredTrace = enteredObservation.formatted;
|
|
244
292
|
} else {
|
|
245
293
|
domTextAtEntered = document.body.textContent ?? "";
|
|
294
|
+
enteredTrace = sampleDomTrace();
|
|
246
295
|
}
|
|
247
296
|
evidence.checkpoints.push(kind);
|
|
248
297
|
return;
|
|
@@ -265,11 +314,14 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
265
314
|
evidence.checkpoints.push(kind);
|
|
266
315
|
},
|
|
267
316
|
async stepUntil(condition, stepOptions = {}) {
|
|
317
|
+
const stepBound = stepOptions.maxSteps ?? 120;
|
|
318
|
+
stepTrace = { bound: stepBound };
|
|
268
319
|
const boundedOptions = stepOptions.diagnostics || !playthroughOptions?.observe ? stepOptions : {
|
|
269
320
|
...stepOptions,
|
|
270
321
|
diagnostics: playthroughOptions.observe
|
|
271
322
|
};
|
|
272
323
|
const steps = await runBoundedUntil(condition, boundedOptions);
|
|
324
|
+
stepTrace = { bound: stepBound, completed: steps };
|
|
273
325
|
if (evidence.primaryInputs === 0) {
|
|
274
326
|
throw new Error(
|
|
275
327
|
'stepUntil must follow performInput("primary", ...). A menu/help click is not gameplay evidence.'
|
|
@@ -285,15 +337,19 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
285
337
|
playthroughOptions.observe,
|
|
286
338
|
"outcome"
|
|
287
339
|
);
|
|
340
|
+
outcomeTrace = outcomeObservation.formatted;
|
|
288
341
|
if (outcomeObservation.fingerprint === enteredObservation.fingerprint) {
|
|
289
342
|
throw new Error(
|
|
290
343
|
`The authoritative observation did not change from checkpoint("entered") to the outcome. Timeline: ${formatObservationTimeline(enteredObservation, afterPrimaryObservation, outcomeObservation)}`
|
|
291
344
|
);
|
|
292
345
|
}
|
|
293
|
-
} else
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
346
|
+
} else {
|
|
347
|
+
outcomeTrace = sampleDomTrace();
|
|
348
|
+
if (steps === 0 && domTextAtEntered !== void 0 && (document.body.textContent ?? "") === domTextAtEntered) {
|
|
349
|
+
throw new Error(
|
|
350
|
+
'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.'
|
|
351
|
+
);
|
|
352
|
+
}
|
|
297
353
|
}
|
|
298
354
|
evidence.boundedRuns += 1;
|
|
299
355
|
assertionsAtOutcome = expect.getState().assertionCalls;
|
|
@@ -328,7 +384,15 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
328
384
|
);
|
|
329
385
|
}
|
|
330
386
|
evidence.verified = true;
|
|
387
|
+
} catch (error) {
|
|
388
|
+
metadata.trace = createFailureTrace();
|
|
389
|
+
try {
|
|
390
|
+
await annotate(metadata.trace, REACT_PLAYTHROUGH_TRACE_ANNOTATION);
|
|
391
|
+
} catch {
|
|
392
|
+
}
|
|
393
|
+
throw error;
|
|
331
394
|
} finally {
|
|
395
|
+
metadata.trace ??= createFailureTrace();
|
|
332
396
|
for (const event of INPUT_EVENTS) {
|
|
333
397
|
document.removeEventListener(event, recordInput, true);
|
|
334
398
|
}
|
|
@@ -421,21 +485,41 @@ function errorLocation(value) {
|
|
|
421
485
|
const match = value.match(/(?:^|\n)\s*at\s+(.*?\.(?:test|spec)\.[^\n]+:\d+:\d+)/);
|
|
422
486
|
return match?.[1];
|
|
423
487
|
}
|
|
488
|
+
function failureTrace(test2) {
|
|
489
|
+
const annotations = test2.annotations();
|
|
490
|
+
let annotationTrace;
|
|
491
|
+
for (let index = annotations.length - 1; index >= 0; index -= 1) {
|
|
492
|
+
if (annotations[index].type === REACT_PLAYTHROUGH_TRACE_ANNOTATION) {
|
|
493
|
+
annotationTrace = annotations[index].message;
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
const metadata = test2.meta().reactPlaythrough;
|
|
498
|
+
const trace = annotationTrace ?? (isMetadata(metadata) ? metadata.trace : void 0);
|
|
499
|
+
return trace ? truncateReporterLine(trace, 720) : void 0;
|
|
500
|
+
}
|
|
501
|
+
function truncateReporterLine(value, limit) {
|
|
502
|
+
const compact = (0, import_node_util.stripVTControlCharacters)(value).replace(/\s+/g, " ").trim();
|
|
503
|
+
if (compact.length <= limit) return compact;
|
|
504
|
+
return `${compact.slice(0, Math.max(0, limit - 1))}\u2026`;
|
|
505
|
+
}
|
|
424
506
|
function toModuleResult(module2, projectRoot) {
|
|
425
507
|
const tests = [...module2.children.allTests()];
|
|
508
|
+
const moduleErrors = module2.errors().map((error) => firstLine(error.message)).filter((message) => Boolean(message));
|
|
426
509
|
const errors = [
|
|
427
|
-
...
|
|
510
|
+
...moduleErrors,
|
|
428
511
|
...tests.flatMap(
|
|
429
512
|
(test2) => (test2.result().errors ?? []).map((error) => firstLine(error.message))
|
|
430
513
|
)
|
|
431
514
|
].filter((message) => Boolean(message));
|
|
432
515
|
const failures = tests.filter((test2) => test2.result().state === "failed").map((test2) => {
|
|
433
|
-
const raw = test2.result().errors?.
|
|
516
|
+
const raw = test2.result().errors?.at(-1)?.message ?? module2.errors()[0]?.message ?? "Unknown failure";
|
|
434
517
|
return {
|
|
435
518
|
test: test2.fullName,
|
|
436
519
|
cause: firstLine(raw) ?? "Unknown failure",
|
|
437
520
|
location: test2.location ? `${(0, import_node_path.relative)(projectRoot, module2.moduleId).replaceAll("\\", "/")}:${test2.location.line}:${test2.location.column}` : errorLocation(raw),
|
|
438
|
-
hint: failureHint(raw)
|
|
521
|
+
hint: failureHint(raw),
|
|
522
|
+
trace: failureTrace(test2)
|
|
439
523
|
};
|
|
440
524
|
});
|
|
441
525
|
if (failures.length === 0 && tests.length === 0 && module2.errors().length > 0) {
|
|
@@ -451,11 +535,12 @@ function toModuleResult(module2, projectRoot) {
|
|
|
451
535
|
file: (0, import_node_path.relative)(projectRoot, module2.moduleId).replaceAll("\\", "/"),
|
|
452
536
|
state: module2.state(),
|
|
453
537
|
errors,
|
|
538
|
+
primaryError: moduleErrors[0] ?? failures[0]?.cause,
|
|
454
539
|
tests: tests.map(toAuditInput),
|
|
455
540
|
failures
|
|
456
541
|
};
|
|
457
542
|
}
|
|
458
|
-
function
|
|
543
|
+
function formatReactFailureSummary(modules) {
|
|
459
544
|
const failures = modules.flatMap(
|
|
460
545
|
(module2) => (module2.failures ?? []).map((failure) => ({ ...failure, file: module2.file }))
|
|
461
546
|
);
|
|
@@ -465,6 +550,7 @@ function formatFailureSummary(modules) {
|
|
|
465
550
|
lines.push(`FAILURE_${index + 1}: ${failure.file}`);
|
|
466
551
|
lines.push(`TEST: ${failure.test}`);
|
|
467
552
|
lines.push(`CAUSE: ${failure.cause}`);
|
|
553
|
+
if (failure.trace) lines.push(`TRACE: ${failure.trace}`);
|
|
468
554
|
if (failure.location) lines.push(`AT: ${failure.location}`);
|
|
469
555
|
if (failure.hint) lines.push(`HINT: ${failure.hint}`);
|
|
470
556
|
}
|
|
@@ -539,7 +625,7 @@ function assessReactPlaythroughReport(input) {
|
|
|
539
625
|
const tests = input.modules.flatMap((module2) => module2.tests);
|
|
540
626
|
const audit = auditReactPlaythroughRun(tests);
|
|
541
627
|
if (!audit.passed) {
|
|
542
|
-
const cause = productionModule.errors[0] ?? input.unhandledErrors?.[0] ?? audit.issues[0] ?? "The production playthrough did not leave complete gameplay evidence.";
|
|
628
|
+
const cause = productionModule.primaryError ?? productionModule.errors[0] ?? input.unhandledErrors?.[0] ?? audit.issues[0] ?? "The production playthrough did not leave complete gameplay evidence.";
|
|
543
629
|
return {
|
|
544
630
|
...base,
|
|
545
631
|
status: "FAILED",
|
|
@@ -609,7 +695,7 @@ var ReactPlaythroughReporter = class {
|
|
|
609
695
|
} else {
|
|
610
696
|
console.log(output);
|
|
611
697
|
}
|
|
612
|
-
const summary =
|
|
698
|
+
const summary = formatReactFailureSummary(
|
|
613
699
|
testModules.map((module2) => toModuleResult(module2, this.projectRoot))
|
|
614
700
|
);
|
|
615
701
|
if (report.failsRun && summary[0] === "TEST_RESULT: PASS") {
|
|
@@ -69,6 +69,29 @@ var INPUT_EVENTS = [
|
|
|
69
69
|
"touchend"
|
|
70
70
|
];
|
|
71
71
|
var MIN_CHECKPOINTS = 2;
|
|
72
|
+
var REACT_PLAYTHROUGH_TRACE_ANNOTATION = "miaoda:react-playthrough-trace";
|
|
73
|
+
var MAX_TRACE_VALUE_LENGTH = 180;
|
|
74
|
+
var MAX_TRACE_LENGTH = 720;
|
|
75
|
+
function truncateTraceValue(value, limit) {
|
|
76
|
+
const compact = value.replace(/\s+/g, " ").trim();
|
|
77
|
+
if (compact.length <= limit) return compact;
|
|
78
|
+
return `${compact.slice(0, Math.max(0, limit - 1))}\u2026`;
|
|
79
|
+
}
|
|
80
|
+
function formatReactPlaythroughFailureTrace(trace) {
|
|
81
|
+
const stages = [
|
|
82
|
+
["entered", trace.entered],
|
|
83
|
+
["after-primary", trace.afterPrimary],
|
|
84
|
+
["last", trace.last]
|
|
85
|
+
].filter((stage) => stage[1] !== void 0).map(
|
|
86
|
+
([stage, value]) => `${stage}=${truncateTraceValue(value, MAX_TRACE_VALUE_LENGTH)}`
|
|
87
|
+
);
|
|
88
|
+
const details = [
|
|
89
|
+
trace.checkpoints.length > 0 ? `checkpoints=${trace.checkpoints.join(",")}` : "checkpoints=none",
|
|
90
|
+
trace.step ? `stepUntil=${trace.step.completed ?? "failed"}/${trace.step.bound}` : void 0
|
|
91
|
+
].filter((detail) => Boolean(detail));
|
|
92
|
+
const formatted = `${stages.join(" -> ")}${stages.length ? "; " : ""}${details.join("; ")}`;
|
|
93
|
+
return truncateTraceValue(formatted, MAX_TRACE_LENGTH);
|
|
94
|
+
}
|
|
72
95
|
var MAX_FORMATTED_OBSERVATION_LENGTH = 500;
|
|
73
96
|
function formatObservation(fingerprint) {
|
|
74
97
|
if (fingerprint.length <= MAX_FORMATTED_OBSERVATION_LENGTH) return fingerprint;
|
|
@@ -114,34 +137,55 @@ function describeMissingEvidence(evidence) {
|
|
|
114
137
|
}
|
|
115
138
|
return "a complete playthrough verification marker";
|
|
116
139
|
}
|
|
117
|
-
function
|
|
140
|
+
function createEvidence() {
|
|
118
141
|
return {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
assertionsAfterOutcome: 0,
|
|
127
|
-
checkpoints: [],
|
|
128
|
-
verified: false
|
|
129
|
-
}
|
|
142
|
+
domInputEvents: 0,
|
|
143
|
+
entryInputs: 0,
|
|
144
|
+
primaryInputs: 0,
|
|
145
|
+
boundedRuns: 0,
|
|
146
|
+
assertionsAfterOutcome: 0,
|
|
147
|
+
checkpoints: [],
|
|
148
|
+
verified: false
|
|
130
149
|
};
|
|
131
150
|
}
|
|
151
|
+
function createMetadata(waiverReason) {
|
|
152
|
+
return { version: 3, waiverReason, evidence: createEvidence() };
|
|
153
|
+
}
|
|
132
154
|
function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
133
155
|
const reason = normalizePlaythroughWaiverReason(waiverReason);
|
|
134
156
|
const metadata = createMetadata(reason);
|
|
135
157
|
test("production game completes a bounded playthrough", {
|
|
136
158
|
skip: Boolean(reason),
|
|
137
159
|
meta: { reactPlaythrough: metadata }
|
|
138
|
-
}, async ({ expect }) => {
|
|
160
|
+
}, async ({ annotate, expect }) => {
|
|
161
|
+
metadata.evidence = createEvidence();
|
|
162
|
+
metadata.trace = void 0;
|
|
139
163
|
const evidence = metadata.evidence;
|
|
140
164
|
let assertionsAtOutcome;
|
|
141
165
|
let enteredRecorded = false;
|
|
142
166
|
let domTextAtEntered;
|
|
143
167
|
let enteredObservation;
|
|
144
168
|
let afterPrimaryObservation;
|
|
169
|
+
let enteredTrace;
|
|
170
|
+
let afterPrimaryTrace;
|
|
171
|
+
let outcomeTrace;
|
|
172
|
+
let stepTrace;
|
|
173
|
+
const sampleDomTrace = () => JSON.stringify((document.body.textContent ?? "").replace(/\s+/g, " ").trim());
|
|
174
|
+
const sampleLastTrace = () => {
|
|
175
|
+
if (!playthroughOptions?.observe) return sampleDomTrace();
|
|
176
|
+
try {
|
|
177
|
+
return sampleObservation(playthroughOptions.observe, "outcome").formatted;
|
|
178
|
+
} catch (error) {
|
|
179
|
+
return `<observe unavailable: ${String(error)}>`;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
const createFailureTrace = () => formatReactPlaythroughFailureTrace({
|
|
183
|
+
entered: enteredTrace,
|
|
184
|
+
afterPrimary: afterPrimaryTrace,
|
|
185
|
+
last: outcomeTrace ?? sampleLastTrace(),
|
|
186
|
+
checkpoints: [...evidence.checkpoints],
|
|
187
|
+
step: stepTrace
|
|
188
|
+
});
|
|
145
189
|
const recordInput = () => {
|
|
146
190
|
evidence.domInputEvents += 1;
|
|
147
191
|
};
|
|
@@ -186,6 +230,9 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
186
230
|
playthroughOptions.observe,
|
|
187
231
|
"after-primary"
|
|
188
232
|
);
|
|
233
|
+
afterPrimaryTrace = afterPrimaryObservation.formatted;
|
|
234
|
+
} else {
|
|
235
|
+
afterPrimaryTrace = sampleDomTrace();
|
|
189
236
|
}
|
|
190
237
|
}
|
|
191
238
|
},
|
|
@@ -207,8 +254,10 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
207
254
|
playthroughOptions.observe,
|
|
208
255
|
"entered"
|
|
209
256
|
);
|
|
257
|
+
enteredTrace = enteredObservation.formatted;
|
|
210
258
|
} else {
|
|
211
259
|
domTextAtEntered = document.body.textContent ?? "";
|
|
260
|
+
enteredTrace = sampleDomTrace();
|
|
212
261
|
}
|
|
213
262
|
evidence.checkpoints.push(kind);
|
|
214
263
|
return;
|
|
@@ -231,11 +280,14 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
231
280
|
evidence.checkpoints.push(kind);
|
|
232
281
|
},
|
|
233
282
|
async stepUntil(condition, stepOptions = {}) {
|
|
283
|
+
const stepBound = stepOptions.maxSteps ?? 120;
|
|
284
|
+
stepTrace = { bound: stepBound };
|
|
234
285
|
const boundedOptions = stepOptions.diagnostics || !playthroughOptions?.observe ? stepOptions : {
|
|
235
286
|
...stepOptions,
|
|
236
287
|
diagnostics: playthroughOptions.observe
|
|
237
288
|
};
|
|
238
289
|
const steps = await runBoundedUntil(condition, boundedOptions);
|
|
290
|
+
stepTrace = { bound: stepBound, completed: steps };
|
|
239
291
|
if (evidence.primaryInputs === 0) {
|
|
240
292
|
throw new Error(
|
|
241
293
|
'stepUntil must follow performInput("primary", ...). A menu/help click is not gameplay evidence.'
|
|
@@ -251,15 +303,19 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
251
303
|
playthroughOptions.observe,
|
|
252
304
|
"outcome"
|
|
253
305
|
);
|
|
306
|
+
outcomeTrace = outcomeObservation.formatted;
|
|
254
307
|
if (outcomeObservation.fingerprint === enteredObservation.fingerprint) {
|
|
255
308
|
throw new Error(
|
|
256
309
|
`The authoritative observation did not change from checkpoint("entered") to the outcome. Timeline: ${formatObservationTimeline(enteredObservation, afterPrimaryObservation, outcomeObservation)}`
|
|
257
310
|
);
|
|
258
311
|
}
|
|
259
|
-
} else
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
312
|
+
} else {
|
|
313
|
+
outcomeTrace = sampleDomTrace();
|
|
314
|
+
if (steps === 0 && domTextAtEntered !== void 0 && (document.body.textContent ?? "") === domTextAtEntered) {
|
|
315
|
+
throw new Error(
|
|
316
|
+
'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.'
|
|
317
|
+
);
|
|
318
|
+
}
|
|
263
319
|
}
|
|
264
320
|
evidence.boundedRuns += 1;
|
|
265
321
|
assertionsAtOutcome = expect.getState().assertionCalls;
|
|
@@ -294,7 +350,15 @@ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
|
|
|
294
350
|
);
|
|
295
351
|
}
|
|
296
352
|
evidence.verified = true;
|
|
353
|
+
} catch (error) {
|
|
354
|
+
metadata.trace = createFailureTrace();
|
|
355
|
+
try {
|
|
356
|
+
await annotate(metadata.trace, REACT_PLAYTHROUGH_TRACE_ANNOTATION);
|
|
357
|
+
} catch {
|
|
358
|
+
}
|
|
359
|
+
throw error;
|
|
297
360
|
} finally {
|
|
361
|
+
metadata.trace ??= createFailureTrace();
|
|
298
362
|
for (const event of INPUT_EVENTS) {
|
|
299
363
|
document.removeEventListener(event, recordInput, true);
|
|
300
364
|
}
|
|
@@ -387,21 +451,41 @@ function errorLocation(value) {
|
|
|
387
451
|
const match = value.match(/(?:^|\n)\s*at\s+(.*?\.(?:test|spec)\.[^\n]+:\d+:\d+)/);
|
|
388
452
|
return match?.[1];
|
|
389
453
|
}
|
|
454
|
+
function failureTrace(test2) {
|
|
455
|
+
const annotations = test2.annotations();
|
|
456
|
+
let annotationTrace;
|
|
457
|
+
for (let index = annotations.length - 1; index >= 0; index -= 1) {
|
|
458
|
+
if (annotations[index].type === REACT_PLAYTHROUGH_TRACE_ANNOTATION) {
|
|
459
|
+
annotationTrace = annotations[index].message;
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const metadata = test2.meta().reactPlaythrough;
|
|
464
|
+
const trace = annotationTrace ?? (isMetadata(metadata) ? metadata.trace : void 0);
|
|
465
|
+
return trace ? truncateReporterLine(trace, 720) : void 0;
|
|
466
|
+
}
|
|
467
|
+
function truncateReporterLine(value, limit) {
|
|
468
|
+
const compact = stripVTControlCharacters(value).replace(/\s+/g, " ").trim();
|
|
469
|
+
if (compact.length <= limit) return compact;
|
|
470
|
+
return `${compact.slice(0, Math.max(0, limit - 1))}\u2026`;
|
|
471
|
+
}
|
|
390
472
|
function toModuleResult(module, projectRoot) {
|
|
391
473
|
const tests = [...module.children.allTests()];
|
|
474
|
+
const moduleErrors = module.errors().map((error) => firstLine(error.message)).filter((message) => Boolean(message));
|
|
392
475
|
const errors = [
|
|
393
|
-
...
|
|
476
|
+
...moduleErrors,
|
|
394
477
|
...tests.flatMap(
|
|
395
478
|
(test2) => (test2.result().errors ?? []).map((error) => firstLine(error.message))
|
|
396
479
|
)
|
|
397
480
|
].filter((message) => Boolean(message));
|
|
398
481
|
const failures = tests.filter((test2) => test2.result().state === "failed").map((test2) => {
|
|
399
|
-
const raw = test2.result().errors?.
|
|
482
|
+
const raw = test2.result().errors?.at(-1)?.message ?? module.errors()[0]?.message ?? "Unknown failure";
|
|
400
483
|
return {
|
|
401
484
|
test: test2.fullName,
|
|
402
485
|
cause: firstLine(raw) ?? "Unknown failure",
|
|
403
486
|
location: test2.location ? `${relative(projectRoot, module.moduleId).replaceAll("\\", "/")}:${test2.location.line}:${test2.location.column}` : errorLocation(raw),
|
|
404
|
-
hint: failureHint(raw)
|
|
487
|
+
hint: failureHint(raw),
|
|
488
|
+
trace: failureTrace(test2)
|
|
405
489
|
};
|
|
406
490
|
});
|
|
407
491
|
if (failures.length === 0 && tests.length === 0 && module.errors().length > 0) {
|
|
@@ -417,11 +501,12 @@ function toModuleResult(module, projectRoot) {
|
|
|
417
501
|
file: relative(projectRoot, module.moduleId).replaceAll("\\", "/"),
|
|
418
502
|
state: module.state(),
|
|
419
503
|
errors,
|
|
504
|
+
primaryError: moduleErrors[0] ?? failures[0]?.cause,
|
|
420
505
|
tests: tests.map(toAuditInput),
|
|
421
506
|
failures
|
|
422
507
|
};
|
|
423
508
|
}
|
|
424
|
-
function
|
|
509
|
+
function formatReactFailureSummary(modules) {
|
|
425
510
|
const failures = modules.flatMap(
|
|
426
511
|
(module) => (module.failures ?? []).map((failure) => ({ ...failure, file: module.file }))
|
|
427
512
|
);
|
|
@@ -431,6 +516,7 @@ function formatFailureSummary(modules) {
|
|
|
431
516
|
lines.push(`FAILURE_${index + 1}: ${failure.file}`);
|
|
432
517
|
lines.push(`TEST: ${failure.test}`);
|
|
433
518
|
lines.push(`CAUSE: ${failure.cause}`);
|
|
519
|
+
if (failure.trace) lines.push(`TRACE: ${failure.trace}`);
|
|
434
520
|
if (failure.location) lines.push(`AT: ${failure.location}`);
|
|
435
521
|
if (failure.hint) lines.push(`HINT: ${failure.hint}`);
|
|
436
522
|
}
|
|
@@ -505,7 +591,7 @@ function assessReactPlaythroughReport(input) {
|
|
|
505
591
|
const tests = input.modules.flatMap((module) => module.tests);
|
|
506
592
|
const audit = auditReactPlaythroughRun(tests);
|
|
507
593
|
if (!audit.passed) {
|
|
508
|
-
const cause = productionModule.errors[0] ?? input.unhandledErrors?.[0] ?? audit.issues[0] ?? "The production playthrough did not leave complete gameplay evidence.";
|
|
594
|
+
const cause = productionModule.primaryError ?? productionModule.errors[0] ?? input.unhandledErrors?.[0] ?? audit.issues[0] ?? "The production playthrough did not leave complete gameplay evidence.";
|
|
509
595
|
return {
|
|
510
596
|
...base,
|
|
511
597
|
status: "FAILED",
|
|
@@ -575,7 +661,7 @@ var ReactPlaythroughReporter = class {
|
|
|
575
661
|
} else {
|
|
576
662
|
console.log(output);
|
|
577
663
|
}
|
|
578
|
-
const summary =
|
|
664
|
+
const summary = formatReactFailureSummary(
|
|
579
665
|
testModules.map((module) => toModuleResult(module, this.projectRoot))
|
|
580
666
|
);
|
|
581
667
|
if (report.failsRun && summary[0] === "TEST_RESULT: PASS") {
|