Skip to content

Instantly share code, notes, and snippets.

@joshkel
Created August 14, 2025 18:52
Show Gist options
  • Select an option

  • Save joshkel/5c62dade4da0aa1f8abe9bb1185f5fd1 to your computer and use it in GitHub Desktop.

Select an option

Save joshkel/5c62dade4da0aa1f8abe9bb1185f5fd1 to your computer and use it in GitHub Desktop.
Jest mock canvas
/**
* Creates a mock HTMLCanvasElement and CanvasRenderingContext2D that records
* all calls made to it.
*
* jest-canvas-mock offers a potential alternative, but it's only designed to
* work within jsdom: https://github.com/hustcc/jest-canvas-mock/issues/124
*
* This is not a complete implementation; it mocks enough properties to work
* with Chart.js.
*/
export function mockCanvas(): HTMLCanvasElement & { _calls: unknown[][] } {
const _calls: unknown[][] = [];
const canvas: Partial<HTMLCanvasElement> & { _calls: unknown[][] } = {
_calls,
width: 800,
height: 600,
getContext(contextId) {
if (contextId !== '2d') {
throw new Error('Unsupported');
}
return new Proxy(ctx, {
get(target, prop, _receiver) {
if (!(prop in target)) {
throw new Error(`Unable to get ctx.${String(prop)}`);
}
return (target as any)[prop];
},
set(target, prop, value, _receiver) {
if (!(prop in target)) {
throw new Error(`Unable to set ctx.${String(prop)}`);
}
(target as any)[prop] = value;
return true;
},
}) as any;
},
// Used by Chart.js implementation
['length' as any]: undefined,
['canvas' as any]: undefined,
};
const textMetrics: TextMetrics = {
width: 10,
actualBoundingBoxAscent: 0,
actualBoundingBoxDescent: 0,
fontBoundingBoxAscent: 0,
fontBoundingBoxDescent: 0,
emHeightAscent: 0,
emHeightDescent: 0,
hangingBaseline: 0,
alphabeticBaseline: 0,
ideographicBaseline: 0,
actualBoundingBoxLeft: 0,
actualBoundingBoxRight: 0,
};
const ctx: any = {
canvas: canvas as HTMLCanvasElement,
};
for (const simpleMethod of [
'arc',
'beginPath',
'clearRect',
'clip',
'closePath',
'fill',
'fillRect',
'fillText',
'lineTo',
'moveTo',
'rect',
'resetTransform',
'restore',
'rotate',
'save',
'setLineDash',
'setTransform',
'stroke',
'translate',
]) {
ctx[simpleMethod] = function (...args: unknown[]) {
_calls.push([simpleMethod, ...args]);
};
}
for (const [method, result] of [['measureText', textMetrics]] as [string, unknown][]) {
ctx[method] = function (...args: unknown[]) {
_calls.push([method, ...args]);
return result;
};
}
for (const setter of [
'fillStyle',
'font',
'lineCap',
'lineDashOffset',
'lineJoin',
'lineWidth',
'strokeStyle',
'textAlign',
'textBaseline',
]) {
Object.defineProperty(ctx, setter, {
get() {
return this[`_${setter}`];
},
set(value) {
_calls.push([setter, value]);
this[`_${setter}`] = value;
},
});
}
return new Proxy(canvas, {
get(target, prop, _receiver) {
if (!(prop in target)) {
throw new Error(`Unable to get canvas.${String(prop)}`);
}
return (target as any)[prop];
},
set(_target, prop, value, _receiver) {
throw new Error(`Unable to set canvas.${String(prop)} to ${value}`);
},
}) as HTMLCanvasElement & { _calls: unknown[][] };
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment