casewright
v0.0.5
Published
Generate interactive HTML test documentation right from your Playwright test automation solution
Downloads
429
Maintainers
Readme
Casewright
Generate interactive HTML test documentation right from your Playwright test automation solution. Transform code-level test cases into human-readable documentation.
Casewright works two ways:
- Reporter mode — runs as a Playwright reporter, enriching the docs with test status, duration, and screenshots.
- Static mode —
casewright generateparses your test sources without running anything, producing docs from the source code alone.
Installation
npm install casewrightQuick Start
Reporter mode (with execution data)
Add Casewright as a reporter in your playwright.config.ts:
// playwright.config.ts
import { defineConfig } from '@playwright/test'
export default defineConfig({
reporter: [['html'], ['casewright']],
})Run your tests and documentation will be generated automatically:
npx playwright testStatic mode (no execution)
Generate docs from test sources without running them:
npx casewright generateCasewright will look for playwright.config.ts in the current directory and use its testDir (or read it from casewright.config.ts if present — see Static mode for details).
Either way, open ./casewright/documentation.html to view your test documentation.
Configuration
Create a configuration file for more control:
npx casewright initThis creates casewright.config.ts:
// casewright.config.ts
import type { CasewrightConfig } from 'casewright'
const config: Partial<CasewrightConfig> = {
places: {
// Patterns to identify Page Object Models or containers
patterns: [/Page$/, /Component$/, /View$/],
mappings: {
LoginPage: 'Login Screen',
},
},
elements: {
mappings: {
submitBtn: 'Submit Button',
},
},
actions: {
// Method name → template. Use {target} for the element, {0}, {1} for arguments.
mappings: {
click: 'Tap {target}',
fill: 'Input {0} into {target}',
},
},
steps: {
// Method/function names to exclude from documentation
ignoreMethods: ['goto', 'waitForThisPageToLoad'],
},
values: {
// When true, plain identifiers (e.g. `adminEmail`) are statically resolved
// to their initializer's literal value instead of humanised to "Admin Email".
// Property-chain resolution (`text.foo.bar`) is always on. Default: false.
resolveVariables: false,
},
// Path to the test directory; only used by `casewright generate`. If unset,
// generate falls back to reading testDir from playwright.config.ts.
// testDir: './tests',
// Path to tsconfig.json for resolving path aliases (@models/*, etc).
// Defaults to ./tsconfig.json if unset.
tsConfigPath: './tsconfig.json',
output: {
dir: './casewright',
filename: 'documentation.html',
},
title: 'Test Documentation',
}
export default configGetting maximum value from Casewright
Casewright is an opinionated reporter. To bring the maximum possible value and to correctly convert your Playwright solution into test documentation, it expects (he-he 🥸) that you:
- Use folders inside your tests folder as global categories, for example,
tests/admin,tests/seller,tests/buyeretc. - Use
test.describeas test suites,testas, well, tests, andtest.stepas test steps. Casewright ignores files as it's usually a strictly technical aspect of solution, not a semantical one. - Try to avoid code outside of
test.step. Lines outside of steps will still be parsed and added to documentation but it's gonna feel weird to have steps along with orphaned lines. - Separate semantically important actions from assertions and do not mix them within one custom function. If your POM function signs in and expects to be signed in at the same time, you are doing something wrong. If some actions or assertions are not that important or are strictly technical, it's okay to hide them in this way.
- Thoroughly think about your naming convention and try to remain as declarative as you can.
- Let the value resolver work for you, or pull values into named consts when it can't. Casewright statically resolves property chains of any depth (
dictionary.greetings.welcome→"Welcome back"when that JSON/TS value is reachable) and decomposes template literals (`${first} ${last}`→"Ada Lovelace"). It does NOT resolve function calls, arithmetic, ternaries, or any expression it cannot reach statically. For those, extract a named const so the call site contains only an identifier:
// ❌ Function call — renders as raw code with a ⚠ marker
await usernameField.fill(randomToken(8) + '_robot')
// ✅ Named const — humanised by default, or resolved with values.resolveVariables
const guestUsername = randomToken(8) + '_robot'
await usernameField.fill(guestUsername)
// → Enter Guest Username in Username FieldWhenever the resolver can't reach a literal value, the affected step shows a small ⚠ next to the text with a tooltip listing the unresolved expression — that tells you exactly which call sites are worth refactoring.
String and number literals are fine as-is — only extract them when the variable name communicates intent better than the raw value.
- Remember that the Casewright reporter is still a custom reporter, so it can be called conditionally. The simplest way is to introduce an environment variable to control whether you want to re-create documentation:
// playwright.config.ts
import { defineConfig } from '@playwright/test'
const reporters: any[] = [['html']]
if (process.env.DOCS) {
reporters.push(['casewright'])
}
export default defineConfig({
reporter: reporters,
})For doc-only updates that don't need execution data, npx casewright generate skips the test run entirely.
Features
Automatic Name Conversion
Casewright automatically converts code identifiers to human-readable text:
submitButton→ "Submit Button"dashboardPage.userProfile→ "User Profile on Dashboard Page" (whendashboardPagematches the place patterns)page.goto('/')→ "Navigate to /"
Entity Types
Documentation recognizes four entity types:
- Places: Page Object Models or containers (matched by patterns like
/Page$/) - Elements: Locators on places (e.g., buttons, inputs)
- Actions: Playwright API calls or custom functions
- Variables: Named values passed to actions
Value Resolution
Argument expressions are resolved to their actual values when Casewright can statically reach them. This applies to property chains of any depth, JSON dictionaries imported as data, and (opt-in) plain identifiers.
Property chains (always on)
const copy = { checkout: { cta: { confirm: 'Place order' } } }
await expect(button).toHaveText(copy.checkout.cta.confirm)
// → Check that Button has text "Place order"Works through nested object literals, local consts, named imports, and one level of re-exports.
JSON imports
A first-class case for text dictionaries:
// data/i18n.json
{ "greetings": { "welcome": "Welcome back" } }import i18n from '@data/i18n.json' // or with { type: 'json' } in newer TS
await expect(banner).toHaveText(i18n.greetings.welcome)
// → Check that Banner has text "Welcome back"Both default and named JSON imports are supported. The file is read, parsed, and walked the same way as TypeScript object literals.
Template literals
Template literals are decomposed: each ${expr} runs through the same resolver, and literal segments are kept verbatim:
const planet = 'Mars'
const habitat = 'colony'
await expect(banner).toHaveText(`${planet} ${habitat}`)
// → Check that Banner has text "Mars colony" (with resolveVariables: true)
// → Check that Banner has text "Planet Habitat" (default, no marker)
// With a property chain inside the template (always resolved):
const profile = { name: 'Ada' }
await expect(heading).toHaveText(`Welcome, ${profile.name}`)
// → "Welcome, Ada"Variable resolution (opt-in)
By default plain identifiers humanise to their name (adminEmail → Admin Email). Set values.resolveVariables: true to instead resolve them to their initializer value:
const guestUsername = 'turing42'
await usernameField.fill(guestUsername)
// → Enter "turing42" in Username Field (resolveVariables: true)
// → Enter Guest Username in Username Field (default)let and var are resolved on a best-effort basis using their initializer. Reassignment between declaration and call site is NOT detected — refactor to const if accurate output matters for a mutated variable.
Unresolved markers (⚠)
When Casewright attempts to resolve an expression and fails (parameter root, computed property key, function-call leaf, spread in target path, etc.), it falls back to the humanised name and adds a small ⚠ marker to the affected step. Hover the marker for a tooltip listing exactly which expressions failed to resolve. This is your map for "where could the docs be richer if I refactored this call site?"
TypeScript Path Alias Support
Casewright resolves TypeScript path aliases (e.g. @models/*, @utils/*) when searching for @overwrite, @ignore, and @expectation JSDoc tags across files, and when resolving values through imports. By default it picks up ./tsconfig.json from the current working directory; set tsConfigPath in your config to point at a different file.
Without a tsconfig, only relative imports (./, ../) are followed.
Custom Function Documentation
Document custom functions with the @overwrite JSDoc tag:
/**
* @overwrite Check that cards are sorted in {order} order
*/
async function assertCardSorting(page: Page, order: 'asc' | 'desc') {
// implementation
}The {order} placeholder is replaced with the argument at the call site, following the same identifier/property/JSON resolution rules described above (types preserved on this path, so the ternary placeholder below keeps working).
Ternary Placeholders
Use {param?trueText:falseText} to render different text based on a boolean parameter:
/**
* @overwrite {accept?Accept:Dismiss} the next confirmation modal
*/
async function processNextModal(page: Page, accept = true) {
// implementation
}When accept is truthy, renders as "Accept the next confirmation modal". When falsy, renders as "Dismiss the next confirmation modal".
Ignoring Items
Use the @ignore JSDoc tag to exclude suites, tests, steps, or custom functions from documentation:
/**
* @ignore
*/
test.describe('Technical setup', () => {
// this entire suite is hidden from docs
})
/**
* @ignore
*/
async function internalSetup() {
// calls to this function won't appear as steps
}@ignore works on test.describe, test, test.step, and any custom function. When placed on a suite, all tests inside are ignored as well.
Config-Level Ignoring
For methods that should never appear as documentation steps across your entire project, use the steps.ignoreMethods config instead of adding @ignore to each one individually:
// casewright.config.ts
const config: Partial<CasewrightConfig> = {
steps: {
ignoreMethods: [
'goto',
'waitForThisPageToLoad',
'waitForLoad',
'readSetupData',
'writeTeardownData',
],
},
}This filters out matching method calls everywhere — page.goto(), homePage.goto(), and standalone readSetupData() alike.
If a method has an @overwrite JSDoc tag, it will still appear in documentation regardless of the ignore list. Explicit JSDoc always takes priority over config-level filtering.
Marking Expectations
Use @expectation to mark a custom function as a verification step. Expectation steps are visually highlighted with a blue accent in the output:
/**
* @overwrite Verify that user {name} is displayed
* @expectation
*/
async function assertUserDisplayed(page: Page, name: string) {
// implementation
}Expectations are also auto-detected for:
- Any
expect()call addDocumentationLinetext starting with "check that"
Documentation Line Helper
Use addDocumentationLine to insert custom text steps that don't correspond to any code:
import { addDocumentationLine } from 'casewright'
test('checkout flow', async ({ page }) => {
await test.step('Complete purchase', async () => {
addDocumentationLine('User has items in cart from previous test')
await checkoutPage.submit()
addDocumentationLine('Check that order confirmation is displayed')
})
})Lines starting with "check that" (case-insensitive) are automatically marked as expectations.
Overwriting Step Documentation
Use overwriteDocumentationLines inside a test.step to replace all auto-detected steps with your own list:
import { overwriteDocumentationLines } from 'casewright'
await test.step('Process complex data', async () => {
overwriteDocumentationLines([
'Fetch data from external API',
'Transform response into table format',
'Check that all rows are valid',
])
// actual implementation — ignored by Casewright
const data = await api.fetch()
const rows = transform(data)
expect(rows.every((r) => r.valid)).toBe(true)
})Documentation Screenshots
Use addDocumentationScreenshot to capture annotated screenshots that appear in the generated docs:
import { addDocumentationScreenshot } from 'casewright'
await test.step('Review dashboard', async () => {
await addDocumentationScreenshot(page, 'dashboard-overview', {
label: 'Dashboard with metrics',
highlight: [{ locator: page.getByTestId('metrics-panel') }],
notes: [
{
text: 'Key metrics section',
target: page.getByTestId('metrics-panel'),
angle: 270, // note above element
},
],
})
})Options:
highlight— elements to outline (configurable border width/color/style)notes— text annotations with arrows pointing to elementsfullPage— capture the full scrollable pagelabel— display text in documentation (defaults to the name)
In static mode, screenshot calls are still detected at parse time and represented as steps, but no image is captured — the doc displays a placeholder line instead of the screenshot. Use reporter mode to capture actual images.
Static Mode
casewright generate produces documentation from test sources alone, without running Playwright:
npx casewright generate # uses testDir from casewright.config.ts or playwright.config.ts
npx casewright generate ./tests # explicit test directory
npx casewright generate -c ./my-config.ts ./teststestDir resolution (first match wins):
- Positional CLI argument (
casewright generate ./tests). testDirfield at the root ofcasewright.config.ts.- Static parse of
playwright.config.tsin the current directory. Casewright reads the literaltestDirproperty out of thedefineConfig({...})argument; if absent, falls back to the config file's directory (Playwright's documented default). - If none of the above succeeds, the command exits with a clear error.
If playwright.config.ts sets testDir to a non-literal expression (process.env.X ?? './t', computed value, etc.), Casewright warns and asks you to set testDir explicitly in casewright.config.ts. The Playwright config is never executed.
What static mode omits:
- Test status badges and the pass/fail/skip summary (no execution data exists).
- Step durations.
- Captured screenshots (the placeholder mentioned above is shown instead).
- The sidebar filter buttons (All / Passed / Failed / Skipped) — meaningless without status.
Everything else — suite hierarchy, test names, step text, expectations, screenshots metadata, the search box, dark/light theme, ⚠ markers — appears identically.
A "Statically generated — no execution data" banner sits below the title so readers can immediately tell the docs lack execution information.
Interactive HTML Output
Generated documentation includes:
- Collapsible tree view (suites → tests → steps)
- Search and filter functionality (filters only in reporter mode)
- Status indicators in reporter mode (passed / failed / skipped)
- Dark/light theme toggle
- Duration tracking in reporter mode
- Print-friendly layout
- Inline ⚠ markers for unresolved value expressions, with hover tooltips
API
Reporter Options
Pass options directly to the reporter:
;[
'casewright',
{
title: 'My Test Documentation',
output: {
dir: './custom-dir',
filename: 'tests.html',
},
places: {
patterns: [/Page$/, /Screen$/],
},
values: {
resolveVariables: true,
},
},
]Programmatic Usage
Generate HTML from a manually-built model:
import { generateHtml, createDocModel, defaultConfig } from 'casewright'
const model = createDocModel()
// ... populate suites/tests/steps
const html = generateHtml(model, defaultConfig)Build a model from a list of test files (the static generate command does this internally):
import {
buildDocModelFromSources,
generateHtml,
SourceParser,
defaultConfig,
} from 'casewright'
const parser = new SourceParser(defaultConfig)
const model = buildDocModelFromSources(
['./tests/login.spec.ts', './tests/checkout.spec.ts'],
'./tests',
parser,
)
const html = generateHtml(model, defaultConfig)Read testDir from a Playwright config without running anything:
import { readPlaywrightTestDir } from 'casewright'
const resolved = readPlaywrightTestDir(process.cwd())
if (resolved) {
console.log(resolved.configFile, resolved.testDir, resolved.fromLiteral)
}CLI Commands
# Create a configuration file
npx casewright init
# Generate documentation statically (no test execution)
npx casewright generate [testDir]
npx casewright generate ./tests --config ./casewright.config.ts
# Show help
npx casewright help
# Show version
npx casewright --versionLicense
MIT
