Skip to content

Reporters

Reporters

Reporters format and output script execution results. Stepwright includes built-in reporters and supports custom implementations.

Built-in Reporters

ConsoleReporter

Outputs results to the terminal:

import { Stepwright, ConsoleReporter } from '@korvol/stepwright';
const script = Stepwright.create('My Script')
.reporter(new ConsoleReporter())
.step('Navigate', ...)
.step('Click', ...);

Console Options

new ConsoleReporter({
colors: true, // Enable colored output
timestamps: true, // Show timestamps
verbose: false, // Show detailed step info
})

Example Output

[10:30:00] Starting: My Script
[10:30:01] ✓ Navigate (1.2s)
[10:30:02] ✓ Click (0.5s)
[10:30:02] ✓ Verify (0.3s)
[10:30:02] ────────────────────
[10:30:02] Passed: 3/3 steps
[10:30:02] Duration: 2.0s

JSONReporter

Outputs results as JSON:

import { Stepwright, JSONReporter } from '@korvol/stepwright';
const script = Stepwright.create('My Script')
.reporter(new JSONReporter({
outputPath: './results/output.json',
}));

JSON Options

new JSONReporter({
outputPath: './results.json', // Write to file
pretty: true, // Pretty-print JSON
includeArtifacts: true, // Include artifact paths
})

Example Output

{
"name": "My Script",
"success": true,
"duration": 2000,
"stepsCompleted": 3,
"totalSteps": 3,
"steps": [
{
"name": "Navigate",
"status": "passed",
"duration": 1200
},
{
"name": "Click",
"status": "passed",
"duration": 500
},
{
"name": "Verify",
"status": "passed",
"duration": 300
}
]
}

Multiple Reporters

Add multiple reporters for different outputs:

Stepwright.create('My Script')
.reporter(new ConsoleReporter()) // Terminal output
.reporter(new JSONReporter({ // JSON file
outputPath: './results.json'
}))
.reporter(new CustomSlackReporter()) // Custom Slack notifications

Default Reporter

If no reporter is specified, ConsoleReporter is used by default:

// These are equivalent:
Stepwright.create('Script').step(...).run();
Stepwright.create('Script')
.reporter(new ConsoleReporter())
.step(...)
.run();

Reporter Interface

Interface Definition

interface Reporter {
/** Called when script starts */
onStart?(config: StepwrightConfig): void | Promise<void>;
/** Called when a step starts */
onStepStart?(step: StepResult): void | Promise<void>;
/** Called when a step ends */
onStepEnd?(step: StepResult): void | Promise<void>;
/** Called when script ends */
onEnd?(result: StepwrightResult): void | Promise<void>;
}

Lifecycle Events

onStart()
└─ onStepStart(step1)
└─ onStepEnd(step1)
└─ onStepStart(step2)
└─ onStepEnd(step2)
└─ ...
onEnd()

Creating Custom Reporters

Basic Custom Reporter

import type { Reporter, StepwrightConfig, StepwrightResult, StepResult } from '@korvol/stepwright';
class MyReporter implements Reporter {
onStart(config: StepwrightConfig) {
console.log('Starting:', config.name);
}
onStepStart(step: StepResult) {
console.log('Running:', step.name);
}
onStepEnd(step: StepResult) {
const icon = step.status === 'passed' ? '✓' : '✗';
console.log(`${icon} ${step.name} (${step.duration}ms)`);
}
onEnd(result: StepwrightResult) {
console.log('Completed:', result.success ? 'PASS' : 'FAIL');
}
}
// Usage
Stepwright.create('Script')
.reporter(new MyReporter())
.step(...);

Async Reporter

Reporters can use async methods:

class AsyncReporter implements Reporter {
async onEnd(result: StepwrightResult) {
await sendToDatabase(result);
await notifySlack(result);
}
}

Stateful Reporter

Track state across events:

class StatefulReporter implements Reporter {
private startTime = 0;
private failedSteps: string[] = [];
onStart() {
this.startTime = Date.now();
this.failedSteps = [];
}
onStepEnd(step: StepResult) {
if (step.status === 'failed') {
this.failedSteps.push(step.name);
}
}
onEnd(result: StepwrightResult) {
const duration = Date.now() - this.startTime;
console.log(`Total time: ${duration}ms`);
if (this.failedSteps.length > 0) {
console.log('Failed steps:', this.failedSteps.join(', '));
}
}
}

Example: Slack Reporter

class SlackReporter implements Reporter {
private webhookUrl: string;
constructor(webhookUrl: string) {
this.webhookUrl = webhookUrl;
}
async onEnd(result: StepwrightResult) {
const message = {
text: result.success
? `✓ ${result.steps[0]?.name} passed (${result.stepsCompleted}/${result.totalSteps})`
: `✗ ${result.failedStep?.name} failed: ${result.failedStep?.error.message}`,
};
await fetch(this.webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(message),
});
}
}

Example: GitHub Actions Reporter

class GitHubActionsReporter implements Reporter {
onStepEnd(step: StepResult) {
if (step.status === 'failed') {
// GitHub Actions annotation format
console.log(`::error title=${step.name}::${step.error?.message}`);
}
}
onEnd(result: StepwrightResult) {
// Set output for workflow
console.log(`::set-output name=success::${result.success}`);
console.log(`::set-output name=steps_completed::${result.stepsCompleted}`);
}
}

Best Practices

Combine Console and File Output

.reporter(new ConsoleReporter()) // Real-time feedback
.reporter(new JSONReporter({ // Persistent record
outputPath: `./results/${Date.now()}.json`
}))

Handle Reporter Errors

class SafeReporter implements Reporter {
async onEnd(result: StepwrightResult) {
try {
await sendToExternalService(result);
} catch (error) {
console.error('Reporter error:', error);
// Don't rethrow - script should still succeed
}
}
}

Keep Reporters Lightweight

Avoid heavy operations in onStepStart/onStepEnd:

// Bad: Slow I/O in step events
class SlowReporter implements Reporter {
async onStepEnd(step: StepResult) {
await fs.writeFile(`step-${step.index}.json`, ...); // Slow!
}
}
// Good: Batch writes in onEnd
class FastReporter implements Reporter {
private steps: StepResult[] = [];
onStepEnd(step: StepResult) {
this.steps.push(step);
}
async onEnd() {
await fs.writeFile('all-steps.json', JSON.stringify(this.steps));
}
}

Next Steps