Skip to content

Commit 19ec2ab

Browse files
JasonWarrenUKclaude
andcommitted
fix(tui): use @opentui/core mock in vitest suites, mark TR.A6 complete
Wires all 12 tests/tui/** suites to the shared @opentui/core test double via a one-line, hoisting-safe vi.mock (the async factory only calls import() and closes over nothing, avoiding the vi.mock hoisting trap that previously broke app.test.ts). All 12 suites now collect and pass under vitest (134 tests), unblocking a unified `bun run test:all` green run alongside the 474 bun tests. Updates the roadmap to document the bun:ffi finding and resolution. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 13e51f0 commit 19ec2ab

13 files changed

Lines changed: 89 additions & 23 deletions

docs/roadmaps/v5a-2606/enhanced.md

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -91,21 +91,38 @@ because the shell rollout and signature features build on them.
9191
test:all` still exits non-zero, but only because of the pre-existing
9292
TUI/vitest incompatibility tracked as **TR.A6** below — unrelated to
9393
storage and out of scope for this task.
94-
- [ ] **TR.A6** `fix/vitest-tui-suite-compat` — All 12 `tests/tui/**` suites
95-
fail to *load* under `vitest` (0 tests collected, not test failures):
96-
`tests/tui/app.test.ts` throws `ReferenceError: Cannot access
97-
'__vi_import_1__' before initialization` (a `vi.mock` factory hoisting
98-
issue); the other 11 (`theme`, `appShell`, `panel`, `keymap`,
99-
`dashboard`, `file-picker`, `history`, `mapping-builder`,
100-
`mapping-editor`, `mapping-save`, `settings`) all fail with
101-
`TypeError: Unknown file extension ".scm"` — vitest's Node loader
102-
chokes on `@opentui/core`'s bundled `highlights.scm` tree-sitter
103-
asset. Confirmed still present after TR.A5 (Bun/Node storage split)
104-
landed, so it's a separate blocker. **Fix:** either exclude
105-
`tests/tui/**` from the vitest config (formalising what TR.A4 already
106-
decided — these stay `bun test`-only) or resolve the `.scm` loader
107-
and hoisting issues so they run under both. Blocks a single unified
108-
green `bun run test:all`; does not block any Phase A-E branch.
94+
- [x] **TR.A6** `fix/vitest-tui-suite-compat` — All 12 `tests/tui/**` suites
95+
failed to *load* under `vitest` (0 tests collected, not test failures):
96+
a `vi.mock` factory hoisting `ReferenceError` in `app.test.ts`, plus
97+
`TypeError: Unknown file extension ".scm"` in the other 11 —
98+
`@opentui/core`'s bundled `highlights.scm`/`.wasm` tree-sitter assets,
99+
Bun-only `with { type: "file" }` imports vitest's Node loader can't
100+
resolve. **Investigated and rejected:** stubbing the `.scm`/`.wasm`
101+
imports via a Vite `load` plugin worked, but inlining `@opentui/core`
102+
(required so the plugin's transform applies) surfaced a harder wall —
103+
the same monolithic bundled chunk
104+
(`node_modules/@opentui/core/index-h3dbfsf6.js`) has unconditional
105+
top-level `import ... from "bun:ffi"` (the native Zig renderer
106+
binding). `bun:ffi` has no Node polyfill, so the real package cannot
107+
load under vitest at all, full stop — not a config problem. The
108+
official `@opentui/core/testing` subpath doesn't help either; it
109+
re-imports the same chunk. **Actual fix:** a hand-written shared test
110+
double, `tests/fixtures/tui/opentui.ts` — real named classes
111+
(`BoxRenderable`, `TextRenderable`, `SelectRenderable`, `RGBA`, etc.)
112+
matching opentui's actual behaviour where suites assert on it
113+
(constructor names, `RGBA.fromHex`/`.equals()` channel maths mirrored
114+
from opentui's real `hexToRgb`, `TextRenderable.content` string→
115+
StyledText coercion, colour-option coercion on both construction and
116+
reassignment). Each suite adds a one-line, hoisting-safe
117+
`vi.mock('@opentui/core', async () => import('.../opentui'))`
118+
the async factory only calls `import()` and closes over nothing, so
119+
it's immune to the original hoisting trap. Also needed
120+
`test.server.deps.inline: ['opentui-spinner']` in `vite.config.ts`:
121+
that package's own nested `@opentui/core` import is externalised by
122+
default, which bypasses `vi.mock` entirely unless inlined. **Done:**
123+
`bunx vitest run` — 53 files, 596 tests green (14 TUI files, 134
124+
tests); `bun run test:core` — 474 pass, unaffected. `bun run
125+
test:all` is unified green.
109126

110127
## Phase S — Security & dependency maintenance
111128

tests/tui/app.test.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2-
import { TUI } from '../../src/tui/app';
3-
import { createMockRenderer } from '../fixtures/tui/tui';
42

5-
vi.mock('@opentui/core', () => ({
6-
createCliRenderer: vi.fn().mockResolvedValue(createMockRenderer()),
7-
}));
3+
// @opentui/core can only load under Bun (see tests/fixtures/tui/opentui.ts),
4+
// so it's replaced with a shared test double. The factory only calls import()
5+
// and closes over nothing, so it's safe under vi.mock's hoisting.
6+
vi.mock('@opentui/core', async () => import('../fixtures/tui/opentui'));
7+
8+
import { TUI } from '../../src/tui/app';
89

910
describe('TUI', () => {
1011
let mockExit: any;

tests/tui/components/appShell.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
import { describe, it, expect, beforeEach } from 'vitest';
1+
import { describe, it, expect, beforeEach, vi } from 'vitest';
22
import { appShell } from '../../../src/tui/components/appShell';
33
import { APP_VERSION } from '../../../src/tui/utils/layout';
44
import * as fixtures from '../../fixtures/tui/tui';
55

6+
// @opentui/core can only load under Bun (see tests/fixtures/tui/opentui.ts),
7+
// so it's replaced with a shared test double.
8+
vi.mock('@opentui/core', async () => import('../../fixtures/tui/opentui'));
9+
610
describe('appShell()', () => {
711
let ctx: ReturnType<typeof fixtures.createMockContext>;
812

tests/tui/components/panel.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { describe, it, expect, beforeEach } from 'vitest';
1+
import { describe, it, expect, beforeEach, vi } from 'vitest';
2+
3+
// @opentui/core can only load under Bun (see tests/fixtures/tui/opentui.ts),
4+
// so it's replaced with a shared test double.
5+
vi.mock('@opentui/core', async () => import('../../fixtures/tui/opentui'));
6+
27
import { RGBA } from '@opentui/core';
38
import { panel } from '../../../src/tui/components/panel';
49
import { theme } from '../../../assets/brand/theme';

tests/tui/screens/dashboard.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
22
import { Dashboard } from '../../../src/tui/screens/dashboard';
33
import * as fixtures from '../../fixtures/tui/tui';
44

5+
// @opentui/core can only load under Bun (see tests/fixtures/tui/opentui.ts),
6+
// so it's replaced with a shared test double.
7+
vi.mock('@opentui/core', async () => import('../../fixtures/tui/opentui'));
8+
59
// Mock storage so render() can load config without hitting the filesystem
610
vi.mock('../../../src/lib/storage', () => ({
711
createStorage: () => ({

tests/tui/screens/file-picker.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import * as tuiFixtures from '../../fixtures/tui/tui';
44
import * as filePickerFixtures from '../../fixtures/tui/screens/file-picker';
55
import fs from 'node:fs/promises';
66

7+
// @opentui/core can only load under Bun (see tests/fixtures/tui/opentui.ts),
8+
// so it's replaced with a shared test double.
9+
vi.mock('@opentui/core', async () => import('../../fixtures/tui/opentui'));
10+
711
vi.mock('node:fs/promises', () => ({
812
default: {
913
readdir: vi.fn(),

tests/tui/screens/history.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
33
import { HistoryScreen } from '../../../src/tui/screens/history';
44
import * as fixtures from '../../fixtures/tui/tui';
55

6+
// @opentui/core can only load under Bun (see tests/fixtures/tui/opentui.ts),
7+
// so it's replaced with a shared test double.
8+
vi.mock('@opentui/core', async () => import('../../fixtures/tui/opentui'));
9+
610
// Mock storage so render() can load history without hitting the filesystem
711
vi.mock('../../../src/lib/storage', () => ({
812
createStorage: () => ({

tests/tui/screens/mapping-builder.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
22
import { MappingBuilderScreen } from '../../../src/tui/screens/mapping-builder';
33
import * as fixtures from '../../fixtures/tui/tui';
44

5+
// @opentui/core can only load under Bun (see tests/fixtures/tui/opentui.ts),
6+
// so it's replaced with a shared test double.
7+
vi.mock('@opentui/core', async () => import('../../fixtures/tui/opentui'));
8+
59
// Mock createStorage — include ALL methods to avoid leaking incomplete mocks
610
vi.mock('../../../src/lib/storage', () => ({
711
createStorage: () => ({

tests/tui/screens/mapping-editor.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
22
import { MappingEditorScreen } from '../../../src/tui/screens/mapping-editor';
33
import * as fixtures from '../../fixtures/tui/tui';
44

5+
// @opentui/core can only load under Bun (see tests/fixtures/tui/opentui.ts),
6+
// so it's replaced with a shared test double.
7+
vi.mock('@opentui/core', async () => import('../../fixtures/tui/opentui'));
8+
59
// Mock createStorage — include ALL methods to avoid leaking incomplete mocks
610
vi.mock('../../../src/lib/storage', () => ({
711
createStorage: () => ({

tests/tui/screens/mapping-save.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
22
import { MappingSaveScreen } from '../../../src/tui/screens/mapping-save';
33
import * as fixtures from '../../fixtures/tui/tui';
44

5+
// @opentui/core can only load under Bun (see tests/fixtures/tui/opentui.ts),
6+
// so it's replaced with a shared test double.
7+
vi.mock('@opentui/core', async () => import('../../fixtures/tui/opentui'));
8+
59
// Mock createStorage — include ALL methods to avoid leaking incomplete mocks
610
vi.mock('../../../src/lib/storage', () => ({
711
createStorage: () => ({

0 commit comments

Comments
 (0)