Skip to content

Conversation

@chrisgervang
Copy link
Collaborator

@chrisgervang chrisgervang commented Jan 28, 2026

Summary

This PR implements the exploration phase of the tape-to-vitest migration as outlined in the RFC.

What's included:

  • Vitest configuration with multi-environment support (node, headless browser)
  • Idempotent migration script that converts tape tests to vitest
  • Test-utils updates for vitest compatibility
  • 188 test files converted from tape to vitest

Current test status:

Environment Passed Failed Total
Node 577 69 651
Headless 582 18 605

Known issues requiring follow-up:

1. Deep import paths (prerequisite to migration)
Tests use internal deep imports that are not part of the public API:

@deck.gl/aggregation-layers/gpu-grid-layer/gpu-grid-cell-layer

These should be fixed in the tape tests on master first, then re-run the migration script.

2. Tests for removed components

  • gpu-grid-layer.spec.ts tests GPUGridLayer which was removed from the codebase
  • This test file should be deleted

3. Environment-specific failures

  • Node environment has more failures due to missing WebGL/browser APIs
  • Some tests require real WebGL context (headless browser)
  • ReadableStream polyfill issue in Node for some loader tests

Migration approach

The migration script (scripts/tape-to-vitest-migration.cjs) is idempotent:

  • Reads original tape source from master branch
  • Converts to vitest syntax while preserving assertion messages
  • Can be re-run after fixing issues on master

Typed array equality

Added a custom equality tester to match tape's deepEqual behavior for typed arrays. This is marked as TODO for removal once tests are updated to use explicit comparisons.

Next steps (per RFC)

  1. Fix deep import paths in tape tests on master
  2. Delete obsolete test files (GPUGridLayer)
  3. Re-run migration script
  4. Address remaining environment-specific failures
  5. Move to implementation phase

🤖 Generated with Claude Code


Note

Medium Risk
Touches the project-wide test runner/CI pipeline and introduces Playwright-based browser testing plus new image-processing deps, which may cause CI breakage or platform-specific flakiness despite minimal impact on runtime code.

Overview
Switches the repo’s primary test runner from tape/ocular to Vitest by adding a multi-project vitest.workspace.ts (node smoke tests + Playwright-backed headless/headed browser suites) and updating package scripts (test, test-fast, test-headless, test-browser, test-ci) to use it.

Mass-converts module tests to Vitest syntax (replacing tape-* imports/assertions with vitest’s test/expect) and adds new Vitest-based interaction test entrypoints under test/interaction/.

Updates @deck.gl/test-utils for Vitest compatibility, replacing @probe.gl/test-utils spies with vi.spyOn/mock APIs, adding browser driver type declarations, and adjusting interaction runner event handling.

CI/dev plumbing: GitHub Actions now installs Playwright Chromium and Puppeteer Chrome before running yarn test-ci; new deps are added for Vitest browser mode and screenshot diffing (@vitest/browser, playwright, pixelmatch, sharp, etc.), .gitignore ignores screenshot outputs, and .yarnrc is added to ignore engine constraints.

Written by Cursor Bugbot for commit a4ffad0. This will update automatically on new commits. Configure here.

chrisgervang and others added 4 commits January 28, 2026 10:00
- Add vitest.config.ts and vitest.workspace.ts for multi-environment testing
- Add test/setup/ with node and browser setup files
- Setup files include typed array equality tester for comparing Float32Array with plain arrays
- Update package.json with vitest dependencies
- Update .gitignore for vitest cache

Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) <[email protected]>
- Add idempotent migration script that reads tape source from master branch
- Script converts tape assertions to vitest equivalents while preserving assertion messages
- Update test-utils to work with vitest
- Update RFC with migration approach details

The migration script handles:
- Import statement conversion (tape-catch/tape-promise → vitest)
- Assertion conversions (t.ok → toBeTruthy, t.equal → toBe, etc.)
- Preserves assertion messages using vitest's expect(value, 'message') syntax
- Handles edge cases like t.pass/t.fail in arrow functions
- Properly converts t.true/t.false to toBeTruthy/toBeFalsy (tape semantics)

Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) <[email protected]>
Convert 188 test files from tape to vitest using the migration script.

