I built a CLI that scaffolds a production-grade Playwright project in 30 seconds

I recently published an article about structuring Playwright projects properly. The response confirmed what I already suspected: people know their test suites are messy, they just don't have time to fix the architecture when there's a sprint to deliver and a product owner asking why the regression suite is still red.
So instead of writing another article about how things should be organized, I turned that architecture into a single command.
npx histrion create
You answer five questions about your project, and about thirty seconds later you've got a fully configured Playwright setup with Page Object Model, typed fixtures, data builders with Faker.js, visual regression, a custom reporter, CI/CD, and 16 documentation guides. Dependencies installed, Git initialized, first commit done. You open VS Code and start writing tests.
The problem I kept solving by hand
Every time I started a new QA engagement, I spent the first week building the same infrastructure from scratch: folder structure, base classes, environment config, Biome setup, fixture wiring, custom reporter, CI pipeline. Then I'd spend a second week explaining the whole architecture to the team, which really means answering the same Slack questions forty times with slightly different phrasing.
The architecture itself isn't complicated. But getting every detail right (import paths, type annotations, config options, the exact order in which things need to be wired) takes time. And when your client is paying for test results, not infrastructure, that first week of setup feels like pure overhead even though it's what makes the next six months possible. After the third engagement where I rebuilt the same thing from memory, I decided to stop being my own worst bottleneck.
What you get
After running npx histrion create, your project is built on four layers of abstraction where each one hides the complexity of the one below it:
┌─────────────────────────────────────────────────┐
│ Tests (tests/e2e/*.spec.ts) │
│ Scenarios and assertions. Never touches │
│ Playwright directly. Reads like a spec. │
├─────────────────────────────────────────────────┤
│ Fixtures (src/fixtures/) │
│ Dependency injection. Creates Page Objects │
│ and injects them into tests automatically. │
├─────────────────────────────────────────────────┤
│ Pages & Components (src/pages/ + src/ │
│ components/) │
│ Pages = one class per page. Components = │
│ reusable UI fragments (tables, modals, │
│ navbars). Pages compose Components. │
├─────────────────────────────────────────────────┤
│ Core (src/core/) │
│ Abstract base classes: BasePage, BaseComponent │
│ Navigation, logging, interactions, HTTP. │
└─────────────────────────────────────────────────┘
Alongside these four layers, support modules handle the rest: config for environments and credentials, data for builders and types, utils for the logger and custom matchers, API clients, and reporters. These aren't layers in the dependency hierarchy, they're shared tooling that any layer can reach into.
src/
├── core/ ← Abstract base classes (BasePage, BaseComponent, BaseAPI)
├── components/ ← Reusable UI pieces (Table, Modal, Form, Toast)
├── pages/ ← One Page Object per page of your app
├── fixtures/ ← Type-safe dependency injection
├── api/ ← HTTP clients for test setup/teardown
├── data/ ← Fluent test data builders
├── config/ ← Environment & credential management
├── reporters/ ← Custom HTML reporter
└── utils/ ← Logger, visual helpers, custom matchers
Dependencies flow downward only. Tests never import from @playwright/test directly, they import from your fixtures file, which injects Page Objects automatically.
The golden rule in practice
The entire framework enforces one principle: tests never contain selectors. If a test file has a CSS selector, a data-testid, or a raw page.click() call in it, something went wrong.
Here's what a test actually looks like in a Histrion project:
test('admin can approve a pending application', async ({ dashboardPage }) => {
await dashboardPage.navigateTo('applications');
await dashboardPage.applications.filterByStatus('pending');
await dashboardPage.applications.approve(0);
await dashboardPage.toast.expectSuccess('Application approved');
});
No page.click(), no data-testid, no waitForSelector. A product owner could read this and understand what it verifies. All the implementation details (which button to click, which selector to target, how to wait for the network) live inside Page Objects where they belong.
When the UI changes, and if you've worked on a web app for longer than three months you know it will, you update one Page Object. Every test that touches that page keeps working. That's the whole point.
The tedious stuff nobody wants to build
Beyond the architecture, Histrion sets up all the things that are essential but soul-crushingly boring to build from scratch. The kind of work that everyone agrees is important and nobody volunteers for.
Auth state caching. A global setup authenticates your test users once and saves their browser state to disk. Individual tests reuse that state and skip the login flow entirely. Your 200-test suite doesn't log in 200 times, which is good because watching a login form fill itself 200 times is the QA equivalent of watching paint dry.
Fluent data builders. Instead of hardcoding { firstName: 'Test', email: 'test@example.com' } in every test file (we've all done it), you use builders that generate random, realistic data with Faker.js, a library that produces plausible names, emails, addresses, and anything else you'd find in a real form:
const contact = ContactBuilder.create()
.withEmail('invalid-email')
.build();
Every field gets a random but valid default. You only override what the test actually cares about. When someone adds a required field to ContactFormData next month, you update the builder's defaults in one place. Every test keeps working without being touched.
API helpers for setup and teardown. A test that verifies "user can delete a record" shouldn't spend 30 seconds navigating forms to create that record first. API clients handle preconditions in milliseconds, so the test only exercises what it's supposed to test.
Visual regression with smart masking. Screenshots compared against baselines, with timestamps, avatars, and any dynamic content automatically masked. Because nothing kills confidence in visual tests faster than a failure caused by the clock moving forward one second.
A custom HTML reporter. Dark mode, filterable by status and tags, with full error details for failed tests. It auto-opens in your browser after each run. Playwright's built-in reporter does the job, but after enough late-night debugging sessions you start developing strong opinions about what a test report should look like.
Structured logger. Every action is traced with timestamps and context:
14:32:01 ■ ContactPage │ ▸ Navigating to /contact
14:32:01 ■ ContactPage │ fill "first name" with "Émile"
14:32:01 ■ ContactPage │ fill "last name" with "Renard"
14:32:02 ■ ContactPage │ ▸ Submitting contact form
14:32:02 ■ ContactPage │ click Send button
14:32:03 ■ ContactPage │ ✓ Success alert visible
When a test fails in CI at 2am and you're staring at a Slack notification on your phone, this log tells you exactly what happened and where it stopped. Worth its weight in lost sleep.
Biome instead of ESLint + Prettier. One tool instead of two, no config debates, instant feedback. The generated code passes biome check on the first run. If you've ever spent a Friday afternoon resolving conflicts between ESLint rules and Prettier opinions, you know exactly why this matters.
GitHub Actions. Matrix strategy for cross-browser testing, manual dispatch with environment selection, artifact upload, and report deployment to GitHub Pages. The kind of pipeline that takes half a day to write correctly and that everyone copies from the last project with minor adjustments they forget to make.
Adding a new page: the 3-step workflow
Once the project is scaffolded, extending coverage always follows the same rhythm.
1. Create the Page Object. One class, private locators, public methods:
// src/pages/settings.page.ts
export class SettingsPage extends BasePage {
readonly path = '/settings';
readonly pageTitle = /Settings/;
private readonly nameInput = this.page.getByLabel('Display name');
private readonly saveButton = this.page.getByRole('button', { name: 'Save' });
async updateName(name: string): Promise<void> {
await this.fill(this.nameInput, name, 'name');
await this.click(this.saveButton, 'Save');
}
}
2. Register the fixture (one line in src/fixtures/index.ts):
settingsPage: async ({ page }, use) => {
await use(new SettingsPage(page));
},
3. Write the test:
test('can update display name', async ({ settingsPage }) => {
await settingsPage.navigate();
await settingsPage.updateName('New Name');
});
Page Object, fixture, test. Same rhythm every time, regardless of how complex the page is. Once the pattern clicks, adding coverage for a new page takes about ten minutes, and most of that is spent figuring out the right locators.
The documentation nobody writes
Here's an uncomfortable truth about test frameworks: the person who built it knows how everything connects, and everyone else reverse-engineers the architecture by reading existing tests and guessing. I've seen teams where the original author left six months ago and the remaining developers treat the framework like a haunted house. They walk through it carefully, try not to touch anything they don't understand, and when something breaks they add a sleep(5000) and hope for the best.
Histrion generates 16 markdown guides in a local docs/ folder. They cover everything from getting started to best practices to architecture decisions, written for developers who are new to the framework. The folder is gitignored so it doesn't clutter the repo, and the files are Obsidian-compatible if that's how you manage your knowledge.
When a new team member joins, you point them to docs/00-index.md. They read for an hour and they can start contributing instead of spending their first two weeks afraid of breaking something.
What Histrion is not
Histrion isn't a testing framework, Playwright handles that part. Histrion is an opinion about how to organize a Playwright project, packaged as a CLI that saves you a week of setup and a month of explaining the architecture to people.
It's not prescriptive about your application either. The generated pages (LoginPage, DashboardPage, ContactPage) are examples that demonstrate the patterns. Delete them and build your own once you understand the structure. The architecture stays, the examples were always meant to be replaced.
And it's not a runtime dependency. After scaffolding, Histrion disappears from your project entirely. Your package.json depends on Playwright, TypeScript, Biome, and dotenv. If you uninstall Histrion after scaffolding, your project won't notice, which is exactly how a scaffolding tool should behave.
Try it
npx histrion create
Source on GitHub: github.com/Kan-Bull/histrion
If you want the full theory behind the architecture, the companion article breaks down why each layer exists and what problems it solves. And if you use Histrion on a real project, I'd genuinely like to hear what worked and what didn't.
Histrion is MIT-licensed and open to contributions.






