Expect Assertion API

@typecad/expect is a hardware test framework with a testing-library style API. You write assertions in TypeScript; the cuttlefish-test CLI turns them into firmware, runs them on the board, and reports the results back over serial. All the comparing and checking happens on your computer — the firmware running on the board only sends back the raw values it measured.

Install

New projects created with cuttlefish create already include @typecad/expect. To add it to an existing project, install it alongside @typecad/cuttlefish:

npm install @typecad/expect @typecad/cuttlefish

The fluent chain

import { describe, done } from '@typecad/expect';

describe('Group name')
  .it('test case name')
    .expect(actualValue).toBe(expectedValue)
  .it('another case')
    .expect(actualValue).toBeGreaterThan(floorValue);

done();
  • describe(name: string): Suite — opens a group.
  • Suite.it(name: string): Suite — opens a test case.
  • Suite.expect(actual: number | (() => number)): Expectation — assert a numeric value.
  • Suite.expectString(actual: string | (() => string)): StringExpectation — assert a string value.
  • done(): voidrequired as the last statement; emits the end-of-suite sentinel and enters an idle loop so the host knows the firmware is finished.

Every matcher returns Suite, so you chain .it().expect().matcher() without semicolons between steps.

Numeric matchers (Expectation)

MatcherAsserts
.toBe(expected)actual === expected
.toBeGreaterThan(n)actual > n
.toBeGreaterThanOrEqual(n)actual >= n
.toBeLessThan(n)actual < n
.toBeLessThanOrEqual(n)actual <= n
.toBeCloseTo(n, precision)`
.toBeWithinRange(min, max)actual >= min && actual <= max
.toBeTruthy()actual !== 0
.toBeFalsy()actual === 0
.toNotBe(expected)actual !== expected

actual may be a value or a zero-arg function returning a value (() => sensor.read()).

String matchers (StringExpectation)

MatcherAsserts
.toBe(expected)exact string equality
.toContain(substring)substring is present
.toHaveLength(n)string length equals n
.toNotBe(expected)not equal

String expectations are for software string variables, not raw hardware reads.

Worked example

import { describe, done } from '@typecad/expect';
import { ADC } from '@typecad/board';

const a0 = new ADC(0);
const a1 = new ADC(1);

describe('A0 analog read')
  .it('reads a value in valid ADC range')
    .expect(a0.read()).toBeWithinRange(0, 4095)
  .it('reads less than mid-scale when grounded')
    .expect(a0.read()).toBeLessThan(2048);

describe('A1 analog read')
  .it('returns a non-negative value')
    .expect(a1.read()).toBeGreaterThanOrEqual(0)
  .it('is within ADC range')
    .expect(a1.read()).toBeLessThanOrEqual(4095);

done();

You can also assert on plain TypeScript values (enums, arrays, destructuring, function results), not just hardware reads:

import { describe, done } from '@typecad/expect';

enum Mode { Idle = 0, Active = 1 }
const baseline: ReadonlyArray<number> = [1, 2, 3, 4];

function baselineSum(): number {
  let total = 0;
  for (const v of baseline) total += v;
  return total;
}

describe('Logic correctness')
  .it('sums readonly arrays correctly')
    .expect(baselineSum()).toBe(10)
  .it('returns enum-backed values correctly')
    .expect(chooseMode(true)).toBe(1);

done();

The cuttlefish-test CLI

cuttlefish-test [options] [files...]
FlagShortDefaultDescription
--port <port>-pfrom configSerial port (e.g. COM4, /dev/ttyACM0)
--board <target>-bfrom configBoard target override
--build-target <fqbn>from configFramework-specific build target / FQBN override
--baud <rate>115200Serial baud rate
--timeout <ms>-t30000Serial read timeout in milliseconds
--include <glob>-ifrom configTest file glob pattern (repeatable)
--exclude <glob>-xfrom configTest file glob pattern to skip (repeatable)
--config <file>cuttlefish.config.tsUse a different config file — see Multiple boards
--verbose-voffShow raw serial output and per-assertion detail
--help-hPrint help and exit

Positional file arguments are treated as --include patterns (overriding config). Defaults for board, target, and buildTarget fall back to your cuttlefish.config.ts root fields when the test section doesn’t set them — so only --port is typically required on the command line.

Test configuration

Add a test section to cuttlefish.config.ts:

const test = {
  include: ['tests/**/*.test.ts'],   // glob patterns for test files (default `tests/**/*.test.ts`)
  exclude: ['tests/slow/**'],        // optional — patterns to skip after discovery
  port: 'COM4',                      // required — serial port (or use --port)
  baudRate: 115200,                  // optional — default 115200
  timeout: 30000,                    // optional — default 30000 ms
  serialOpenDelay: 500,              // optional — delay before opening serial (after reset); default 500
  resetAfterOpen: true,              // optional — toggle ESP32-style DTR/RTS reset after opening
  buildTarget: 'esp32s3_devkitc/esp32s3/procpu', // optional — overrides the root frameworkData.buildTarget
  board: 'esp32s3_devkitc/esp32s3/procpu',   // optional — overrides the root board
  verbose: false,                    // optional — debug serial output and per-assertion detail
};

Merge precedence: defaults → config file → CLI flags (each level overrides the previous, per field).

Serial protocol

While running, the board and your computer exchange short text lines to report each test’s outcome.

⚙️ Advanced details — the serial protocol

The firmware emits structured lines the host parses:

  • [TC:SUITE_START]
  • [TC:DESCRIBE:<name>]
  • [TC:IT:<name>]
  • [TC:EXPECT:<matcher>:<expected>:<actual>]
  • [TC:SUITE_END]

All non-[TC: lines are treated as debug output and shown with --verbose.

Multiple boards from one suite

One test suite can serve several boards. Keep a config file per target in the project root and pick one at run time with --config:

project/
  cuttlefish.config.ts          ← default board
  esp32-devkit.config.ts        ← alternate target
  uno.config.ts                 ← alternate target
  tests/                        ← board-agnostic tests (LED, A0, bus objects)
  tests/boards/blackpill/       ← pin-name-specific tests, per board
  tests/boards/uno/
npx cuttlefish-test                                # default config
npx cuttlefish-test --config esp32-devkit.config.ts

Two conventions keep the shared files portable:

  1. Tests that name pins directly (groups of digital pins, PWM channels, chip-select pins) live under tests/boards/<board>/, using that board’s pin names. Each config’s test.include picks the shared files plus its own board directory.
  2. Shared tests stick to names boards commonly export — LED, BUTTON, the bus objects (UART0, I2C0, SPI0) — and to constructed hardware like new GPIO(2, GPIO.OUTPUT) when any valid pin number will do.

cuttlefish.config.ts is still read for editor tooling; the --config file wins for the run.

Per-target skip directives

File-level comments control which targets run a test:

// @typecad-skip-target esp32s3: the ESP32-S3 has no DAC.
// @typecad-only-target stm32f411: uses the STM32 iwdg watchdog.

Targets are matched against: your target, the board-target parts of frameworkData.buildTarget, and the full board target. Skipped files appear in the output (vitest-style ↓ tests/x.test.ts (skipped)); run with --verbose to print the reason.

Limitations

  • No callback suites — groups and cases are defined by fluent chaining, not describe("name", () => { ... }).
  • No async tests — timing is implicit (the board executes sequentially; the host waits on serial).
  • Sequential execution — all describe blocks in a file run once, in order, from the top of the program. No beforeEach/afterEach.
  • One file per upload — each test file produces one sketch and one upload cycle.
  • Numeric types only for hardware values — Cuttlefish maps hardware reads to int/float. Use expectString only for software string variables.

Back to Testing & Diagnostics.