Back to Deck Gl

RFC: Migrate from Tape to Vitest

dev-docs/RFCs/proposals/vitest-migration-rfc.md

9.3.228.5 KB
Original Source

RFC: Migrate from Tape to Vitest

  • Author: Chris Gervang
  • Date: January 2026
  • Status: Draft

Overview

This RFC proposes migrating deck.gl's test infrastructure from tape (assertion framework) + ocular-test (test runner from @vis.gl/dev-tools) to vitest (which serves as both runner and assertion library).

The migration aims to:

  • Modernize the test infrastructure with a widely-adopted, actively maintained framework
  • Improve developer experience with better error messages, watch mode, and IDE integration
  • Reduce complexity by consolidating runner and assertions into a single tool
  • Maintain the same CLI commands for backwards compatibility

Background

Current Architecture

ocular-test (runner from @vis.gl/dev-tools)
├── Vite (dev server for browser tests)
├── BrowserTestDriver (@probe.gl/test-utils)
├── c8 (coverage)
└── tape (assertions via tape-promise/tape)

Entry points:

  • test/node.ts - Minimal smoke test (only imports-spec + core-layers.spec)
  • test/browser.ts - Comprehensive - runs ALL tests (./modules + ./render + ./interaction)

Important architectural note: The previous design intentionally ran all tests in the browser (source of truth for a WebGL library), with Node serving only as a smoke test.

Current test commands:

  • yarn test - runs ocular-test
  • yarn test-fast - runs ocular-lint && ocular-test node
  • yarn cover - runs ocular-test cover

Pain Points

  1. Fragmented tooling: Test runner (ocular-test), assertions (tape), coverage (c8) are separate tools
  2. Tape is minimalist: Limited error messages, no built-in mocking, requires wrappers like tape-promise
  3. Custom infrastructure: BrowserTestDriver requires custom hooks (window.browserTestDriver_finish)
  4. Developer experience: No watch mode, no IDE integration for running individual tests

Proposal

Replace ocular-test and tape with vitest:

vitest (runner + assertions)
├── Vite (built-in - same foundation as ocular-test)
├── Playwright (browser mode - replaces BrowserTestDriver)
├── v8 coverage (built-in)
└── expect() assertions (replaces tape)

Why Vitest?

  1. Built on Vite: Same bundler that ocular-test uses, ensuring compatibility
  2. All-in-one: Runner, assertions, mocking, coverage in a single package
  3. Modern DX: Watch mode, parallel execution, better error messages
  4. Industry standard: Widely adopted, well-documented, actively maintained
  5. TypeScript-first: Native TypeScript support without additional configuration

Multi-Environment Architecture

We adopt a hybrid approach using vitest workspaces:

  • Browser runs ALL tests (source of truth for correctness)
  • Node runs pure unit tests (fast feedback during development)

This preserves the previous design philosophy where browser tests are comprehensive, while adding fast local iteration via Node.

File naming convention:

PatternDescription
*.node.spec.tsNode-only smoke tests (fast, no WebGL)
*.spec.tsBrowser tests (WebGL, real DOM, etc.)

Vitest workspace configuration:

typescript
// vitest.workspace.ts
export default defineWorkspace([
  // Node project - simple smoke tests (*.node.spec.ts only)
  {
    test: {
      name: 'node',
      environment: 'node',
      include: ['test/modules/**/*.node.spec.ts'],
      setupFiles: ['./test/setup/vitest-node-setup.ts']
    }
  },
  // Headless project - unit tests in headless browser (CI)
  {
    test: {
      name: 'headless',
      include: ['test/modules/**/*.spec.ts'],
      exclude: ['test/modules/**/*.node.spec.ts'],
      browser: {
        enabled: true,
        name: 'chromium',
        provider: 'playwright',
        headless: true
      }
    }
  },
  // Browser project - full suite in headed browser (local dev)
  {
    test: {
      name: 'browser',
      include: ['test/modules/**/*.spec.ts'],
      exclude: ['test/modules/**/*.node.spec.ts'],
      browser: {
        enabled: true,
        name: 'chromium',
        provider: 'playwright',
        headless: false
      }
    }
  }
]);

