miaoda-game-devkit 0.6.1 → 0.6.3
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/dist/cli/phaser-lint.js +187 -19
- package/dist/cli/react-lint.js +187 -19
- package/dist/react/index.js +11 -1
- package/dist/react/index.mjs +11 -1
- package/dist/react/testing.d.mts +19 -2
- package/dist/react/testing.d.ts +19 -2
- package/dist/react/testing.js +177 -31
- package/dist/react/testing.mjs +177 -31
- package/dist/react/vitest-config.js +342 -65
- package/dist/react/vitest-config.mjs +342 -65
- package/dist/react/vitest-setup.js +87 -4
- package/dist/react/vitest-setup.mjs +87 -4
- package/dist/rules/react-test-boundary-plugin.js +14 -1
- package/package.json +1 -1
|
@@ -5,6 +5,87 @@ var import_vitest = require("@testing-library/jest-dom/vitest");
|
|
|
5
5
|
var import_react = require("@testing-library/react");
|
|
6
6
|
var import_vitest2 = require("vitest");
|
|
7
7
|
|
|
8
|
+
// src/react/react-error-diagnostics.ts
|
|
9
|
+
var MAX_DIAGNOSTIC_LENGTH = 1e3;
|
|
10
|
+
function truncate(value) {
|
|
11
|
+
const trimmed = value.trim();
|
|
12
|
+
if (trimmed.length <= MAX_DIAGNOSTIC_LENGTH) return trimmed;
|
|
13
|
+
return `${trimmed.slice(0, MAX_DIAGNOSTIC_LENGTH - 1)}\u2026`;
|
|
14
|
+
}
|
|
15
|
+
function safeJson(value) {
|
|
16
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
17
|
+
try {
|
|
18
|
+
return JSON.stringify(value, (_key, nested) => {
|
|
19
|
+
if (typeof nested === "bigint") return `${nested}n`;
|
|
20
|
+
if (typeof nested === "function") {
|
|
21
|
+
return `Function<${nested.name || "anonymous"}>`;
|
|
22
|
+
}
|
|
23
|
+
if (typeof nested === "symbol") return nested.toString();
|
|
24
|
+
if (nested && typeof nested === "object") {
|
|
25
|
+
if (seen.has(nested)) return "[Circular]";
|
|
26
|
+
seen.add(nested);
|
|
27
|
+
}
|
|
28
|
+
return nested;
|
|
29
|
+
});
|
|
30
|
+
} catch {
|
|
31
|
+
return void 0;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function collectEntries(value, fallbackCode, seen) {
|
|
35
|
+
if (typeof value === "string") {
|
|
36
|
+
return value.trim() ? [{ code: fallbackCode, message: truncate(value) }] : [];
|
|
37
|
+
}
|
|
38
|
+
if (value === null || value === void 0 || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "symbol") {
|
|
39
|
+
return [{ code: fallbackCode, message: String(value) }];
|
|
40
|
+
}
|
|
41
|
+
if (typeof value === "function") {
|
|
42
|
+
return [
|
|
43
|
+
{ code: fallbackCode, message: `Function<${value.name || "anonymous"}>` }
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
if (seen.has(value)) return [];
|
|
47
|
+
seen.add(value);
|
|
48
|
+
if (Array.isArray(value)) {
|
|
49
|
+
return value.flatMap((item) => collectEntries(item, fallbackCode, seen));
|
|
50
|
+
}
|
|
51
|
+
const record = value;
|
|
52
|
+
const code = typeof record.code === "string" && record.code.trim() ? record.code.trim() : fallbackCode;
|
|
53
|
+
const entries = [];
|
|
54
|
+
if (typeof record.message === "string" && record.message.trim()) {
|
|
55
|
+
entries.push({ code, message: truncate(record.message) });
|
|
56
|
+
}
|
|
57
|
+
if (record.cause !== void 0) {
|
|
58
|
+
entries.push(...collectEntries(record.cause, fallbackCode, seen));
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(record.errors)) {
|
|
61
|
+
entries.push(...collectEntries(record.errors, fallbackCode, seen));
|
|
62
|
+
}
|
|
63
|
+
if (entries.length > 0) return entries;
|
|
64
|
+
if (typeof record.stack === "string" && record.stack.trim()) {
|
|
65
|
+
return [{ code, message: truncate(record.stack) }];
|
|
66
|
+
}
|
|
67
|
+
const json = safeJson(value);
|
|
68
|
+
return json && json !== "{}" ? [{ code, message: truncate(json) }] : [];
|
|
69
|
+
}
|
|
70
|
+
function extractFailureEntries(value, fallbackCode = "TEST_FAILURE") {
|
|
71
|
+
const entries = collectEntries(value, fallbackCode, /* @__PURE__ */ new WeakSet());
|
|
72
|
+
const keys = /* @__PURE__ */ new Set();
|
|
73
|
+
return entries.filter((entry) => {
|
|
74
|
+
const key = `${entry.code}\0${entry.message}`;
|
|
75
|
+
if (keys.has(key)) return false;
|
|
76
|
+
keys.add(key);
|
|
77
|
+
return true;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function runtimeFailureError(code, value) {
|
|
81
|
+
const entries = extractFailureEntries(value, code);
|
|
82
|
+
const error = new Error(
|
|
83
|
+
entries.map((entry) => entry.message).join(" | ") || "No readable runtime error value"
|
|
84
|
+
);
|
|
85
|
+
error.code = code;
|
|
86
|
+
return error;
|
|
87
|
+
}
|
|
88
|
+
|
|
8
89
|
// src/testing/jsdom-canvas.ts
|
|
9
90
|
var contexts = /* @__PURE__ */ new WeakMap();
|
|
10
91
|
function createCanvasContext(canvas) {
|
|
@@ -73,21 +154,23 @@ function clearJSDOMStorage() {
|
|
|
73
154
|
installJSDOMCanvasContext();
|
|
74
155
|
var failures = [];
|
|
75
156
|
var consoleError;
|
|
76
|
-
var onError = (event) => failures.push(
|
|
77
|
-
|
|
157
|
+
var onError = (event) => failures.push(
|
|
158
|
+
runtimeFailureError("WINDOW_ERROR", event.error ?? event.message)
|
|
159
|
+
);
|
|
160
|
+
var onRejection = (event) => failures.push(runtimeFailureError("UNHANDLED_REJECTION", event.reason));
|
|
78
161
|
(0, import_vitest2.beforeEach)(() => {
|
|
79
162
|
failures = [];
|
|
80
163
|
window.addEventListener("error", onError);
|
|
81
164
|
window.addEventListener("unhandledrejection", onRejection);
|
|
82
165
|
consoleError = import_vitest2.vi.spyOn(console, "error").mockImplementation((...args) => {
|
|
83
|
-
failures.push(args);
|
|
166
|
+
failures.push(runtimeFailureError("CONSOLE_ERROR", args));
|
|
84
167
|
});
|
|
85
168
|
});
|
|
86
169
|
(0, import_vitest2.afterEach)(() => {
|
|
87
170
|
try {
|
|
88
171
|
(0, import_react.cleanup)();
|
|
89
172
|
} catch (error) {
|
|
90
|
-
failures.push(error);
|
|
173
|
+
failures.push(runtimeFailureError("CLEANUP_ERROR", error));
|
|
91
174
|
} finally {
|
|
92
175
|
clearJSDOMStorage();
|
|
93
176
|
window.removeEventListener("error", onError);
|
|
@@ -3,6 +3,87 @@ import "@testing-library/jest-dom/vitest";
|
|
|
3
3
|
import { cleanup } from "@testing-library/react";
|
|
4
4
|
import { afterEach, beforeEach, vi } from "vitest";
|
|
5
5
|
|
|
6
|
+
// src/react/react-error-diagnostics.ts
|
|
7
|
+
var MAX_DIAGNOSTIC_LENGTH = 1e3;
|
|
8
|
+
function truncate(value) {
|
|
9
|
+
const trimmed = value.trim();
|
|
10
|
+
if (trimmed.length <= MAX_DIAGNOSTIC_LENGTH) return trimmed;
|
|
11
|
+
return `${trimmed.slice(0, MAX_DIAGNOSTIC_LENGTH - 1)}\u2026`;
|
|
12
|
+
}
|
|
13
|
+
function safeJson(value) {
|
|
14
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
15
|
+
try {
|
|
16
|
+
return JSON.stringify(value, (_key, nested) => {
|
|
17
|
+
if (typeof nested === "bigint") return `${nested}n`;
|
|
18
|
+
if (typeof nested === "function") {
|
|
19
|
+
return `Function<${nested.name || "anonymous"}>`;
|
|
20
|
+
}
|
|
21
|
+
if (typeof nested === "symbol") return nested.toString();
|
|
22
|
+
if (nested && typeof nested === "object") {
|
|
23
|
+
if (seen.has(nested)) return "[Circular]";
|
|
24
|
+
seen.add(nested);
|
|
25
|
+
}
|
|
26
|
+
return nested;
|
|
27
|
+
});
|
|
28
|
+
} catch {
|
|
29
|
+
return void 0;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function collectEntries(value, fallbackCode, seen) {
|
|
33
|
+
if (typeof value === "string") {
|
|
34
|
+
return value.trim() ? [{ code: fallbackCode, message: truncate(value) }] : [];
|
|
35
|
+
}
|
|
36
|
+
if (value === null || value === void 0 || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "symbol") {
|
|
37
|
+
return [{ code: fallbackCode, message: String(value) }];
|
|
38
|
+
}
|
|
39
|
+
if (typeof value === "function") {
|
|
40
|
+
return [
|
|
41
|
+
{ code: fallbackCode, message: `Function<${value.name || "anonymous"}>` }
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
if (seen.has(value)) return [];
|
|
45
|
+
seen.add(value);
|
|
46
|
+
if (Array.isArray(value)) {
|
|
47
|
+
return value.flatMap((item) => collectEntries(item, fallbackCode, seen));
|
|
48
|
+
}
|
|
49
|
+
const record = value;
|
|
50
|
+
const code = typeof record.code === "string" && record.code.trim() ? record.code.trim() : fallbackCode;
|
|
51
|
+
const entries = [];
|
|
52
|
+
if (typeof record.message === "string" && record.message.trim()) {
|
|
53
|
+
entries.push({ code, message: truncate(record.message) });
|
|
54
|
+
}
|
|
55
|
+
if (record.cause !== void 0) {
|
|
56
|
+
entries.push(...collectEntries(record.cause, fallbackCode, seen));
|
|
57
|
+
}
|
|
58
|
+
if (Array.isArray(record.errors)) {
|
|
59
|
+
entries.push(...collectEntries(record.errors, fallbackCode, seen));
|
|
60
|
+
}
|
|
61
|
+
if (entries.length > 0) return entries;
|
|
62
|
+
if (typeof record.stack === "string" && record.stack.trim()) {
|
|
63
|
+
return [{ code, message: truncate(record.stack) }];
|
|
64
|
+
}
|
|
65
|
+
const json = safeJson(value);
|
|
66
|
+
return json && json !== "{}" ? [{ code, message: truncate(json) }] : [];
|
|
67
|
+
}
|
|
68
|
+
function extractFailureEntries(value, fallbackCode = "TEST_FAILURE") {
|
|
69
|
+
const entries = collectEntries(value, fallbackCode, /* @__PURE__ */ new WeakSet());
|
|
70
|
+
const keys = /* @__PURE__ */ new Set();
|
|
71
|
+
return entries.filter((entry) => {
|
|
72
|
+
const key = `${entry.code}\0${entry.message}`;
|
|
73
|
+
if (keys.has(key)) return false;
|
|
74
|
+
keys.add(key);
|
|
75
|
+
return true;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function runtimeFailureError(code, value) {
|
|
79
|
+
const entries = extractFailureEntries(value, code);
|
|
80
|
+
const error = new Error(
|
|
81
|
+
entries.map((entry) => entry.message).join(" | ") || "No readable runtime error value"
|
|
82
|
+
);
|
|
83
|
+
error.code = code;
|
|
84
|
+
return error;
|
|
85
|
+
}
|
|
86
|
+
|
|
6
87
|
// src/testing/jsdom-canvas.ts
|
|
7
88
|
var contexts = /* @__PURE__ */ new WeakMap();
|
|
8
89
|
function createCanvasContext(canvas) {
|
|
@@ -71,21 +152,23 @@ function clearJSDOMStorage() {
|
|
|
71
152
|
installJSDOMCanvasContext();
|
|
72
153
|
var failures = [];
|
|
73
154
|
var consoleError;
|
|
74
|
-
var onError = (event) => failures.push(
|
|
75
|
-
|
|
155
|
+
var onError = (event) => failures.push(
|
|
156
|
+
runtimeFailureError("WINDOW_ERROR", event.error ?? event.message)
|
|
157
|
+
);
|
|
158
|
+
var onRejection = (event) => failures.push(runtimeFailureError("UNHANDLED_REJECTION", event.reason));
|
|
76
159
|
beforeEach(() => {
|
|
77
160
|
failures = [];
|
|
78
161
|
window.addEventListener("error", onError);
|
|
79
162
|
window.addEventListener("unhandledrejection", onRejection);
|
|
80
163
|
consoleError = vi.spyOn(console, "error").mockImplementation((...args) => {
|
|
81
|
-
failures.push(args);
|
|
164
|
+
failures.push(runtimeFailureError("CONSOLE_ERROR", args));
|
|
82
165
|
});
|
|
83
166
|
});
|
|
84
167
|
afterEach(() => {
|
|
85
168
|
try {
|
|
86
169
|
cleanup();
|
|
87
170
|
} catch (error) {
|
|
88
|
-
failures.push(error);
|
|
171
|
+
failures.push(runtimeFailureError("CLEANUP_ERROR", error));
|
|
89
172
|
} finally {
|
|
90
173
|
clearJSDOMStorage();
|
|
91
174
|
window.removeEventListener("error", onError);
|
|
@@ -40,6 +40,12 @@ function internalGameImports(node) {
|
|
|
40
40
|
return importKind === "type" ? [] : [specifier.local.name];
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
|
+
function isStepProperty(node) {
|
|
44
|
+
return node.key.type === "Identifier" && node.key.name === "step" || node.key.type === "Literal" && node.key.value === "step";
|
|
45
|
+
}
|
|
46
|
+
function isEmptyFunction(value) {
|
|
47
|
+
return (value.type === "ArrowFunctionExpression" || value.type === "FunctionExpression") && value.body.type === "BlockStatement" && value.body.body.length === 0;
|
|
48
|
+
}
|
|
43
49
|
var rule = {
|
|
44
50
|
meta: {
|
|
45
51
|
type: "problem",
|
|
@@ -50,7 +56,8 @@ var rule = {
|
|
|
50
56
|
boundExpect: "Use the expect provided by playthroughTest. An imported Vitest expect may belong to a different module instance and cannot provide reliable assertion evidence.",
|
|
51
57
|
providedUser: "Use the user provided by playthroughTest; do not import or create another userEvent instance in the production playthrough.",
|
|
52
58
|
productionTestingImport: "Production source must not import miaoda-game-devkit/react/testing. Inject test clocks and observers through the production App factory boundary.",
|
|
53
|
-
productionEntry: "Render <App /> from the production playthrough. Import Controller and Telemetry helpers when needed, but do not render an internal game component directly."
|
|
59
|
+
productionEntry: "Render <App /> from the production playthrough. Import Controller and Telemetry helpers when needed, but do not render an internal game component directly.",
|
|
60
|
+
emptyStep: "A deterministic step must advance production-owned time, frames, or queued work. An empty step is not gameplay evidence. Inject ManualGameClock through <App /> and call clock.stepFrame(...)."
|
|
54
61
|
},
|
|
55
62
|
schema: []
|
|
56
63
|
},
|
|
@@ -85,6 +92,12 @@ var rule = {
|
|
|
85
92
|
if (opening.name?.type === "JSXIdentifier" && opening.name.name && internalGameBindings.has(opening.name.name)) {
|
|
86
93
|
context.report({ node, messageId: "productionEntry" });
|
|
87
94
|
}
|
|
95
|
+
},
|
|
96
|
+
Property(node) {
|
|
97
|
+
if (!isProductionPlaythrough) return;
|
|
98
|
+
if (isStepProperty(node) && isEmptyFunction(node.value)) {
|
|
99
|
+
context.report({ node, messageId: "emptyStep" });
|
|
100
|
+
}
|
|
88
101
|
}
|
|
89
102
|
};
|
|
90
103
|
}
|