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

@typecad/expect ships in the TypeCAD monorepo. In a workspace project it’s already available; otherwise 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).matcher(expected)
  .it('another case')
    .expect(actualValue).matcher(expected);

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 (() => A0.readAnalog()).

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 { A0, A1 } from '@typecad/board';

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

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

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 <pkg>-bfrom configBoard package 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)
--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:

test: {
  include: ['tests/**/*.test.ts'],   // required — glob patterns for test files
  exclude: ['tests/slow/**'],        // optional — patterns to skip after discovery
  port: 'COM4',                      // required — serial port
  baudRate: 115200,                  // required — default 115200
  timeout: 30000,                    // required — 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: 'arduino:avr:uno',    // optional — overrides the root frameworkData.buildTarget
  board: '@typecad/board-arduino-uno', // optional — overrides the root board
  // verbose: not settable from config; use the --verbose CLI flag
}

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.

Per-target skip directives

File-level comments control which targets run a test:

// @typecad-skip-target esp32: ESP32 lacks the AVR watchdog API.
// @typecad-only-target avr,megaavr: uses AVR watchdog registers.

Targets are matched against: your target, the FQBN parts of frameworkData.buildTarget, the full FQBN, and the board package name. 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, inside setup(). 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.