Why Playwright Instead of Puppeteer?

Vitest browser mode only supports Playwright or WebdriverIO as providers - Puppeteer is not an option. This is actually beneficial:

  • Playwright has better parallel execution
  • Native TypeScript support
  • More robust browser automation APIs
  • Better cross-browser testing support

The existing Puppeteer usage (via @probe.gl/test-utils BrowserTestDriver) will be replaced with Playwright's native APIs.

API Changes

Test file changes:

typescript
// Before (tape)
import test from 'tape-promise/tape';

test('color#parseColor', t => {
  const result = parseColor([127, 128, 129]);
  t.deepEqual(result, [127, 128, 129, 255], 'expected result');
  t.end();
});

// After (vitest)
import {test, expect} from 'vitest';

test('color#parseColor', () => {
  const result = parseColor([127, 128, 129]);
  expect(result).toEqual([127, 128, 129, 255]);
});

Assertion mapping:

tapevitest
t.ok(value)expect(value).toBeTruthy()
t.notOk(value)expect(value).toBeFalsy()
t.equal(a, b) / t.is(a, b)expect(a).toBe(b)
t.deepEqual(a, b)expect(a).toEqual(b)
t.throws(fn)expect(fn).toThrow()
t.end()(not needed)

CLI Commands

Command Mapping (Old → New)

Old CommandNew CommandNotes
yarn testyarn testNow runs node + headless + render
yarn test-fastyarn test-fastSame: lint + node smoke test
yarn coveryarn test-headless --coverageRedundant script removed
yarn test ciyarn test-ciExplicit CI command
(none)yarn test-headlessNew: browser unit tests only
(none)yarn test-renderNew: render/interaction tests only
(none)yarn test-browserNew: headed browser + render

Command Matrix

CommandLintNodeHeadlessCoverageRender
test
test-fast
test-headless
test-render
test-ci
test-browser✓ (headed)

Note: Coverage can be added to any vitest command with --coverage.

Scripts

json
{
  "scripts": {
    "test": "vitest run --project node --project headless && npm run test-render",
    "test-fast": "ocular-lint && vitest run --project node",
    "test-headless": "vitest run --project headless",
    "test-render": "ocular-test browser-headless",
    "test-ci": "vitest run --project node --project headless --coverage && npm run test-render",
    "test-browser": "vitest run --project browser && npm run test-render"
  }
}

File Naming Convention

PatternEnvironmentUse Case
*.node.spec.tsNode onlyFast smoke tests (imports, basic logic)
*.spec.tsBrowser (headless/headed)Full test suite with WebGL, DOM

@deck.gl/test-utils Updates

The @deck.gl/test-utils module uses spies for testing layer lifecycle methods. To make test-utils framework-agnostic and avoid module loading issues (vitest imports hanging in Node), we use an Injectable Spy API.

Injectable Spy API

Users pass their own createSpy function instead of test-utils importing test frameworks directly:

typescript
// vitest user
import {testLayer} from '@deck.gl/test-utils';
import {vi} from 'vitest';

await testLayer({
  createSpy: (obj, method) => vi.spyOn(obj, method),
  Layer: ScatterplotLayer,
  testCases: [...]
});

// tape/probe.gl user
import {testLayer} from '@deck.gl/test-utils';
import {makeSpy} from '@probe.gl/test-utils';

await testLayer({
  createSpy: makeSpy,
  Layer: ScatterplotLayer,
  testCases: [...]
});

Why Injectable?

Importing vitest outside of a vitest test context causes "Vitest failed to access its internal state" errors and hangs. By making createSpy injectable, test-utils has zero test framework dependencies and works in any environment.

Backward Compatibility

