miaoda-game-devkit 0.2.14 → 0.2.16
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 +9 -4
- package/dist/cli/phaser-lint.js +3 -2
- package/dist/cli/react-lint.js +3 -2
- package/dist/react/index.d.mts +6 -6
- package/dist/react/index.d.ts +6 -6
- package/dist/react/index.js +8 -8
- package/dist/react/index.mjs +7 -7
- package/dist/react/testing.d.mts +85 -1
- package/dist/react/testing.d.ts +85 -1
- package/dist/react/testing.js +173 -6
- package/dist/react/testing.mjs +164 -1
- package/dist/react/vitest-config.js +328 -9
- package/dist/react/vitest-config.mjs +327 -8
- package/package.json +3 -3
package/dist/react/testing.mjs
CHANGED
|
@@ -52,6 +52,169 @@ var ManualGameClock = class {
|
|
|
52
52
|
return this.timers.size;
|
|
53
53
|
}
|
|
54
54
|
};
|
|
55
|
+
|
|
56
|
+
// src/react/react-playthrough.ts
|
|
57
|
+
import { render } from "@testing-library/react";
|
|
58
|
+
import { expect, test } from "vitest";
|
|
59
|
+
|
|
60
|
+
// src/react/react-playthrough-core.ts
|
|
61
|
+
import { act } from "@testing-library/react";
|
|
62
|
+
function normalizePlaythroughWaiverReason(waiverReason) {
|
|
63
|
+
if (waiverReason === void 0) return void 0;
|
|
64
|
+
const reason = waiverReason.trim();
|
|
65
|
+
if (reason.length < 20) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
"playthroughTest.skip reason must contain at least 20 characters."
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return reason;
|
|
71
|
+
}
|
|
72
|
+
async function runBoundedUntil(condition, options = {}) {
|
|
73
|
+
const maxSteps = options.maxSteps ?? 120;
|
|
74
|
+
if (!Number.isSafeInteger(maxSteps) || maxSteps < 0 || maxSteps > 1e4) {
|
|
75
|
+
throw new RangeError(
|
|
76
|
+
"stepUntil maxSteps must be a safe integer between 0 and 10000."
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
for (let step = 0; step <= maxSteps; step += 1) {
|
|
80
|
+
if (condition()) return step;
|
|
81
|
+
if (step < maxSteps) {
|
|
82
|
+
await act(async () => {
|
|
83
|
+
await options.step?.(step + 1);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Playthrough outcome was not reached within ${maxSteps} steps.`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/react/react-playthrough.ts
|
|
93
|
+
var INPUT_EVENTS = [
|
|
94
|
+
"click",
|
|
95
|
+
"keydown",
|
|
96
|
+
"keyup",
|
|
97
|
+
"pointerdown",
|
|
98
|
+
"pointerup",
|
|
99
|
+
"touchstart",
|
|
100
|
+
"touchend"
|
|
101
|
+
];
|
|
102
|
+
function createMetadata(waiverReason) {
|
|
103
|
+
return {
|
|
104
|
+
version: 1,
|
|
105
|
+
waiverReason,
|
|
106
|
+
evidence: {
|
|
107
|
+
domInputEvents: 0,
|
|
108
|
+
boundedRuns: 0,
|
|
109
|
+
assertionsAfterOutcome: 0,
|
|
110
|
+
verified: false
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function definePlaythrough(element, run, waiverReason) {
|
|
115
|
+
const reason = normalizePlaythroughWaiverReason(waiverReason);
|
|
116
|
+
const metadata = createMetadata(reason);
|
|
117
|
+
test("production game completes a bounded playthrough", {
|
|
118
|
+
skip: Boolean(reason),
|
|
119
|
+
meta: { reactPlaythrough: metadata }
|
|
120
|
+
}, async () => {
|
|
121
|
+
const evidence = metadata.evidence;
|
|
122
|
+
let assertionsAtOutcome;
|
|
123
|
+
const recordInput = () => {
|
|
124
|
+
evidence.domInputEvents += 1;
|
|
125
|
+
};
|
|
126
|
+
for (const event of INPUT_EVENTS) {
|
|
127
|
+
document.addEventListener(event, recordInput, true);
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const view = render(element);
|
|
131
|
+
if (view.container.childNodes.length === 0) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
"playthroughTest must render the production game entry."
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
await run({
|
|
137
|
+
view,
|
|
138
|
+
async stepUntil(condition, options = {}) {
|
|
139
|
+
const steps = await runBoundedUntil(condition, options);
|
|
140
|
+
if (evidence.domInputEvents === 0) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
"stepUntil reached an outcome before any production DOM input."
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
evidence.boundedRuns += 1;
|
|
146
|
+
assertionsAtOutcome = expect.getState().assertionCalls;
|
|
147
|
+
return steps;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
const assertionCalls = expect.getState().assertionCalls;
|
|
151
|
+
evidence.assertionsAfterOutcome = assertionsAtOutcome === void 0 ? 0 : assertionCalls - assertionsAtOutcome;
|
|
152
|
+
if (evidence.domInputEvents === 0) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
"playthroughTest did not observe production DOM input."
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (evidence.boundedRuns === 0) {
|
|
158
|
+
throw new Error("playthroughTest must complete one bounded stepUntil.");
|
|
159
|
+
}
|
|
160
|
+
if (evidence.assertionsAfterOutcome === 0) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
"Assert an authoritative game outcome after stepUntil returns."
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
evidence.verified = true;
|
|
166
|
+
} finally {
|
|
167
|
+
for (const event of INPUT_EVENTS) {
|
|
168
|
+
document.removeEventListener(event, recordInput, true);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
var playthroughTest = Object.assign(
|
|
174
|
+
(element, run) => definePlaythrough(element, run),
|
|
175
|
+
{
|
|
176
|
+
skip: (reason, element, run) => definePlaythrough(element, run, reason)
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
function auditReactPlaythroughRun(tests) {
|
|
180
|
+
const declared = tests.filter((candidate) => candidate.metadata);
|
|
181
|
+
const valid = declared.filter(({ state, metadata }) => {
|
|
182
|
+
const evidence = metadata?.evidence;
|
|
183
|
+
return state === "passed" && evidence?.verified === true && evidence.domInputEvents > 0 && evidence.boundedRuns > 0 && evidence.assertionsAfterOutcome > 0;
|
|
184
|
+
});
|
|
185
|
+
const waivers = declared.filter(
|
|
186
|
+
({ state, metadata }) => state === "skipped" && (metadata?.waiverReason?.trim().length ?? 0) >= 20
|
|
187
|
+
);
|
|
188
|
+
const issues = [];
|
|
189
|
+
if (declared.length === 0) {
|
|
190
|
+
issues.push(
|
|
191
|
+
"\u7F3A\u5C11\u751F\u4EA7\u6E38\u620F\u53EF\u73A9\u6027\u9A8C\u8BC1\uFF1A\u4F7F\u7528 playthroughTest \u6E32\u67D3 <App />\uFF0C\u901A\u8FC7\u771F\u5B9E DOM \u8F93\u5165\u5B8C\u6210\u4E00\u4E2A\u6709\u6B65\u6570\u4E0A\u9650\u7684\u5173\u952E\u6D41\u7A0B\u3002"
|
|
192
|
+
);
|
|
193
|
+
} else {
|
|
194
|
+
for (const candidate of declared) {
|
|
195
|
+
const isValid = valid.includes(candidate);
|
|
196
|
+
const isWaived = waivers.includes(candidate);
|
|
197
|
+
if (isValid || isWaived) continue;
|
|
198
|
+
if (candidate.state === "skipped") {
|
|
199
|
+
issues.push(
|
|
200
|
+
`\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`
|
|
201
|
+
);
|
|
202
|
+
} else if (candidate.state !== "passed") {
|
|
203
|
+
issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7684\u72B6\u6001\u4E3A ${candidate.state}\u3002`);
|
|
204
|
+
} else {
|
|
205
|
+
issues.push(
|
|
206
|
+
`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u6CA1\u6709\u7559\u4E0B\u5B8C\u6574\u7684\u8F93\u5165\u3001\u6709\u9650\u63A8\u8FDB\u548C\u7ED3\u679C\u65AD\u8A00\u8BC1\u636E\u3002`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (issues.length > 0) return { passed: false, waived: false, issues };
|
|
212
|
+
if (valid.length > 0) return { passed: true, waived: false, issues: [] };
|
|
213
|
+
if (waivers.length > 0) return { passed: true, waived: true, issues: [] };
|
|
214
|
+
return { passed: false, waived: false, issues };
|
|
215
|
+
}
|
|
55
216
|
export {
|
|
56
|
-
ManualGameClock
|
|
217
|
+
ManualGameClock,
|
|
218
|
+
auditReactPlaythroughRun,
|
|
219
|
+
playthroughTest
|
|
57
220
|
};
|
|
@@ -23,9 +23,327 @@ __export(react_vitest_config_exports, {
|
|
|
23
23
|
defineReactGameVitestConfig: () => defineReactGameVitestConfig
|
|
24
24
|
});
|
|
25
25
|
module.exports = __toCommonJS(react_vitest_config_exports);
|
|
26
|
+
var import_node_fs2 = require("fs");
|
|
27
|
+
var import_node_path2 = require("path");
|
|
28
|
+
var import_config = require("vitest/config");
|
|
29
|
+
|
|
30
|
+
// src/react/react-playthrough-reporter.ts
|
|
26
31
|
var import_node_fs = require("fs");
|
|
27
32
|
var import_node_path = require("path");
|
|
28
|
-
|
|
33
|
+
|
|
34
|
+
// src/react/react-playthrough.ts
|
|
35
|
+
var import_react2 = require("@testing-library/react");
|
|
36
|
+
var import_vitest = require("vitest");
|
|
37
|
+
|
|
38
|
+
// src/react/react-playthrough-core.ts
|
|
39
|
+
var import_react = require("@testing-library/react");
|
|
40
|
+
function normalizePlaythroughWaiverReason(waiverReason) {
|
|
41
|
+
if (waiverReason === void 0) return void 0;
|
|
42
|
+
const reason = waiverReason.trim();
|
|
43
|
+
if (reason.length < 20) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
"playthroughTest.skip reason must contain at least 20 characters."
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return reason;
|
|
49
|
+
}
|
|
50
|
+
async function runBoundedUntil(condition, options = {}) {
|
|
51
|
+
const maxSteps = options.maxSteps ?? 120;
|
|
52
|
+
if (!Number.isSafeInteger(maxSteps) || maxSteps < 0 || maxSteps > 1e4) {
|
|
53
|
+
throw new RangeError(
|
|
54
|
+
"stepUntil maxSteps must be a safe integer between 0 and 10000."
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
for (let step = 0; step <= maxSteps; step += 1) {
|
|
58
|
+
if (condition()) return step;
|
|
59
|
+
if (step < maxSteps) {
|
|
60
|
+
await (0, import_react.act)(async () => {
|
|
61
|
+
await options.step?.(step + 1);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Playthrough outcome was not reached within ${maxSteps} steps.`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/react/react-playthrough.ts
|
|
71
|
+
var INPUT_EVENTS = [
|
|
72
|
+
"click",
|
|
73
|
+
"keydown",
|
|
74
|
+
"keyup",
|
|
75
|
+
"pointerdown",
|
|
76
|
+
"pointerup",
|
|
77
|
+
"touchstart",
|
|
78
|
+
"touchend"
|
|
79
|
+
];
|
|
80
|
+
function createMetadata(waiverReason) {
|
|
81
|
+
return {
|
|
82
|
+
version: 1,
|
|
83
|
+
waiverReason,
|
|
84
|
+
evidence: {
|
|
85
|
+
domInputEvents: 0,
|
|
86
|
+
boundedRuns: 0,
|
|
87
|
+
assertionsAfterOutcome: 0,
|
|
88
|
+
verified: false
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function definePlaythrough(element, run, waiverReason) {
|
|
93
|
+
const reason = normalizePlaythroughWaiverReason(waiverReason);
|
|
94
|
+
const metadata = createMetadata(reason);
|
|
95
|
+
(0, import_vitest.test)("production game completes a bounded playthrough", {
|
|
96
|
+
skip: Boolean(reason),
|
|
97
|
+
meta: { reactPlaythrough: metadata }
|
|
98
|
+
}, async () => {
|
|
99
|
+
const evidence = metadata.evidence;
|
|
100
|
+
let assertionsAtOutcome;
|
|
101
|
+
const recordInput = () => {
|
|
102
|
+
evidence.domInputEvents += 1;
|
|
103
|
+
};
|
|
104
|
+
for (const event of INPUT_EVENTS) {
|
|
105
|
+
document.addEventListener(event, recordInput, true);
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const view = (0, import_react2.render)(element);
|
|
109
|
+
if (view.container.childNodes.length === 0) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
"playthroughTest must render the production game entry."
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
await run({
|
|
115
|
+
view,
|
|
116
|
+
async stepUntil(condition, options = {}) {
|
|
117
|
+
const steps = await runBoundedUntil(condition, options);
|
|
118
|
+
if (evidence.domInputEvents === 0) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
"stepUntil reached an outcome before any production DOM input."
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
evidence.boundedRuns += 1;
|
|
124
|
+
assertionsAtOutcome = import_vitest.expect.getState().assertionCalls;
|
|
125
|
+
return steps;
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
const assertionCalls = import_vitest.expect.getState().assertionCalls;
|
|
129
|
+
evidence.assertionsAfterOutcome = assertionsAtOutcome === void 0 ? 0 : assertionCalls - assertionsAtOutcome;
|
|
130
|
+
if (evidence.domInputEvents === 0) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
"playthroughTest did not observe production DOM input."
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
if (evidence.boundedRuns === 0) {
|
|
136
|
+
throw new Error("playthroughTest must complete one bounded stepUntil.");
|
|
137
|
+
}
|
|
138
|
+
if (evidence.assertionsAfterOutcome === 0) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
"Assert an authoritative game outcome after stepUntil returns."
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
evidence.verified = true;
|
|
144
|
+
} finally {
|
|
145
|
+
for (const event of INPUT_EVENTS) {
|
|
146
|
+
document.removeEventListener(event, recordInput, true);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
var playthroughTest = Object.assign(
|
|
152
|
+
(element, run) => definePlaythrough(element, run),
|
|
153
|
+
{
|
|
154
|
+
skip: (reason, element, run) => definePlaythrough(element, run, reason)
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
function auditReactPlaythroughRun(tests) {
|
|
158
|
+
const declared = tests.filter((candidate) => candidate.metadata);
|
|
159
|
+
const valid = declared.filter(({ state, metadata }) => {
|
|
160
|
+
const evidence = metadata?.evidence;
|
|
161
|
+
return state === "passed" && evidence?.verified === true && evidence.domInputEvents > 0 && evidence.boundedRuns > 0 && evidence.assertionsAfterOutcome > 0;
|
|
162
|
+
});
|
|
163
|
+
const waivers = declared.filter(
|
|
164
|
+
({ state, metadata }) => state === "skipped" && (metadata?.waiverReason?.trim().length ?? 0) >= 20
|
|
165
|
+
);
|
|
166
|
+
const issues = [];
|
|
167
|
+
if (declared.length === 0) {
|
|
168
|
+
issues.push(
|
|
169
|
+
"\u7F3A\u5C11\u751F\u4EA7\u6E38\u620F\u53EF\u73A9\u6027\u9A8C\u8BC1\uFF1A\u4F7F\u7528 playthroughTest \u6E32\u67D3 <App />\uFF0C\u901A\u8FC7\u771F\u5B9E DOM \u8F93\u5165\u5B8C\u6210\u4E00\u4E2A\u6709\u6B65\u6570\u4E0A\u9650\u7684\u5173\u952E\u6D41\u7A0B\u3002"
|
|
170
|
+
);
|
|
171
|
+
} else {
|
|
172
|
+
for (const candidate of declared) {
|
|
173
|
+
const isValid = valid.includes(candidate);
|
|
174
|
+
const isWaived = waivers.includes(candidate);
|
|
175
|
+
if (isValid || isWaived) continue;
|
|
176
|
+
if (candidate.state === "skipped") {
|
|
177
|
+
issues.push(
|
|
178
|
+
`\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`
|
|
179
|
+
);
|
|
180
|
+
} else if (candidate.state !== "passed") {
|
|
181
|
+
issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7684\u72B6\u6001\u4E3A ${candidate.state}\u3002`);
|
|
182
|
+
} else {
|
|
183
|
+
issues.push(
|
|
184
|
+
`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u6CA1\u6709\u7559\u4E0B\u5B8C\u6574\u7684\u8F93\u5165\u3001\u6709\u9650\u63A8\u8FDB\u548C\u7ED3\u679C\u65AD\u8A00\u8BC1\u636E\u3002`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (issues.length > 0) return { passed: false, waived: false, issues };
|
|
190
|
+
if (valid.length > 0) return { passed: true, waived: false, issues: [] };
|
|
191
|
+
if (waivers.length > 0) return { passed: true, waived: true, issues: [] };
|
|
192
|
+
return { passed: false, waived: false, issues };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/react/react-playthrough-reporter.ts
|
|
196
|
+
var PRODUCTION_PLAYTHROUGH_FILE = "tests/production-playthrough.test.tsx";
|
|
197
|
+
function isMetadata(value) {
|
|
198
|
+
if (!value || typeof value !== "object") return false;
|
|
199
|
+
const metadata = value;
|
|
200
|
+
return metadata.version === 1 && Boolean(metadata.evidence);
|
|
201
|
+
}
|
|
202
|
+
function toAuditInput(test2) {
|
|
203
|
+
const metadata = test2.meta().reactPlaythrough;
|
|
204
|
+
return {
|
|
205
|
+
name: test2.fullName,
|
|
206
|
+
state: test2.result().state,
|
|
207
|
+
metadata: isMetadata(metadata) ? metadata : void 0
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function firstLine(value) {
|
|
211
|
+
if (typeof value !== "string") return void 0;
|
|
212
|
+
return value.split("\n").map((line) => line.trim()).find(Boolean);
|
|
213
|
+
}
|
|
214
|
+
function toModuleResult(module2, projectRoot) {
|
|
215
|
+
const tests = [...module2.children.allTests()];
|
|
216
|
+
const errors = [
|
|
217
|
+
...module2.errors().map((error) => firstLine(error.message)),
|
|
218
|
+
...tests.flatMap(
|
|
219
|
+
(test2) => (test2.result().errors ?? []).map((error) => firstLine(error.message))
|
|
220
|
+
)
|
|
221
|
+
].filter((message) => Boolean(message));
|
|
222
|
+
return {
|
|
223
|
+
file: (0, import_node_path.relative)(projectRoot, module2.moduleId).replaceAll("\\", "/"),
|
|
224
|
+
state: module2.state(),
|
|
225
|
+
errors,
|
|
226
|
+
tests: tests.map(toAuditInput)
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function assessReactPlaythroughReport(input) {
|
|
230
|
+
const base = { file: input.expectedFile };
|
|
231
|
+
if (!input.expectedFileScheduled) {
|
|
232
|
+
if (input.expectedFileExists) {
|
|
233
|
+
return {
|
|
234
|
+
...base,
|
|
235
|
+
status: "NOT_CHECKED",
|
|
236
|
+
next: `\u672C\u6B21\u662F\u805A\u7126\u8FD0\u884C\uFF0C\u672A\u68C0\u67E5\u6700\u4F4E\u53EF\u73A9\u6D41\u7A0B\uFF1B\u63D0\u4EA4\u524D\u8FD0\u884C pnpm test\uFF0C\u786E\u4FDD ${input.expectedFile} \u901A\u8FC7\u3002`,
|
|
237
|
+
failsRun: false
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
...base,
|
|
242
|
+
status: "FAILED",
|
|
243
|
+
cause: "\u751F\u4EA7\u53EF\u73A9\u6027\u6D4B\u8BD5\u6587\u4EF6\u4E0D\u5B58\u5728\u3002",
|
|
244
|
+
next: "\u521B\u5EFA\u8BE5\u6587\u4EF6\uFF1A\u4ECE <App /> \u89E6\u53D1\u771F\u5B9E DOM \u8F93\u5165\uFF0C\u7528 stepUntil \u6709\u754C\u63A8\u8FDB\u5230\u73A9\u5BB6\u53EF\u89C1\u7ED3\u679C\u6216\u6E38\u620F\u771F\u5B9E\u72B6\u6001\u7684\u53EA\u8BFB snapshot\uFF0C\u518D\u65AD\u8A00\u7ED3\u679C\u3002",
|
|
245
|
+
failsRun: true
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const productionModule = input.modules.find(
|
|
249
|
+
(module2) => module2.file === input.expectedFile
|
|
250
|
+
);
|
|
251
|
+
const executedProductionFlow = productionModule?.tests.some(
|
|
252
|
+
(test2) => test2.state !== "skipped" && test2.state !== "pending"
|
|
253
|
+
);
|
|
254
|
+
if (input.focusedSelection && !executedProductionFlow) {
|
|
255
|
+
return {
|
|
256
|
+
...base,
|
|
257
|
+
status: "NOT_CHECKED",
|
|
258
|
+
next: `\u672C\u6B21\u540D\u79F0\u6216\u884C\u53F7\u8FC7\u6EE4\u6CA1\u6709\u6267\u884C\u6700\u4F4E\u53EF\u73A9\u6D41\u7A0B\uFF1B\u63D0\u4EA4\u524D\u8FD0\u884C pnpm test\uFF0C\u786E\u4FDD ${input.expectedFile} \u901A\u8FC7\u3002`,
|
|
259
|
+
failsRun: false
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
if (!productionModule || productionModule.tests.length === 0) {
|
|
263
|
+
return {
|
|
264
|
+
...base,
|
|
265
|
+
status: "NOT_RUN",
|
|
266
|
+
cause: productionModule?.errors[0] ?? input.unhandledErrors?.[0] ?? "\u751F\u4EA7\u53EF\u73A9\u6027\u6D4B\u8BD5\u672A\u5B8C\u6210\u6536\u96C6\u6216\u6267\u884C\u3002",
|
|
267
|
+
next: "\u5148\u4FEE\u590D Vitest \u4E0A\u65B9\u9996\u4E2A\u8BED\u6CD5\u3001\u5BFC\u5165\u6216\u6536\u96C6\u9519\u8BEF\uFF0C\u518D\u8FD0\u884C pnpm test\uFF1B\u4E0D\u8981\u7528 skip \u63A9\u76D6\u52A0\u8F7D\u5931\u8D25\u3002",
|
|
268
|
+
failsRun: true
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const tests = input.modules.flatMap((module2) => module2.tests);
|
|
272
|
+
const audit = auditReactPlaythroughRun(tests);
|
|
273
|
+
if (!audit.passed) {
|
|
274
|
+
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
|
+
const timedOut = /outcome was not reached within \d+ steps/i.test(cause);
|
|
276
|
+
return {
|
|
277
|
+
...base,
|
|
278
|
+
status: "FAILED",
|
|
279
|
+
cause,
|
|
280
|
+
next: timedOut ? "\u771F\u5B9E\u8F93\u5165\u5DF2\u6267\u884C\uFF0C\u4F46\u73A9\u6CD5\u6CA1\u6709\u5728\u4E0A\u9650\u5185\u4EA7\u751F\u7ED3\u679C\uFF1B\u68C0\u67E5\u70B9\u51FB\u6216\u6309\u952E\u662F\u5426\u771F\u7684\u8C03\u7528\u6E38\u620F\u7684\u751F\u4EA7\u63A7\u5236\u65B9\u6CD5\uFF0C\u4EE5\u53CA\u6E38\u620F\u771F\u5B9E\u72B6\u6001\u662F\u5426\u5230\u8FBE\u5E76\u663E\u793A\u7ED3\u679C\u3002" : "\u4ECE\u751F\u4EA7\u5165\u53E3\u6267\u884C\u771F\u5B9E DOM \u8F93\u5165\uFF0C\u7528 stepUntil \u6709\u754C\u63A8\u8FDB\u5230\u73A9\u5BB6\u53EF\u89C1\u7ED3\u679C\u6216\u6E38\u620F\u771F\u5B9E\u72B6\u6001\u7684\u53EA\u8BFB snapshot\uFF0C\u5E76\u5728\u5176\u540E\u65AD\u8A00\uFF1B\u4E0D\u8981\u76F4\u8FBE\u5185\u90E8\u5173\u5361\u6216\u4FEE\u6539\u73A9\u6CD5\u72B6\u6001\u3002",
|
|
281
|
+
failsRun: true
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
if (audit.waived) {
|
|
285
|
+
return {
|
|
286
|
+
...base,
|
|
287
|
+
status: "WAIVED",
|
|
288
|
+
waiverReasons: tests.map((test2) => test2.metadata?.waiverReason).filter((reason) => Boolean(reason)),
|
|
289
|
+
failsRun: false
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
return { ...base, status: "PASS", failsRun: false };
|
|
293
|
+
}
|
|
294
|
+
function formatReactPlaythroughReport(report) {
|
|
295
|
+
const lines = [`REACT_PLAYTHROUGH: ${report.status}`, `FILE: ${report.file}`];
|
|
296
|
+
if (report.cause) lines.push(`CAUSE: ${report.cause}`);
|
|
297
|
+
if (report.waiverReasons?.length) {
|
|
298
|
+
lines.push(`REASON: ${report.waiverReasons.join("\uFF1B")}`);
|
|
299
|
+
}
|
|
300
|
+
if (report.next) lines.push(`NEXT: ${report.next}`);
|
|
301
|
+
return `
|
|
302
|
+
${lines.join("\n")}`;
|
|
303
|
+
}
|
|
304
|
+
var ReactPlaythroughReporter = class {
|
|
305
|
+
/** 绑定模板根目录,以稳定识别生产可玩性测试而不依赖测试名称。 */
|
|
306
|
+
constructor(projectRoot) {
|
|
307
|
+
this.projectRoot = projectRoot;
|
|
308
|
+
this.expectedModuleId = (0, import_node_path.resolve)(projectRoot, this.expectedFile);
|
|
309
|
+
}
|
|
310
|
+
projectRoot;
|
|
311
|
+
expectedFile = PRODUCTION_PLAYTHROUGH_FILE;
|
|
312
|
+
expectedModuleId;
|
|
313
|
+
expectedFileScheduled = false;
|
|
314
|
+
focusedSelection = false;
|
|
315
|
+
/** 记录本轮是否实际选择了生产流程文件,用于区分聚焦运行与门禁失败。 */
|
|
316
|
+
onTestRunStart(specifications) {
|
|
317
|
+
this.expectedFileScheduled = specifications.some(
|
|
318
|
+
(specification) => (0, import_node_path.resolve)(specification.moduleId) === this.expectedModuleId
|
|
319
|
+
);
|
|
320
|
+
this.focusedSelection = specifications.some(
|
|
321
|
+
(specification) => Boolean(specification.project.globalConfig.testNamePattern) || Boolean(specification.testNamePattern) || Boolean(specification.testLines?.length)
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
/** 测试运行结束后执行项目级主流程门禁,并写入最终退出码。 */
|
|
325
|
+
onTestRunEnd(testModules, unhandledErrors) {
|
|
326
|
+
const report = assessReactPlaythroughReport({
|
|
327
|
+
expectedFile: this.expectedFile,
|
|
328
|
+
expectedFileExists: (0, import_node_fs.existsSync)(this.expectedModuleId),
|
|
329
|
+
expectedFileScheduled: this.expectedFileScheduled,
|
|
330
|
+
focusedSelection: this.focusedSelection,
|
|
331
|
+
modules: testModules.map(
|
|
332
|
+
(module2) => toModuleResult(module2, this.projectRoot)
|
|
333
|
+
),
|
|
334
|
+
unhandledErrors: unhandledErrors.map((error) => firstLine(error.message)).filter((message) => Boolean(message))
|
|
335
|
+
});
|
|
336
|
+
const output = formatReactPlaythroughReport(report);
|
|
337
|
+
if (report.failsRun) {
|
|
338
|
+
console.error(output);
|
|
339
|
+
process.exitCode = 1;
|
|
340
|
+
} else if (report.status === "WAIVED" || report.status === "NOT_CHECKED") {
|
|
341
|
+
console.warn(output);
|
|
342
|
+
} else {
|
|
343
|
+
console.log(output);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
};
|
|
29
347
|
|
|
30
348
|
// src/testing/vitest-node-args.ts
|
|
31
349
|
function getJSDOMWorkerExecArgv() {
|
|
@@ -35,16 +353,16 @@ function getJSDOMWorkerExecArgv() {
|
|
|
35
353
|
|
|
36
354
|
// src/react-vitest-config.ts
|
|
37
355
|
function resolvePhaser3BrowserEntry(projectRoot) {
|
|
38
|
-
const manifestPath = (0,
|
|
39
|
-
if (!(0,
|
|
356
|
+
const manifestPath = (0, import_node_path2.resolve)(projectRoot, "node_modules/phaser/package.json");
|
|
357
|
+
if (!(0, import_node_fs2.existsSync)(manifestPath)) return void 0;
|
|
40
358
|
try {
|
|
41
|
-
const manifest = JSON.parse((0,
|
|
359
|
+
const manifest = JSON.parse((0, import_node_fs2.readFileSync)(manifestPath, "utf8"));
|
|
42
360
|
if (!manifest.version?.startsWith("3.")) return void 0;
|
|
43
|
-
const browserEntry = (0,
|
|
44
|
-
(0,
|
|
361
|
+
const browserEntry = (0, import_node_path2.resolve)(
|
|
362
|
+
(0, import_node_path2.dirname)(manifestPath),
|
|
45
363
|
manifest.browser ?? "dist/phaser.js"
|
|
46
364
|
);
|
|
47
|
-
return (0,
|
|
365
|
+
return (0, import_node_fs2.existsSync)(browserEntry) ? browserEntry : void 0;
|
|
48
366
|
} catch {
|
|
49
367
|
return void 0;
|
|
50
368
|
}
|
|
@@ -56,7 +374,7 @@ function defineReactGameVitestConfig(options) {
|
|
|
56
374
|
alias: {
|
|
57
375
|
...phaser3BrowserEntry ? { phaser: phaser3BrowserEntry } : {},
|
|
58
376
|
...options.aliases,
|
|
59
|
-
"@": (0,
|
|
377
|
+
"@": (0, import_node_path2.resolve)(options.projectRoot, "src")
|
|
60
378
|
}
|
|
61
379
|
},
|
|
62
380
|
test: {
|
|
@@ -79,7 +397,8 @@ function defineReactGameVitestConfig(options) {
|
|
|
79
397
|
sequence: {
|
|
80
398
|
setupFiles: "list"
|
|
81
399
|
},
|
|
82
|
-
|
|
400
|
+
// minimal 保留业务失败;附加 reporter 负责项目级最低可玩性门禁。
|
|
401
|
+
reporters: ["minimal", new ReactPlaythroughReporter(options.projectRoot)],
|
|
83
402
|
restoreMocks: true,
|
|
84
403
|
clearMocks: true,
|
|
85
404
|
testTimeout: options.testTimeout,
|