miaoda-game-devkit 0.10.0 → 0.10.2
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/lint/setup.mjs +91 -4
- package/dist/react/testing.d.mts +4 -1
- package/dist/react/testing.d.ts +4 -1
- package/dist/react/testing.js +42 -1
- package/dist/react/testing.mjs +42 -1
- package/dist/react/vitest-config.js +76 -10
- package/dist/react/vitest-config.mjs +77 -11
- package/dist/react/vitest-setup.js +91 -4
- package/dist/react/vitest-setup.mjs +91 -4
- package/package.json +1 -1
package/dist/lint/setup.mjs
CHANGED
|
@@ -1,8 +1,92 @@
|
|
|
1
1
|
// src/lint/setup.ts
|
|
2
2
|
import { afterEach } from "vitest";
|
|
3
3
|
|
|
4
|
-
// src/testing/jsdom-
|
|
4
|
+
// src/testing/jsdom-webgl.ts
|
|
5
|
+
var CONSTANT_NAME = /^[A-Z][A-Z0-9_]*$/;
|
|
6
|
+
var TEXTURE_UNIT_LIMIT = 32;
|
|
7
|
+
var TEXTURE_SIZE_LIMIT = 4096;
|
|
8
|
+
var VIEWPORT_LIMIT = 4096;
|
|
9
|
+
function parameterValue(name) {
|
|
10
|
+
if (name === "VERSION") return "WebGL 2.0 (miaoda-game-devkit stub)";
|
|
11
|
+
if (name === "SHADING_LANGUAGE_VERSION") return "WebGL GLSL ES 3.00 (stub)";
|
|
12
|
+
if (name === "VENDOR" || name === "RENDERER") return "miaoda-game-devkit";
|
|
13
|
+
if (name === "MAX_VIEWPORT_DIMS") {
|
|
14
|
+
return new Int32Array([VIEWPORT_LIMIT, VIEWPORT_LIMIT]);
|
|
15
|
+
}
|
|
16
|
+
if (name === "VIEWPORT" || name === "SCISSOR_BOX") {
|
|
17
|
+
return new Int32Array([0, 0, VIEWPORT_LIMIT, VIEWPORT_LIMIT]);
|
|
18
|
+
}
|
|
19
|
+
if (name.includes("MAX_") && name.includes("SIZE")) return TEXTURE_SIZE_LIMIT;
|
|
20
|
+
if (name.startsWith("MAX_")) return TEXTURE_UNIT_LIMIT;
|
|
21
|
+
if (name.endsWith("_BITS")) return 8;
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
function createWebGLContext(canvas, attributes) {
|
|
25
|
+
const constants = /* @__PURE__ */ new Map();
|
|
26
|
+
const constantNames = /* @__PURE__ */ new Map();
|
|
27
|
+
const state = {
|
|
28
|
+
canvas,
|
|
29
|
+
drawingBufferWidth: canvas.width,
|
|
30
|
+
drawingBufferHeight: canvas.height
|
|
31
|
+
};
|
|
32
|
+
const method = (name) => {
|
|
33
|
+
if (name === "getContextAttributes") return () => attributes;
|
|
34
|
+
if (name === "getExtension") return () => null;
|
|
35
|
+
if (name === "getSupportedExtensions") return () => [];
|
|
36
|
+
if (name === "getParameter") {
|
|
37
|
+
return (pname) => parameterValue(constantNames.get(pname) ?? "UNKNOWN");
|
|
38
|
+
}
|
|
39
|
+
if (name === "getShaderPrecisionFormat") {
|
|
40
|
+
return () => ({ rangeMin: 127, rangeMax: 127, precision: 23 });
|
|
41
|
+
}
|
|
42
|
+
if (name === "getShaderInfoLog" || name === "getProgramInfoLog") return () => "";
|
|
43
|
+
if (name === "getError") return () => 0;
|
|
44
|
+
if (name === "isContextLost") return () => false;
|
|
45
|
+
if (name.startsWith("get") && name.endsWith("Parameter")) return () => true;
|
|
46
|
+
if (name.startsWith("get")) return () => null;
|
|
47
|
+
if (name.startsWith("create") || name.startsWith("getUniformLocation")) {
|
|
48
|
+
return () => ({});
|
|
49
|
+
}
|
|
50
|
+
return () => void 0;
|
|
51
|
+
};
|
|
52
|
+
return new Proxy(state, {
|
|
53
|
+
get(target, property) {
|
|
54
|
+
if (property in target) return target[property];
|
|
55
|
+
if (typeof property !== "string") return void 0;
|
|
56
|
+
if (CONSTANT_NAME.test(property)) {
|
|
57
|
+
const existing = constants.get(property);
|
|
58
|
+
if (existing !== void 0) return existing;
|
|
59
|
+
const value = constants.size + 1;
|
|
60
|
+
constants.set(property, value);
|
|
61
|
+
constantNames.set(value, property);
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
const implementation = method(property);
|
|
65
|
+
target[property] = implementation;
|
|
66
|
+
return implementation;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
5
70
|
var contexts = /* @__PURE__ */ new WeakMap();
|
|
71
|
+
function getJSDOMWebGLContext(canvas, options) {
|
|
72
|
+
const existing = contexts.get(canvas);
|
|
73
|
+
if (existing) return existing;
|
|
74
|
+
const attributes = {
|
|
75
|
+
alpha: true,
|
|
76
|
+
antialias: false,
|
|
77
|
+
depth: true,
|
|
78
|
+
premultipliedAlpha: true,
|
|
79
|
+
preserveDrawingBuffer: false,
|
|
80
|
+
stencil: false,
|
|
81
|
+
...typeof options === "object" && options !== null ? options : {}
|
|
82
|
+
};
|
|
83
|
+
const context = createWebGLContext(canvas, attributes);
|
|
84
|
+
contexts.set(canvas, context);
|
|
85
|
+
return context;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/testing/jsdom-canvas.ts
|
|
89
|
+
var contexts2 = /* @__PURE__ */ new WeakMap();
|
|
6
90
|
function createCanvasContext(canvas) {
|
|
7
91
|
const imageData = (width = 1, height = 1) => ({
|
|
8
92
|
data: new Uint8ClampedArray(width * height * 4),
|
|
@@ -49,12 +133,15 @@ function createCanvasContext(canvas) {
|
|
|
49
133
|
return context;
|
|
50
134
|
}
|
|
51
135
|
function installJSDOMCanvasContext() {
|
|
52
|
-
HTMLCanvasElement.prototype.getContext = function getContext(contextId) {
|
|
136
|
+
HTMLCanvasElement.prototype.getContext = function getContext(contextId, options) {
|
|
137
|
+
if (contextId === "webgl" || contextId === "webgl2") {
|
|
138
|
+
return getJSDOMWebGLContext(this, options);
|
|
139
|
+
}
|
|
53
140
|
if (contextId !== "2d") return null;
|
|
54
|
-
const existing =
|
|
141
|
+
const existing = contexts2.get(this);
|
|
55
142
|
if (existing) return existing;
|
|
56
143
|
const context = createCanvasContext(this);
|
|
57
|
-
|
|
144
|
+
contexts2.set(this, context);
|
|
58
145
|
return context;
|
|
59
146
|
};
|
|
60
147
|
}
|
package/dist/react/testing.d.mts
CHANGED
|
@@ -50,7 +50,10 @@ interface StepUntilOptions {
|
|
|
50
50
|
step?: (step: number) => void | Promise<void>;
|
|
51
51
|
/**
|
|
52
52
|
* Optional read-only state used to make bounded failures actionable.
|
|
53
|
-
*
|
|
53
|
+
*
|
|
54
|
+
* The value is sampled when the bound is exhausted, and additionally at a fixed
|
|
55
|
+
* step interval so an exhausted bound can distinguish a run that is still
|
|
56
|
+
* progressing from one whose state already settled.
|
|
54
57
|
*/
|
|
55
58
|
diagnostics?: () => unknown;
|
|
56
59
|
/** 测试超时或运行取消时停止继续推进,通常由 playthroughTest 自动传入。 */
|
package/dist/react/testing.d.ts
CHANGED
|
@@ -50,7 +50,10 @@ interface StepUntilOptions {
|
|
|
50
50
|
step?: (step: number) => void | Promise<void>;
|
|
51
51
|
/**
|
|
52
52
|
* Optional read-only state used to make bounded failures actionable.
|
|
53
|
-
*
|
|
53
|
+
*
|
|
54
|
+
* The value is sampled when the bound is exhausted, and additionally at a fixed
|
|
55
|
+
* step interval so an exhausted bound can distinguish a run that is still
|
|
56
|
+
* progressing from one whose state already settled.
|
|
54
57
|
*/
|
|
55
58
|
diagnostics?: () => unknown;
|
|
56
59
|
/** 测试超时或运行取消时停止继续推进,通常由 playthroughTest 自动传入。 */
|
package/dist/react/testing.js
CHANGED
|
@@ -311,6 +311,39 @@ function formatDiagnostics(read) {
|
|
|
311
311
|
return `diagnostics() threw: ${String(error)}`;
|
|
312
312
|
}
|
|
313
313
|
}
|
|
314
|
+
var STAGNATION_SAMPLE_INTERVAL = 32;
|
|
315
|
+
var STAGNATION_SAMPLE_MINIMUM = 3;
|
|
316
|
+
var StagnationProbe = class {
|
|
317
|
+
constructor(read) {
|
|
318
|
+
this.read = read;
|
|
319
|
+
this.disabled = !read;
|
|
320
|
+
}
|
|
321
|
+
read;
|
|
322
|
+
fingerprint;
|
|
323
|
+
unchangedSamples = 0;
|
|
324
|
+
disabled = false;
|
|
325
|
+
settledAtStep;
|
|
326
|
+
sample(step) {
|
|
327
|
+
if (this.disabled || step % STAGNATION_SAMPLE_INTERVAL !== 0) return;
|
|
328
|
+
let next;
|
|
329
|
+
try {
|
|
330
|
+
next = JSON.stringify(this.read?.()) ?? "undefined";
|
|
331
|
+
} catch {
|
|
332
|
+
this.disabled = true;
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (this.fingerprint === next) {
|
|
336
|
+
this.unchangedSamples += 1;
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
this.fingerprint = next;
|
|
340
|
+
this.unchangedSamples = 1;
|
|
341
|
+
this.settledAtStep = step;
|
|
342
|
+
}
|
|
343
|
+
get settled() {
|
|
344
|
+
return !this.disabled && this.unchangedSamples >= STAGNATION_SAMPLE_MINIMUM;
|
|
345
|
+
}
|
|
346
|
+
};
|
|
314
347
|
function normalizePlaythroughWaiverReason(waiverReason) {
|
|
315
348
|
if (waiverReason === void 0) return void 0;
|
|
316
349
|
const reason = waiverReason.trim();
|
|
@@ -330,6 +363,7 @@ async function runBoundedUntil(condition, options = {}) {
|
|
|
330
363
|
"stepUntil maxSteps must be a safe integer between 0 and 10000."
|
|
331
364
|
);
|
|
332
365
|
}
|
|
366
|
+
const probe = new StagnationProbe(options.diagnostics);
|
|
333
367
|
for (let step = 0; step <= maxSteps; step += 1) {
|
|
334
368
|
throwIfAborted(options.signal);
|
|
335
369
|
if (condition()) return step;
|
|
@@ -338,11 +372,18 @@ async function runBoundedUntil(condition, options = {}) {
|
|
|
338
372
|
await options.step?.(step + 1);
|
|
339
373
|
});
|
|
340
374
|
throwIfAborted(options.signal);
|
|
375
|
+
probe.sample(step + 1);
|
|
341
376
|
}
|
|
342
377
|
}
|
|
343
378
|
const diagnostics = formatDiagnostics(options.diagnostics);
|
|
344
|
-
const guidance = options.step ? "The step callback ran, but the authoritative outcome did not change." : "No step callback was provided, and the condition did not become true after the stage action. This does not identify a clock problem; inspect the condition, production input wiring, and observed state boundary.";
|
|
345
379
|
const suffix = diagnostics ? ` Last diagnostics: ${diagnostics}` : "";
|
|
380
|
+
if (probe.settled) {
|
|
381
|
+
throw codedError(
|
|
382
|
+
"PLAYTHROUGH_STATE_STAGNANT",
|
|
383
|
+
`Playthrough state stopped changing at step ${probe.settledAtStep} of ${maxSteps} and stayed identical until the bound. The run is already settled, so raising maxSteps cannot help: compare the settled state against the outcome this stage asserts. A settled state usually means the game reached a terminal result other than the asserted one, or that the stage never advances the production Controller at all.${suffix}`
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
const guidance = options.step ? "The step callback ran, but the authoritative outcome did not change." : "No step callback was provided, and the condition did not become true after the stage action. This does not identify a clock problem; inspect the condition, production input wiring, and observed state boundary.";
|
|
346
387
|
throw codedError(
|
|
347
388
|
options.step ? "PLAYTHROUGH_BOUND_EXHAUSTED" : "PLAYTHROUGH_OUTCOME_NOT_REACHED",
|
|
348
389
|
`Playthrough outcome was not reached within ${maxSteps} steps. ${guidance}${suffix}`
|
package/dist/react/testing.mjs
CHANGED
|
@@ -273,6 +273,39 @@ function formatDiagnostics(read) {
|
|
|
273
273
|
return `diagnostics() threw: ${String(error)}`;
|
|
274
274
|
}
|
|
275
275
|
}
|
|
276
|
+
var STAGNATION_SAMPLE_INTERVAL = 32;
|
|
277
|
+
var STAGNATION_SAMPLE_MINIMUM = 3;
|
|
278
|
+
var StagnationProbe = class {
|
|
279
|
+
constructor(read) {
|
|
280
|
+
this.read = read;
|
|
281
|
+
this.disabled = !read;
|
|
282
|
+
}
|
|
283
|
+
read;
|
|
284
|
+
fingerprint;
|
|
285
|
+
unchangedSamples = 0;
|
|
286
|
+
disabled = false;
|
|
287
|
+
settledAtStep;
|
|
288
|
+
sample(step) {
|
|
289
|
+
if (this.disabled || step % STAGNATION_SAMPLE_INTERVAL !== 0) return;
|
|
290
|
+
let next;
|
|
291
|
+
try {
|
|
292
|
+
next = JSON.stringify(this.read?.()) ?? "undefined";
|
|
293
|
+
} catch {
|
|
294
|
+
this.disabled = true;
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (this.fingerprint === next) {
|
|
298
|
+
this.unchangedSamples += 1;
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
this.fingerprint = next;
|
|
302
|
+
this.unchangedSamples = 1;
|
|
303
|
+
this.settledAtStep = step;
|
|
304
|
+
}
|
|
305
|
+
get settled() {
|
|
306
|
+
return !this.disabled && this.unchangedSamples >= STAGNATION_SAMPLE_MINIMUM;
|
|
307
|
+
}
|
|
308
|
+
};
|
|
276
309
|
function normalizePlaythroughWaiverReason(waiverReason) {
|
|
277
310
|
if (waiverReason === void 0) return void 0;
|
|
278
311
|
const reason = waiverReason.trim();
|
|
@@ -292,6 +325,7 @@ async function runBoundedUntil(condition, options = {}) {
|
|
|
292
325
|
"stepUntil maxSteps must be a safe integer between 0 and 10000."
|
|
293
326
|
);
|
|
294
327
|
}
|
|
328
|
+
const probe = new StagnationProbe(options.diagnostics);
|
|
295
329
|
for (let step = 0; step <= maxSteps; step += 1) {
|
|
296
330
|
throwIfAborted(options.signal);
|
|
297
331
|
if (condition()) return step;
|
|
@@ -300,11 +334,18 @@ async function runBoundedUntil(condition, options = {}) {
|
|
|
300
334
|
await options.step?.(step + 1);
|
|
301
335
|
});
|
|
302
336
|
throwIfAborted(options.signal);
|
|
337
|
+
probe.sample(step + 1);
|
|
303
338
|
}
|
|
304
339
|
}
|
|
305
340
|
const diagnostics = formatDiagnostics(options.diagnostics);
|
|
306
|
-
const guidance = options.step ? "The step callback ran, but the authoritative outcome did not change." : "No step callback was provided, and the condition did not become true after the stage action. This does not identify a clock problem; inspect the condition, production input wiring, and observed state boundary.";
|
|
307
341
|
const suffix = diagnostics ? ` Last diagnostics: ${diagnostics}` : "";
|
|
342
|
+
if (probe.settled) {
|
|
343
|
+
throw codedError(
|
|
344
|
+
"PLAYTHROUGH_STATE_STAGNANT",
|
|
345
|
+
`Playthrough state stopped changing at step ${probe.settledAtStep} of ${maxSteps} and stayed identical until the bound. The run is already settled, so raising maxSteps cannot help: compare the settled state against the outcome this stage asserts. A settled state usually means the game reached a terminal result other than the asserted one, or that the stage never advances the production Controller at all.${suffix}`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
const guidance = options.step ? "The step callback ran, but the authoritative outcome did not change." : "No step callback was provided, and the condition did not become true after the stage action. This does not identify a clock problem; inspect the condition, production input wiring, and observed state boundary.";
|
|
308
349
|
throw codedError(
|
|
309
350
|
options.step ? "PLAYTHROUGH_BOUND_EXHAUSTED" : "PLAYTHROUGH_OUTCOME_NOT_REACHED",
|
|
310
351
|
`Playthrough outcome was not reached within ${maxSteps} steps. ${guidance}${suffix}`
|
|
@@ -265,6 +265,39 @@ function formatDiagnostics(read) {
|
|
|
265
265
|
return `diagnostics() threw: ${String(error)}`;
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
|
+
var STAGNATION_SAMPLE_INTERVAL = 32;
|
|
269
|
+
var STAGNATION_SAMPLE_MINIMUM = 3;
|
|
270
|
+
var StagnationProbe = class {
|
|
271
|
+
constructor(read) {
|
|
272
|
+
this.read = read;
|
|
273
|
+
this.disabled = !read;
|
|
274
|
+
}
|
|
275
|
+
read;
|
|
276
|
+
fingerprint;
|
|
277
|
+
unchangedSamples = 0;
|
|
278
|
+
disabled = false;
|
|
279
|
+
settledAtStep;
|
|
280
|
+
sample(step) {
|
|
281
|
+
if (this.disabled || step % STAGNATION_SAMPLE_INTERVAL !== 0) return;
|
|
282
|
+
let next;
|
|
283
|
+
try {
|
|
284
|
+
next = JSON.stringify(this.read?.()) ?? "undefined";
|
|
285
|
+
} catch {
|
|
286
|
+
this.disabled = true;
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (this.fingerprint === next) {
|
|
290
|
+
this.unchangedSamples += 1;
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
this.fingerprint = next;
|
|
294
|
+
this.unchangedSamples = 1;
|
|
295
|
+
this.settledAtStep = step;
|
|
296
|
+
}
|
|
297
|
+
get settled() {
|
|
298
|
+
return !this.disabled && this.unchangedSamples >= STAGNATION_SAMPLE_MINIMUM;
|
|
299
|
+
}
|
|
300
|
+
};
|
|
268
301
|
function normalizePlaythroughWaiverReason(waiverReason) {
|
|
269
302
|
if (waiverReason === void 0) return void 0;
|
|
270
303
|
const reason = waiverReason.trim();
|
|
@@ -284,6 +317,7 @@ async function runBoundedUntil(condition, options = {}) {
|
|
|
284
317
|
"stepUntil maxSteps must be a safe integer between 0 and 10000."
|
|
285
318
|
);
|
|
286
319
|
}
|
|
320
|
+
const probe = new StagnationProbe(options.diagnostics);
|
|
287
321
|
for (let step = 0; step <= maxSteps; step += 1) {
|
|
288
322
|
throwIfAborted(options.signal);
|
|
289
323
|
if (condition()) return step;
|
|
@@ -292,11 +326,18 @@ async function runBoundedUntil(condition, options = {}) {
|
|
|
292
326
|
await options.step?.(step + 1);
|
|
293
327
|
});
|
|
294
328
|
throwIfAborted(options.signal);
|
|
329
|
+
probe.sample(step + 1);
|
|
295
330
|
}
|
|
296
331
|
}
|
|
297
332
|
const diagnostics = formatDiagnostics(options.diagnostics);
|
|
298
|
-
const guidance = options.step ? "The step callback ran, but the authoritative outcome did not change." : "No step callback was provided, and the condition did not become true after the stage action. This does not identify a clock problem; inspect the condition, production input wiring, and observed state boundary.";
|
|
299
333
|
const suffix = diagnostics ? ` Last diagnostics: ${diagnostics}` : "";
|
|
334
|
+
if (probe.settled) {
|
|
335
|
+
throw codedError(
|
|
336
|
+
"PLAYTHROUGH_STATE_STAGNANT",
|
|
337
|
+
`Playthrough state stopped changing at step ${probe.settledAtStep} of ${maxSteps} and stayed identical until the bound. The run is already settled, so raising maxSteps cannot help: compare the settled state against the outcome this stage asserts. A settled state usually means the game reached a terminal result other than the asserted one, or that the stage never advances the production Controller at all.${suffix}`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
const guidance = options.step ? "The step callback ran, but the authoritative outcome did not change." : "No step callback was provided, and the condition did not become true after the stage action. This does not identify a clock problem; inspect the condition, production input wiring, and observed state boundary.";
|
|
300
341
|
throw codedError(
|
|
301
342
|
options.step ? "PLAYTHROUGH_BOUND_EXHAUSTED" : "PLAYTHROUGH_OUTCOME_NOT_REACHED",
|
|
302
343
|
`Playthrough outcome was not reached within ${maxSteps} steps. ${guidance}${suffix}`
|
|
@@ -1266,7 +1307,9 @@ function repairGuidance(code) {
|
|
|
1266
1307
|
case "STAGE_STATE_UNCHANGED":
|
|
1267
1308
|
return "The stage ran and asserted, but its observable state matched the previous milestone. Preserve the intended gameplay result and observe the same production Controller rendered by <App />. Do not substitute arbitrary labels or a weaker state change.";
|
|
1268
1309
|
case "PLAYTHROUGH_BOUND_EXHAUSTED":
|
|
1269
|
-
return "
|
|
1310
|
+
return "First read the phase or result field of Last diagnostics. If the run already reached a terminal result other than the one asserted, the game is over and the bound is irrelevant: assert the result the shipped constants actually produce (loss, draw, and ranking are legitimate terminal results), or fix the mechanic that was supposed to make the intended objective reachable. Raising maxSteps or rewriting the in-test play order cannot pass a finished game. If the run is still mid-game, keep the intended outcome unchanged and confirm that each deterministic step advances the same production Controller rendered by <App />; if state remains unchanged, inject ManualGameClock through the production App factory and observe that Controller through Telemetry. Never replace the outcome with navigation, an intermediate phase, a no-op step, or a weaker assertion, and never retune the production constants that define the challenge.";
|
|
1311
|
+
case "PLAYTHROUGH_STATE_STAGNANT":
|
|
1312
|
+
return "The observed state stopped changing well before the bound, so the bound is not the problem and raising maxSteps cannot help. Two causes: the run already reached a terminal result other than the one asserted \u2014 assert the result the shipped constants actually produce, or fix the mechanic meant to make the intended objective reachable \u2014 or the stage never advances the production Controller at all, in which case inject ManualGameClock through the production App factory and observe that Controller through Telemetry. Do not retune production constants and do not weaken the assertion.";
|
|
1270
1313
|
case "PLAYTHROUGH_OUTCOME_NOT_REACHED":
|
|
1271
1314
|
return "The stage action completed, but its until condition never became true. Inspect TRACE and Last diagnostics, then confirm that the production DOM input reaches the rendered App, until describes the result caused by this stage, and observe reads the matching authoritative state when used. Add a deterministic step only if the gameplay is actually driven by time or frames; do not add a no-op step or weaken the intended outcome.";
|
|
1272
1315
|
case "PLAYTHROUGH_CLOCK_NOT_ADVANCED":
|
|
@@ -1452,7 +1495,36 @@ function resolvePhaser3BrowserEntry(projectRoot) {
|
|
|
1452
1495
|
return void 0;
|
|
1453
1496
|
}
|
|
1454
1497
|
}
|
|
1498
|
+
var COVERAGE_INCLUDE = [
|
|
1499
|
+
"src/game/core/**/*.{ts,tsx}",
|
|
1500
|
+
"src/game/runtime/**/*.{ts,tsx}"
|
|
1501
|
+
];
|
|
1502
|
+
var COVERAGE_EXCLUDE = [
|
|
1503
|
+
"src/game/{core,runtime}/**/*.d.ts",
|
|
1504
|
+
"src/game/{core,runtime}/**/*.{spec,test}.{ts,tsx}"
|
|
1505
|
+
];
|
|
1506
|
+
var GATE_SCAN_EXCLUDE = [
|
|
1507
|
+
"src/**/*.d.ts",
|
|
1508
|
+
"src/**/*.{spec,test}.{ts,tsx}",
|
|
1509
|
+
"src/game/example/**"
|
|
1510
|
+
];
|
|
1511
|
+
function assertCoverageScopeIsPopulated(projectRoot) {
|
|
1512
|
+
const scan = (pattern) => (0, import_node_fs3.globSync)(pattern, { cwd: projectRoot, exclude: GATE_SCAN_EXCLUDE });
|
|
1513
|
+
if (COVERAGE_INCLUDE.flatMap(scan).length > 0) return;
|
|
1514
|
+
const misplaced = scan("src/**/{core,runtime}/**/*.{ts,tsx}");
|
|
1515
|
+
if (misplaced.length === 0) return;
|
|
1516
|
+
throw new Error(
|
|
1517
|
+
[
|
|
1518
|
+
"The coverage gate matched no source file, so it proves nothing.",
|
|
1519
|
+
"Deterministic rules belong in src/game/core/; the loop, scheduling,",
|
|
1520
|
+
"Controller, and Telemetry belong in src/game/runtime/.",
|
|
1521
|
+
"Move these files under those two directories:",
|
|
1522
|
+
...misplaced.slice(0, 10).map((file) => ` ${file}`)
|
|
1523
|
+
].join("\n")
|
|
1524
|
+
);
|
|
1525
|
+
}
|
|
1455
1526
|
function defineReactGameVitestConfig(options) {
|
|
1527
|
+
assertCoverageScopeIsPopulated(options.projectRoot);
|
|
1456
1528
|
const phaser3BrowserEntry = resolvePhaser3BrowserEntry(options.projectRoot);
|
|
1457
1529
|
return (0, import_config.defineConfig)({
|
|
1458
1530
|
// Keep discovery and dependency resolution anchored to the generated app even
|
|
@@ -1495,14 +1567,8 @@ function defineReactGameVitestConfig(options) {
|
|
|
1495
1567
|
clearMocks: true,
|
|
1496
1568
|
coverage: {
|
|
1497
1569
|
provider: "v8",
|
|
1498
|
-
include:
|
|
1499
|
-
|
|
1500
|
-
"src/game/runtime/**/*.{ts,tsx}"
|
|
1501
|
-
],
|
|
1502
|
-
exclude: [
|
|
1503
|
-
"src/game/{core,runtime}/**/*.d.ts",
|
|
1504
|
-
"src/game/{core,runtime}/**/*.{spec,test}.{ts,tsx}"
|
|
1505
|
-
],
|
|
1570
|
+
include: COVERAGE_INCLUDE,
|
|
1571
|
+
exclude: COVERAGE_EXCLUDE,
|
|
1506
1572
|
reporter: ["text-summary"],
|
|
1507
1573
|
thresholds: {
|
|
1508
1574
|
perFile: true,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/react-vitest-config.ts
|
|
2
|
-
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
2
|
+
import { existsSync as existsSync3, globSync, readFileSync as readFileSync2 } from "fs";
|
|
3
3
|
import { dirname as dirname2, resolve as resolve3 } from "path";
|
|
4
4
|
import { defineConfig } from "vitest/config";
|
|
5
5
|
|
|
@@ -231,6 +231,39 @@ function formatDiagnostics(read) {
|
|
|
231
231
|
return `diagnostics() threw: ${String(error)}`;
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
|
+
var STAGNATION_SAMPLE_INTERVAL = 32;
|
|
235
|
+
var STAGNATION_SAMPLE_MINIMUM = 3;
|
|
236
|
+
var StagnationProbe = class {
|
|
237
|
+
constructor(read) {
|
|
238
|
+
this.read = read;
|
|
239
|
+
this.disabled = !read;
|
|
240
|
+
}
|
|
241
|
+
read;
|
|
242
|
+
fingerprint;
|
|
243
|
+
unchangedSamples = 0;
|
|
244
|
+
disabled = false;
|
|
245
|
+
settledAtStep;
|
|
246
|
+
sample(step) {
|
|
247
|
+
if (this.disabled || step % STAGNATION_SAMPLE_INTERVAL !== 0) return;
|
|
248
|
+
let next;
|
|
249
|
+
try {
|
|
250
|
+
next = JSON.stringify(this.read?.()) ?? "undefined";
|
|
251
|
+
} catch {
|
|
252
|
+
this.disabled = true;
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (this.fingerprint === next) {
|
|
256
|
+
this.unchangedSamples += 1;
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
this.fingerprint = next;
|
|
260
|
+
this.unchangedSamples = 1;
|
|
261
|
+
this.settledAtStep = step;
|
|
262
|
+
}
|
|
263
|
+
get settled() {
|
|
264
|
+
return !this.disabled && this.unchangedSamples >= STAGNATION_SAMPLE_MINIMUM;
|
|
265
|
+
}
|
|
266
|
+
};
|
|
234
267
|
function normalizePlaythroughWaiverReason(waiverReason) {
|
|
235
268
|
if (waiverReason === void 0) return void 0;
|
|
236
269
|
const reason = waiverReason.trim();
|
|
@@ -250,6 +283,7 @@ async function runBoundedUntil(condition, options = {}) {
|
|
|
250
283
|
"stepUntil maxSteps must be a safe integer between 0 and 10000."
|
|
251
284
|
);
|
|
252
285
|
}
|
|
286
|
+
const probe = new StagnationProbe(options.diagnostics);
|
|
253
287
|
for (let step = 0; step <= maxSteps; step += 1) {
|
|
254
288
|
throwIfAborted(options.signal);
|
|
255
289
|
if (condition()) return step;
|
|
@@ -258,11 +292,18 @@ async function runBoundedUntil(condition, options = {}) {
|
|
|
258
292
|
await options.step?.(step + 1);
|
|
259
293
|
});
|
|
260
294
|
throwIfAborted(options.signal);
|
|
295
|
+
probe.sample(step + 1);
|
|
261
296
|
}
|
|
262
297
|
}
|
|
263
298
|
const diagnostics = formatDiagnostics(options.diagnostics);
|
|
264
|
-
const guidance = options.step ? "The step callback ran, but the authoritative outcome did not change." : "No step callback was provided, and the condition did not become true after the stage action. This does not identify a clock problem; inspect the condition, production input wiring, and observed state boundary.";
|
|
265
299
|
const suffix = diagnostics ? ` Last diagnostics: ${diagnostics}` : "";
|
|
300
|
+
if (probe.settled) {
|
|
301
|
+
throw codedError(
|
|
302
|
+
"PLAYTHROUGH_STATE_STAGNANT",
|
|
303
|
+
`Playthrough state stopped changing at step ${probe.settledAtStep} of ${maxSteps} and stayed identical until the bound. The run is already settled, so raising maxSteps cannot help: compare the settled state against the outcome this stage asserts. A settled state usually means the game reached a terminal result other than the asserted one, or that the stage never advances the production Controller at all.${suffix}`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
const guidance = options.step ? "The step callback ran, but the authoritative outcome did not change." : "No step callback was provided, and the condition did not become true after the stage action. This does not identify a clock problem; inspect the condition, production input wiring, and observed state boundary.";
|
|
266
307
|
throw codedError(
|
|
267
308
|
options.step ? "PLAYTHROUGH_BOUND_EXHAUSTED" : "PLAYTHROUGH_OUTCOME_NOT_REACHED",
|
|
268
309
|
`Playthrough outcome was not reached within ${maxSteps} steps. ${guidance}${suffix}`
|
|
@@ -1232,7 +1273,9 @@ function repairGuidance(code) {
|
|
|
1232
1273
|
case "STAGE_STATE_UNCHANGED":
|
|
1233
1274
|
return "The stage ran and asserted, but its observable state matched the previous milestone. Preserve the intended gameplay result and observe the same production Controller rendered by <App />. Do not substitute arbitrary labels or a weaker state change.";
|
|
1234
1275
|
case "PLAYTHROUGH_BOUND_EXHAUSTED":
|
|
1235
|
-
return "
|
|
1276
|
+
return "First read the phase or result field of Last diagnostics. If the run already reached a terminal result other than the one asserted, the game is over and the bound is irrelevant: assert the result the shipped constants actually produce (loss, draw, and ranking are legitimate terminal results), or fix the mechanic that was supposed to make the intended objective reachable. Raising maxSteps or rewriting the in-test play order cannot pass a finished game. If the run is still mid-game, keep the intended outcome unchanged and confirm that each deterministic step advances the same production Controller rendered by <App />; if state remains unchanged, inject ManualGameClock through the production App factory and observe that Controller through Telemetry. Never replace the outcome with navigation, an intermediate phase, a no-op step, or a weaker assertion, and never retune the production constants that define the challenge.";
|
|
1277
|
+
case "PLAYTHROUGH_STATE_STAGNANT":
|
|
1278
|
+
return "The observed state stopped changing well before the bound, so the bound is not the problem and raising maxSteps cannot help. Two causes: the run already reached a terminal result other than the one asserted \u2014 assert the result the shipped constants actually produce, or fix the mechanic meant to make the intended objective reachable \u2014 or the stage never advances the production Controller at all, in which case inject ManualGameClock through the production App factory and observe that Controller through Telemetry. Do not retune production constants and do not weaken the assertion.";
|
|
1236
1279
|
case "PLAYTHROUGH_OUTCOME_NOT_REACHED":
|
|
1237
1280
|
return "The stage action completed, but its until condition never became true. Inspect TRACE and Last diagnostics, then confirm that the production DOM input reaches the rendered App, until describes the result caused by this stage, and observe reads the matching authoritative state when used. Add a deterministic step only if the gameplay is actually driven by time or frames; do not add a no-op step or weaken the intended outcome.";
|
|
1238
1281
|
case "PLAYTHROUGH_CLOCK_NOT_ADVANCED":
|
|
@@ -1418,7 +1461,36 @@ function resolvePhaser3BrowserEntry(projectRoot) {
|
|
|
1418
1461
|
return void 0;
|
|
1419
1462
|
}
|
|
1420
1463
|
}
|
|
1464
|
+
var COVERAGE_INCLUDE = [
|
|
1465
|
+
"src/game/core/**/*.{ts,tsx}",
|
|
1466
|
+
"src/game/runtime/**/*.{ts,tsx}"
|
|
1467
|
+
];
|
|
1468
|
+
var COVERAGE_EXCLUDE = [
|
|
1469
|
+
"src/game/{core,runtime}/**/*.d.ts",
|
|
1470
|
+
"src/game/{core,runtime}/**/*.{spec,test}.{ts,tsx}"
|
|
1471
|
+
];
|
|
1472
|
+
var GATE_SCAN_EXCLUDE = [
|
|
1473
|
+
"src/**/*.d.ts",
|
|
1474
|
+
"src/**/*.{spec,test}.{ts,tsx}",
|
|
1475
|
+
"src/game/example/**"
|
|
1476
|
+
];
|
|
1477
|
+
function assertCoverageScopeIsPopulated(projectRoot) {
|
|
1478
|
+
const scan = (pattern) => globSync(pattern, { cwd: projectRoot, exclude: GATE_SCAN_EXCLUDE });
|
|
1479
|
+
if (COVERAGE_INCLUDE.flatMap(scan).length > 0) return;
|
|
1480
|
+
const misplaced = scan("src/**/{core,runtime}/**/*.{ts,tsx}");
|
|
1481
|
+
if (misplaced.length === 0) return;
|
|
1482
|
+
throw new Error(
|
|
1483
|
+
[
|
|
1484
|
+
"The coverage gate matched no source file, so it proves nothing.",
|
|
1485
|
+
"Deterministic rules belong in src/game/core/; the loop, scheduling,",
|
|
1486
|
+
"Controller, and Telemetry belong in src/game/runtime/.",
|
|
1487
|
+
"Move these files under those two directories:",
|
|
1488
|
+
...misplaced.slice(0, 10).map((file) => ` ${file}`)
|
|
1489
|
+
].join("\n")
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1421
1492
|
function defineReactGameVitestConfig(options) {
|
|
1493
|
+
assertCoverageScopeIsPopulated(options.projectRoot);
|
|
1422
1494
|
const phaser3BrowserEntry = resolvePhaser3BrowserEntry(options.projectRoot);
|
|
1423
1495
|
return defineConfig({
|
|
1424
1496
|
// Keep discovery and dependency resolution anchored to the generated app even
|
|
@@ -1461,14 +1533,8 @@ function defineReactGameVitestConfig(options) {
|
|
|
1461
1533
|
clearMocks: true,
|
|
1462
1534
|
coverage: {
|
|
1463
1535
|
provider: "v8",
|
|
1464
|
-
include:
|
|
1465
|
-
|
|
1466
|
-
"src/game/runtime/**/*.{ts,tsx}"
|
|
1467
|
-
],
|
|
1468
|
-
exclude: [
|
|
1469
|
-
"src/game/{core,runtime}/**/*.d.ts",
|
|
1470
|
-
"src/game/{core,runtime}/**/*.{spec,test}.{ts,tsx}"
|
|
1471
|
-
],
|
|
1536
|
+
include: COVERAGE_INCLUDE,
|
|
1537
|
+
exclude: COVERAGE_EXCLUDE,
|
|
1472
1538
|
reporter: ["text-summary"],
|
|
1473
1539
|
thresholds: {
|
|
1474
1540
|
perFile: true,
|
|
@@ -184,8 +184,92 @@ function runtimeFailureError(code, value) {
|
|
|
184
184
|
return error;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
// src/testing/jsdom-
|
|
187
|
+
// src/testing/jsdom-webgl.ts
|
|
188
|
+
var CONSTANT_NAME = /^[A-Z][A-Z0-9_]*$/;
|
|
189
|
+
var TEXTURE_UNIT_LIMIT = 32;
|
|
190
|
+
var TEXTURE_SIZE_LIMIT = 4096;
|
|
191
|
+
var VIEWPORT_LIMIT = 4096;
|
|
192
|
+
function parameterValue(name) {
|
|
193
|
+
if (name === "VERSION") return "WebGL 2.0 (miaoda-game-devkit stub)";
|
|
194
|
+
if (name === "SHADING_LANGUAGE_VERSION") return "WebGL GLSL ES 3.00 (stub)";
|
|
195
|
+
if (name === "VENDOR" || name === "RENDERER") return "miaoda-game-devkit";
|
|
196
|
+
if (name === "MAX_VIEWPORT_DIMS") {
|
|
197
|
+
return new Int32Array([VIEWPORT_LIMIT, VIEWPORT_LIMIT]);
|
|
198
|
+
}
|
|
199
|
+
if (name === "VIEWPORT" || name === "SCISSOR_BOX") {
|
|
200
|
+
return new Int32Array([0, 0, VIEWPORT_LIMIT, VIEWPORT_LIMIT]);
|
|
201
|
+
}
|
|
202
|
+
if (name.includes("MAX_") && name.includes("SIZE")) return TEXTURE_SIZE_LIMIT;
|
|
203
|
+
if (name.startsWith("MAX_")) return TEXTURE_UNIT_LIMIT;
|
|
204
|
+
if (name.endsWith("_BITS")) return 8;
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
function createWebGLContext(canvas, attributes) {
|
|
208
|
+
const constants = /* @__PURE__ */ new Map();
|
|
209
|
+
const constantNames = /* @__PURE__ */ new Map();
|
|
210
|
+
const state = {
|
|
211
|
+
canvas,
|
|
212
|
+
drawingBufferWidth: canvas.width,
|
|
213
|
+
drawingBufferHeight: canvas.height
|
|
214
|
+
};
|
|
215
|
+
const method = (name) => {
|
|
216
|
+
if (name === "getContextAttributes") return () => attributes;
|
|
217
|
+
if (name === "getExtension") return () => null;
|
|
218
|
+
if (name === "getSupportedExtensions") return () => [];
|
|
219
|
+
if (name === "getParameter") {
|
|
220
|
+
return (pname) => parameterValue(constantNames.get(pname) ?? "UNKNOWN");
|
|
221
|
+
}
|
|
222
|
+
if (name === "getShaderPrecisionFormat") {
|
|
223
|
+
return () => ({ rangeMin: 127, rangeMax: 127, precision: 23 });
|
|
224
|
+
}
|
|
225
|
+
if (name === "getShaderInfoLog" || name === "getProgramInfoLog") return () => "";
|
|
226
|
+
if (name === "getError") return () => 0;
|
|
227
|
+
if (name === "isContextLost") return () => false;
|
|
228
|
+
if (name.startsWith("get") && name.endsWith("Parameter")) return () => true;
|
|
229
|
+
if (name.startsWith("get")) return () => null;
|
|
230
|
+
if (name.startsWith("create") || name.startsWith("getUniformLocation")) {
|
|
231
|
+
return () => ({});
|
|
232
|
+
}
|
|
233
|
+
return () => void 0;
|
|
234
|
+
};
|
|
235
|
+
return new Proxy(state, {
|
|
236
|
+
get(target, property) {
|
|
237
|
+
if (property in target) return target[property];
|
|
238
|
+
if (typeof property !== "string") return void 0;
|
|
239
|
+
if (CONSTANT_NAME.test(property)) {
|
|
240
|
+
const existing = constants.get(property);
|
|
241
|
+
if (existing !== void 0) return existing;
|
|
242
|
+
const value = constants.size + 1;
|
|
243
|
+
constants.set(property, value);
|
|
244
|
+
constantNames.set(value, property);
|
|
245
|
+
return value;
|
|
246
|
+
}
|
|
247
|
+
const implementation = method(property);
|
|
248
|
+
target[property] = implementation;
|
|
249
|
+
return implementation;
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
188
253
|
var contexts = /* @__PURE__ */ new WeakMap();
|
|
254
|
+
function getJSDOMWebGLContext(canvas, options) {
|
|
255
|
+
const existing = contexts.get(canvas);
|
|
256
|
+
if (existing) return existing;
|
|
257
|
+
const attributes = {
|
|
258
|
+
alpha: true,
|
|
259
|
+
antialias: false,
|
|
260
|
+
depth: true,
|
|
261
|
+
premultipliedAlpha: true,
|
|
262
|
+
preserveDrawingBuffer: false,
|
|
263
|
+
stencil: false,
|
|
264
|
+
...typeof options === "object" && options !== null ? options : {}
|
|
265
|
+
};
|
|
266
|
+
const context = createWebGLContext(canvas, attributes);
|
|
267
|
+
contexts.set(canvas, context);
|
|
268
|
+
return context;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// src/testing/jsdom-canvas.ts
|
|
272
|
+
var contexts2 = /* @__PURE__ */ new WeakMap();
|
|
189
273
|
function createCanvasContext(canvas) {
|
|
190
274
|
const imageData = (width = 1, height = 1) => ({
|
|
191
275
|
data: new Uint8ClampedArray(width * height * 4),
|
|
@@ -232,12 +316,15 @@ function createCanvasContext(canvas) {
|
|
|
232
316
|
return context;
|
|
233
317
|
}
|
|
234
318
|
function installJSDOMCanvasContext() {
|
|
235
|
-
HTMLCanvasElement.prototype.getContext = function getContext(contextId) {
|
|
319
|
+
HTMLCanvasElement.prototype.getContext = function getContext(contextId, options) {
|
|
320
|
+
if (contextId === "webgl" || contextId === "webgl2") {
|
|
321
|
+
return getJSDOMWebGLContext(this, options);
|
|
322
|
+
}
|
|
236
323
|
if (contextId !== "2d") return null;
|
|
237
|
-
const existing =
|
|
324
|
+
const existing = contexts2.get(this);
|
|
238
325
|
if (existing) return existing;
|
|
239
326
|
const context = createCanvasContext(this);
|
|
240
|
-
|
|
327
|
+
contexts2.set(this, context);
|
|
241
328
|
return context;
|
|
242
329
|
};
|
|
243
330
|
}
|
|
@@ -182,8 +182,92 @@ function runtimeFailureError(code, value) {
|
|
|
182
182
|
return error;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
// src/testing/jsdom-
|
|
185
|
+
// src/testing/jsdom-webgl.ts
|
|
186
|
+
var CONSTANT_NAME = /^[A-Z][A-Z0-9_]*$/;
|
|
187
|
+
var TEXTURE_UNIT_LIMIT = 32;
|
|
188
|
+
var TEXTURE_SIZE_LIMIT = 4096;
|
|
189
|
+
var VIEWPORT_LIMIT = 4096;
|
|
190
|
+
function parameterValue(name) {
|
|
191
|
+
if (name === "VERSION") return "WebGL 2.0 (miaoda-game-devkit stub)";
|
|
192
|
+
if (name === "SHADING_LANGUAGE_VERSION") return "WebGL GLSL ES 3.00 (stub)";
|
|
193
|
+
if (name === "VENDOR" || name === "RENDERER") return "miaoda-game-devkit";
|
|
194
|
+
if (name === "MAX_VIEWPORT_DIMS") {
|
|
195
|
+
return new Int32Array([VIEWPORT_LIMIT, VIEWPORT_LIMIT]);
|
|
196
|
+
}
|
|
197
|
+
if (name === "VIEWPORT" || name === "SCISSOR_BOX") {
|
|
198
|
+
return new Int32Array([0, 0, VIEWPORT_LIMIT, VIEWPORT_LIMIT]);
|
|
199
|
+
}
|
|
200
|
+
if (name.includes("MAX_") && name.includes("SIZE")) return TEXTURE_SIZE_LIMIT;
|
|
201
|
+
if (name.startsWith("MAX_")) return TEXTURE_UNIT_LIMIT;
|
|
202
|
+
if (name.endsWith("_BITS")) return 8;
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
function createWebGLContext(canvas, attributes) {
|
|
206
|
+
const constants = /* @__PURE__ */ new Map();
|
|
207
|
+
const constantNames = /* @__PURE__ */ new Map();
|
|
208
|
+
const state = {
|
|
209
|
+
canvas,
|
|
210
|
+
drawingBufferWidth: canvas.width,
|
|
211
|
+
drawingBufferHeight: canvas.height
|
|
212
|
+
};
|
|
213
|
+
const method = (name) => {
|
|
214
|
+
if (name === "getContextAttributes") return () => attributes;
|
|
215
|
+
if (name === "getExtension") return () => null;
|
|
216
|
+
if (name === "getSupportedExtensions") return () => [];
|
|
217
|
+
if (name === "getParameter") {
|
|
218
|
+
return (pname) => parameterValue(constantNames.get(pname) ?? "UNKNOWN");
|
|
219
|
+
}
|
|
220
|
+
if (name === "getShaderPrecisionFormat") {
|
|
221
|
+
return () => ({ rangeMin: 127, rangeMax: 127, precision: 23 });
|
|
222
|
+
}
|
|
223
|
+
if (name === "getShaderInfoLog" || name === "getProgramInfoLog") return () => "";
|
|
224
|
+
if (name === "getError") return () => 0;
|
|
225
|
+
if (name === "isContextLost") return () => false;
|
|
226
|
+
if (name.startsWith("get") && name.endsWith("Parameter")) return () => true;
|
|
227
|
+
if (name.startsWith("get")) return () => null;
|
|
228
|
+
if (name.startsWith("create") || name.startsWith("getUniformLocation")) {
|
|
229
|
+
return () => ({});
|
|
230
|
+
}
|
|
231
|
+
return () => void 0;
|
|
232
|
+
};
|
|
233
|
+
return new Proxy(state, {
|
|
234
|
+
get(target, property) {
|
|
235
|
+
if (property in target) return target[property];
|
|
236
|
+
if (typeof property !== "string") return void 0;
|
|
237
|
+
if (CONSTANT_NAME.test(property)) {
|
|
238
|
+
const existing = constants.get(property);
|
|
239
|
+
if (existing !== void 0) return existing;
|
|
240
|
+
const value = constants.size + 1;
|
|
241
|
+
constants.set(property, value);
|
|
242
|
+
constantNames.set(value, property);
|
|
243
|
+
return value;
|
|
244
|
+
}
|
|
245
|
+
const implementation = method(property);
|
|
246
|
+
target[property] = implementation;
|
|
247
|
+
return implementation;
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
186
251
|
var contexts = /* @__PURE__ */ new WeakMap();
|
|
252
|
+
function getJSDOMWebGLContext(canvas, options) {
|
|
253
|
+
const existing = contexts.get(canvas);
|
|
254
|
+
if (existing) return existing;
|
|
255
|
+
const attributes = {
|
|
256
|
+
alpha: true,
|
|
257
|
+
antialias: false,
|
|
258
|
+
depth: true,
|
|
259
|
+
premultipliedAlpha: true,
|
|
260
|
+
preserveDrawingBuffer: false,
|
|
261
|
+
stencil: false,
|
|
262
|
+
...typeof options === "object" && options !== null ? options : {}
|
|
263
|
+
};
|
|
264
|
+
const context = createWebGLContext(canvas, attributes);
|
|
265
|
+
contexts.set(canvas, context);
|
|
266
|
+
return context;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/testing/jsdom-canvas.ts
|
|
270
|
+
var contexts2 = /* @__PURE__ */ new WeakMap();
|
|
187
271
|
function createCanvasContext(canvas) {
|
|
188
272
|
const imageData = (width = 1, height = 1) => ({
|
|
189
273
|
data: new Uint8ClampedArray(width * height * 4),
|
|
@@ -230,12 +314,15 @@ function createCanvasContext(canvas) {
|
|
|
230
314
|
return context;
|
|
231
315
|
}
|
|
232
316
|
function installJSDOMCanvasContext() {
|
|
233
|
-
HTMLCanvasElement.prototype.getContext = function getContext(contextId) {
|
|
317
|
+
HTMLCanvasElement.prototype.getContext = function getContext(contextId, options) {
|
|
318
|
+
if (contextId === "webgl" || contextId === "webgl2") {
|
|
319
|
+
return getJSDOMWebGLContext(this, options);
|
|
320
|
+
}
|
|
234
321
|
if (contextId !== "2d") return null;
|
|
235
|
-
const existing =
|
|
322
|
+
const existing = contexts2.get(this);
|
|
236
323
|
if (existing) return existing;
|
|
237
324
|
const context = createCanvasContext(this);
|
|
238
|
-
|
|
325
|
+
contexts2.set(this, context);
|
|
239
326
|
return context;
|
|
240
327
|
};
|
|
241
328
|
}
|