Changes include:
- Replace tape imports with vitest (test, expect, describe)
- Convert tape assertions to vitest equivalents
- Remove tape test function wrappers
- Preserve assertion messages for better debugging

Test status after conversion:
- 467 test files passed (1740 tests)
- 91 test files still need attention (106 tests)

Remaining failures are primarily:
- Tests for removed components (GPUGridLayer)
- Environment-specific issues (Node vs browser WebGL)
- Tests requiring additional migration work

Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) <[email protected]>
Mark the custom typed array equality tester as temporary. Once all tests
are updated to use explicit typed array comparisons (e.g.,
expect(Array.from(typedArray)).toEqual([...])), this custom tester
should be removed to enforce stricter type checking in tests.

Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) <[email protected]>
'Throws on invalid Kepler schema version'
);
t.end();
expect(() => parseMap(json), /Only support Kepler v1/).toThrow();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regex passed to expect() instead of toThrow()

Medium Severity

The tape t.throws(fn, /regex/, 'message') conversion is incorrect. The regex /Only support Kepler v1/ that should match the thrown error message is passed as the second argument to expect() (which is the custom failure message), not to .toThrow(). The correct vitest syntax is expect(fn).toThrow(/regex/). This test will pass if ANY error is thrown, rather than verifying the specific error message.

Fix in Cursor Fix in Web