For existing tape users who don't pass createSpy, test-utils lazily imports @probe.gl/test-utils with a deprecation warning:

typescript
// Internal fallback (only used if createSpy not provided)
async function getDefaultSpyFactory() {
  try {
    const {makeSpy} = await import('@probe.gl/test-utils');
    console.warn(
      '[@deck.gl/test-utils] Implicit @probe.gl/test-utils usage is deprecated. ' +
      'Pass createSpy option explicitly.'
    );
    return makeSpy;
  } catch {
    throw new Error('createSpy option required.');
  }
}

Peer Dependencies

@probe.gl/test-utils is a required peer dependency because the default entry point (@deck.gl/test-utils) has a static import from tape.ts. Only vitest is optional (for the ./vitest sub-entry):

json
"peerDependencies": {
  "@probe.gl/test-utils": "^4.1.1",
  "vitest": "^4.0.18"
},
"peerDependenciesMeta": {
  "vitest": { "optional": true }
}

Note: Users who only use @deck.gl/test-utils/vitest don't technically need @probe.gl/test-utils, but it remains required for backward compatibility with existing 9.x users.

User Experience Matrix

UsercreateSpy provided?Result
Vitest userYes (vi.spyOn)Works, no warnings
Tape user (new)Yes (makeSpy)Works, no warnings
Tape user (existing)NoWorks with deprecation warning
Neither installedNoClear error message

Deprecation Timeline

VersionBehavior
9.3.xcreateSpy optional, defaults to probe.gl with warning
10.0.0createSpy required, remove probe.gl fallback

Tape Backward Compatibility Testing

A smoke test verifies the probe.gl fallback path in CI:

bash
DECK_TEST_UTILS_USE_PROBE_GL=1 yarn test-tape-compat

Implementation Plan

Phase 1: Infrastructure Setup

1.1 Install dependencies:

bash
yarn add -D @vitest/browser @vitest/browser-playwright playwright

Node 18 Compatibility: Confirmed - Vitest 2.1.9 requires ^18.0.0 || >=20.0.0, Playwright requires >=18.

1.2 Update vitest.config.ts with workspace projects (see Multi-Environment Architecture above)

1.3 Create setup files:

  • test/setup/vitest-node-setup.ts - JSDOM polyfills (from current test/node.ts)
  • test/setup/vitest-browser-setup.ts - Minimal (browser provides DOM)

1.4 Add npm scripts for each environment

Phase 2: Update @deck.gl/test-utils

  • Implement dual-API spy abstraction (vitest preferred, probe.gl as fallback)
  • Add both vitest and @probe.gl/test-utils as optional peer dependencies
  • Add deprecation warning for probe.gl path
  • Create tape backward compatibility smoke test (test/smoke/tape-compat.spec.ts)
  • Add DECK_TEST_UTILS_USE_PROBE_GL environment variable for testing fallback path

Phase 3: Migrate Test Files (~185 files)

  • Convert tape imports to vitest
  • Transform assertions
  • Remove t.end() calls
  • Update callback patterns (onError: t.notOkonError: (err) => expect(err).toBeFalsy())

Phase 4: Discovery - Run Node Tests and Identify Browser Dependencies

