miaoda-game-devkit 0.2.16 → 0.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -6
- package/dist/{gameplay-audit-DmbBA0M_.d.mts → gameplay-audit-CKgNYw2r.d.mts} +46 -13
- package/dist/{gameplay-audit-DmbBA0M_.d.ts → gameplay-audit-CKgNYw2r.d.ts} +46 -13
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +346 -48
- package/dist/index.mjs +346 -48
- package/dist/react/testing.d.mts +36 -2
- package/dist/react/testing.d.ts +36 -2
- package/dist/react/testing.js +141 -15
- package/dist/react/testing.mjs +130 -14
- package/dist/react/vitest-config.js +146 -18
- package/dist/react/vitest-config.mjs +135 -17
- package/dist/vitest-config.d.mts +1 -1
- package/dist/vitest-config.d.ts +1 -1
- package/dist/vitest-config.js +9 -9
- package/dist/vitest-config.mjs +9 -9
- package/package.json +5 -1
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/react-vitest-config.ts
|
|
@@ -33,10 +43,21 @@ var import_node_path = require("path");
|
|
|
33
43
|
|
|
34
44
|
// src/react/react-playthrough.ts
|
|
35
45
|
var import_react2 = require("@testing-library/react");
|
|
46
|
+
var import_user_event = __toESM(require("@testing-library/user-event"));
|
|
36
47
|
var import_vitest = require("vitest");
|
|
37
48
|
|
|
38
49
|
// src/react/react-playthrough-core.ts
|
|
39
50
|
var import_react = require("@testing-library/react");
|
|
51
|
+
function formatDiagnostics(read) {
|
|
52
|
+
if (!read) return void 0;
|
|
53
|
+
try {
|
|
54
|
+
const value = read();
|
|
55
|
+
if (typeof value === "string") return value;
|
|
56
|
+
return JSON.stringify(value);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return `diagnostics() threw: ${String(error)}`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
40
61
|
function normalizePlaythroughWaiverReason(waiverReason) {
|
|
41
62
|
if (waiverReason === void 0) return void 0;
|
|
42
63
|
const reason = waiverReason.trim();
|
|
@@ -62,8 +83,11 @@ async function runBoundedUntil(condition, options = {}) {
|
|
|
62
83
|
});
|
|
63
84
|
}
|
|
64
85
|
}
|
|
86
|
+
const diagnostics = formatDiagnostics(options.diagnostics);
|
|
87
|
+
const guidance = options.step ? "The step callback ran, but the authoritative outcome did not change." : "No step callback was provided, so time-driven gameplay was not advanced. Inject a ManualGameClock for this test and pass step: () => clock.stepFrame().";
|
|
88
|
+
const suffix = diagnostics ? ` Last diagnostics: ${diagnostics}` : "";
|
|
65
89
|
throw new Error(
|
|
66
|
-
`Playthrough outcome was not reached within ${maxSteps} steps
|
|
90
|
+
`Playthrough outcome was not reached within ${maxSteps} steps. ${guidance}${suffix}`
|
|
67
91
|
);
|
|
68
92
|
}
|
|
69
93
|
|
|
@@ -77,27 +101,54 @@ var INPUT_EVENTS = [
|
|
|
77
101
|
"touchstart",
|
|
78
102
|
"touchend"
|
|
79
103
|
];
|
|
104
|
+
function describeMissingEvidence(evidence) {
|
|
105
|
+
if (!evidence || evidence.entryInputs === 0) return "entry \u8F93\u5165";
|
|
106
|
+
if (evidence.primaryInputs === 0) return "primary \u8F93\u5165";
|
|
107
|
+
if (!evidence.checkpoints.includes("entered")) return "entered checkpoint";
|
|
108
|
+
if (evidence.boundedRuns === 0) return "\u6709\u754C stepUntil";
|
|
109
|
+
if (evidence.assertionsAfterOutcome === 0) return "stepUntil \u540E\u7684\u7ED3\u679C\u65AD\u8A00";
|
|
110
|
+
if (evidence.changedOutcomeCheckpoints === 0) {
|
|
111
|
+
return "\u53D1\u751F\u72B6\u6001\u53D8\u5316\u7684 progress/terminal checkpoint";
|
|
112
|
+
}
|
|
113
|
+
return "\u5B8C\u6574\u7684 playthrough \u6821\u9A8C\u6807\u8BB0";
|
|
114
|
+
}
|
|
80
115
|
function createMetadata(waiverReason) {
|
|
81
116
|
return {
|
|
82
|
-
version:
|
|
117
|
+
version: 2,
|
|
83
118
|
waiverReason,
|
|
84
119
|
evidence: {
|
|
85
120
|
domInputEvents: 0,
|
|
121
|
+
entryInputs: 0,
|
|
122
|
+
primaryInputs: 0,
|
|
86
123
|
boundedRuns: 0,
|
|
87
124
|
assertionsAfterOutcome: 0,
|
|
125
|
+
changedOutcomeCheckpoints: 0,
|
|
126
|
+
checkpoints: [],
|
|
88
127
|
verified: false
|
|
89
128
|
}
|
|
90
129
|
};
|
|
91
130
|
}
|
|
131
|
+
function snapshotFingerprint(snapshot) {
|
|
132
|
+
try {
|
|
133
|
+
const serialized = JSON.stringify(snapshot);
|
|
134
|
+
if (serialized === void 0) throw new Error("unsupported value");
|
|
135
|
+
return serialized;
|
|
136
|
+
} catch {
|
|
137
|
+
throw new Error(
|
|
138
|
+
"checkpoint snapshot must be JSON-serializable. Pass a read-only Telemetry snapshot or a small visible-state object."
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
92
142
|
function definePlaythrough(element, run, waiverReason) {
|
|
93
143
|
const reason = normalizePlaythroughWaiverReason(waiverReason);
|
|
94
144
|
const metadata = createMetadata(reason);
|
|
95
145
|
(0, import_vitest.test)("production game completes a bounded playthrough", {
|
|
96
146
|
skip: Boolean(reason),
|
|
97
147
|
meta: { reactPlaythrough: metadata }
|
|
98
|
-
}, async () => {
|
|
148
|
+
}, async ({ expect }) => {
|
|
99
149
|
const evidence = metadata.evidence;
|
|
100
150
|
let assertionsAtOutcome;
|
|
151
|
+
let enteredSnapshot;
|
|
101
152
|
const recordInput = () => {
|
|
102
153
|
evidence.domInputEvents += 1;
|
|
103
154
|
};
|
|
@@ -111,25 +162,94 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
111
162
|
"playthroughTest must render the production game entry."
|
|
112
163
|
);
|
|
113
164
|
}
|
|
165
|
+
const user = import_user_event.default.setup();
|
|
114
166
|
await run({
|
|
115
167
|
view,
|
|
168
|
+
user,
|
|
169
|
+
expect,
|
|
170
|
+
async performInput(kind, input) {
|
|
171
|
+
if (kind === "entry" && enteredSnapshot !== void 0) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
'performInput("entry") must run before checkpoint("entered"). Group multiple setup actions in the same callback.'
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
if (kind === "primary" && enteredSnapshot === void 0) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
'Before performInput("primary"), run an entry input and checkpoint("entered", snapshot).'
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
const inputsBefore = evidence.domInputEvents;
|
|
182
|
+
await input();
|
|
183
|
+
if (evidence.domInputEvents === inputsBefore) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`performInput("${kind}") did not dispatch a supported production DOM input. Use the provided user or dispatch a real keyboard, pointer, or touch event to the production target.`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
if (kind === "entry") evidence.entryInputs += 1;
|
|
189
|
+
else evidence.primaryInputs += 1;
|
|
190
|
+
},
|
|
191
|
+
checkpoint(kind, snapshot) {
|
|
192
|
+
const fingerprint = snapshotFingerprint(snapshot);
|
|
193
|
+
if (kind === "entered") {
|
|
194
|
+
if (enteredSnapshot !== void 0) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
'checkpoint("entered") may only be recorded once, before the primary input.'
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
if (evidence.entryInputs === 0) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
'checkpoint("entered") must follow performInput("entry", ...).'
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
enteredSnapshot = fingerprint;
|
|
205
|
+
evidence.checkpoints.push(kind);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (evidence.primaryInputs === 0) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
`checkpoint("${kind}") must follow performInput("primary", ...).`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
if (evidence.boundedRuns === 0) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`checkpoint("${kind}") must be recorded after stepUntil returns.`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
if (assertionsAtOutcome === void 0 || expect.getState().assertionCalls <= assertionsAtOutcome) {
|
|
219
|
+
throw new Error(
|
|
220
|
+
`Use the expect provided by playthroughTest to assert the authoritative result after stepUntil, then record checkpoint("${kind}", snapshot).`
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
if (fingerprint === enteredSnapshot) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`checkpoint("${kind}") matches the entered snapshot. Assert a production state change caused by the primary input.`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
evidence.changedOutcomeCheckpoints += 1;
|
|
229
|
+
evidence.checkpoints.push(kind);
|
|
230
|
+
},
|
|
116
231
|
async stepUntil(condition, options = {}) {
|
|
117
232
|
const steps = await runBoundedUntil(condition, options);
|
|
118
|
-
if (evidence.
|
|
233
|
+
if (evidence.primaryInputs === 0) {
|
|
119
234
|
throw new Error(
|
|
120
|
-
|
|
235
|
+
'stepUntil must follow performInput("primary", ...). A menu/help click is not gameplay evidence.'
|
|
121
236
|
);
|
|
122
237
|
}
|
|
123
238
|
evidence.boundedRuns += 1;
|
|
124
|
-
assertionsAtOutcome =
|
|
239
|
+
assertionsAtOutcome = expect.getState().assertionCalls;
|
|
125
240
|
return steps;
|
|
126
241
|
}
|
|
127
242
|
});
|
|
128
|
-
const assertionCalls =
|
|
243
|
+
const assertionCalls = expect.getState().assertionCalls;
|
|
129
244
|
evidence.assertionsAfterOutcome = assertionsAtOutcome === void 0 ? 0 : assertionCalls - assertionsAtOutcome;
|
|
130
|
-
if (evidence.
|
|
245
|
+
if (evidence.entryInputs === 0) {
|
|
131
246
|
throw new Error(
|
|
132
|
-
|
|
247
|
+
'playthroughTest must perform an entry input with performInput("entry", ...).'
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
if (evidence.primaryInputs === 0) {
|
|
251
|
+
throw new Error(
|
|
252
|
+
'playthroughTest must perform a core game action with performInput("primary", ...).'
|
|
133
253
|
);
|
|
134
254
|
}
|
|
135
255
|
if (evidence.boundedRuns === 0) {
|
|
@@ -137,7 +257,12 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
137
257
|
}
|
|
138
258
|
if (evidence.assertionsAfterOutcome === 0) {
|
|
139
259
|
throw new Error(
|
|
140
|
-
"
|
|
260
|
+
"Use the expect provided by playthroughTest to assert an authoritative game outcome after stepUntil returns."
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
if (evidence.changedOutcomeCheckpoints === 0) {
|
|
264
|
+
throw new Error(
|
|
265
|
+
'After asserting the result, record checkpoint("progress", snapshot) or checkpoint("terminal", snapshot). The snapshot must differ from checkpoint("entered").'
|
|
141
266
|
);
|
|
142
267
|
}
|
|
143
268
|
evidence.verified = true;
|
|
@@ -158,7 +283,9 @@ function auditReactPlaythroughRun(tests) {
|
|
|
158
283
|
const declared = tests.filter((candidate) => candidate.metadata);
|
|
159
284
|
const valid = declared.filter(({ state, metadata }) => {
|
|
160
285
|
const evidence = metadata?.evidence;
|
|
161
|
-
return state === "passed" && evidence?.verified === true && evidence.domInputEvents > 0 && evidence.boundedRuns > 0 && evidence.assertionsAfterOutcome > 0
|
|
286
|
+
return state === "passed" && evidence?.verified === true && evidence.domInputEvents > 0 && evidence.entryInputs > 0 && evidence.primaryInputs > 0 && evidence.boundedRuns > 0 && evidence.assertionsAfterOutcome > 0 && evidence.changedOutcomeCheckpoints > 0 && evidence.checkpoints.includes("entered") && evidence.checkpoints.some(
|
|
287
|
+
(checkpoint) => checkpoint === "progress" || checkpoint === "terminal"
|
|
288
|
+
);
|
|
162
289
|
});
|
|
163
290
|
const waivers = declared.filter(
|
|
164
291
|
({ state, metadata }) => state === "skipped" && (metadata?.waiverReason?.trim().length ?? 0) >= 20
|
|
@@ -166,7 +293,7 @@ function auditReactPlaythroughRun(tests) {
|
|
|
166
293
|
const issues = [];
|
|
167
294
|
if (declared.length === 0) {
|
|
168
295
|
issues.push(
|
|
169
|
-
|
|
296
|
+
'\u7F3A\u5C11\u751F\u4EA7\u6E38\u620F\u53EF\u73A9\u6027\u9A8C\u8BC1\uFF1A\u4F7F\u7528 playthroughTest \u6E32\u67D3 <App />\uFF0C\u4F9D\u6B21\u6267\u884C performInput("entry")\u3001checkpoint("entered")\u3001performInput("primary")\u3001stepUntil\u3001\u7528 playthroughTest \u63D0\u4F9B\u7684 expect \u65AD\u8A00\u7ED3\u679C\uFF0C\u5E76\u8BB0\u5F55 progress/terminal checkpoint\u3002'
|
|
170
297
|
);
|
|
171
298
|
} else {
|
|
172
299
|
for (const candidate of declared) {
|
|
@@ -180,9 +307,8 @@ function auditReactPlaythroughRun(tests) {
|
|
|
180
307
|
} else if (candidate.state !== "passed") {
|
|
181
308
|
issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7684\u72B6\u6001\u4E3A ${candidate.state}\u3002`);
|
|
182
309
|
} else {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
);
|
|
310
|
+
const missing = describeMissingEvidence(candidate.metadata?.evidence);
|
|
311
|
+
issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7F3A\u5C11${missing}\u3002`);
|
|
186
312
|
}
|
|
187
313
|
}
|
|
188
314
|
}
|
|
@@ -197,7 +323,7 @@ var PRODUCTION_PLAYTHROUGH_FILE = "tests/production-playthrough.test.tsx";
|
|
|
197
323
|
function isMetadata(value) {
|
|
198
324
|
if (!value || typeof value !== "object") return false;
|
|
199
325
|
const metadata = value;
|
|
200
|
-
return metadata.version ===
|
|
326
|
+
return metadata.version === 2 && Boolean(metadata.evidence);
|
|
201
327
|
}
|
|
202
328
|
function toAuditInput(test2) {
|
|
203
329
|
const metadata = test2.meta().reactPlaythrough;
|
|
@@ -241,7 +367,7 @@ function assessReactPlaythroughReport(input) {
|
|
|
241
367
|
...base,
|
|
242
368
|
status: "FAILED",
|
|
243
369
|
cause: "\u751F\u4EA7\u53EF\u73A9\u6027\u6D4B\u8BD5\u6587\u4EF6\u4E0D\u5B58\u5728\u3002",
|
|
244
|
-
next:
|
|
370
|
+
next: '\u521B\u5EFA\u8BE5\u6587\u4EF6\uFF1A\u4ECE <App /> \u901A\u8FC7\u771F\u5B9E DOM \u8F93\u5165\u4F9D\u6B21\u6267\u884C performInput("entry")\u3001checkpoint("entered")\u3001performInput("primary")\u3001stepUntil\u3001\u7528 playthroughTest \u63D0\u4F9B\u7684 expect \u65AD\u8A00\u7ED3\u679C\uFF0C\u5E76\u8BB0\u5F55 progress/terminal checkpoint\u3002Canvas \u7528\u771F\u5B9E DOM \u4E8B\u4EF6\u8F93\u5165\u5E76\u8BFB\u53D6 Telemetry snapshot\u3002',
|
|
245
371
|
failsRun: true
|
|
246
372
|
};
|
|
247
373
|
}
|
|
@@ -273,11 +399,13 @@ function assessReactPlaythroughReport(input) {
|
|
|
273
399
|
if (!audit.passed) {
|
|
274
400
|
const cause = productionModule.errors[0] ?? input.unhandledErrors?.[0] ?? audit.issues[0] ?? "\u751F\u4EA7\u53EF\u73A9\u6D41\u7A0B\u6CA1\u6709\u7559\u4E0B\u5B8C\u6574\u8BC1\u636E\u3002";
|
|
275
401
|
const timedOut = /outcome was not reached within \d+ steps/i.test(cause);
|
|
402
|
+
const missingStep = /No step callback was provided/i.test(cause);
|
|
403
|
+
const missingStructuredEvidence = /performInput|checkpoint/.test(cause);
|
|
276
404
|
return {
|
|
277
405
|
...base,
|
|
278
406
|
status: "FAILED",
|
|
279
407
|
cause,
|
|
280
|
-
next: timedOut ? "\u771F\u5B9E\u8F93\u5165\u5DF2\u6267\u884C\uFF0C\u4F46\u73A9\u6CD5\u6CA1\u6709\u5728\u4E0A\u9650\u5185\u4EA7\u751F\u7ED3\u679C\uFF1B\u68C0\u67E5\
|
|
408
|
+
next: missingStep ? "\u8BE5\u6D41\u7A0B\u662F\u65F6\u95F4\u6216\u5E27\u9A71\u52A8\u7684\uFF0C\u4F46 stepUntil \u6CA1\u6709\u63A8\u8FDB\u6E38\u620F\u65F6\u95F4\uFF1B\u4E3A\u751F\u4EA7\u6E38\u620F\u6CE8\u5165 devkit \u7684 GameClock\uFF0C\u5E76\u4F20\u5165 step: () => clock.stepFrame()\u3002\u4E0D\u8981\u7528\u771F\u5B9E setTimeout\u3002" : timedOut ? "\u771F\u5B9E\u8F93\u5165\u5DF2\u6267\u884C\uFF0C\u4F46\u73A9\u6CD5\u6CA1\u6709\u5728\u4E0A\u9650\u5185\u4EA7\u751F\u7ED3\u679C\uFF1B\u68C0\u67E5\u751F\u4EA7\u63A7\u5236\u662F\u5426\u6536\u5230\u8F93\u5165\uFF0C\u518D\u67E5\u770B\u8D85\u65F6\u9519\u8BEF\u4E2D\u7684 Last diagnostics \u5224\u65AD\u662F\u6E38\u620F\u5FAA\u73AF\u3001\u89C4\u5219\u72B6\u6001\u8FD8\u662F UI \u540C\u6B65\u672A\u63A8\u8FDB\u3002" : missingStructuredEvidence ? '\u6309\u987A\u5E8F\u8865\u9F50\uFF1AperformInput("entry") \u540E\u8BB0\u5F55 entered snapshot\uFF1B\u518D\u7528 performInput("primary") \u6267\u884C\u6838\u5FC3\u64CD\u4F5C\uFF0CstepUntil \u7B49\u5F85\u53D8\u5316\uFF0C\u65AD\u8A00\u540E\u8BB0\u5F55 progress \u6216 terminal snapshot\u3002' : "\u4ECE\u751F\u4EA7\u5165\u53E3\u6267\u884C\u771F\u5B9E DOM \u8F93\u5165\uFF0C\u7528 stepUntil \u6709\u754C\u63A8\u8FDB\u5230\u73A9\u5BB6\u53EF\u89C1\u7ED3\u679C\u6216\u6E38\u620F\u771F\u5B9E\u72B6\u6001\u7684\u53EA\u8BFB snapshot\uFF0C\u5E76\u5728\u5176\u540E\u65AD\u8A00\uFF1B\u4E0D\u8981\u76F4\u8FBE\u5185\u90E8\u5173\u5361\u6216\u4FEE\u6539\u73A9\u6CD5\u72B6\u6001\u3002",
|
|
281
409
|
failsRun: true
|
|
282
410
|
};
|
|
283
411
|
}
|
|
@@ -9,10 +9,21 @@ import { relative, resolve } from "path";
|
|
|
9
9
|
|
|
10
10
|
// src/react/react-playthrough.ts
|
|
11
11
|
import { render } from "@testing-library/react";
|
|
12
|
-
import
|
|
12
|
+
import userEvent from "@testing-library/user-event";
|
|
13
|
+
import { test } from "vitest";
|
|
13
14
|
|
|
14
15
|
// src/react/react-playthrough-core.ts
|
|
15
16
|
import { act } from "@testing-library/react";
|
|
17
|
+
function formatDiagnostics(read) {
|
|
18
|
+
if (!read) return void 0;
|
|
19
|
+
try {
|
|
20
|
+
const value = read();
|
|
21
|
+
if (typeof value === "string") return value;
|
|
22
|
+
return JSON.stringify(value);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
return `diagnostics() threw: ${String(error)}`;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
16
27
|
function normalizePlaythroughWaiverReason(waiverReason) {
|
|
17
28
|
if (waiverReason === void 0) return void 0;
|
|
18
29
|
const reason = waiverReason.trim();
|
|
@@ -38,8 +49,11 @@ async function runBoundedUntil(condition, options = {}) {
|
|
|
38
49
|
});
|
|
39
50
|
}
|
|
40
51
|
}
|
|
52
|
+
const diagnostics = formatDiagnostics(options.diagnostics);
|
|
53
|
+
const guidance = options.step ? "The step callback ran, but the authoritative outcome did not change." : "No step callback was provided, so time-driven gameplay was not advanced. Inject a ManualGameClock for this test and pass step: () => clock.stepFrame().";
|
|
54
|
+
const suffix = diagnostics ? ` Last diagnostics: ${diagnostics}` : "";
|
|
41
55
|
throw new Error(
|
|
42
|
-
`Playthrough outcome was not reached within ${maxSteps} steps
|
|
56
|
+
`Playthrough outcome was not reached within ${maxSteps} steps. ${guidance}${suffix}`
|
|
43
57
|
);
|
|
44
58
|
}
|
|
45
59
|
|
|
@@ -53,27 +67,54 @@ var INPUT_EVENTS = [
|
|
|
53
67
|
"touchstart",
|
|
54
68
|
"touchend"
|
|
55
69
|
];
|
|
70
|
+
function describeMissingEvidence(evidence) {
|
|
71
|
+
if (!evidence || evidence.entryInputs === 0) return "entry \u8F93\u5165";
|
|
72
|
+
if (evidence.primaryInputs === 0) return "primary \u8F93\u5165";
|
|
73
|
+
if (!evidence.checkpoints.includes("entered")) return "entered checkpoint";
|
|
74
|
+
if (evidence.boundedRuns === 0) return "\u6709\u754C stepUntil";
|
|
75
|
+
if (evidence.assertionsAfterOutcome === 0) return "stepUntil \u540E\u7684\u7ED3\u679C\u65AD\u8A00";
|
|
76
|
+
if (evidence.changedOutcomeCheckpoints === 0) {
|
|
77
|
+
return "\u53D1\u751F\u72B6\u6001\u53D8\u5316\u7684 progress/terminal checkpoint";
|
|
78
|
+
}
|
|
79
|
+
return "\u5B8C\u6574\u7684 playthrough \u6821\u9A8C\u6807\u8BB0";
|
|
80
|
+
}
|
|
56
81
|
function createMetadata(waiverReason) {
|
|
57
82
|
return {
|
|
58
|
-
version:
|
|
83
|
+
version: 2,
|
|
59
84
|
waiverReason,
|
|
60
85
|
evidence: {
|
|
61
86
|
domInputEvents: 0,
|
|
87
|
+
entryInputs: 0,
|
|
88
|
+
primaryInputs: 0,
|
|
62
89
|
boundedRuns: 0,
|
|
63
90
|
assertionsAfterOutcome: 0,
|
|
91
|
+
changedOutcomeCheckpoints: 0,
|
|
92
|
+
checkpoints: [],
|
|
64
93
|
verified: false
|
|
65
94
|
}
|
|
66
95
|
};
|
|
67
96
|
}
|
|
97
|
+
function snapshotFingerprint(snapshot) {
|
|
98
|
+
try {
|
|
99
|
+
const serialized = JSON.stringify(snapshot);
|
|
100
|
+
if (serialized === void 0) throw new Error("unsupported value");
|
|
101
|
+
return serialized;
|
|
102
|
+
} catch {
|
|
103
|
+
throw new Error(
|
|
104
|
+
"checkpoint snapshot must be JSON-serializable. Pass a read-only Telemetry snapshot or a small visible-state object."
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
68
108
|
function definePlaythrough(element, run, waiverReason) {
|
|
69
109
|
const reason = normalizePlaythroughWaiverReason(waiverReason);
|
|
70
110
|
const metadata = createMetadata(reason);
|
|
71
111
|
test("production game completes a bounded playthrough", {
|
|
72
112
|
skip: Boolean(reason),
|
|
73
113
|
meta: { reactPlaythrough: metadata }
|
|
74
|
-
}, async () => {
|
|
114
|
+
}, async ({ expect }) => {
|
|
75
115
|
const evidence = metadata.evidence;
|
|
76
116
|
let assertionsAtOutcome;
|
|
117
|
+
let enteredSnapshot;
|
|
77
118
|
const recordInput = () => {
|
|
78
119
|
evidence.domInputEvents += 1;
|
|
79
120
|
};
|
|
@@ -87,13 +128,77 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
87
128
|
"playthroughTest must render the production game entry."
|
|
88
129
|
);
|
|
89
130
|
}
|
|
131
|
+
const user = userEvent.setup();
|
|
90
132
|
await run({
|
|
91
133
|
view,
|
|
134
|
+
user,
|
|
135
|
+
expect,
|
|
136
|
+
async performInput(kind, input) {
|
|
137
|
+
if (kind === "entry" && enteredSnapshot !== void 0) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
'performInput("entry") must run before checkpoint("entered"). Group multiple setup actions in the same callback.'
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
if (kind === "primary" && enteredSnapshot === void 0) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
'Before performInput("primary"), run an entry input and checkpoint("entered", snapshot).'
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
const inputsBefore = evidence.domInputEvents;
|
|
148
|
+
await input();
|
|
149
|
+
if (evidence.domInputEvents === inputsBefore) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`performInput("${kind}") did not dispatch a supported production DOM input. Use the provided user or dispatch a real keyboard, pointer, or touch event to the production target.`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
if (kind === "entry") evidence.entryInputs += 1;
|
|
155
|
+
else evidence.primaryInputs += 1;
|
|
156
|
+
},
|
|
157
|
+
checkpoint(kind, snapshot) {
|
|
158
|
+
const fingerprint = snapshotFingerprint(snapshot);
|
|
159
|
+
if (kind === "entered") {
|
|
160
|
+
if (enteredSnapshot !== void 0) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
'checkpoint("entered") may only be recorded once, before the primary input.'
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (evidence.entryInputs === 0) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
'checkpoint("entered") must follow performInput("entry", ...).'
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
enteredSnapshot = fingerprint;
|
|
171
|
+
evidence.checkpoints.push(kind);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (evidence.primaryInputs === 0) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`checkpoint("${kind}") must follow performInput("primary", ...).`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
if (evidence.boundedRuns === 0) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`checkpoint("${kind}") must be recorded after stepUntil returns.`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
if (assertionsAtOutcome === void 0 || expect.getState().assertionCalls <= assertionsAtOutcome) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
`Use the expect provided by playthroughTest to assert the authoritative result after stepUntil, then record checkpoint("${kind}", snapshot).`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
if (fingerprint === enteredSnapshot) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
`checkpoint("${kind}") matches the entered snapshot. Assert a production state change caused by the primary input.`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
evidence.changedOutcomeCheckpoints += 1;
|
|
195
|
+
evidence.checkpoints.push(kind);
|
|
196
|
+
},
|
|
92
197
|
async stepUntil(condition, options = {}) {
|
|
93
198
|
const steps = await runBoundedUntil(condition, options);
|
|
94
|
-
if (evidence.
|
|
199
|
+
if (evidence.primaryInputs === 0) {
|
|
95
200
|
throw new Error(
|
|
96
|
-
|
|
201
|
+
'stepUntil must follow performInput("primary", ...). A menu/help click is not gameplay evidence.'
|
|
97
202
|
);
|
|
98
203
|
}
|
|
99
204
|
evidence.boundedRuns += 1;
|
|
@@ -103,9 +208,14 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
103
208
|
});
|
|
104
209
|
const assertionCalls = expect.getState().assertionCalls;
|
|
105
210
|
evidence.assertionsAfterOutcome = assertionsAtOutcome === void 0 ? 0 : assertionCalls - assertionsAtOutcome;
|
|
106
|
-
if (evidence.
|
|
211
|
+
if (evidence.entryInputs === 0) {
|
|
107
212
|
throw new Error(
|
|
108
|
-
|
|
213
|
+
'playthroughTest must perform an entry input with performInput("entry", ...).'
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
if (evidence.primaryInputs === 0) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
'playthroughTest must perform a core game action with performInput("primary", ...).'
|
|
109
219
|
);
|
|
110
220
|
}
|
|
111
221
|
if (evidence.boundedRuns === 0) {
|
|
@@ -113,7 +223,12 @@ function definePlaythrough(element, run, waiverReason) {
|
|
|
113
223
|
}
|
|
114
224
|
if (evidence.assertionsAfterOutcome === 0) {
|
|
115
225
|
throw new Error(
|
|
116
|
-
"
|
|
226
|
+
"Use the expect provided by playthroughTest to assert an authoritative game outcome after stepUntil returns."
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
if (evidence.changedOutcomeCheckpoints === 0) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
'After asserting the result, record checkpoint("progress", snapshot) or checkpoint("terminal", snapshot). The snapshot must differ from checkpoint("entered").'
|
|
117
232
|
);
|
|
118
233
|
}
|
|
119
234
|
evidence.verified = true;
|
|
@@ -134,7 +249,9 @@ function auditReactPlaythroughRun(tests) {
|
|
|
134
249
|
const declared = tests.filter((candidate) => candidate.metadata);
|
|
135
250
|
const valid = declared.filter(({ state, metadata }) => {
|
|
136
251
|
const evidence = metadata?.evidence;
|
|
137
|
-
return state === "passed" && evidence?.verified === true && evidence.domInputEvents > 0 && evidence.boundedRuns > 0 && evidence.assertionsAfterOutcome > 0
|
|
252
|
+
return state === "passed" && evidence?.verified === true && evidence.domInputEvents > 0 && evidence.entryInputs > 0 && evidence.primaryInputs > 0 && evidence.boundedRuns > 0 && evidence.assertionsAfterOutcome > 0 && evidence.changedOutcomeCheckpoints > 0 && evidence.checkpoints.includes("entered") && evidence.checkpoints.some(
|
|
253
|
+
(checkpoint) => checkpoint === "progress" || checkpoint === "terminal"
|
|
254
|
+
);
|
|
138
255
|
});
|
|
139
256
|
const waivers = declared.filter(
|
|
140
257
|
({ state, metadata }) => state === "skipped" && (metadata?.waiverReason?.trim().length ?? 0) >= 20
|
|
@@ -142,7 +259,7 @@ function auditReactPlaythroughRun(tests) {
|
|
|
142
259
|
const issues = [];
|
|
143
260
|
if (declared.length === 0) {
|
|
144
261
|
issues.push(
|
|
145
|
-
|
|
262
|
+
'\u7F3A\u5C11\u751F\u4EA7\u6E38\u620F\u53EF\u73A9\u6027\u9A8C\u8BC1\uFF1A\u4F7F\u7528 playthroughTest \u6E32\u67D3 <App />\uFF0C\u4F9D\u6B21\u6267\u884C performInput("entry")\u3001checkpoint("entered")\u3001performInput("primary")\u3001stepUntil\u3001\u7528 playthroughTest \u63D0\u4F9B\u7684 expect \u65AD\u8A00\u7ED3\u679C\uFF0C\u5E76\u8BB0\u5F55 progress/terminal checkpoint\u3002'
|
|
146
263
|
);
|
|
147
264
|
} else {
|
|
148
265
|
for (const candidate of declared) {
|
|
@@ -156,9 +273,8 @@ function auditReactPlaythroughRun(tests) {
|
|
|
156
273
|
} else if (candidate.state !== "passed") {
|
|
157
274
|
issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7684\u72B6\u6001\u4E3A ${candidate.state}\u3002`);
|
|
158
275
|
} else {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
);
|
|
276
|
+
const missing = describeMissingEvidence(candidate.metadata?.evidence);
|
|
277
|
+
issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7F3A\u5C11${missing}\u3002`);
|
|
162
278
|
}
|
|
163
279
|
}
|
|
164
280
|
}
|
|
@@ -173,7 +289,7 @@ var PRODUCTION_PLAYTHROUGH_FILE = "tests/production-playthrough.test.tsx";
|
|
|
173
289
|
function isMetadata(value) {
|
|
174
290
|
if (!value || typeof value !== "object") return false;
|
|
175
291
|
const metadata = value;
|
|
176
|
-
return metadata.version ===
|
|
292
|
+
return metadata.version === 2 && Boolean(metadata.evidence);
|
|
177
293
|
}
|
|
178
294
|
function toAuditInput(test2) {
|
|
179
295
|
const metadata = test2.meta().reactPlaythrough;
|
|
@@ -217,7 +333,7 @@ function assessReactPlaythroughReport(input) {
|
|
|
217
333
|
...base,
|
|
218
334
|
status: "FAILED",
|
|
219
335
|
cause: "\u751F\u4EA7\u53EF\u73A9\u6027\u6D4B\u8BD5\u6587\u4EF6\u4E0D\u5B58\u5728\u3002",
|
|
220
|
-
next:
|
|
336
|
+
next: '\u521B\u5EFA\u8BE5\u6587\u4EF6\uFF1A\u4ECE <App /> \u901A\u8FC7\u771F\u5B9E DOM \u8F93\u5165\u4F9D\u6B21\u6267\u884C performInput("entry")\u3001checkpoint("entered")\u3001performInput("primary")\u3001stepUntil\u3001\u7528 playthroughTest \u63D0\u4F9B\u7684 expect \u65AD\u8A00\u7ED3\u679C\uFF0C\u5E76\u8BB0\u5F55 progress/terminal checkpoint\u3002Canvas \u7528\u771F\u5B9E DOM \u4E8B\u4EF6\u8F93\u5165\u5E76\u8BFB\u53D6 Telemetry snapshot\u3002',
|
|
221
337
|
failsRun: true
|
|
222
338
|
};
|
|
223
339
|
}
|
|
@@ -249,11 +365,13 @@ function assessReactPlaythroughReport(input) {
|
|
|
249
365
|
if (!audit.passed) {
|
|
250
366
|
const cause = productionModule.errors[0] ?? input.unhandledErrors?.[0] ?? audit.issues[0] ?? "\u751F\u4EA7\u53EF\u73A9\u6D41\u7A0B\u6CA1\u6709\u7559\u4E0B\u5B8C\u6574\u8BC1\u636E\u3002";
|
|
251
367
|
const timedOut = /outcome was not reached within \d+ steps/i.test(cause);
|
|
368
|
+
const missingStep = /No step callback was provided/i.test(cause);
|
|
369
|
+
const missingStructuredEvidence = /performInput|checkpoint/.test(cause);
|
|
252
370
|
return {
|
|
253
371
|
...base,
|
|
254
372
|
status: "FAILED",
|
|
255
373
|
cause,
|
|
256
|
-
next: timedOut ? "\u771F\u5B9E\u8F93\u5165\u5DF2\u6267\u884C\uFF0C\u4F46\u73A9\u6CD5\u6CA1\u6709\u5728\u4E0A\u9650\u5185\u4EA7\u751F\u7ED3\u679C\uFF1B\u68C0\u67E5\
|
|
374
|
+
next: missingStep ? "\u8BE5\u6D41\u7A0B\u662F\u65F6\u95F4\u6216\u5E27\u9A71\u52A8\u7684\uFF0C\u4F46 stepUntil \u6CA1\u6709\u63A8\u8FDB\u6E38\u620F\u65F6\u95F4\uFF1B\u4E3A\u751F\u4EA7\u6E38\u620F\u6CE8\u5165 devkit \u7684 GameClock\uFF0C\u5E76\u4F20\u5165 step: () => clock.stepFrame()\u3002\u4E0D\u8981\u7528\u771F\u5B9E setTimeout\u3002" : timedOut ? "\u771F\u5B9E\u8F93\u5165\u5DF2\u6267\u884C\uFF0C\u4F46\u73A9\u6CD5\u6CA1\u6709\u5728\u4E0A\u9650\u5185\u4EA7\u751F\u7ED3\u679C\uFF1B\u68C0\u67E5\u751F\u4EA7\u63A7\u5236\u662F\u5426\u6536\u5230\u8F93\u5165\uFF0C\u518D\u67E5\u770B\u8D85\u65F6\u9519\u8BEF\u4E2D\u7684 Last diagnostics \u5224\u65AD\u662F\u6E38\u620F\u5FAA\u73AF\u3001\u89C4\u5219\u72B6\u6001\u8FD8\u662F UI \u540C\u6B65\u672A\u63A8\u8FDB\u3002" : missingStructuredEvidence ? '\u6309\u987A\u5E8F\u8865\u9F50\uFF1AperformInput("entry") \u540E\u8BB0\u5F55 entered snapshot\uFF1B\u518D\u7528 performInput("primary") \u6267\u884C\u6838\u5FC3\u64CD\u4F5C\uFF0CstepUntil \u7B49\u5F85\u53D8\u5316\uFF0C\u65AD\u8A00\u540E\u8BB0\u5F55 progress \u6216 terminal snapshot\u3002' : "\u4ECE\u751F\u4EA7\u5165\u53E3\u6267\u884C\u771F\u5B9E DOM \u8F93\u5165\uFF0C\u7528 stepUntil \u6709\u754C\u63A8\u8FDB\u5230\u73A9\u5BB6\u53EF\u89C1\u7ED3\u679C\u6216\u6E38\u620F\u771F\u5B9E\u72B6\u6001\u7684\u53EA\u8BFB snapshot\uFF0C\u5E76\u5728\u5176\u540E\u65AD\u8A00\uFF1B\u4E0D\u8981\u76F4\u8FBE\u5185\u90E8\u5173\u5361\u6216\u4FEE\u6539\u73A9\u6CD5\u72B6\u6001\u3002",
|
|
257
375
|
failsRun: true
|
|
258
376
|
};
|
|
259
377
|
}
|
package/dist/vitest-config.d.mts
CHANGED
package/dist/vitest-config.d.ts
CHANGED
package/dist/vitest-config.js
CHANGED
|
@@ -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\
|
|
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\
|
|
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\
|
|
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}` };
|