GPUGridLayer was removed from the codebase. This test file
references internal paths that no longer exist.

Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) <[email protected]>
Vitest's MockInstance doesn't have a `.called` property like sinon spies.
Updated the migration script to convert to idiomatic vitest assertions:
- expect(spy.called).toBeTruthy() → expect(spy).toHaveBeenCalled()
- expect(spy.called).toBeFalsy() → expect(spy).not.toHaveBeenCalled()

Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) <[email protected]>
The @vitest/browser package depends on msw which depends on [email protected]
that requires Node 20+. Since deck.gl supports Node 14+ at runtime but
vitest is only needed for development, we ignore engine checks to allow
installation on older Node versions.

Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) <[email protected]>
Update migration script to only import describe when actually used,
and run prettier after conversion for consistent formatting.

Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) <[email protected]>
The import/namespace rule cannot parse vite 5.x which is brought in by
vitest (master uses vite 4.x from @vis.gl/dev-tools).

Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) <[email protected]>
chrisgervang and others added 3 commits January 28, 2026 21:05
Instead of removing t.pass() calls, convert them to console.log()
to preserve the pass messages for debugging.

Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) <[email protected]>
Update migration script to convert test.skip/test.only signatures
from (t =>) to (() =>).

Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) <[email protected]>
Pass msg parameter to expect() so assertion messages are displayed
on test failure: expect(cond, msg).toBeTruthy()

Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) <[email protected]>
Convert test('name', async t => expr) patterns to remove unused t
parameter: test('name', async () => expr)

Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) <[email protected]>
test(`getTextAccessor#${textLabelField.type}`, () => {
const accessor = getTextAccessor(textLabelField, [data]);
t.deepEquals(accessor(data), expected, `getTextAccessor correctly returns ${expected}`);
t.end();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused test data array TEXT_PIXEL_OFFSET_TESTS

Low Severity

The TEXT_PIXEL_OFFSET_TESTS constant defines an array of test cases but is never used. Unlike the other test data arrays in the file (COLOR_TESTS, SIZE_TESTS, TEXT_TESTS) which all have corresponding for...of loops that generate tests, TEXT_PIXEL_OFFSET_TESTS has no such test loop. This dead code adds confusion and maintenance burden.

Fix in Cursor Fix in Web

The CI was failing because Playwright browsers weren't installed and
the test command was using a non-existent "ci" vitest project. This
fix adds the Playwright installation step and uses the proper headless
project with coverage enabled.

Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) <[email protected]>
"@luma.gl/core": "~9.2.6",
"@luma.gl/engine": "~9.2.6",
"@probe.gl/test-utils": "^4.1.0"
"vitest": "^2.1.0"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're stuck on vitest 2 until we upgrade from node 18.

"@deck.gl/core": "~9.2.0",
"@luma.gl/core": "~9.2.6",
"@luma.gl/engine": "~9.2.6",
"@probe.gl/test-utils": "^4.1.0"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The RFC outlines the goal being to implement a backwards-compatibility layer and deprecation warnings in @deck.gl/test-utils. I'm not sure what users use it for, but gets a significant number of downloads still so we need to not break anything.

I'm thinking we'll need to keep some old test infrastructure around for this module to ensure correctness on both tape and vitest until deck v10

Temporarily, I've removed tape and probe.gl from deck's test utils until I get all tests to pass without the extra complexity.

package.json Outdated
"test": "ocular-test",
"test-fast": "ocular-lint && ocular-test node",
"test": "vitest run",
"test-node": "vitest run --project node",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trying to run all of the tests in the node environment resulted in a lot of failures (which is not a surprise), so I'm going to instead implement the same test config for node that we have on master, which is basically a smoke-test.

I'll keep test-fast but remove test-node

"publish-beta": "ocular-publish version-only-beta",
"publish-prod": "ocular-publish version-only-prod",
"start": "open https://deck.gl/docs/get-started/getting-started",
"test": "ocular-test",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need ocular-test anymore. Vitest seems to offer everything you need to run tests.

Please let me know if ocular-test did something internally that we lose because we're removing it

"jsdom": "^20.0.0",
"playwright": "^1.58.0",
"pre-commit": "^1.2.2",
"puppeteer": "^24.26.1",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hoping to remove puppeteer after all tests pass

// import test from 'tape-promise/tape' -> import {test, expect, describe} from 'vitest'
// import test from 'tape-catch' -> import {test, expect, describe} from 'vitest'
// import test from 'tape' -> import {test, expect, describe} from 'vitest'
result = result.replace(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using a string replace have you considered something like https://github.com/facebook/jscodeshift ?

That way you can operate on the syntax tree and it's a little less hairy than trying to use a regex to parse

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is one-time-use and already working reliably, I don’t see a strong reason to change it right now - but I’m open to revisiting if this there's something claude can't figure out

The vitest glob patterns picked up tests that were commented out or
never imported in the original test suite:
- path-tesselator.spec.ts (commented out in layers/index.ts)
- polygon-tesselation.spec.ts (commented out in layers/index.ts)
- geocoders.spec.ts (never imported, no widgets index)

Also fix floating point precision in geocoders test using toBeCloseTo.

Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) <[email protected]>

// Remove spies
Object.keys(spyMap).forEach(k => spyMap[k].reset());
Object.keys(spyMap).forEach(k => spyMap[k].mockClear());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spy cleanup uses mockClear instead of mockRestore

Medium Severity

The spy cleanup after each test case uses mockClear() instead of mockRestore(). In vitest, mockClear() only clears call history but leaves the spy attached to the prototype. When different test cases spy on different methods, old spies accumulate on Object.getPrototypeOf(layer). If a later test case calls vi.spyOn on an already-spied method, vitest wraps the existing spy with another spy layer, potentially causing nested spies, incorrect call counts, or performance degradation across test iterations.

Additional Locations (1)

Fix in Cursor Fix in Web

chrisgervang and others added 2 commits January 29, 2026 16:19
- Rename node-only tests to *.node.spec.ts convention
- Configure vitest workspace with three projects:
  - node: smoke tests (*.node.spec.ts only)
  - headless: unit tests in headless browser
  - browser: full suite in headed browser for local dev
- Update package.json scripts:
  - test: node + headless + render (full suite)
  - test-fast: lint + node smoke tests
  - test-headless: browser unit tests only
  - test-render: render/interaction tests only
  - test-ci: node + headless + coverage + render
  - test-browser: headed browser + render
- Update CI to use test-ci and install Puppeteer browsers
- Comment out module tests in test/browser.ts so test-render
  only runs render and interaction tests

Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) <[email protected]>
- Add command mapping matrix (old → new)
- Add command matrix showing what each script runs
- Update file naming convention to *.node.spec.ts
- Update vitest workspace config to match implementation
- Document Phase 4 outcome: ~95% tests need browser environment
- List node smoke tests and excluded tests

Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) <[email protected]>
chrisgervang and others added 6 commits January 29, 2026 19:26
- Update migration script to convert makeSpy from @probe.gl/test-utils
  to vi.spyOn from vitest
- Re-run migration on affected test files to fix util.inherits error
  that occurred when @probe.gl/test-utils was loaded in browser
- Fix jupyter-widget test that used t.describe() (converted to proper
  describe/test structure)
