Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
9109505
RFC: Migrate from tape to vitest
chrisgervang Jan 27, 2026
659c49f
RFC: Add multi-environment architecture and expanded implementation plan
chrisgervang Jan 27, 2026
ee46a47
RFC: Add @deck.gl/test-utils deprecation timeline
chrisgervang Jan 27, 2026
04b8519
chore(test): add vitest infrastructure
chrisgervang Jan 28, 2026
718a052
chore(test): add tape-to-vitest migration script
chrisgervang Jan 28, 2026
4949e46
chore(test): convert test files from tape to vitest
chrisgervang Jan 28, 2026
d632932
chore(test): add TODO for typed array equality tester removal
chrisgervang Jan 28, 2026
541f0c1
chore(test): remove obsolete gpu-grid-layer test
chrisgervang Jan 28, 2026
3b187e2
chore(test): convert spy.called to vitest toHaveBeenCalled()
chrisgervang Jan 29, 2026
fd05149
chore: ignore yarn engine checks for vitest dependencies
chrisgervang Jan 29, 2026
8c1954a
chore(test): remove unused describe imports from vitest conversions
chrisgervang Jan 29, 2026
47e1d61
chore: ignore vite.config.local.mjs in eslint
chrisgervang Jan 29, 2026
14d3c7c
chore(test): convert t.pass() to console.log() in vitest migration
chrisgervang Jan 29, 2026
f14ccae
chore(test): handle test.skip and test.only in vitest migration
chrisgervang Jan 29, 2026
9e75ff6
fix(test): preserve assertion message in assert callback conversion
chrisgervang Jan 29, 2026
c2955bb
chore(test): handle expression body arrow functions in migration
chrisgervang Jan 29, 2026
2f77513
fix(ci): install Playwright browsers and use headless project
chrisgervang Jan 29, 2026
c8eef73
fix(test): exclude tests that were not in original CI suite
chrisgervang Jan 29, 2026
cfffba1
chore(test): restructure test commands with vitest projects
chrisgervang Jan 30, 2026
b0280a0
docs(rfc): update vitest migration RFC with implementation details
chrisgervang Jan 30, 2026
84ff56c
fix(test): convert makeSpy to vi.spyOn for browser compatibility
chrisgervang Jan 30, 2026
4a35838
fix(test): convert spy.restore/reset to mockRestore/mockReset
chrisgervang Jan 30, 2026
f2402ce
fix(test): convert spy.called to toHaveBeenCalled matcher
chrisgervang Jan 30, 2026
e95ae27
fix(test): handle nested t.test() conversion in migration script
chrisgervang Jan 30, 2026
d616d74
fix(test): address diff comment issues
chrisgervang Jan 30, 2026
cc69f73
chore(test): add manual fix skip list to migration script
chrisgervang Jan 30, 2026
6161023
fix(test): convert spy.callCount to toHaveBeenCalledTimes
chrisgervang Jan 30, 2026
fd9930c
fix(test): pass message to expect assertions for better error messages
chrisgervang Jan 30, 2026
8fce5f5
fix(test): convert spy.callCount to toHaveBeenCalledTimes in terrain …
chrisgervang Jan 30, 2026
4766b53
chore(test): exclude tests that were commented out on master
chrisgervang Jan 30, 2026
7054272
feat(test): add browser commands for vitest test runner
chrisgervang Jan 30, 2026
a9edc69
feat(test): add interaction and render test specs for vitest
chrisgervang Jan 30, 2026
b1e947f
docs: update CONTRIBUTING and RFC with vitest migration progress
chrisgervang Jan 30, 2026
a4ffad0
chore(deps): add image comparison dependencies for vitest
chrisgervang Jan 30, 2026
facc654
fix(scripts): remove unused fullCall variable in migration script
chrisgervang Jan 30, 2026
354485a
fix(test): add await detection for testLayerAsync and exclude failing…
chrisgervang Jan 30, 2026
a14cd26
fix(test): rename jupyter-widget utility file to not have .spec suffix
chrisgervang Jan 30, 2026
ed053cc
fix(test-utils): use mockRestore instead of mockClear for spy cleanup
chrisgervang Jan 30, 2026
cea4d17
chore(test): reduce verbose logging and use TAP for CI
chrisgervang Feb 1, 2026
8bcb3ee
RFC: Migrate from tape to vitest
chrisgervang Jan 27, 2026
d44d9d5
RFC: Add multi-environment architecture and expanded implementation plan
chrisgervang Jan 27, 2026
237a9f6
RFC: Add @deck.gl/test-utils deprecation timeline
chrisgervang Jan 27, 2026
7750fed
RFC: Update CLI commands and finalize Phase 4 discovery outcome
chrisgervang Jan 30, 2026
a30500c
fix(test-utils): enable es2022 for top-level await support
chrisgervang Feb 2, 2026
5e4683f
chore: merge chr/tape-to-vitest into chr/vitest-setup
chrisgervang Feb 2, 2026
d5a9bbf
feat(test-utils): add backward compatibility for tape/probe.gl users
chrisgervang Feb 2, 2026
c711bb8
fix(vitest): pre-bundle dependencies to prevent CI flakiness
chrisgervang Feb 2, 2026
6c04566
ci: split test steps for better visibility
chrisgervang Feb 3, 2026
ad34130
ci: use npx for vitest commands
chrisgervang Feb 3, 2026
16b3e9d
fix(test-utils): make spy framework initialization lazy
chrisgervang Feb 4, 2026
305668e
fix(test-utils): enable test-utils import in Node with NullDevice fal…
chrisgervang Feb 4, 2026
beb5176
feat(test-utils): add Injectable Spy API for framework-agnostic testing
chrisgervang Feb 4, 2026
5443914
Merge branch 'origin/chr/tape-to-vitest' into chr/vitest-setup
chrisgervang Feb 4, 2026
e8aa70c
chore(test): add createSpy to all testLayer calls (migration script o…
chrisgervang Feb 4, 2026
2ab06f0
chore(test): separate render tests into dedicated vitest project
chrisgervang Feb 5, 2026
a6acf0c
chore(test): fix test-render script and add separate entry points
chrisgervang Feb 5, 2026
4f7be42
chore(test-utils): add vi.spyOn default to vitest entry point
chrisgervang Feb 5, 2026
1f8ff88
chore(test): update migration script to use @deck.gl/test-utils/vitest
chrisgervang Feb 5, 2026
5002217
chore(test): migrate unit tests to @deck.gl/test-utils/vitest
chrisgervang Feb 5, 2026
329e6cc
chore(test): update core-layers.node.spec.ts to use vitest entry
chrisgervang Feb 5, 2026
8250c0f
chore(test): remove old tape/probe.gl test entry points
chrisgervang Feb 5, 2026
f067e09
chore(test): convert interaction tests to native vitest browser mode
chrisgervang Feb 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ dist/
node_modules/
coverage/
test/**/*-failed.png
test/**/__screenshots__/
.nyc_output/
.reify-cache/

Expand Down
377 changes: 377 additions & 0 deletions dev-docs/RFCs/proposals/vitest-migration-rfc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,377 @@
# 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:**
| Pattern | Description |
|---------|-------------|
| `*.spec.ts` | Default - runs in both environments |
| `*.browser.spec.ts` | Browser-only (WebGL, real DOM, etc.) |

**Vitest workspace configuration:**
```typescript
// vitest.config.ts
export default defineConfig({
test: {
projects: [
{
test: {
name: 'node',
environment: 'node',
include: ['test/modules/**/*.spec.ts'],
exclude: ['test/modules/**/*.browser.spec.ts'],
setupFiles: ['./test/setup/vitest-node-setup.ts']
}
},
{
test: {
name: 'browser',
include: ['test/modules/**/*.spec.ts'], // ALL tests
browser: {
enabled: true,
provider: playwright(),
instances: [{browser: 'chromium'}]
}
}
},
{
test: {
name: 'headless',
include: ['test/modules/**/*.spec.ts'], // ALL tests
browser: {
enabled: true,
headless: true,
provider: playwright(),
instances: [{browser: 'chromium'}]
}
}
}
]
}
});
```

**CLI commands:**
```json
{
"test": "vitest run",
"test-node": "vitest run --project node",
"test-browser": "vitest run --project browser",
"test-headless": "vitest run --project headless"
}
```

### 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:**

| tape | vitest |
|------|--------|
| `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 Compatibility

Commands remain the same:

```json
{
"scripts": {
"test": "vitest run",
"test-fast": "ocular-lint && vitest run",
"cover": "vitest run --coverage"
}
}
```

`yarn test ci` continues to work - vitest auto-detects CI environments.

### @deck.gl/test-utils Updates

The `@deck.gl/test-utils` module uses `makeSpy` from `@probe.gl/test-utils`. This will be replaced with vitest's built-in `vi.spyOn`:

```typescript
// Before
import {makeSpy} from '@probe.gl/test-utils';
const spy = makeSpy(Object.getPrototypeOf(layer), 'updateState');

// After
import {vi} from 'vitest';
const spy = vi.spyOn(Object.getPrototypeOf(layer), 'updateState');
```

## 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
- Replace `makeSpy` with `vi.spyOn`
- Add vitest as peer dependency

### Phase 3: Migrate Test Files (~185 files)
- Convert tape imports to vitest
- Transform assertions
- Remove `t.end()` calls
- Update callback patterns (`onError: t.notOk` → `onError: (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

### 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.js` → `test/render/index.spec.ts`
- `test/interaction/index.js` → `test/interaction/index.spec.ts`

### Phase 6: Cleanup
- Remove `tap-spec`, `tape-catch` dependencies
- Remove test entry points from `.ocularrc.js`
- Delete `test/node.ts`, `test/browser.ts`, `.nycrc`

## 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

| Risk | Mitigation |
|------|------------|
| Browser tests may behave differently | Vitest browser mode uses Playwright, similar to current Puppeteer-based setup |
| Coverage format changes | Vitest v8 provider outputs lcov format, same as current setup |
| Breaking changes for external consumers of test-utils | Add vitest as peer dependency, document migration |
| Many tests fail in Node environment | Discovery phase allows fallback to browser-only approach (Option A) |
| Puppeteer → Playwright migration breaks snapshot comparison | Vitest 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 |

## 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.

| Phase | Version | Timeline | Changes |
|-------|---------|----------|---------|
| **Compatibility** | 9.3.x | Next minor release | Add vitest as peer dependency alongside `@probe.gl/test-utils`. Both tape and vitest patterns work. |
| **Deprecation Warning** | 9.4.x | +1 minor release | Console warnings for tape-based patterns (`makeSpy`, `assert: t.ok`). Documentation updated with vitest examples. |
| **Removal** | 10.0.0 | Next major release | Remove 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

- [Vitest Documentation](https://vitest.dev/)
- [Vitest Browser Mode](https://vitest.dev/guide/browser/)
- [ocular-test source](https://github.com/visgl/dev-tools/blob/master/modules/dev-tools/src/test.ts)
2 changes: 1 addition & 1 deletion modules/test-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"@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.

"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.

},
"gitHead": "13ace64fc2cee08c133afc882fc307253489a4e4"
}
Loading
Loading