Writing Jest Unit Tests for Handsontable
When to Use Unit Tests vs E2E
"E2E" below means a new Playwright test in
tests/e2e/(skillhandsontable-playwright-e2e). Thehandsontable()/selectCell()globals mentioned later are the legacy Jasmine helpers — do not add new*.spec.js.
Favor E2E tests over unit tests. The key rule: if a unit test requires mocking a module, write an E2E test instead. Mocking couples tests tightly to internal module shape, making code resistant to refactoring and extension - every internal restructure forces test updates even when behavior hasn't changed.
- Good unit test candidates: Pure logic, utility functions, data transformations, calculations - anything that needs no mocking.
- Use E2E instead for: DOM interaction, rendering, browser events, visual behavior, and anything that would require mocking modules to test in isolation.
- Anti-pattern: Unit tests that mock internal modules just to achieve isolation. This creates brittle tests that resist refactoring.
Conventions
- File naming: Always
*.unit.js. - Location: Co-located with source in
src/**/__tests__/directories (e.g.,src/plugins/filters/__tests__/filters.unit.js). - Framework: Jest with
jsdomenvironment andjest-jasmine2test runner. - Imports: Explicit imports are required. Unlike E2E tests, there are no auto-injected globals like
handsontable()orselectCell().
Module Name Mapping (jest.config.js)
Jest resolves these aliases automatically:
'handsontable'and'handsontable/...'resolve tosrc/andsrc/...'walkontable'and'walkontable/...'resolve tosrc/3rdparty/walkontable/src/andsrc/3rdparty/walkontable/src/...- CSS/SCSS imports resolve to
test/__mocks__/styleMock.js(returns an empty object).
Pre-configured Mocks
The bootstrap file (test/bootstrap.js) provides these automatically:
ResizeObservermock (test/__mocks__/resizeObserverMock.js).IntersectionObservermock (test/__mocks__/intersectionObserverMock.js).
For custom mocking, use jest.fn() for stubs and jest.spyOn(object, 'method') for spying on existing methods.
Run Commands
- All unit tests:
npm run test:unit --prefix handsontable - Targeted:
npm run test:unit --prefix handsontable --testPathPattern=<regex>- the pattern is matched against test file paths (e.g.filters,ghostTable.unit,metaManager) - Example:
npm run test:unit --prefix handsontable --testPathPattern=filters
Large Dataset Testing
When the code under test handles data arrays, include tests with 50k+ rows. Use forEach loops to populate arrays - never arr.push(...largeArray) (causes stack overflow at scale).
Test Structure Example
import { calculateSomething } from '../utils';
describe('calculateSomething', () => {
it('should return the sum for positive inputs', () => {
expect(calculateSomething(2, 3)).toBe(5);
});
it('should handle zero values', () => {
expect(calculateSomething(0, 0)).toBe(0);
});
it('should throw for invalid input', () => {
expect(() => calculateSomething(null)).toThrow();
});
});
Common Mistakes
- Writing unit tests for DOM or rendering behavior (use E2E instead).
- Covering only the happy path - always test edge cases, error states, and boundary conditions.
- Skipping large dataset tests when the code processes arrays.
- Forgetting to test
updateSettings()andenablePlugin()/disablePlugin()cycles for plugin logic. - Using globals from E2E helpers (
handsontable(),selectCell()) - these are not available in unit tests.
TypeScript Type Tests
Type tests (*.types.ts) verify that the generated declarations in tmp/ are correct after running npm run build:types. They live in src/**/__tests__/ alongside unit and E2E tests — never under test/types/ (that directory holds only the tsconfig.json that drives compilation).
File pattern: *.types.ts — e.g., src/__tests__/common.types.ts, src/__tests__/core/namespace.types.ts
Run: node_modules/.bin/tsc --noEmit -p test/types/tsconfig.json
Key rule — no declare, only real assignments. Every line must be a real const x: Type = actualValue assignment so the compiler verifies assignability against the generated tmp/ types. A declare let x: SomeType bypasses the check entirely.
Cover both access patterns:
// ESM/modular: import from subpath, assign to namespace type
import { DateEditor } from 'handsontable/editors';
const _editor: Handsontable.editors.DateEditor = new DateEditor(hot);
// UMD/namespace: extract constructor from namespace object, instantiate it
const EditorCtor = Handsontable.editors.DateEditor;
const _umdEditor: Handsontable.editors.DateEditor = new EditorCtor(hot);
Negative assertions use @ts-expect-error to prove internal symbols are NOT exported:
// @ts-expect-error SelectionManager is not part of the public API
import type { SelectionManager } from 'handsontable';
Further Reading
handsontable/.ai/TESTING.mdfor the full testing strategy.handsontable/jest.config.jsfor Jest configuration.handsontable/test/__mocks__/for available mock implementations.handsontable/test/bootstrap.jsfor the test setup.