gridland
Guides

Testing

Test Gridland components with @gridland/testing

Installation

Terminal
bun add -D @gridland/testing
Terminal
npm install --save-dev @gridland/testing
Terminal
yarn add -D @gridland/testing
Terminal
pnpm add -D @gridland/testing

Rendering a Component

renderTui mounts a Gridland component into an in-memory buffer and returns a TuiInstance with helpers for querying the screen, sending keys, and advancing the render loop. It is synchronous.

example.test.tsx
import { renderTui } from "@gridland/testing"
import { expect, test } from "bun:test"

test("renders hello world", () => {
  const tui = renderTui(
    <box border padding={1}>
      <text>Hello World</text>
    </box>,
    { cols: 40, rows: 10 },
  )

  expect(tui.screen.contains("Hello World")).toBe(true)

  tui.unmount()
})

cols defaults to 80 and rows defaults to 24 if not passed.

Querying the Screen

tui.screen exposes methods that read the rendered cell buffer directly — no ANSI parsing.

example.test.ts
// String containment
expect(tui.screen.contains("Hello World")).toBe(true)

// Regex
expect(tui.screen.matches(/Hello\s+World/)).toBe(true)

// Full screen text, trailing spaces trimmed per line
const output = tui.screen.text()

// A single line by index (0-based)
const firstLine = tui.screen.line(0)

// All non-empty lines
const lines = tui.screen.lines()

Available Screen Methods

MethodDescription
text()Full screen content with trailing whitespace trimmed
rawText()Full screen content preserving all spaces
contains(text)true if the trimmed text includes the given string
matches(pattern)true if the trimmed text matches the given regex
line(n)The nth line (0-indexed)
lines()All non-empty lines as an array
frames()Captured frame snapshots (one per render pass)
attributeAt(row, col)Raw u32 text attributes at a cell
fgAt(row, col)Foreground RGBA tuple [r, g, b, a] at a cell
width / heightBuffer dimensions in cells

Simulating Input

tui.keys is a KeySender that dispatches synthetic keypress events into the focus system just like real key input.

input.test.tsx
import { renderTui } from "@gridland/testing"
import { expect, test } from "bun:test"
import { MyInputComponent } from "./my-input"

test("accepts typed input", () => {
  const tui = renderTui(<MyInputComponent />, { cols: 40, rows: 10 })

  tui.keys.type("hello")
  tui.keys.enter()

  tui.flush()

  expect(tui.screen.contains("hello")).toBe(true)

  tui.unmount()
})

Available Key Helpers

HelperDescription
keys.type(str)Type a string character by character
keys.press(char)Press a single character key
keys.raw(data)Send a raw sequence (e.g. escape codes)
keys.enter()Press Enter
keys.escape()Press Escape
keys.tab()Press Tab
keys.backspace()Press Backspace
keys.delete()Press Delete
keys.space()Press Space
keys.up() / keys.down()Arrow keys
keys.left() / keys.right()Arrow keys
keys.home() / keys.end()Home / End
keys.pageUp() / keys.pageDown()Page Up / Page Down

Flushing Renders

React updates scheduled during a test (setState, effects, timers) are not visible until the next render pass. Call tui.flush() to force a synchronous render cycle and capture a new frame.

tui.keys.down()
tui.flush()                          // commit pending state and re-render
expect(tui.screen.line(0)).toBe("> second option")

tui.rerender(node) replaces the rendered tree entirely, which is useful when testing controlled components:

const { rerender, screen, unmount } = renderTui(<Counter value={0} />)
rerender(<Counter value={1} />)
expect(screen.contains("1")).toBe(true)
unmount()

Waiting for Async Updates

tui.waitFor polls the screen until a condition holds or a timeout expires. Pass a string to wait for screen content, or a function that throws until the condition is satisfied.

async.test.tsx
import { renderTui } from "@gridland/testing"
import { expect, test } from "bun:test"
import { AsyncComponent } from "./async-component"

test("waits for async state", async () => {
  const tui = renderTui(<AsyncComponent />)

  // Wait for a literal string to appear
  await tui.waitFor("Loaded")

  // Or wait for an arbitrary assertion
  await tui.waitFor(() => {
    expect(tui.screen.contains("Ready")).toBe(true)
  })

  tui.unmount()
})

waitFor accepts { timeout, interval } (defaults: 3000ms timeout, 50ms poll interval).

Cleaning Up Between Tests

Each instance returned from renderTui must be unmounted to release its renderer. The top-level cleanup helper unmounts every active instance — wire it into afterEach once and you can omit the per-test tui.unmount() call.

setup.ts
import { cleanup } from "@gridland/testing"
import { afterEach } from "bun:test"

afterEach(() => {
  cleanup()
})

Snapshot Testing

screen.text() returns a deterministic string that is well-suited for snapshot tests. Set explicit cols and rows so snapshots do not drift with terminal dimensions.

snapshot.test.ts
const tui = renderTui(<MyApp />, { cols: 40, rows: 12 })
expect(tui.screen.text()).toMatchSnapshot()
tui.unmount()

Test Runner Compatibility

@gridland/testing works with any test runner that supports ESM:

  • Bun — Recommended; used by Gridland's own test suite
  • Vitest — Works with proper module resolution
  • Jest — Works with ESM transforms configured

Bun is the recommended test runner for Gridland projects. It has native ESM support, runs renderTui fast enough for thousands of snapshots per second, and is the runner Gridland's own test suite uses.

Tips

  • Set explicit cols and rows for deterministic layout
  • Call tui.flush() after any input or state change before asserting on the screen
  • Print tui.screen.text() to the console when debugging a failing assertion
  • Always tui.unmount() at the end of a test — or call cleanup() in afterEach once