The hybrid approach serves as a discovery mechanism:

  1. Run yarn test-node and observe failures
  2. Failures reveal browser-only dependencies:
    • WebGL/GPU operations (@luma.gl/*)
    • Real DOM APIs not in JSDOM
    • Browser-specific APIs (fetch quirks, Web Workers)
    • Dependencies that check typeof window
    • Canvas 2D context beyond JSDOM's mock

Decision point after discovery:

  • Few failures (~10-20%) → Keep hybrid, rename failures to .browser.spec.ts
  • Many failures (~50%+) → Fall back to browser-only approach

Outcome: Nearly all tests (~95%+) require browser environment due to WebGL/luma.gl dependencies. We adopted a simplified approach:

  • Node project: Only runs *.node.spec.ts files (smoke tests)
  • Browser projects: Run all other *.spec.ts files

Node smoke tests (2 files):

  • imports.node.spec.ts - Verifies module exports
  • core-layers.node.spec.ts - Basic layer instantiation

Excluded tests (need fixes before inclusion):

  • path-tesselator.spec.ts - Was commented out in original suite
  • polygon-tesselation.spec.ts - Was commented out in original suite
  • geocoders.spec.ts - Never imported in original suite

Phase 5: Migrate Snapshot & Interaction Tests

Current state:

  • 35 test files in test/render/ with 150 golden images
  • 3 test files in test/interaction/
  • Both use tape + probe.gl's BrowserTestDriver (Puppeteer)
  • SnapshotTestRunner uses window.browserTestDriver_captureAndDiffScreen
  • InteractionTestRunner uses window.browserTestDriver_emulateInput

5.1 Convert to vitest syntax:

  • Replace import test from 'tape' with import {test, expect} from 'vitest'
  • Update assertion syntax

5.2 Update SnapshotTestRunner for Playwright:

  • Replace browserTestDriver_captureAndDiffScreen with Playwright's page.screenshot()
  • Use @vitest/browser's page context
  • Keep golden image comparison logic

5.3 Update InteractionTestRunner for Playwright:

  • Replace browserTestDriver_emulateInput with Playwright APIs:
    • page.mouse.move(), page.mouse.click(), page.keyboard.press()

5.4 Add to browser project:

typescript
{
  name: 'browser',
  include: [
    'test/modules/**/*.spec.ts',
    'test/render/**/*.spec.ts',      // Add render tests
    'test/interaction/**/*.spec.ts'  // Add interaction tests
  ]
}

Files to modify:

  • modules/test-utils/src/snapshot-test-runner.ts
  • modules/test-utils/src/interaction-test-runner.ts
  • test/render/index.jstest/render/index.spec.ts
  • test/interaction/index.jstest/interaction/index.spec.ts

Outcome:

Custom vitest browser commands created in test/setup/browser-commands.ts:

  • captureAndDiffScreen - Takes screenshots via Playwright, compares with golden images using sharp + pixelmatch
  • emulateInput - Emulates mouse/keyboard events via Playwright's Frame API
  • isHeadless - Returns browser headless mode status

Test runners migrated:

  • SnapshotTestRunner now uses commands.captureAndDiffScreen() instead of window.browserTestDriver_captureAndDiffScreen()
  • InteractionTestRunner now uses commands.emulateInput() instead of window.browserTestDriver_emulateInput()
  • TestRunner base class updated to remove probe.gl dependencies

New test entry points created:

  • test/render/index.spec.ts - Vitest version of render tests
  • test/interaction/index.spec.ts - Vitest version of interaction tests
  • test/interaction/map-controller.spec.ts - MapController tests using expect()
  • test/interaction/picking.spec.ts - Picking tests using expect()

Interaction tests passing - All 10 MapController tests and 1 Picking test pass.

Render tests need additional work:

  • "Unimplemented type: 4" errors from PNG processing in vitest browser workers
  • Temporarily excluded from vitest workspace until resolved
  • May require regenerating golden images or fixing client-side dependencies

Type declarations added:

  • test/setup/vitest-browser-commands.d.ts - Extends @vitest/browser/context BrowserCommands interface

Dependencies added:

  • pixelmatch - Image comparison
  • pngjs - PNG parsing (not used directly, but required by some deps)
  • sharp - Robust image processing (handles various PNG color types)

Phase 6: Cleanup

  • Remove tap-spec, tape-catch dependencies
  • Remove test and test-browser entry points from .ocularrc.js (now using vitest)
  • Keep tape-compat, bench, bench-browser, and size entries in .ocularrc.js
  • Add yarn test-tape-compat to GitHub CI workflow
  • Delete test/browser.ts, .nycrc
  • Keep test/node.ts (used by vitest node setup)

