Getting started
razo is a Playwright controls framework built around one idea: the output is the product. Every action and assertion narrates itself twice — as a human-readable sentence in the Playwright HTML report, and as a structured StepEvent written to test-results/<test>/razo-steps.json for AI failure analysis.
Install
npm install -D @razohq/razo @playwright/testWire the reporter
razo ships its own Playwright reporter. Add it alongside the built-in html reporter — it writes one razo-steps.json per test:
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
reporter: [['html'], ['@razohq/razo/reporter']],
// recommended: action/assertion timeouts below the test timeout, so a
// hanging action fails inside its step (emitting its StepEvent) instead
// of killing the test with no narration
expect: { timeout: 3_000 },
use: { actionTimeout: 5_000 },
});By default the artifacts land in test-results/. Pass outputDir to change it:
reporter: [['html'], ['@razohq/razo/reporter', { outputDir: 'test-results' }]],First narrated test
A control needs a locator, and a human name — the name is what shows up in the narration. Here's the demo test that ships with razo, exporting a 3D model:
import { test } from '@playwright/test';
import { Input, Select, RadioButton, Checkbox, Button, Label } from '@razohq/razo';
test('export a 3MF model', async ({ page }) => {
await page.goto('/');
const filename = new Input(page, 'filename', 'Filename');
const format = new Select(page, 'format', 'Format');
const quality = new RadioButton(page, 'quality', 'Quality');
const includeSupports = new Checkbox(page, 'include-supports', 'Include supports');
const exportButton = new Button(page, 'export', 'Export');
const status = new Label(page, 'status', 'Export status');
await filename.fill('mi-llavero');
await format.choose('3MF');
await quality.select('Standard');
await includeSupports.check();
await exportButton.click({ as: 'Confirm the export' });
await status.expectText('Exported mi-llavero.3mf (Standard)');
});{ as: 'Confirm the export' } overrides the sentence with a business-level name — the StepEvent still records the real control and selector underneath.
What you get
The Playwright HTML report shows one test.step() per line, in plain English:
✓ Type "mi-llavero" into field "Filename"
✓ Choose "3MF" in select "Format"
✓ Select option "Standard" in radio group "Quality"
✓ Check checkbox "Include supports"
✓ Confirm the export
✓ Assert label "Export status" has text "Exported mi-llavero.3mf (Standard)"
test-results/demo-export-a-3mf-model/razo-steps.json carries the same steps as structured data — this is the abridged real output for that test:
{
"test": "export a 3MF model",
"file": "tests/demo.spec.ts",
"status": "passed",
"durationMs": 551,
"steps": [
{
"action": "fill",
"controlType": "field",
"name": "Filename",
"sentence": "Type \"mi-llavero\" into field \"Filename\"",
"detail": "mi-llavero",
"selector": "[data-testid=\"filename\"]",
"status": "passed",
"timestamp": "2026-07-17T02:43:15.144Z"
},
{
"action": "click",
"controlType": "button",
"name": "Export",
"sentence": "Confirm the export",
"selector": "[data-testid=\"export\"]",
"status": "passed",
"timestamp": "2026-07-17T02:43:15.258Z"
},
{
"action": "assert-text",
"controlType": "label",
"name": "Export status",
"sentence": "Assert label \"Export status\" has text \"Exported mi-llavero.3mf (Standard)\"",
"expected": "Exported mi-llavero.3mf (Standard)",
"selector": "[data-testid=\"status\"]",
"status": "passed",
"timestamp": "2026-07-17T02:43:15.261Z"
}
]
}Every action carries its controlType, human name and the exact sentence shown in the report; assertions add expected, and a failed assertion adds actual — read straight from the DOM. The full field reference and a failing example are in the next page below.
Next steps
- Controls & locators — the full control catalog and how to locate elements beyond
data-testid. - The artifact — the sentence grammar, the
StepEventcontract, and howrazo-analyzeconsumes it. - Recovering from locator drift — deterministic healing that never masks a real regression.