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/test

Wire 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