Phase 7: Migrate Benchmarks and Size Tests (Future)

Remaining ocular-test entry points that still need migration:

javascript
// .ocularrc.js
entry: {
  'tape-compat': 'test/smoke/tape-compat.spec.ts',  // Keep for backward compat testing
  bench: 'test/bench/index.js',                 // TODO: Migrate to vitest bench
  'bench-browser': 'test/bench/browser.html',   // TODO: Migrate to vitest bench
  size: 'test/size/import-nothing.js'           // TODO: Migrate to vitest
}

7.1 Benchmark Migration:

Vitest has built-in benchmark support via vitest bench:

typescript
// Before (probe.gl/bench)
import {Bench} from '@probe.gl/bench';
const bench = new Bench()
  .add('parseColor', () => parseColor([127, 128, 129]));
bench.run();

// After (vitest bench)
import {bench, describe} from 'vitest';

describe('color', () => {
  bench('parseColor', () => {
    parseColor([127, 128, 129]);
  });
});

Commands:

json
{
  "bench": "vitest bench",
  "bench-browser": "vitest bench --browser"
}

7.2 Size Test Migration:

Size tests verify bundle sizes don't regress. Options:

  1. Use vitest with custom reporter that measures bundle size
  2. Keep as separate script using esbuild --analyze
  3. Integrate with size-limit package

Note: This phase is lower priority as benchmarks and size tests are run manually, not in CI.

Scope

  • ~185 test files in test/modules/
  • ~2800 assertions to convert
  • 1 test utility module (@deck.gl/test-utils)
  • 35 render test files with 150 golden images
  • 3 interaction test files

Verification

  1. yarn test-node - runs unit tests in Node (fast feedback)
  2. yarn test-browser - runs ALL tests in Chromium (unit + render + interaction)
  3. yarn test-headless - runs ALL tests headlessly (CI)
  4. Render tests: Golden image comparison passes for all 150 images
  5. Interaction tests: Controller/picking tests pass

Risks and Mitigations

RiskMitigation
Browser tests may behave differentlyVitest browser mode uses Playwright, similar to current Puppeteer-based setup
Coverage format changesVitest v8 provider outputs lcov format, same as current setup
Breaking changes for external consumers of test-utilsAdd vitest as peer dependency, document migration
Many tests fail in Node environmentDiscovery phase allows fallback to browser-only approach (Option A)
Puppeteer → Playwright migration breaks snapshot comparisonVitest requires Playwright; will need to regenerate golden images if pixel differences occur
CI takes longer (running tests twice)Node tests are fast; browser failures are the blocking check

Known Issues and Technical Debt

luma.gl Async Shader Error Reporting (WebGL Program Cleanup)

Problem: Vitest caught 18 unhandled promise rejections during test runs:

TypeError: Failed to execute 'getProgramInfoLog' on 'WebGL2RenderingContext':
parameter 1 is not of type 'WebGLProgram'.

Root Cause: luma.gl compiles shaders asynchronously using KHR_parallel_shader_compile. When shader compilation has errors, the error reporting code runs asynchronously:

javascript
// In @luma.gl/webgl webgl-render-pipeline.js
const linkErrorLog = this.device.gl.getProgramInfoLog(this.handle);
this.device.reportError(new Error(`${errorType}: ${linkErrorLog}`), this)();

When test cleanup (layerManager.finalize()) destroys WebGL resources before this async error reporting completes, this.handle becomes invalid, causing getProgramInfoLog to throw.

Why Tape Didn't Report This: The tape/probe.gl test runner didn't catch unhandled promise rejections as aggressively as vitest does. The underlying race condition existed but was silently ignored.

Current Fix: Added cleanupAfterLayerTestsAsync() which yields to the event loop before destroying resources:

