API Reference
All public exports from the @averspec/core package.
Domain Definition
Section titled “Domain Definition”defineDomain(config)
Section titled “defineDomain(config)”Creates a domain with a named vocabulary.
import { defineDomain, action, query, assertion } from '@averspec/core'
const cart = defineDomain({ name: 'shopping-cart', actions: { addItem: action<{ name: string; qty: number }>(), }, queries: { cartTotal: query<number>(), }, assertions: { hasItems: assertion<{ count: number }>(), },})from averspec import domain, action, query, assertionfrom dataclasses import dataclass
@dataclassclass AddItemPayload: name: str qty: int
@domain("shopping-cart")class Cart: add_item = action(AddItemPayload) cart_total = query(None, int) has_items = assertion(int)require "averspec"
class Cart < Aver::Domain domain_name "shopping-cart"
action :add_item query :cart_total assertion :has_itemsendimport aver "github.com/averspec/aver-go"
type CartDomain struct { AddItem aver.Action[AddItemPayload] CartTotal aver.Query[struct{}, int] HasItems aver.Assertion[HasItemsPayload]}
var Cart = aver.NewDomain[CartDomain]("shopping-cart")use averspec::{aver_domain, Domain, Action, Query, Assertion};
aver_domain! { struct CartMarkers { add_item: Action<AddItemPayload>, cart_total: Query<(), i32>, has_items: Assertion<HasItemsPayload>, }}
let cart = Domain::new("shopping-cart", CartMarkers::new());import dev.averspec.*
object CartDomain { val d = domain("shopping-cart") { action<AddItemPayload>("add item") query<Unit, Int>("cart total") assertion<HasItemsPayload>("has items") }
val addItem get() = d.markers["add item"] as ActionMarker<AddItemPayload> val cartTotal get() = d.markers["cart total"] as QueryMarker<Unit, Int> val hasItems get() = d.markers["has items"] as AssertionMarker<HasItemsPayload>}Returns: Domain — a domain object with vocabulary metadata and an extend() method.
action<Payload>(opts?)
Section titled “action<Payload>(opts?)”Creates an action marker. Actions perform side effects and return void.
addItem: action<{ name: string }>() // typed payloadcheckout: action() // no payload (void)addItem: action<{ name: string }>({ // with telemetry declaration telemetry: (p) => ({ span: 'cart.add-item', attributes: { 'item.name': p.name } }),})add_item = action(AddItemPayload) # typed payloadcheckout = action() # no payloadadd_item = action(AddItemPayload, # with telemetry telemetry={"span": "cart.add-item"})action :add_item # untyped (Ruby is dynamic)action :checkoutaction :add_item, telemetry: { span: "cart.add-item" }AddItem aver.Action[AddItemPayload] // typed payload via genericsCheckout aver.Action[struct{}] // no payloadadd_item: Action<AddItemPayload>, // typed payloadcheckout: Action<()>, // no payloadaction<AddItemPayload>("add item") // typed payloadaction<Unit>("checkout") // no payloadOptions: MarkerOptions<P> — optional object with a telemetry: TelemetryDeclaration<P> property.
query<Return>(opts?) / query<Payload, Return>(opts?)
Section titled “query<Return>(opts?) / query<Payload, Return>(opts?)”Creates a query marker. Queries read data and return a typed result. Two overloads:
cartTotal: query<number>() // no input, returns numbertasksByStatus: query<{ status: string }, Task[]>() // input + return typecartTotal: query<number>({ // with telemetry telemetry: { span: 'cart.total' },})cart_total = query(None, int) # no input, returns inttasks_by_status = query(str, list) # input + return typequery :cart_total # return type checked at runtimequery :tasks_by_statusCartTotal aver.Query[struct{}, int] // no input, returns intTasksByStatus aver.Query[string, []Task] // input + return typecart_total: Query<(), i32>, // no input, returns i32tasks_by_status: Query<String, Vec<Task>>, // input + return typequery<Unit, Int>("cart total") // no input, returns Intquery<String, List<Task>>("tasks by status") // input + return typeassertion<Payload>(opts?)
Section titled “assertion<Payload>(opts?)”Creates an assertion marker. Assertions verify expectations and throw on failure.
hasItems: assertion<{ count: number }>() // typed payloadisEmpty: assertion() // no payloadhas_items = assertion(int) # typed payloadis_empty = assertion() # no payloadassertion :has_itemsassertion :is_emptyHasItems aver.Assertion[HasItemsPayload]IsEmpty aver.Assertion[struct{}]has_items: Assertion<HasItemsPayload>,is_empty: Assertion<()>,assertion<Int>("has items")assertion<Unit>("is empty")domain.extend(name, config)
Section titled “domain.extend(name, config)”Extends a domain with additional vocabulary. The extended domain inherits all items from the parent. The name is passed as the first argument.
const cartUI = cart.extend('shopping-cart-ui', { assertions: { showsSpinner: assertion(), },})@domain("shopping-cart-ui", extends=Cart)class CartUI: shows_spinner = assertion()class CartUI < Cart domain_name "shopping-cart-ui" assertion :shows_spinnerendtype CartUIDomain struct { CartDomain ShowsSpinner aver.Assertion[struct{}]}
var CartUI = aver.Extend(Cart, aver.NewDomain[CartUIDomain]("shopping-cart-ui"))// Rust domains compose by embedding marker structsstruct CartUIMarkers { cart: CartMarkers, shows_spinner: Assertion<()>,}val cartUI = domain("shopping-cart-ui", parent = CartDomain.d) { assertion<Unit>("shows spinner")}Adapters
Section titled “Adapters”adapt(domain, config)
Section titled “adapt(domain, config)”Creates an adapter binding a domain to a protocol with handler implementations.
import { adapt, unit } from '@averspec/core'
const adapter = adapt(cart, { protocol: unit(() => []), actions: { addItem: async (ctx, payload) => { /* ... */ }, }, queries: { cartTotal: async (ctx) => { /* ... */ }, }, assertions: { hasItems: async (ctx, payload) => { /* ... */ }, },})TypeScript enforces that every action, query, and assertion declared in the domain is provided. Missing handlers are compile errors.
from averspec import implement, unit
adapter = implement(Cart, protocol=unit(lambda: []))
@adapter.handle(Cart.add_item)def add_item(ctx, p): ctx.append({"name": p.name, "qty": p.qty})
@adapter.handle(Cart.cart_total)def cart_total(ctx): return sum(item["qty"] for item in ctx)
@adapter.handle(Cart.has_items)def has_items(ctx, count): assert len(ctx) == countclass CartUnitAdapter < Aver::Adapter domain Cart protocol :unit, -> { [] }
def add_item(ctx, name:, qty:) ctx << { name: name, qty: qty } end
def cart_total(ctx) ctx.sum { |i| i[:qty] } end
def has_items(ctx, count:) raise "Expected #{count} items" unless ctx.size == count endendimport ( aver "github.com/averspec/aver-go" "github.com/averspec/aver-go/protocols/unit")
var UnitAdapter = aver.Implement( Cart, unit.New(func() *CartCtx { return &CartCtx{} }), func(h *aver.HandlerBuilder[CartDomain, *CartCtx]) { aver.OnActionFn(h, Cart.AddItem, func(ctx *CartCtx, p AddItemPayload) { ctx.Items = append(ctx.Items, p) }) aver.OnQuery(h, Cart.CartTotal, func(ctx *CartCtx, _ struct{}) (int, error) { return len(ctx.Items), nil }) aver.OnAssertion(h, Cart.HasItems, func(ctx *CartCtx, p HasItemsPayload) error { if len(ctx.Items) != p.Count { return fmt.Errorf("expected %d", p.Count) } return nil }) },)let m = cart.markers();let mut builder = AdapterBuilder::<CartMarkers, Vec<Item>>::new();
builder.on_action(&m.add_item, |ctx, p| { ctx.push(Item { name: p.name.clone(), qty: p.qty });});builder.on_query(&m.cart_total, |ctx, _| ctx.len());builder.on_assertion(&m.has_items, |ctx, p| { if ctx.len() == p.count { Ok(()) } else { Err(format!("expected {} items", p.count)) }});
let adapter = builder.build();val unitAdapter = implement<CartCtx>(CartDomain.d, UnitProtocol { CartCtx() }) { onAction(CartDomain.addItem) { ctx, payload -> ctx.items.add(payload) } onQuery(CartDomain.cartTotal) { ctx, _ -> ctx.items.size } onAssertion(CartDomain.hasItems) { ctx, count -> if (ctx.items.size != count) throw AssertionError("Expected $count items") }}Returns: Adapter — an adapter object with domain, protocol, and handler references.
Protocols
Section titled “Protocols”unit(factory)
Section titled “unit(factory)”Built-in protocol for testing against your code’s public interfaces directly. Zero dependencies.
import { unit } from '@averspec/core'
protocol: unit(() => new Cart()) // object contextprotocol: unit(() => ({ db: new DB() })) // compound contextprotocol: unit<Cart[]>(() => []) // typed contextThe factory runs on each test setup, creating a fresh context. Teardown is a no-op.
from averspec import unit
protocol = unit(lambda: Cart()) # object contextprotocol = unit(lambda: {"db": DB()}) # compound contextprotocol = unit(lambda: []) # list contextprotocol :unit, -> { Cart.new } # object contextprotocol :unit, -> { { db: DB.new } } # compound contextprotocol :unit, -> { [] } # array contextimport "github.com/averspec/aver-go/protocols/unit"
unit.New(func() *Cart { return NewCart() })use averspec::UnitProtocol;
let protocol = UnitProtocol::new("unit", || Vec::<Item>::new());import dev.averspec.UnitProtocol
val protocol = UnitProtocol { Cart() }http(options) from @averspec/protocol-http
Section titled “http(options) from @averspec/protocol-http”HTTP protocol providing a fetch-based client.
import { http } from '@averspec/protocol-http'
protocol: http({ baseUrl: 'http://localhost:3000' })from averspec import http
protocol = http(base_url="http://localhost:3000")require "averspec/protocol_http"
protocol :http, base_url: "http://localhost:3000"import avhttp "github.com/averspec/aver-go/protocols/http"
avhttp.New("http://localhost:3000")use averspec::http_protocol::HttpProtocol;
let protocol = HttpProtocol::new("http://localhost:3000");import dev.averspec.HttpProtocol
val protocol = HttpProtocol("http://localhost:3000")Context provides get, post, put, patch, delete methods.
playwright(options?) from @averspec/protocol-playwright
Section titled “playwright(options?) from @averspec/protocol-playwright”Playwright protocol providing a browser page.
import { playwright } from '@averspec/protocol-playwright'
protocol: playwright()Context is a Playwright Page. Browser is launched once and reused; a fresh page is created per test.
withFixture(protocol, fixture)
Section titled “withFixture(protocol, fixture)”Wraps a protocol with before/after hooks.
import { withFixture } from '@averspec/core'
const wrapped = withFixture(myProtocol, { before: async () => { /* runs before setup */ }, after: async () => { /* runs after teardown */ },})from averspec import with_fixture
wrapped = with_fixture( my_protocol, before=lambda: seed_db(), after_setup=lambda ctx: navigate(ctx), after=lambda: cleanup(),)wrapped = Aver.with_fixture( my_protocol, before: -> { seed_db }, after_setup: ->(ctx) { navigate(ctx) }, after: -> { cleanup },)aver.WithFixture(t, aver.FixtureOptions[*Ctx]{ Before: func(t *testing.T) *Ctx { return setup() }, AfterSetup: func(t *testing.T, ctx *Ctx) { navigate(ctx) }, After: func(t *testing.T, ctx *Ctx) { cleanup(ctx) },}, func(t *testing.T, ctx *Ctx) { // test body})use averspec::fixture::WithFixture;
let wrapped = WithFixture::new(protocol) .before_each(|ctx| { seed_db(ctx); }) .after_setup(|ctx| { navigate(ctx); }) .after_each(|ctx| { cleanup(ctx); });// Fixtures are configured via JUnit @BeforeEach / @AfterEach// or through protocol lifecycle hookssuite(domain, adapter?)
Section titled “suite(domain, adapter?)”Creates a test suite for a domain.
import { suite } from '@averspec/core'
// Multi-adapter: resolves from registryconst { test } = suite(cart)
// Single adapter: passed directlyconst { test } = suite(cart, unitAdapter)from averspec import suite
s = suite(Cart)require "averspec/rspec"
Aver.register CartUnitAdapter
RSpec.describe Cart do # ctx is available inside each testendallSuites := aver.Suites(Cart, UnitAdapter, HTTPAdapter)let suite = Suite::new("Cart", markers, adapter, Box::new(protocol));val s = suite(CartDomain.d, unitAdapter)Returns: SuiteReturn with the following:
| Property | Type | Description |
|---|---|---|
test | (name, fn) => void | Wraps Vitest’s test() with domain proxies |
it | (name, fn) => void | Alias for test |
describe | (name, fn) => void | Wraps Vitest’s describe() for grouping |
context | (name, fn) => void | Alias for describe |
act | ActProxy | Programmatic access to actions |
query | QueryProxy | Programmatic access to queries |
assert | AssertProxy | Programmatic access to assertions |
setup | () => Promise<void> | Manual setup (for programmatic use) |
teardown | () => Promise<void> | Manual teardown (for programmatic use) |
getTrace | () => TraceEntry[] | Get the current test steps |
getCoverage | () => VocabularyCoverage | Get vocabulary coverage stats |
getPlannedTests | (name) => PlannedTest[] | Preview what test names would be registered |
test(name, fn)
Section titled “test(name, fn)”Wraps Vitest’s test(), passing typed domain proxies via callback:
test('add item', async ({ given, when, query, assert, trace }) => { await given.addItem({ name: 'Widget' }) await when.checkout() await assert.hasItems({ count: 1 }) const total = await query.cartTotal() const entries = trace() // trace is a function})@s.testdef test_add_item(ctx): ctx.given.add_item(AddItemPayload(name="Widget")) ctx.when.checkout() ctx.then.has_items(1) total = ctx.query.cart_total() entries = ctx.trace()it "adds an item" do ctx.given.add_item(name: "Widget") ctx.when.checkout ctx.then.has_items(count: 1) total = ctx.query.cart_total entries = ctx.traceendfunc TestAddItem(t *testing.T) { allSuites.Run(t, "add item", func(ctx *aver.TestContext[CartDomain]) { aver.Given(ctx, Cart.AddItem, AddItemPayload{Name: "Widget"}) aver.When(ctx, Cart.Checkout, struct{}{}) aver.Then(ctx, Cart.HasItems, HasItemsPayload{Count: 1}) total := Cart.CartTotal.From(ctx) })}suite.run("add item", |m, ctx| { ctx.given(&m.add_item, AddItemPayload { name: "Widget".into() }); ctx.when(&m.checkout, ()); ctx.then(&m.has_items, HasItemsPayload { count: 1 }); let total = ctx.query(&m.cart_total, ()); let entries = ctx.trace();});@Testfun `add item`() = s.run { ctx -> ctx.Given(CartDomain.addItem, AddItemPayload("Widget")) ctx.When(CartDomain.checkout) ctx.Then(CartDomain.hasItems, 1) val total = ctx.Query(CartDomain.cartTotal) val entries = ctx.trace()}The callback receives a TestContext:
| Property | Type | Description |
|---|---|---|
act | ActProxy<D> | Typed proxy for actions |
given | ActProxy<D> | Alias for act — setup steps (Given-When-Then) |
when | ActProxy<D> | Alias for act — trigger steps (Given-When-Then) |
query | QueryProxy<D> | Typed proxy for queries |
assert | AssertProxy<D> | Typed proxy for assertions |
then | AssertProxy<D> | Alias for assert — verification steps (Given-When-Then) |
trace | () => TraceEntry[] | Returns the current test steps (callable) |
Configuration
Section titled “Configuration”defineConfig(config)
Section titled “defineConfig(config)”Creates an Aver configuration and auto-registers adapters.
import { defineConfig } from '@averspec/core'import { unitAdapter } from './adapters/cart.unit'import { httpAdapter } from './adapters/cart.http'
export default defineConfig({ adapters: [unitAdapter, httpAdapter],})from averspec import define_configfrom adapters.cart_unit import adapter as unit_adapterfrom adapters.cart_http import adapter as http_adapter
define_config(adapters=[unit_adapter, http_adapter])Aver.configure do |config| config.register CartUnitAdapter config.register CartHttpAdapterend// Go registers adapters directly in test filesvar allSuites = aver.Suites(Cart, UnitAdapter, HTTPAdapter)// Rust passes adapter + protocol directly to Suite::newlet suite = Suite::new("Cart", markers, adapter, Box::new(protocol));import dev.averspec.defineConfig
val config = defineConfig { adapter(unitAdapter) adapter(httpAdapter)}AverConfigInput:
| Property | Type | Default | Description |
|---|---|---|---|
adapters | Adapter[] | required | Adapters to register |
coverage | { minPercentage?: number } | { minPercentage: 0 } | Vocabulary coverage threshold |
teardownFailureMode | 'fail' | 'warn' | 'fail' | Whether teardown errors fail the test |
registerAdapter(adapter)
Section titled “registerAdapter(adapter)”Manually registers an adapter in the global registry.
findAdapter(domain)
Section titled “findAdapter(domain)”Returns the first registered adapter matching a domain, or undefined.
findAdapters(domain)
Section titled “findAdapters(domain)”Returns all registered adapters matching a domain.
getAdapters()
Section titled “getAdapters()”Returns all registered adapters.
resetRegistry()
Section titled “resetRegistry()”Clears all registered adapters. Useful in test setup.
getRegistrySnapshot() / restoreRegistrySnapshot(snapshot)
Section titled “getRegistrySnapshot() / restoreRegistrySnapshot(snapshot)”Capture and restore registry state. Useful for test isolation.
withRegistry(fn)
Section titled “withRegistry(fn)”Runs a function with an isolated registry that resets afterward.
Telemetry
Section titled “Telemetry”TelemetryCollector
Section titled “TelemetryCollector”Interface for providing spans to the framework. Set on Protocol.telemetry.
interface TelemetryCollector { getSpans(): CollectedSpan[] reset(): void}CollectedSpan
Section titled “CollectedSpan”Span data for telemetry verification.
interface CollectedSpan { readonly traceId: string readonly spanId: string readonly parentSpanId?: string readonly name: string readonly attributes: Readonly<Record<string, unknown>> readonly links?: ReadonlyArray<SpanLink>}
interface SpanLink { readonly traceId: string readonly spanId: string}createOtlpReceiver()
Section titled “createOtlpReceiver()”Creates an OTLP HTTP receiver for cross-process telemetry testing.
import { createOtlpReceiver } from '@averspec/telemetry'
const receiver = createOtlpReceiver()await receiver.start()// receiver.port — port the OTLP HTTP endpoint listens on// receiver.getSpans() — returns CollectedSpan[]// receiver.reset() — clears collected spans// receiver.stop() — shuts down the serverThe receiver implements TelemetryCollector so it can be set directly on a protocol’s telemetry property.
verifyCorrelation(trace, spans)
Section titled “verifyCorrelation(trace, spans)”Verifies that correlated trace entries have causally connected spans.
import { verifyCorrelation } from '@averspec/core/internals'Telemetry Declarations
Section titled “Telemetry Declarations”Declared on domain markers via the telemetry option:
// Static — fixed span name and attributesaction({ telemetry: { span: 'order.checkout' } })
// Parameterized — attributes derived from payloadaction<{ orderId: string }>({ telemetry: (p) => ({ span: 'order.checkout', attributes: { 'order.id': p.orderId }, }),})checkout = action(OrderPayload, telemetry={"span": "order.checkout"})
checkout = action(OrderPayload, telemetry=lambda p: { "span": "order.checkout", "attributes": {"order.id": p.order_id}, })action :checkout, telemetry: { span: "order.checkout" }
action :checkout, telemetry: ->(p) { { span: "order.checkout", attributes: { "order.id" => p[:order_id] } }}// Telemetry declarations are configured at the adapter level in Go// via TelemetryExpectation on marker registration// Telemetry expectations are configured via TelemetryExpectation structs// and verified against collected spans after test execution// Telemetry declarations are associated with markers during domain creationaction<OrderPayload>("checkout") { telemetry = TelemetryExpectation(span = "order.checkout")}TelemetryExpectation:
| Property | Type | Description |
|---|---|---|
span | string | OTel span name to match |
attributes | Record<string, TelemetryAttributeValue> | Required span attributes. Primitives for exact match, or asymmetric matchers (e.g. expect.any(String)) |
causes | string[] | Span names this operation causally triggers. Opts in to causal correlation — verifies trace connection (same trace or span link) to the named spans. |
AVER_TELEMETRY_MODE
Section titled “AVER_TELEMETRY_MODE”Environment variable controlling telemetry verification:
| Value | Behavior | Default when |
|---|---|---|
fail | Missing/mismatched spans fail the test | CI is set |
warn | Mismatches recorded but tests pass | CI is not set |
off | No telemetry verification | — |
Test Context
Section titled “Test Context”getTestContext()
Section titled “getTestContext()”Returns the current test context from async-local storage, or undefined if not in a test.
runWithTestContext(context, fn)
Section titled “runWithTestContext(context, fn)”Runs a function within a test context (for framework-level use).
Registry Lifecycle
Section titled “Registry Lifecycle”How Adapters Are Registered
Section titled “How Adapters Are Registered”defineConfig({ adapters }) calls registerAdapter() for each adapter when the config module is evaluated. This is the standard path — your aver.config.ts runs once and registers all adapters for the process.
You can also call registerAdapter() directly in test files or setup files.
When Adapters Are Resolved
Section titled “When Adapters Are Resolved”suite(domain) resolves adapters lazily — at test execution time, not when suite() is called. On first invocation, suite() calls maybeAutoloadConfig() to import aver.config.ts if it hasn’t been loaded yet. Set AVER_AUTOLOAD_CONFIG=false to skip this.
Passing an adapter directly — suite(domain, adapter) — bypasses the registry entirely.
Environment Filtering
Section titled “Environment Filtering”Two environment variables control which tests run:
AVER_ADAPTER=unit— only run tests for adapters whose protocol name matchesAVER_DOMAIN=ShoppingCart— only register tests for the named domain
These map to the CLI flags aver run --adapter unit and aver run --domain ShoppingCart.
Multi-Adapter Dispatch
Section titled “Multi-Adapter Dispatch”When multiple adapters are registered for one domain, suite() creates a parameterized test for each:
add item [unit] ← runs against unit adapteradd item [http] ← runs against http adapterEach adapter gets its own protocol context (fresh setup() / teardown() per test per adapter).
Parent Chain Resolution
Section titled “Parent Chain Resolution”If no adapter is registered for a domain, findAdapter() walks the domain.parent chain. This means an adapter registered for a parent domain works for extended domains that haven’t overridden it.
Test Isolation
Section titled “Test Isolation”The registry is process-global state. If your tests register their own adapters (common in framework-level testing), call resetRegistry() in beforeEach to prevent cross-test leakage:
import { resetRegistry, registerAdapter } from '@averspec/core/internals'
beforeEach(() => { resetRegistry() registerAdapter(myTestAdapter)})From @averspec/core
Section titled “From @averspec/core”import type { // Domain & markers Domain, MarkerOptions,
// Adapters & protocols Adapter, Protocol, TestMetadata, TestCompletion, ProtocolExtensions, Screenshotter,
// Telemetry TelemetryCollector, CollectedSpan, SpanLink, TelemetryMatchResult,
// Suite & testing TestContext, SuiteReturn,
// Config AverConfig, AverConfigInput,
// Trace TraceEntry, TraceAttachment,} from '@averspec/core'From @averspec/core/internals
Section titled “From @averspec/core/internals”These types are not re-exported from @averspec/core. Import them from the @averspec/core/internals subpath.
import type { // Domain & markers ActionMarker, QueryMarker, AssertionMarker, TelemetryExpectation, TelemetryDeclaration, TelemetryAttributeValue, AsymmetricMatcher,
// Suite & testing ActProxy, QueryProxy, AssertProxy, PlannedTest, RunningTestContext,
// Config CoverageConfig, TeardownFailureMode,
// Trace & coverage VocabularyCoverage,
// Correlation CorrelationResult, CorrelationGroup, CorrelationViolation,
// Registry RegistrySnapshot,} from '@averspec/core/internals'TraceEntry
Section titled “TraceEntry”interface TraceEntry { kind: 'action' | 'query' | 'assertion' | 'test' category?: 'given' | 'when' | 'act' | 'query' | 'then' | 'assert' name: string domainName?: string payload: unknown status: 'pass' | 'fail' result?: unknown error?: unknown startAt?: number endAt?: number durationMs?: number attachments?: TraceAttachment[] metadata?: Record<string, unknown> correlationId?: string telemetry?: TelemetryMatchResult}Protocol<Context>
Section titled “Protocol<Context>”interface Protocol<Context> { readonly name: string setup(): Promise<Context> teardown(ctx: Context): Promise<void> onTestStart?(ctx: Context, meta: TestMetadata): Promise<void> | void onTestFail?(ctx: Context, meta: TestCompletion): Promise<TestFailureResult> | TestFailureResult onTestEnd?(ctx: Context, meta: TestCompletion): Promise<void> | void extensions?: ProtocolExtensions telemetry?: TelemetryCollector}The lifecycle hooks are optional. onTestStart runs before each test body. onTestFail runs when a test fails and can return TraceAttachment[] (e.g., screenshots). onTestEnd runs after each test regardless of outcome.
Approval Testing from @averspec/approvals
Section titled “Approval Testing from @averspec/approvals”approve(value, options?)
Section titled “approve(value, options?)”Approves a value against a stored baseline. Auto-detects serializer: objects use JSON, strings use text. Also exported as characterize for characterization test contexts.
import { approve } from '@averspec/approvals'// or: import { characterize } from '@averspec/approvals'
await approve({ count: 42 }) // default name "approval"await approve(reportText, { name: 'report' }) // named approvalfrom averspec import approve# or: from averspec import characterize
approve({"count": 42})approve(report_text, name="report")Aver::Approvals.approve({ count: 42 })Aver::Approvals.approve(report_text, name: "report")aver.Approve(map[string]int{"count": 42})aver.Approve(reportText, aver.ApprovalOptions{Name: "report"})use averspec::approvals::approve;
approve(&serde_json::json!({"count": 42}), "approval", &dir)?;import dev.averspec.approve
approve("""{"count": 42}""", approvalFile)approve(reportText, approvalFile, scrub = Scrubbers.UUID)First run fails with “Baseline missing”. Run npx aver approve to create baselines.
Baselines are stored in __approvals__/<test-name>/ next to the test file. Commit .approved files; gitignore .received and .diff files.
Options:
| Property | Type | Default | Description |
|---|---|---|---|
name | string | 'approval' | Name for the approval file |
fileExtension | string | auto | Override file extension |
filePath | string | auto | Override test file path (for programmatic use) |
testName | string | auto | Override test name (for programmatic use) |
serializer | SerializerName | auto | Serializer to use ('json', 'text', or custom name) |
comparator | Comparator | default | Custom comparison function (approved, received) => { equal: boolean } |
approve.visual(nameOrOptions)
Section titled “approve.visual(nameOrOptions)”Approves a screenshot against a stored baseline image. Requires a protocol with screenshotter extension (e.g., Playwright). Throws an error on protocols without one.
await approve.visual('board-state') // full pageawait approve.visual({ name: 'backlog', region: 'backlog' }) // scoped regionOptions (when passing object):
| Property | Type | Required | Description |
|---|---|---|---|
name | string | yes | Name for the approval image file |
region | string | no | Named region (maps to CSS selector in adapter) |
threshold | number | no | Pixel difference threshold (0-1) for visual comparison |
Screenshotter from @averspec/core
Section titled “Screenshotter from @averspec/core”Extension interface for visual approval support. Protocols implement this.
interface Screenshotter { capture(outputPath: string, options?: { region?: string }): Promise<void> regions?: Record<string, string>}Playwright configures regions at adapter creation:
const proto = playwright({ regions: { 'board': '.board', 'backlog': '[data-testid="column-backlog"]', },})Test Runner Integration
Section titled “Test Runner Integration”approve() integrates with test runners by throwing standard Error-based assertion errors when a baseline mismatch is detected. The test runner catches these errors and reports them as test failures.
- Vitest and Jest work out of the box — both catch thrown errors as assertion failures
- Other test runners need to support standard
Error-based assertions (most do) - Run
npx aver approveto update baselines. Under the hood this setsAVER_APPROVE=1, which tellsapprove()to write received values as the new baselines instead of comparing.
aver run
Section titled “aver run”Runs tests via Vitest.
npx aver run # all testsnpx aver run --adapter unit # filter by adapternpx aver run --domain ShoppingCart # filter by domainnpx aver run --watch # watch modeaver run # all testsaver run --adapter unit # filter by adapteraver run --domain ShoppingCart # filter by domainaver run # all testsaver run --adapter unit # filter by adapteraver run --domain ShoppingCart # filter by domaingo test ./... # all testsAVER_ADAPTER=unit go test ./... # filter by adapterAVER_DOMAIN=ShoppingCart go test ./... # filter by domaincargo test # all testsAVER_ADAPTER=unit cargo test # filter by adapterAVER_DOMAIN=ShoppingCart cargo test # filter by domain./gradlew test # all testsAVER_ADAPTER=unit ./gradlew test # filter by adapterAVER_DOMAIN=ShoppingCart ./gradlew test # filter by domainaver init
Section titled “aver init”Interactive scaffolding wizard. Prompts for domain name and protocol, then generates:
domains/<kebab>.tsadapters/<kebab>.<protocol>.tstests/<kebab>.spec.tsaver.config.ts(if it doesn’t exist)
npx aver initaver initaver init# Define domain, adapter, and test in Go files directly# Define domain, adapter, and test in Rust files directly# Define domain, adapter, and test in Kotlin files directlyaver approve
Section titled “aver approve”Updates approval baselines by running tests with AVER_APPROVE=1.
npx aver approve # approve allnpx aver approve tests/my-test.spec.ts # approve specific filenpx aver approve --adapter playwright # approve for specific adapteraver approve # approve allaver approve tests/test_my_test.py # approve specific fileaver approve --adapter playwright # approve for specific adapteraver approve # approve allaver approve spec/my_test_spec.rb # approve specific fileAVER_APPROVE=1 go test ./... # approve allAVER_APPROVE=1 go test ./path/to/test # approve specificAVER_APPROVE=1 cargo test # approve allAVER_APPROVE=1 cargo test test_name # approve specificAVER_APPROVE=1 ./gradlew test # approve allEventually
Section titled “Eventually”Polls a function until it succeeds or a timeout expires. Useful for async assertions in integration tests.
import { eventually } from '@averspec/core'
await eventually(async () => { const count = await query.taskCount() expect(count).toBe(3)}, { timeout: 5000, interval: 100 })from averspec import eventually
eventually( lambda: assert ctx.query.task_count() == 3, timeout=5.0, interval=0.1,)Aver.eventually(timeout: 5.0, interval: 0.1) do count = ctx.query.task_count raise "expected 3" unless count == 3enderr := aver.Eventually(func() error { count := Cart.TaskCount.From(ctx) if count != 3 { return fmt.Errorf("expected 3, got %d", count) } return nil}, aver.EventuallyOpts{Timeout: 2 * time.Second, Interval: 50 * time.Millisecond})use averspec::{eventually, EventuallyOptions};
eventually(|| { if count != 3 { Err("expected 3".into()) } else { Ok(()) }})?;
eventually_with(EventuallyOptions { timeout: Duration::from_secs(5), ..Default::default() }, || { // ...})?;import dev.averspec.eventually
eventually(timeoutMs = 5000, intervalMs = 100) { val count = ctx.Query(CartDomain.taskCount) assertEquals(3, count)}