Visual Regression Testing with Playwright
Overview
Visual tests live in visual-tests/ and use Playwright with TypeScript. Screenshots are compared against golden records by reg-suit to detect visual regressions. There are two workflows: (a) add visual tests for existing examples, and (b) create a new example in examples/next/docs/ and write visual tests for it.
Custom Fixture: tablePage
Import the custom test runner instead of the default Playwright test:
import { test } from '../../../src/test-runner';
The tablePage fixture (defined in visual-tests/src/test-runner.ts) automatically:
- Navigates to the demo page.
- Waits for the page to load and the table to appear.
- Disables CSS animations and transitions for consistent screenshots.
The fixture provides a standard Playwright Page object, so all Playwright APIs work as expected.
Test Naming
Use __filename as the first argument to test(). The runner auto-generates the test title from the file path. File names should be kebab-case and describe what is being tested (e.g., apply-active-class-name-nested-header.spec.ts).
Screenshot Workflow
Every test follows this pattern: perform setup/actions, then capture a screenshot. You can take multiple screenshots per test.
import { test } from '../../../src/test-runner';
import { helpers } from '../../../src/helpers';
import { selectCell } from '../../../src/page-helpers';
test(__filename, async({ tablePage }) => {
const cell = await selectCell(0, 2);
await cell.click();
await tablePage.screenshot({ path: helpers.screenshotPath() });
});
Always use helpers.screenshotPath() for the path argument. It auto-generates unique, deterministic file names based on the test file path, browser, framework, and screenshot index. Using any other naming approach will break the comparison, which matches screenshots by path.
Test Organization
visual-tests/tests/js-only/-- Tests that run only against the vanilla JS examples.visual-tests/tests/multi-frameworks/-- Tests that run against JS, React, Angular, and Vue examples.visual-tests/tests/cross-browser/-- Tests that verify cross-browser rendering consistency.
Use test.skip(helpers.hotWrapper !== 'js', '...') to restrict a test to a specific framework.
Navigating to Custom Pages
For tests targeting a specific example (not the default demo), use the goto fixture:
test(__filename, async({ goto, tablePage }) => {
await goto(
helpers.setBaseUrl('/my-custom-demo').getFullUrl()
);
await tablePage.screenshot({ path: helpers.screenshotPath() });
});
Available Helpers
visual-tests/src/helpers.ts--screenshotPath(),selectors(CSS selectors for table parts),findCell(),cssPath().visual-tests/src/page-helpers.ts--selectCell(),openEditor(),openContextMenu(),createSelection(),openHeaderDropdownMenu(),filterByValue(),filterByCondition(),scrollTableToTheBottom(),takeScreenshot(), and more.
Running Tests
Check visual-tests/package.json for available scripts. Configuration files:
visual-tests/playwright.config.ts-- Default config.visual-tests/playwright-cross-browser.config.ts-- Cross-browser config.
Example Structure
When creating a new example at examples/next/docs/js/demo/, include:
index.html-- HTML entry point.package.json-- Dependencies (Handsontable, Vite).vite.config.js-- Vite configuration.src/-- Source files for the example.spec/-- Spec metadata for the example.
Common Mistakes
- Not using
helpers.screenshotPath()-- breaks screenshot matching, which is purely path-based. - Forgetting to wait for animations or async rendering before taking a screenshot.
- Placing JS-only tests in
multi-frameworks/or vice versa. - Not disabling animations -- the
tablePagefixture handles this, but if you usegototo navigate to a different page, animations are still disabled automatically by the fixture. - Taking screenshots before the table is fully rendered --
tablePagewaits for the table, but dynamic content (menus, dropdowns) may need explicit waits.