typescript
async function cleanupAfterLayerTestsAsync(resources: TestResources): Promise<Error | null> {
  layerManager.setLayers([]);

  // Wait for pending async operations (luma.gl shader error handling)
  await new Promise(resolve => setTimeout(resolve, 0));

  layerManager.finalize();
  deckRenderer.finalize();
  // ...
}

This is used by testLayerAsync. With vitest's isolate: false configuration, async tests sprinkled throughout the suite provide "sync points" that flush pending promises from preceding synchronous tests.

Limitation: This fix is somewhat fragile because it relies on:

  1. isolate: false - All tests run sequentially in the same browser context
  2. Async tests scattered throughout the suite providing natural delays
  3. Test execution order remaining stable

If someone enables isolate: true or changes test ordering significantly, the unhandled rejections may reappear.

Follow-up Task: Make testLayer async to properly await cleanup:

ApproachEffortImpact
Current (async cleanup in testLayerAsync only)DoneWorks but relies on test ordering
Make testLayer return Promise<void>~39 filesRobust, explicit cleanup

The robust fix requires:

  1. Change testLayer to use cleanupAfterLayerTestsAsync
  2. Update all 39 test files to use async test functions and await testLayer()

Example migration:

typescript
// Before
test('PolygonLayer', () => {
  testLayer({Layer: PolygonLayer, testCases, onError: err => expect(err).toBeFalsy()});
});

// After
test('PolygonLayer', async () => {
  await testLayer({Layer: PolygonLayer, testCases, onError: err => expect(err).toBeFalsy()});
});

This is a mechanical change suitable for a follow-up PR.

Excluded Headless Tests Analysis

The vitest migration required excluding some tests that were working with tape. This section documents each test and the path to re-enabling it.

Tests Never Imported on Master (Keep Excluded)

These tests were already commented out or never imported in the original tape suite:

Test FileReason
carto/index.spec.tsNever imported - tests global CartoLayerLibrary
layers/path-tesselator.spec.tsCommented out on master
layers/polygon-tesselation.spec.tsCommented out on master
widgets/geocoders.spec.tsNo index.ts, never imported
extensions/mask/mask.spec.tsCommented out - luma.gl v9 uniforms API change
extensions/mask/mask-pass.spec.tsCommented out - luma.gl v9 uniforms API change
layers/path-layer/path-layer-vertex.spec.tsCommented out - Transform not exported from @luma.gl/engine
extensions/collision-filter/collision-filter.spec.tsCommented out on master

Tests That Were Passing on Master (Need Investigation)

These tests ran successfully with tape but fail with vitest:

Test FileCategoryIssueFix Effort
core/lib/attribute/attribute.spec.tsPRE_EXISTINGSource code bug: data-column.ts overwrites user stride/offsetFix in source
geo-layers/tile-3d-layer/tile-3d-layer.spec.tsMEDIUMAsync loading race conditions, spy count mismatchMock network requests
core/lib/layer-extension.spec.tsMEDIUMupdateState called twice when swapping extensions (expected: 1)Investigate lifecycle
core/lib/pick-layers.spec.tsMEDIUMPicking spy assertions fire at unexpected timesAdd render cycle waits
geo-layers/terrain-layer.spec.tsMEDIUMNetwork requests + GPU operations timeoutMock terrain loader
carto/layers/h3-tile-layer.spec.tsHARDautoHighlight test times out (>30s) - H3 generation expensiveProfile, simplify data
aggregation-layers/hexbin.spec.tsHARDGPU/WebGL state leakage with isolate: falseIsolate GPU contexts
core/lib/deck-picker.spec.tsHARDPicking FBO persists across testsCleanup GPU resources
core/controllers/controllers.spec.tsMEDIUMTimeline animation state leakageReset controller state
interaction/map-controller.spec.tsMEDIUMTiming-sensitive in headless mode, synthetic eventsIncrease waits, skip some
extensions/terrain/terrain-effect.spec.tsHARDTimeout >30s, heavy GPU stateProfile, simplify data
core/passes/layers-pass.spec.tsMEDIUMglParameters.viewport state leakageReset GL state per test