- Add exclusions for unconverted render/interaction tests

8 test files converted from makeSpy to vi.spyOn:
- attribute.spec.ts, layer.spec.ts, memoize.spec.ts
- collision-filter-effect.spec.ts, terrain-effect.spec.ts
- google-maps-overlay.spec.ts, json-converter.spec.ts
- google-maps-utils.spec.ts

Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) <[email protected]>
Migration script now converts probe.gl spy methods to vitest:
- spy.restore() -> spy.mockRestore()
- spy.reset() -> spy.mockReset()

Applies to all test files, not just those importing makeSpy directly
(includes tests using @deck.gl/test-utils's testLayer spies).

Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) <[email protected]>
The memoize test used spy.called with .toBe(boolean) which doesn't
work with vitest spies. Converted to proper toHaveBeenCalled() /
not.toHaveBeenCalled() matchers.

Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) <[email protected]>
- Add hasNestedTTest flag captured BEFORE Step 1b converts t.test() to test()
- Add Step 1b to convert t.test()/t0.test() etc. early in the conversion pipeline
- Fix Step 2a to use hasNestedTTest flag instead of inline regex check on result
- This correctly converts: test('name', t => { t.test('nested', ...) })
  to: describe('name', () => { test('nested', ...) })

Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) <[email protected]>
- Add Step 14c to migration script for spy.called pattern conversion
- Fix memoize.spec.ts to use manual call tracking instead of vi.spyOn
  (vi.spyOn has issues in vitest browser mode with call-through)
- Note: geocoders.spec.ts toBeCloseTo fix was already in place

Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) <[email protected]>
Files with manual fixes that can't be expressed as general migration
rules are now skipped by the migration script to prevent overwriting:
- memoize.spec.ts: manual call tracking (vi.spyOn browser issues)
- geocoders.spec.ts: toBeCloseTo for floating point DMS comparisons

Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) <[email protected]>
result = result.replace(
/expect\(([^,)]+)\.called\)\.toBeFalsy\(\)/g,
'expect($1).not.toHaveBeenCalled()'
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate spy.called conversion logic in migration script

Low Severity

Step 8b (lines 310-328) and Step 14c (lines 433-470) both convert the same expect(spy.called).toBeTruthy() and expect(spy.called).toBeFalsy() patterns to toHaveBeenCalled(). Step 8b handles these patterns, then Step 14c re-handles them again with slightly different regex patterns. The redundant code could be consolidated into a single location. Step 14c adds .toBe(true) and .toBe(false) variants that Step 8b lacks, suggesting Step 8b could simply be removed entirely if Step 14c covers all cases.

Additional Locations (1)

Fix in Cursor Fix in Web

chrisgervang and others added 2 commits January 29, 2026 20:21
Add Step 14d to migration script to convert probe.gl spy.callCount
pattern to vitest's toHaveBeenCalledTimes matcher:
- expect(spy.callCount).toBe(n) -> expect(spy).toHaveBeenCalledTimes(n)
- expect(spy.callCount, 'msg').toBe(n) -> expect(spy, 'msg').toHaveBeenCalledTimes(n)

Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) <[email protected]>
Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) <[email protected]>
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

chrisgervang and others added 6 commits January 29, 2026 20:49
…and google-maps tests

Re-ran migration script on files that were missed in previous conversion.

Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) <[email protected]>
- path-layer-vertex.spec.ts (Transform not exported from @luma.gl/engine)
- collision-filter.spec.ts (collision-filter extension test)

Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) <[email protected]>
Add custom browser commands for image comparison testing:
- compareImage: pixel-level image comparison using pixelmatch
- Type definitions for browser command interface

Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) <[email protected]>
Scaffold vitest specs for interaction and render tests:
- map-controller.spec.ts: MapController interaction tests
- picking.spec.ts: Picking interaction tests
- render/index.spec.ts: Render test runner entry point

Note: These are currently excluded in vitest.workspace.ts as they
need further conversion work.

Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) <[email protected]>
- CONTRIBUTING.md: Add vitest test commands documentation
- vitest-migration-rfc.md: Update with phase 5 outcomes and status

Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) <[email protected]>
Add dependencies for browser-based image comparison testing:
- pixelmatch: pixel-level image comparison
- pngjs: PNG encoding/decoding
- sharp: image processing

Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants