Test Harness
The test harness tracks which requirements are exercised by your tests. It works with any JavaScript test runner that supports global setup and teardown — Vitest, Jest, Playwright, and others.
How It Works
prepare()runs before tests, parsing requirements into a lookup cacherequirement()calls in tests return descriptions and track coveragefinalize()runs after tests, printing coverage and optionally syncing to cloud
Setup
Vitest
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globalSetup: './vitest.setup.ts',
},
})// vitest.setup.ts
import { prepare, finalize } from '@popoverai/dotrequirements/test'
export function setup() {
prepare()
}
export async function teardown() {
await finalize()
}Using requirement()
The requirement() function returns a formatted string for test descriptions and tracks coverage:
import { requirement } from '@popoverai/dotrequirements/test'
describe(requirement('AUTH-LOGIN-1'), () => {
it(requirement('AUTH-LOGIN-1.0'), async () => {
// Test implementation
})
it(requirement('AUTH-LOGIN-1.1'), async () => {
// Test implementation
})
})Return Value
requirement() returns the requirement’s content, formatted for display:
requirement('AUTH-LOGIN-1')
// → "A registered user, Jamie, can log in to their account"
requirement('AUTH-LOGIN-1.0')
// → "When Jamie provides valid credentials, they are authenticated"
requirement('AUTH-LOGIN-2.given')
// → "Given: Jamie has two-factor authentication enabled"For labeled nodes, the label is included in the output (e.g., “Given: …”).
Multiple Requirements
Track multiple requirements in a single test by passing multiple IDs:
it(requirement('AUTH-LOGIN-1', 'AUTH-SECURITY-1'), async () => {
// Both requirements tracked; returns first one's content
})Error Handling
If a requirement ID doesn’t exist, requirement() throws an error with the invalid path. This ensures typos are caught immediately rather than silently passing coverage.
Path Reference Patterns
Reference requirements using numeric paths or label-based paths:
| Pattern | Example | Description |
|---|---|---|
| Root | AUTH-LOGIN-1 | Root requirement |
| Numeric | AUTH-LOGIN-1.0 | First child (position 0) |
| Numeric nested | AUTH-LOGIN-1.2.0 | First child of third child |
| Label | AUTH-LOGIN-2.given | First “Given” criterion |
| Label disambiguated | AUTH-LOGIN-2.given#1 | Second “Given” criterion |
| Label nested | AUTH-LOGIN-2.then.and | First “And” under first “Then” |
Label paths are:
- Case-insensitive (
given,Given,GIVENall work) - Kebab-case for multi-word labels (
edge-case,happy-path)
Coverage Reporting
Local Report
After tests complete, finalize() prints a coverage summary:
=== Requirements Coverage Report ===
Total Requirements: 12
Tested Requirements: 10
Untested Requirements: 2
Coverage: 83.3%
✓ Tested Requirements:
- AUTH-LOGIN-1: A registered user, Jamie, can log in to their account
- AUTH-LOGIN-1.0: When Jamie provides valid credentials...
✗ Untested Requirements:
- AUTH-LOGIN-2: A user with two-factor auth must provide an OTP
- AUTH-SECURITY-1: Session tokens expire after 8 hours
====================================Coverage is calculated at the full path level—AUTH-LOGIN-1.0 counts separately from AUTH-LOGIN-1.
Cloud Reporting
With cloud credentials configured, coverage is automatically synced to dot•requirements cloud:
- Historical tracking — When each requirement was last tested
- Branch-aware — Separate coverage per Git branch
- Team visibility — Everyone sees the same data at app.dotrequirements.io
Configure by adding to .env.local:
DOTREQUIREMENTS_PROJECT_ID=your-project-id
DOTREQUIREMENTS_PROJECT_SECRET=your-project-secretCloud reporting is error-tolerant—if the request fails, a warning is logged but your tests still pass.
API Reference
prepare(options?)
Call in global setup before tests run. Parses requirements files into a lookup cache for fast requirement() calls.
import { prepare } from '@popoverai/dotrequirements/test'
prepare()
// or with options
prepare({ projectRoot: '/path/to/project' })Options:
| Option | Type | Description |
|---|---|---|
projectRoot | string | Override automatic project root detection |
finalize()
Call in global teardown after tests complete. Aggregates tracking data, prints coverage report, and syncs to cloud if configured.
import { finalize } from '@popoverai/dotrequirements/test'
await finalize()Returns a promise—always await it to ensure the coverage report prints and cleanup completes before the process exits.
requirement(...ids)
Returns requirement content for use as test descriptions. Tracks coverage for all provided IDs.
import { requirement } from '@popoverai/dotrequirements/test'
requirement('AUTH-LOGIN-1') // Single requirement
requirement('AUTH-LOGIN-1.0') // Child by numeric path
requirement('AUTH-LOGIN-1.given') // Child by label path
requirement('REQ-A', 'REQ-B') // Multiple requirementsParameters:
| Parameter | Type | Description |
|---|---|---|
ids | ...string | One or more requirement paths |
Returns: string — The content of the first requirement
Throws: Error if any ID doesn’t resolve to a requirement
File Discovery
The test harness finds **/*.requirements.md files anywhere in your project.
Ignored directories: node_modules/, dist/, .git/, build/, example/, examples/, __tests__/, fixtures/
Project Root Detection
The harness determines project root in this order:
- Explicit
projectRootoption passed toprepare() DOTREQUIREMENTS_PROJECT_ROOTenvironment variable- Cached project root (written by
prepare()) - Walking up from cwd looking for
.requirements/
Running Without Setup
You can use requirement() without prepare(), but:
- Requirements are parsed on-demand (slower for large projects)
- A warning is logged about missing setup
- Coverage tracking still works
For best performance and clean output, always configure global setup/teardown.