How to Automate HTML Canvas Testing with Selenium and Cypress
Test canvas interactions through controls and app state, then verify the pixels with screenshots. Runnable Selenium and Cypress examples included.
To test an HTML canvas, automate the same interactions a user performs, assert the application state the test can observe, and capture or compare the rendered pixels when appearance matters. A canvas drawing is not a collection of ordinary DOM elements, so a DOM locator cannot reliably select individual shapes drawn inside it.
This guide shows a small, deterministic drawing app and runnable Selenium and Cypress tests for it. The examples check both behavior and rendered output, explain how to adapt the pattern to an existing app, and cover screenshot baselines, troubleshooting, and reliability.
1. Why canvas tests need more than DOM assertions
An HTML <canvas> is a drawing surface. JavaScript paints into it, and those marks are pixels rather than individual DOM nodes. A test that confirms the canvas exists—or that a toolbar button is present—does not prove that the expected drawing appeared. Canvas testing research describes this gap between DOM-oriented checks and visual output. See Web Canvas Testing through Visual Inference and A Taxonomy of Testable HTML5 Canvas Issues.
Use three complementary checks:
- Interaction: Did the user action reach the app? For example, did a click trigger the expected drawing handler?
- Application state: Did the app record the expected shape, coordinates, selected tool, or model state? Prefer an app-owned state hook when available.
- Rendered output: Do the pixels look right? Capture the canvas or page and compare it with an approved reference when visual fidelity is a requirement.
These checks answer different questions. State checks are usually easier to diagnose; image checks catch rendering defects that state checks cannot see.
2. Create a deterministic canvas page
The tests below use a tiny page that draws a red circle at the click location and exposes the drawn points as JSON in a hidden output. This gives the tests a reliable state assertion alongside a screenshot. For a real application, keep your production UI and replace the selectors and state hook with the app’s actual controls and model.
Save this as canvas.html and serve it from a local web server so both test frameworks can reach it. For example, with Python installed, run python -m http.server 8000 in the directory containing the file, then open http://localhost:8000/canvas.html.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Canvas test fixture</title>
<style>
body { font: 16px sans-serif; margin: 24px; }
canvas { display: block; border: 1px solid #777; }
</style>
</head>
<body>
<button id="clear" type="button">Clear</button>
<canvas id="drawing" width="400" height="240" aria-label="Drawing canvas"></canvas>
<output id="drawing-state" hidden>[]</output>
<script>
const canvas = document.querySelector('#drawing');
const context = canvas.getContext('2d');
const points = [];
function render() {
context.clearRect(0, 0, canvas.width, canvas.height);
context.fillStyle = '#d22';
for (const point of points) {
context.beginPath();
context.arc(point.x, point.y, 8, 0, Math.PI * 2);
context.fill();
}
document.querySelector('#drawing-state').textContent = JSON.stringify(points);
}
canvas.addEventListener('click', (event) => {
const rect = canvas.getBoundingClientRect();
const x = Math.round((event.clientX - rect.left) * canvas.width / rect.width);
const y = Math.round((event.clientY - rect.top) * canvas.height / rect.height);
points.push({ x, y });
render();
});
document.querySelector('#clear').addEventListener('click', () => {
points.length = 0;
render();
});
render();
</script>
</body>
</html>
The scaling in the click handler maps CSS display coordinates to the canvas’s internal pixel dimensions. That distinction matters when the canvas is styled to a different size or uses a high-resolution backing store.
3. Test canvas behavior and state with Selenium
Selenium WebDriver can run JavaScript in the active page context and capture screenshots. Its JavaScript API documents synchronous and asynchronous script execution; the async form completes when the page calls the supplied callback. See the Selenium WebDriver JavaScript API and its documentation on screenshots.
Install and run
With Node.js available and the fixture server running at port 8000, install Selenium WebDriver:
npm install selenium-webdriver
Save the following as canvas-selenium.js and run node canvas-selenium.js. Selenium Manager can manage drivers for supported browsers; if your environment requires a separately configured driver, configure it for the browser you use.
const assert = require('node:assert/strict');
const fs = require('node:fs/promises');
const { Builder, By, until } = require('selenium-webdriver');
(async () => {
const driver = await new Builder().forBrowser('chrome').build();
try {
await driver.manage().window().setRect({ width: 1000, height: 800 });
await driver.get('http://localhost:8000/canvas.html');
const canvas = await driver.findElement(By.css('#drawing'));
await driver.wait(until.elementIsVisible(canvas), 5000);
const rect = await driver.executeScript((element) => {
const r = element.getBoundingClientRect();
return { x: r.x, y: r.y, width: r.width, height: r.height };
}, canvas);
// Click at a fixed point inside the canvas, away from its border.
await driver.actions().move({
x: Math.round(rect.x + 100),
y: Math.round(rect.y + 80),
}).click().perform();
await driver.wait(async () => {
const raw = await driver.findElement(By.css('#drawing-state')).getText();
return JSON.parse(raw).length === 1;
}, 5000, 'Expected one recorded canvas point');
const points = await driver.executeScript(() => {
return JSON.parse(document.querySelector('#drawing-state').textContent);
});
assert.deepEqual(points, [{ x: 100, y: 80 }]);
// Keep an artifact for inspection or a project-specific visual comparator.
const png = await driver.takeScreenshot();
await fs.writeFile('selenium-canvas.png', Buffer.from(png, 'base64'));
await driver.findElement(By.css('#clear')).click();
const cleared = await driver.executeScript(() => {
return JSON.parse(document.querySelector('#drawing-state').textContent);
});
assert.deepEqual(cleared, []);
console.log('Canvas interaction and state checks passed.');
} finally {
await driver.quit();
}
})().catch((error) => {
console.error(error);
process.exitCode = 1;
});
The click uses a known canvas coordinate, then the test checks app-owned state through executeScript. Selenium scripts run in the selected frame or window; if the canvas lives in an iframe, switch to that frame before locating it or running page-context code. The screenshot is saved as a diagnostic artifact, not automatically compared to a baseline.
4. Test canvas behavior and state with Cypress
Cypress can yield the active document with cy.document(), and its chained assertions retry until they pass or time out. cy.invoke() calls a function on the yielded object. See the official docs for cy.document() and cy.invoke().
Install and run
Install Cypress in a project and open its interactive runner once to create the standard configuration and test folders:
npm install --save-dev cypress
npx cypress open
Set the base URL to http://localhost:8000 in your Cypress configuration, then save this test as cypress/e2e/canvas.cy.js. Run it with npx cypress run.
describe('canvas drawing', () => {
it('records a click, renders it, and clears it', () => {
cy.visit('/canvas.html');
cy.get('#drawing').should('be.visible').click(100, 80);
cy.get('#drawing-state').should(($state) => {
expect(JSON.parse($state.text())).to.deep.equal([{ x: 100, y: 80 }]);
});
// Save a screenshot artifact. Add a project-selected visual comparison
// tool if the test must assert pixels against an approved baseline.
cy.get('#drawing').screenshot('canvas-after-click');
cy.get('#clear').click();
cy.get('#drawing-state').should(($state) => {
expect(JSON.parse($state.text())).to.deep.equal([]);
});
});
});
Configure baseUrl in cypress.config.js, for example:
const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:8000',
},
});
Cypress lists community extensions for visual testing in its official extensions directory. The directory is a discovery list; evaluate and configure a comparison extension that fits your project if pixel-diff assertions are required. A screenshot command by itself captures output but does not establish that it matches a reference.
5. Compare canvas screenshots to a baseline
Use image comparison when the requirement is visual: for example, a chart must retain its layout, a drawing tool must render a shape with the correct color, or a canvas renderer must not regress. Selenium’s screenshot APIs support page or element capture; Cypress’s ecosystem includes visual-testing extensions. The framework captures the image, while the selected comparison workflow determines how a baseline is stored and how differences are judged.
- Choose the capture region. Capture just the canvas when surrounding content is irrelevant. Capture a larger region when layout, clipping, or overlays are part of the requirement.
- Approve a baseline deliberately. Generate it from a known-good app state and review it. Store it with the code or in the baseline system your team uses.
- Keep the environment stable. Use the same browser family and version, viewport, device scale, fonts, canvas dimensions, input data, and app state for baseline creation and comparison.
- Choose a project-specific difference policy. Decide whether any changed pixel fails, whether a tolerance is acceptable, or whether reviewers inspect diffs. Rendering varies by browser and platform; there is no universal tolerance that suits every canvas.
- Inspect the diff and raw capture. A changed image can indicate a real drawing defect, a timing issue, or a rendering-environment change. Keep the actual screenshot as an artifact to make that distinction.
For tests where exact pixels are essential, pin the runtime environment and use the strictest comparison compatible with that environment. For cross-browser coverage, use separate approved baselines or a tolerance policy that reflects the actual visual requirement rather than masking meaningful defects.
6. Adapt the pattern to a real canvas app
Wait for the app to be ready
Do not assume that a fixed delay means rendering has finished. Expose or identify an application-specific readiness signal, such as a completed data-load state, a stable model value, or a known control becoming enabled. Wait for that condition before interacting or capturing. Animation-driven apps may need a test mode that freezes time or advances frames deterministically.
Drive the real user path
Prefer clicks, pointer movement, keyboard shortcuts, tool selection, undo, and reset actions that represent actual user workflows. Use direct JavaScript to inspect state or arrange a test only when that does not bypass the behavior under test. If the test’s purpose is the input handler, a direct call to the drawing function would skip the very path it needs to verify.
Expose state intentionally
If the app owns a drawing model, make a stable test hook available in test builds or assert through visible controls and accessible output. Useful state includes shape type, coordinates, selected tool, layer order, and undo history. Avoid depending on undocumented internal objects that change frequently.
Account for coordinate systems
Canvas drawing involves at least two coordinate spaces: CSS pixels in the displayed element and backing-store pixels in canvas.width and canvas.height. High-DPI rendering, CSS resizing, borders, scrolling, transforms, and browser zoom can make a pointer coordinate differ from a drawing coordinate. Prefer framework-relative clicks at known positions and assert the app’s intended coordinate mapping.
Cover reset and boundary cases
- Clear an empty canvas, then clear a populated one.
- Test clicks near edges and verify the app’s intended clipping or rejection behavior.
- Test repeated actions, undo/redo, resize, and tool changes if those are supported.
- For touch or drag interactions, verify pointer down, movement, and release rather than assuming a click exercises the same code path.
- Check that resizing preserves or intentionally resets the drawing, according to the product requirement.
7. Selenium or Cypress?
| Need | Selenium | Cypress |
|---|---|---|
| Inspect page state | Execute synchronous or asynchronous JavaScript in the current browsing context. | Access the active document and invoke functions on yielded objects. |
| Capture output | Documented page and element screenshot capabilities. | Capture screenshots and use a selected community visual-testing extension for comparison. |
| Assertion workflow | Use the assertions and waits of your chosen test framework and WebDriver setup. | Chained assertions retry until they pass or time out. |
| Decision factor | Often fits an existing WebDriver stack or a test setup already built around Selenium. | Often fits a codebase already using Cypress and its command-and-retry workflow. |
Neither framework is universally better for canvas testing. Choose based on your application’s language and existing test stack, required browser coverage, CI setup, and how your team reviews visual baselines.
8. Troubleshooting common canvas test failures
| Symptom | Likely cause | Fix |
|---|---|---|
| The canvas exists, but the test cannot find a drawn shape. | Shapes are pixels, not DOM nodes. | Assert app-owned drawing state and use a screenshot or image comparison for the rendered result. |
| The click lands in the wrong place. | The test used page coordinates, the canvas moved or scrolled, or CSS size differs from backing-store size. | Click relative to the canvas, wait for stable layout, and account for the app’s coordinate conversion. |
| The screenshot is blank or stale. | The app has not finished loading or drawing when capture runs. | Wait for an app-specific ready condition or expected state, then capture. Avoid arbitrary sleeps where a state condition is available. |
| The test passes state assertions but misses a visual defect. | State and pixels are separate assertions. | Add a baseline comparison for the canvas or relevant region. |
| Visual diffs vary between runs. | Viewport, browser, device scale, fonts, animation timing, data, or rendering environment changed. | Pin capture conditions, make inputs deterministic, disable or control animation, and use reviewed environment-specific baselines. |
| Selenium reports no such element or script sees the wrong document. | The test is in the wrong frame or the page has not loaded the element. | Wait for the element and switch into the frame containing the canvas before locating or scripting against it. |
| Cypress state assertion times out. | The expected state did not update, the selector is wrong, or the handler did not run. | Inspect the command log and screenshot, verify the click target and state hook, and wait on the assertion rather than adding a blind delay. |
| The comparison reports harmless differences. | Rendering environment or antialiasing changed, or the threshold is too strict for the intended policy. | Review the diff and environment. Adjust the policy only if those differences are acceptable; do not raise tolerance to hide real drawing changes. |
9. Performance, reliability, and cost
Canvas tests can become slow and flaky when every case launches a new browser, waits on broad network-idle conditions, or compares unnecessarily large screenshots. Reuse the runner’s normal browser lifecycle, keep fixtures small, wait for the specific state needed, and capture the smallest region that answers the visual question. Keep screenshots for failed runs or required regression checks rather than capturing redundant artifacts at every step.
Visual comparison adds storage, review, and maintenance work because approved baselines must evolve with intentional design changes. Keep baseline updates reviewable and tied to the code change that caused them. State assertions are generally more precise for behavior; image checks are valuable where the user-visible rendering itself is the requirement.
For CI reliability, fix the viewport and browser environment, serve the app from a predictable local URL, seed deterministic input, and ensure the test waits for the app’s own ready state. A screenshot is evidence of what rendered in one environment; it does not by itself prove identical output in every browser or device.
Or skip the browser setup
If the task is to capture a page screenshot rather than exercise your canvas application’s interaction logic, ScreenshotNeo provides a website screenshot API and MCP server. Its API takes a URL and returns an image or PDF; see the API documentation. For example, capture the fixture page with cURL:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Replace the example target with your deployed test page URL. ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.
Sign up for 1,000 free screenshots a month, with no card required.
FAQ
Can Cypress test what is drawn on a canvas?
Yes. It can drive the page and capture the result. For pixel correctness, add a visual comparison workflow; ordinary DOM assertions cannot inspect individual drawn shapes.
Why can’t I select a shape inside a canvas with a normal locator?
The canvas exposes one drawing surface to the DOM. Individual marks are rendered pixels unless the application separately represents them as DOM elements or exposes them through application state.
Should I assert canvas pixels or the drawing model?
Assert both when both matter. Model checks explain behavior and are stable; pixel comparisons catch rendering regressions. Use the least fragile check that still proves the requirement.
Do I need identical baselines for every browser?
That depends on whether cross-browser pixel parity is a requirement. Browser and platform rendering can differ, so define an explicit baseline and comparison policy for the browsers you support.


