Custom Assertions
Custom assertions encapsulate reusable validation logic for your tests.
Export Pattern
// src/assertions/index.ts
import type { AssertionFunction } from "../types";
export const $$AssertionFunctions: AssertionFunction[] = [
isValidEmail,
isPositiveNumber,
matchesSchema,
];Function Signature
Assertions accept 1, 2, or 3 parameters:
// 1 parameter: value only
async function isValidEmail(value: unknown)
// 2 parameters: value and expected
async function isGreaterThan(actual: number, expected: number)
// 3 parameters: value, expected, and options
async function isCloseTo(actual: number, expected: number, delta?: number)Recording Results
Always call $addAssertionResult():
$addAssertionResult({
passed: boolean, // Did the assertion pass?
message: string, // Human-readable result
operator: string, // Assertion name for reports
leftValue: any, // Actual value
rightValue: any, // Expected value
});Example: Simple Assertion
export const isValidEmail: AssertionFunction = {
id: "is-valid-email",
name: "Is Valid Email",
description: "Validates email format",
enabled: true,
deprecated: false,
tested: true,
function: async (value: unknown) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const passed = typeof value === "string" && emailRegex.test(value);
$addAssertionResult({
passed,
message: passed
? `"${value}" is a valid email`
: `"${value}" is not a valid email`,
operator: "isValidEmail",
leftValue: value,
rightValue: "valid email format",
});
},
};Example: Zod Schema Assertion
import { z } from "zod";
const UserSchema = z.object({
id: z.number().positive(),
email: z.string().email(),
name: z.string().min(1),
role: z.enum(["admin", "user", "guest"]),
});
export const isValidUser: AssertionFunction = {
id: "is-valid-user",
name: "Is Valid User",
description: "Validates user object structure",
enabled: true,
deprecated: false,
tested: true,
function: async (value: unknown) => {
const result = UserSchema.safeParse(value);
$addAssertionResult({
passed: result.success,
message: result.success
? "User object is valid"
: `Invalid user: ${result.error.errors.map(e => e.message).join(", ")}`,
operator: "isValidUser",
leftValue: value,
rightValue: "valid user schema",
});
},
};Example: Two-Parameter Assertion
export const isWithinRange: AssertionFunction = {
id: "is-within-range",
name: "Is Within Range",
description: "Checks if value is within expected range",
enabled: true,
deprecated: false,
tested: true,
function: async (value: unknown, range: { min: number; max: number }) => {
const num = Number(value);
const passed = !isNaN(num) && num >= range.min && num <= range.max;
$addAssertionResult({
passed,
message: passed
? `${value} is within range [${range.min}, ${range.max}]`
: `${value} is outside range [${range.min}, ${range.max}]`,
operator: "isWithinRange",
leftValue: value,
rightValue: range,
});
},
};Best Practices
Use safeParse() Not parse()
// ✅ Good: Returns success/error object
const result = schema.safeParse(value);
if (result.success) { ... }
// ❌ Bad: Throws on invalid data
const result = schema.parse(value); // Throws!Always Wrap in Try-Catch
function: async (value) => {
try {
// Your logic
$addAssertionResult({ passed: true, ... });
} catch (error) {
$addAssertionResult({
passed: false,
message: `Error: ${error.message}`,
operator: "myAssertion",
leftValue: value,
rightValue: "expected",
});
}
}Provide Clear Messages
// ✅ Good: Specific, actionable
message: `User email "${user.email}" is not in valid format`
// ❌ Bad: Vague
message: "Invalid"Usage in Tests
Once synced and enabled, your assertion appears in:
- Assert Node → Operator dropdown
- Script Node → Call directly:
MyLib.isValidEmail(value)