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/cuttlefishThe 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(): void— required 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)
| Matcher | Asserts |
|---|---|
.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)
| Matcher | Asserts |
|---|---|
.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...]| Flag | Short | Default | Description |
|---|---|---|---|
--port <port> | -p | from config | Serial port (e.g. COM4, /dev/ttyACM0) |
--board <target> | -b | from config | Board target override |
--build-target <fqbn> | from config | Framework-specific build target / FQBN override | |
--baud <rate> | 115200 | Serial baud rate | |
--timeout <ms> | -t | 30000 | Serial read timeout in milliseconds |
--include <glob> | -i | from config | Test file glob pattern (repeatable) |
--exclude <glob> | -x | from config | Test file glob pattern to skip (repeatable) |
--config <file> | cuttlefish.config.ts | Use a different config file — see Multiple boards | |
--verbose | -v | off | Show raw serial output and per-assertion detail |
--help | -h | Print 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.tsTwo conventions keep the shared files portable:
- 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’stest.includepicks the shared files plus its own board directory. - Shared tests stick to names boards commonly export —
LED,BUTTON, the bus objects (UART0,I2C0,SPI0) — and to constructed hardware likenew 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
describeblocks in a file run once, in order, from the top of the program. NobeforeEach/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. UseexpectStringonly for software string variables.
Back to Testing & Diagnostics.
On This Page