Run your first persona-driven E2E test in 10 minutes.
npm init playwright@latest
TypeScript is recommended but not required. Examples below use TypeScript.
A utility class to capture screenshots and observations
Create a file at tests/utils/ObservationCollector.ts that will track everything during test execution.
import { Page } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
type ObservationType = 'success' | 'note' | 'confusion' | 'frustration';
interface Observation {
type: ObservationType;
description: string;
location?: string;
timestamp: string;
}
interface Screenshot {
name: string;
context: string;
url: string;
base64: string;
}
interface TaskResult {
name: string;
success: boolean;
duration: number;
notes?: string;
}
export class ObservationCollector {
private persona: string;
private outputDir: string;
private observations: Observation[] = [];
private screenshots: Screenshot[] = [];
private tasks: TaskResult[] = [];
private startTime: Date;
private metrics = { pageLoads: 0, clicks: 0, backNavs: 0 };
constructor(persona: string, outputDir = './test-results/personas') {
this.persona = persona;
this.outputDir = outputDir;
this.startTime = new Date();
}
async screenshot(page: Page, name: string, context: string) {
const buffer = await page.screenshot();
this.screenshots.push({
name,
context,
url: page.url(),
base64: buffer.toString('base64')
});
}
observe(type: ObservationType, description: string, location?: string) {
this.observations.push({
type,
description,
location,
timestamp: new Date().toISOString()
});
}
recordTask(name: string, success: boolean, duration: number, notes?: string) {
this.tasks.push({ name, success, duration, notes });
}
trackPageLoad() { this.metrics.pageLoads++; }
trackClick() { this.metrics.clicks++; }
trackBackNav() { this.metrics.backNavs++; }
async save() {
const data = {
persona: this.persona,
session: {
startTime: this.startTime.toISOString(),
endTime: new Date().toISOString(),
...this.metrics
},
tasks: this.tasks,
observations: this.observations,
screenshots: this.screenshots
};
fs.mkdirSync(this.outputDir, { recursive: true });
const filename = `${this.persona.toLowerCase().replace(/\s+/g, '-')}.json`;
fs.writeFileSync(
path.join(this.outputDir, filename),
JSON.stringify(data, null, 2)
);
}
}
Tip
The base64 screenshots are embedded in the JSON output so you can send the entire file to a vision model for analysis without managing separate image files.
Create a fictional user with specific goals and behaviors
Create a test file at tests/personas/dev-dana.spec.ts. We'll use "Dev Dana" - a developer evaluating PersonaSpec.
import { test } from '@playwright/test';
import { ObservationCollector } from '../utils/ObservationCollector';
/**
* Persona: Dev Dana - Developer Evaluating
*
* Background: Senior frontend developer at a Series B startup.
* Skeptical of new testing approaches, has 15 minutes
* between meetings to evaluate if this is worth adopting.
*
* Goals:
* - Understand if PersonaSpec is worth the learning curve
* - Find concrete, runnable code examples
* - Assess implementation effort for their team
*
* Behaviors:
* - Skims headings, jumps to code blocks
* - Looks for "getting started" or quickstart guides
* - Evaluates based on first impressions
*/
const collector = new ObservationCollector('Dev Dana - Developer Evaluating');
// Tests run in serial order (like a real user session)
test.describe.serial('Dev Dana evaluates PersonaSpec', () => {
test.afterAll(async () => {
await collector.save();
});
// Tasks defined in next step...
});
Good Persona Design
Make personas specific enough to guide test design. "A user" is too vague. "A skeptical senior developer with 15 minutes" tells you exactly how to structure the tests.
Each task represents something the persona wants to accomplish
Add task tests inside the describe.serial block. Each follows the 7-step pattern.
test('understand the core concept within 60 seconds', async ({ page }) => {
// 1. Track start time
const startTime = Date.now();
// 2. Initialize success as false (prove it, don't assume)
let success = false;
let notes = '';
// 3. Navigate and screenshot
await page.goto('https://personaspec.dev');
collector.trackPageLoad();
await collector.screenshot(page, 'landing-first-look',
'Dev Dana lands on homepage, scanning for value prop');
// 4. Attempt the task as persona would
const heroText = await page.locator('.hero').textContent();
// 5. Evaluate outcome
if (heroText?.includes('persona') && heroText?.includes('test')) {
success = true;
notes = 'Hero clearly mentions persona-driven testing';
collector.observe('success', 'Value prop clear in hero section', 'Homepage');
} else {
notes = 'Could not quickly understand the core concept';
collector.observe('confusion', 'Value prop not immediately clear', 'Homepage');
}
// 6. Capture result screenshot
await collector.screenshot(page, 'landing-after-scan',
'After scanning hero section for core concept');
// 7. Record the task
const duration = Date.now() - startTime;
collector.recordTask('Understand core concept within 60 seconds', success, duration, notes);
});
test('find a runnable code example', async ({ page }) => {
const startTime = Date.now();
let success = false;
let notes = '';
// Look for "Get Started" link (developer instinct)
const getStarted = page.getByRole('link', { name: /get started/i });
if (await getStarted.isVisible({ timeout: 3000 })) {
await getStarted.click();
collector.trackClick();
collector.trackPageLoad();
await collector.screenshot(page, 'getting-started-page',
'Navigated to Get Started, looking for code');
// Look for code blocks
const codeBlocks = page.locator('.code-block');
if (await codeBlocks.count() > 0) {
success = true;
notes = `Found ${await codeBlocks.count()} code examples`;
collector.observe('success', 'Code examples found on Get Started page');
}
} else {
notes = 'Could not find Get Started link quickly';
collector.observe('frustration', 'No obvious path to code examples');
}
await collector.screenshot(page, 'code-search-result',
'Final state after searching for code examples');
collector.recordTask('Find a runnable code example', success,
Date.now() - startTime, notes);
});
The 7-step pattern ensures consistent, measurable results:
Simulate browsing without a specific goal
End each persona file with an exploration test that catches issues task-focused tests miss.
test('free exploration - browse like a curious developer', async ({ page }) => {
const startTime = Date.now();
// Start on homepage
await page.goto('https://personaspec.dev');
collector.trackPageLoad();
// Define exploration actions
const actions = [
async () => {
await page.locator('a[href*="methodology"]').first().click();
collector.trackClick();
await collector.screenshot(page, 'explore-methodology',
'Clicked methodology link to see details');
},
async () => {
await page.goBack();
collector.trackBackNav();
},
async () => {
await page.locator('a[href*="personas"]').first().click();
collector.trackClick();
await collector.screenshot(page, 'explore-personas',
'Checking out example personas');
},
async () => {
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await collector.screenshot(page, 'explore-scroll-bottom',
'Scrolled to bottom to see all content');
}
];
// Execute each action with error handling
for (const action of actions) {
try {
await action();
await page.waitForTimeout(500); // Simulate human pace
} catch (error) {
collector.observe('note', `Exploration action failed: ${error}`);
}
}
// Free exploration always "succeeds" - observations are the value
collector.recordTask('Free exploration', true,
Date.now() - startTime, 'Browsed homepage, methodology, and personas');
});
Execute tests and feed results to a vision model
Run your persona tests:
npx playwright test tests/personas/dev-dana.spec.ts
This generates a JSON file at test-results/personas/dev-dana---developer-evaluating.json:
Sample Output
{
"persona": "Dev Dana - Developer Evaluating",
"session": {
"startTime": "2024-01-15T10:30:00Z",
"endTime": "2024-01-15T10:32:45Z",
"pageLoads": 4,
"clicks": 5,
"backNavs": 1
},
"tasks": [
{
"name": "Understand core concept within 60 seconds",
"success": true,
"duration": 4500,
"notes": "Hero clearly mentions persona-driven testing"
},
{
"name": "Find a runnable code example",
"success": true,
"duration": 8200,
"notes": "Found 6 code examples"
}
],
"observations": [
{
"type": "success",
"description": "Value prop clear in hero section",
"location": "Homepage"
},
{
"type": "success",
"description": "Code examples found on Get Started page"
}
],
"screenshots": [
{
"name": "landing-first-look",
"context": "Dev Dana lands on homepage, scanning for value prop",
"url": "https://personaspec.dev/",
"base64": "iVBORw0KGgoAAAANSUhEUgAA..."
}
]
}
Feed the JSON file to Claude or another vision model with this prompt:
Here are screenshots and observations from a persona-driven test.
The persona is "Dev Dana - Developer Evaluating":
- Background: Senior frontend dev, skeptical of new approaches, 15 minutes to evaluate
- Goals: Understand if worth adopting, find code examples, assess implementation effort
- Behaviors: Skims headings, jumps to code blocks, looks for quickstart guides
Analyze each screenshot in context of this persona and their tasks.
Identify:
1. UX issues that would frustrate this specific developer
2. Accessibility problems visible in the screenshots
3. Design inconsistencies
4. Whether their goals appear achievable
5. Specific recommendations for improvement
The observations.json file is attached.