test(genbi): make the canvas stub impossible to leave incomplete

The literal version was missing rect, and ECharts calls it — one more unhandled
error, one more red run, one more round. Enumerating the methods a charting
library happens to use is a game with no end, played at a CI round per move.

It is a Proxy now: the handful that must return something are named, everything
else answers as a no-op, and a method nobody has thought of yet is already
handled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jax Liu
2026-09-04 13:53:33 +08:00
co-authored by Claude Opus 5
parent af6d639eaf
commit 143d2272ad
+27 -21
View File
@@ -15,29 +15,35 @@ if (!('ResizeObserver' in globalThis)) {
// Vitest counts those as unhandled errors and exits non-zero on an otherwise
// green run, which is how this first reached CI: every assertion passing, the
// job still red. A stub context is enough; nothing here asserts on pixels.
//
// It is a Proxy rather than a literal so it cannot be incomplete. Enumerating
// methods meant a CI round per missing one — the first version shipped without
// `rect` and found out that way. Anything not named below answers as a no-op.
//
// jsdom *defines* getContext and returns null from it, so testing for the
// method's absence would never install this.
// method's absence would never install this. Ask for a context instead.
if (document.createElement('canvas').getContext('2d') === null) {
HTMLCanvasElement.prototype.getContext = (() => {
const noop = () => {};
return {
canvas: undefined,
clearRect: noop, fillRect: noop, strokeRect: noop,
save: noop, restore: noop, scale: noop, translate: noop, rotate: noop, setTransform: noop,
beginPath: noop, closePath: noop, moveTo: noop, lineTo: noop, arc: noop, bezierCurveTo: noop,
fill: noop, stroke: noop, clip: noop,
createLinearGradient: () => ({ addColorStop: noop }),
createRadialGradient: () => ({ addColorStop: noop }),
createPattern: () => null,
measureText: () => ({ width: 0 }),
fillText: noop, strokeText: noop,
drawImage: noop,
getImageData: () => ({ data: new Uint8ClampedArray(4) }),
putImageData: noop,
createImageData: () => ({ data: new Uint8ClampedArray(4) }),
setLineDash: noop, getLineDash: () => [],
};
}) as unknown as HTMLCanvasElement["getContext"];
const noop = () => {};
const withReturn: Record<string, unknown> = {
measureText: () => ({ width: 0 }),
createLinearGradient: () => ({ addColorStop: noop }),
createRadialGradient: () => ({ addColorStop: noop }),
createPattern: () => null,
getLineDash: () => [],
getImageData: () => ({ data: new Uint8ClampedArray(4) }),
createImageData: () => ({ data: new Uint8ClampedArray(4) }),
isPointInPath: () => false,
isPointInStroke: () => false,
};
const context = new Proxy({} as Record<string, unknown>, {
get: (target, property: string) =>
property in withReturn ? withReturn[property]
: property in target ? target[property]
: noop,
set: (target, property: string, value) => { target[property] = value; return true; },
has: () => true,
});
HTMLCanvasElement.prototype.getContext = (() => context) as unknown as HTMLCanvasElement['getContext'];
}
// jsdom does not implement matchMedia; AntD's responsive + theme code reads it.