A collection of AI-powered testing utilities for modern applications.
This package includes three testing libraries:
| Library | Purpose |
|---|---|
| spec-test | Specification-driven browser testing with AI-powered automation |
| b-test | LLM-powered browser assertions with HTML snapshot diffing |
| db-test | Database state management for deterministic testing |
npm install epic-test
# or
pnpm add epic-test
# or
bun add epic-testDepending on which libraries you use, you may need:
# For spec-test and b-test (browser testing)
npm install playwright
# For db-test (database testing)
npm install @libsql/clientWrite behavior specifications in Epic format markdown and execute them as browser tests:
Note: spec-test only supports the Epic specification format. If you use a different format, you'll need to write an adapter (see spec-test documentation below).
import { SpecTestRunner } from 'epic-test';
const runner = new SpecTestRunner({
baseUrl: 'http://localhost:3000',
headless: true,
});
// Run tests from a markdown spec file (must be in Epic format)
const result = await runner.runFromFile('./specs/login.md');
if (result.success) {
console.log('All tests passed!');
} else {
console.log('Tests failed:', result.failedAt?.context.error);
}
await runner.close();Epic format spec file (login.md):
# Login
## Examples
### Login with valid credentials
#### Steps
* Act: User enters "user@example.com" in email field
* Act: User enters "password123" in password field
* Act: User clicks Login button
* Check: URL contains /dashboard
* Check: Welcome message is displayedUse natural language to assert page conditions:
import { chromium } from 'playwright';
import { Tester } from 'epic-test/b-test';
const browser = await chromium.launch();
const page = await browser.newPage();
const tester = new Tester(page);
await page.goto('https://example.com');
// Take snapshots and assert with natural language
await tester.snapshot();
await page.click('button.submit');
await tester.snapshot();
const hasSuccessMessage = await tester.assert('Success message is displayed');Set up and verify database state for deterministic tests:
import { PreDB, PostDB } from 'epic-test/db-test';
import { db } from './db';
import * as schema from './schema';
// Setup: Set initial database state
await PreDB(db, schema, {
users: [{ id: 1, name: 'Alice', email: 'alice@example.com' }]
});
// Act: Run the code being tested
await createUser({ name: 'Bob', email: 'bob@example.com' });
// Assert: Verify final database state
await PostDB(db, schema, {
users: [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' }
]
});A specification-driven testing library that parses behavior specifications and executes them as browser tests using AI-powered automation.
⚠️ IMPORTANT: Specification Format Requirementspec-test only accepts specifications in the Epic format (see below). If your specifications use a different format (Gherkin, Cucumber, custom markdown, etc.), you MUST write an adapter to transform them into the Epic format before using spec-test.
The built-in parser (
parseSpecFile) will fail silently or incorrectly parse specifications that don't follow the exact Epic format structure.See Adapting Other Formats for guidance on writing adapters.
┌─────────────────────────────────────────────────────────────────┐
│ spec-test Library │
├─────────────────────────────────────────────────────────────────┤
│ parseSpecFile → SpecTestRunner → ExampleResult │
│ │
│ ┌──────────────┴──────────────┐ │
│ │ │ │
│ ┌──────▼──────┐ ┌──────────▼────────┐ │
│ │ Stagehand │ │ B-Test │ │
│ │ (Act steps) │ │ (Check steps) │ │
│ │ AI browser │ │ LLM assertions │ │
│ └─────────────┘ └───────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
IMPORTANT: spec-test requires specifications to follow the Epic format exactly. If you have specifications in a different format, you must either:
- Convert your specifications to match the Epic format
- Write an adapter that transforms your format to Epic format before passing to
SpecTestRunner
The parser expects this exact structure:
# Behavior Name
Description of the behavior.
Directory: `app/features/my-feature/`
## Examples
### Example scenario name
#### Steps
* Act: User performs some action
* Act: User fills in "value" in field name
* Check: URL contains /expected-path
* Check: Success message is displayed| Element | Required Format | Notes |
|---|---|---|
| Behavior name | # Title (H1) |
First H1 in file |
| Examples section | ## Examples (H2) |
Exact text, case-sensitive |
| Example name | ### Name (H3) |
Under Examples section |
| Steps marker | #### Steps (H4) |
Required - steps are only parsed after this marker |
| Act step | * Act: instruction |
Must be after #### Steps |
| Check step | * Check: instruction |
Must be after #### Steps |
If your specifications use ANY format other than Epic format, you MUST write an adapter.
Common scenarios requiring an adapter:
- Different heading levels (e.g.,
#### Examplesinstead of## Examples) - Gherkin/Cucumber format (
Given,When,Then) - Custom markdown structures
- Different keywords (e.g.,
Action:instead ofAct:) - Specifications stored in YAML, JSON, or other formats
Create a function that transforms your format into the TestableSpec interface:
import { SpecTestRunner } from 'epic-test';
import type { TestableSpec, SpecExample, SpecStep } from 'epic-test';
/**
* Adapter for Gherkin/Cucumber format
*/
function parseGherkinToEpic(gherkinContent: string): TestableSpec {
// Parse your Gherkin content
const feature = parseGherkin(gherkinContent);
return {
name: feature.name,
examples: feature.scenarios.map(scenario => ({
name: scenario.name,
steps: scenario.steps.map(step => {
// Map Given/When to Act, Then to Check
const type = (step.keyword === 'Then') ? 'check' : 'act';
return {
type,
instruction: step.text,
...(type === 'check' ? { checkType: 'semantic' } : {})
} as SpecStep;
})
}))
};
}
// Use with SpecTestRunner
const runner = new SpecTestRunner({ baseUrl: 'http://localhost:3000' });
const spec = parseGherkinToEpic(gherkinContent);
const result = await runner.runFromSpec(spec);
console.log(result.success ? 'PASS' : 'FAIL', spec.name);Your adapter must return objects matching these interfaces:
interface TestableSpec {
name: string;
directory?: string;
examples: SpecExample[];
}
interface SpecExample {
name: string;
steps: SpecStep[];
}
type SpecStep =
| { type: 'act'; instruction: string }
| { type: 'check'; instruction: string; checkType?: 'deterministic' | 'semantic' };Do not attempt to modify the built-in parser. Write an adapter instead.
Act Steps - User actions executed with Stagehand AI:
* Act: User clicks the Login button
* Act: User enters "user@example.com" in email field
* Act: User selects "Admin" from the role dropdownCheck Steps - Verifications (deterministic or semantic):
# Deterministic (fast, no AI):
* Check: URL contains /dashboard
* Check: Page title is "Dashboard"
# Semantic (LLM-powered):
* Check: Success notification is displayed
* Check: Error message shows "Invalid credentials"interface SpecTestConfig {
baseUrl: string; // Required - app URL
headless?: boolean; // Default: true
aiModel?: LanguageModelV2; // Override AI model
browserbaseApiKey?: string; // Cloud browser execution
cacheDir?: string; // Enable action caching
cachePerSpec?: boolean; // Per-spec cache directories
}Enable caching for faster subsequent runs:
const runner = new SpecTestRunner({
baseUrl: 'http://localhost:3000',
cacheDir: './cache/e2e-tests',
});
// First run: ~30s per action (LLM inference)
// Subsequent runs: ~3s per action (cached)LLM-powered browser testing utilities with HTML snapshot capture and natural language assertions.
- HTML Snapshots: Capture full page HTML with timestamps
- LLM Assertions: Use natural language to assert conditions
- Snapshot Comparison: Generate structured diffs between states
- Polling: Wait for conditions with intelligent polling
const tester = new Tester(page);
// Capture snapshots
await tester.snapshot();
// Assert conditions
const result = await tester.assert('Login form is visible');
// Compare page states
const diff = await tester.diff();
// Wait for conditions
await tester.waitFor('Loading spinner is gone', 10000);| Code | Description |
|---|---|
NO_PAGE |
No page provided |
NO_SNAPSHOT |
No snapshot for assertion |
SNAPSHOT_FAILED |
Failed to capture page |
ASSERTION_FAILED |
LLM service error |
DIFF_FAILED |
Comparison error |
WAIT_TIMEOUT |
Condition timeout |
Database testing utilities for deterministic state management with Drizzle ORM.
await PreDB(db, schema, {
users: [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
],
posts: [
{ id: 1, userId: 1, title: 'First Post' }
]
});Options:
{
wipe?: boolean; // Delete existing rows (default: true)
resetSequences?: boolean; // Reset auto-increment (default: true)
only?: string[]; // Target specific tables
}await PostDB(db, schema, {
users: [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' } // New user from test
]
});Options:
{
only?: string[]; // Target specific tables
allowExtraRows?: boolean; // Allow extra DB rows (default: false)
loose?: boolean; // Loose comparison (default: false)
}await PreDBFromFile(db, schema, './fixtures/initial-state.json');
// ... run test ...
await PostDBFromFile(db, schema, './fixtures/expected-state.json');| Variable | Library | Description |
|---|---|---|
OPENAI_API_KEY |
spec-test, b-test | Required for AI-powered features |
BROWSERBASE_API_KEY |
spec-test | Cloud browser execution |
# Install dependencies
npm install
# Build
npm run build
# Run tests
npm test
# Run integration tests
npm run test:integrationMIT