Fixed Tests (Re-enabled)

The following tests were fixed and re-enabled:

Test FileIssueFix
carto/layers/schema/carto-raster-tile.spec.tsTileReader.compression global state leakageReset state at start/end of test
carto/layers/schema/carto-raster-tile-loader.spec.tsDependency on above testFixed by above

Browser Project Render Test Exclusion

The browser project (headed mode for local development) currently excludes test/render/**/*.spec.ts. This was necessary because:

  1. Viewport mismatch: The browser project doesn't configure a fixed viewport in its instances, causing golden image comparisons to fail (97-98% match vs 99% threshold)
  2. Headed vs headless: Render tests are sensitive to exact pixel output; headed browser windows may have OS-level rendering differences

Current workaround: Render tests only run in the dedicated render project which has:

  • headless: true
  • Fixed viewport {width: 1024, height: 768} in instances

Future fix: Add viewport configuration to the browser project to enable local headed render test debugging:

typescript
// In browser project config
browser: {
  instances: [{
    browser: 'chromium',
    viewport: {width: 1024, height: 768}  // Match render project
  }]
}

This would allow developers to run render tests in headed mode for visual debugging.

Key Patterns Identified

  1. GPU/WebGL State Leakage (5 tests): Tests using GPU compute (hexbin, deck-picker, terrain-effect) suffer from WebGL context state persisting across tests when isolate: false. This is a trade-off for performance.

  2. Global State Mutations (2 tests): Tests that modify global/static state (like TileReader.compression) without cleanup cause flakiness. Fix: Reset state at start and end of each test.

  3. Async Timing (4 tests): Tests with network loading or complex render cycles have timing issues. Fix: Mock network requests, add explicit waits, or increase timeouts.

  4. Source Code Bugs (1 test): The attribute test reveals a real bug in data-column.ts that should be fixed independently.

Alternatives Considered

Keep ocular-test, only replace tape assertions

  • Rejected: Would require custom integration between ocular-test's BrowserTestDriver and vitest assertions
  • Vitest is designed to be both runner and assertion library

Migrate to Jest

  • Rejected: Jest has slower startup, less Vite integration
  • Vitest is faster and shares the same Vite foundation as ocular-test

Open Questions

  1. Should we convert test file structure to use describe/it blocks, or keep flat test() calls?
  2. Should browser tests run in CI by default, or remain opt-in? Resolved: Browser tests are the source of truth and should run in CI by default.
  3. Timeline for deprecating tape support in @deck.gl/test-utils? Resolved: See deprecation timeline below.
  4. After Phase 4 discovery: What percentage of tests fail in Node? This determines whether to keep hybrid approach or fall back to browser-only.

@deck.gl/test-utils Deprecation Timeline

Goal: Allow external consumers time to migrate while moving the ecosystem forward.

Note: @deck.gl/test-utils is published on npm with ~10k monthly downloads (~1% of core). While primarily intended for internal use, external consumers exist and deserve a migration path.

PhaseVersionTimelineChanges
Compatibility9.3.xNext minor releaseAdd vitest as peer dependency alongside @probe.gl/test-utils. Both tape and vitest patterns work.
Deprecation Warning9.4.x+1 minor releaseConsole warnings for tape-based patterns (makeSpy, assert: t.ok). Documentation updated with vitest examples.
Removal10.0.0Next major releaseRemove tape/probe.gl support. vi.spyOn replaces makeSpy. Callbacks use vitest expect().

Migration guide for external consumers:

typescript
// Before (tape)
import {testLayer} from '@deck.gl/test-utils';
import test from 'tape';

testLayer({assert: test.ok, onError: test.fail});

// After (vitest)
import {testLayer} from '@deck.gl/test-utils';
import {expect} from 'vitest';

testLayer({
  assert: (condition, message) => expect(condition, message).toBeTruthy(),
  onError: (error) => { throw error; }
});

References