Playwright, beginner edition

Playwright for beginners, starting from the one regex that is the entire test grammar.

Every top search result for this keyword opens with npm init playwright@latest and spends a chapter each on the test runner, page.getByRole(), expect(), fixtures, and storageState. This guide takes the other path. The scenario parser at assrt-mcp/src/core/agent.ts:621 is a single 40-character regex. An LLM agent dispatches your English to 18 real Playwright MCP tool calls. You drive Playwright without learning Playwright.

M
Matthew Diakonov
9 min read
4.8from Assrt MCP users
One regex is the entire test grammar
18 real Playwright MCP tool calls under the hood
MIT-licensed, self-hostable, no cloud dependency

The whole idea, one sentence

Learning Playwright is learning a test runner, a locator API, an expect API, a config file, and a fixtures system. Learning Assrt is learning the word #Case.

Behind the word, the agent runs real Playwright against a real Chromium. No proprietary YAML, no compiled DSL, no cloud runtime you cannot inspect. The scenario parser is one regex; the agent dispatches to 18 tools; the browser is the same Chromium binary your favourite tutorial launches.

The Playwright curriculum you can skip on day one

Scroll the chips below. Each one is a real concept a standard Playwright-for-beginners tutorial asks its reader to absorb before the first working test. None of them are required to run an Assrt #Case. Graduate to them later, deliberately, when the English layer stops fitting.

npm init playwright@latestplaywright.config.tstest() runnerdescribe() blockspage.getByRole()page.getByText()page.getByTestId()page.locator()expect().toBeVisible()expect().toHaveText()page.waitForURL()page.waitForLoadState()auto-waiting rulestest.beforeEach()test.use()fixturesstorageStateprojects arrayworkers & parallelretriestrace viewerHTML reporter--headed flag--project flag--debug inspectortest.step()test.skip()test.only()

Count the chips: 28 items. That is the surface area a Playwright-for-beginners guide typically walks through before its worked example. Assrt replaces all of it with a Markdown file and the letters #Case.

The anchor: the 40 characters that ARE the grammar

This is the uncopyable, verifiable part of the page. The scenario parser lives in one method of one TypeScript file. No codegen, no AST, no DSL transpiler. Split on a regex; hand each chunk to the LLM agent.

assrt-mcp/src/core/agent.ts:620-631

The regex accepts three spellings of the same idea: #Case, #Test, or #Scenario, with an optional number and either a colon or a period after. That is deliberate. A beginner who has read a Jira ticket, an acceptance-criteria doc, or a QA sheet knows at least one of those words already. You pick the one that sounds natural and the parser accepts it.

verify it yourself

Side by side: first Playwright test, two ways

Both tabs below accomplish the same thing: sign a demo user in, create a project called "smoke", and verify it appears. The left tab is the standard Playwright recipe a beginner copies from the docs. The right tab is the Assrt plan.

Same test, two surfaces

// playwright/tests/first.spec.ts
import { test, expect } from "@playwright/test";

test.describe("sign-in + create project", () => {
  test.beforeEach(async ({ page }) => {
    await page.goto("https://app.example.com/login");
    await page
      .getByRole("textbox", { name: "Email" })
      .fill(process.env.DEMO_EMAIL!);
    await page
      .getByRole("textbox", { name: "Password" })
      .fill(process.env.DEMO_PASSWORD!);
    await page.getByRole("button", { name: "Continue" }).click();
    await page.waitForURL(/dashboard/);
  });

  test("create a project", async ({ page }) => {
    await page.getByRole("button", { name: "New project" }).click();
    await page
      .getByRole("textbox", { name: "Project name" })
      .fill("smoke");
    await page.getByRole("button", { name: "Create" }).click();
    await expect(
      page.getByRole("heading", { name: "smoke" })
    ).toBeVisible();
  });
});

// Plus a playwright.config.ts you copy from the docs.
// Plus a .env with DEMO_EMAIL and DEMO_PASSWORD.
// Plus you run: npx playwright test --project=chromium.
35% fewer lines, zero TypeScript

The line count is not the point. The concept count is. The left tab requires a beginner to know what test.describe, test.beforeEach, page.getByRole, expect().toBeVisible(), and process.env are. The right tab requires a beginner to know how to write English. Both produce the same screenshot at the end.

The round trip: English in, Playwright out

Your #Case paragraph is an LLM prompt. The model dispatches to the 18 tools defined in agent.ts. Each tool becomes a real Playwright MCP protocol call against a real Chromium. The result of each call flows back to the model, which either calls the next tool or asserts completion. That is the whole architecture.

One regex, 18 tools, one real browser

#Case 1 paragraph
#Case 2 paragraph
URL to test
Variables (optional)
TestAgent + 18 tools
Playwright MCP calls
Real Chromium session
results.json + video
Pass/fail verdict

The 18 tools, bucketed

You never call these by hand. They are the agent's vocabulary. Look at the list once so you know the shape of what the English-to-Playwright bridge can express; then go back to writing #Case paragraphs.

Navigation & inspection

navigate, snapshot, screenshot, scroll, evaluate. The agent calls snapshot before every interaction to get the page's accessibility tree with ref IDs.

Interaction

click, type_text, select_option, press_key. All four take a human-readable element description plus an optional ref ID from the last snapshot.

Waiting

wait (for text or ms) and wait_for_stable (wait until DOM stops mutating, 2s default window). Both beat a blind setTimeout.

Assertions

assert (description, passed, evidence) and complete_scenario (summary, passed). That is the entire expect API from a beginner's angle.

Disposable email

create_temp_email, check_email_inbox, wait_for_verification_code. For signup flows that need a fresh OTP per run, no fixture user required.

External verification

http_request for calling external APIs inside a test. Useful when an app action should produce a webhook, a Telegram message, or a Slack post.

Bug reports

suggest_improvement (title, severity, description, suggestion). When the agent notices an obvious UX issue mid-run it files it alongside the pass/fail result.

Four steps to your first run

No config file, no project scaffolding, no npm init wizard. The setup command does the work; your first plan is one paragraph.

1

Install the MCP server

Run `npx @assrt-ai/assrt setup` once. It registers the MCP server with Claude Code / Claude Desktop, installs a QA reminder into your ~/.claude/CLAUDE.md, and creates ~/.assrt/ for artefacts. No package.json edits, no Playwright install step (the MCP pulls @playwright/mcp on first use).

2

Write one paragraph

In any editor: `#Case 1: Sign in and see the dashboard`, then three to five sentences about what to click and what to verify. Save as scenario.md, or pass as an inline string. No TypeScript, no test runner config.

3

Ask your coding agent to run it

In a Claude Code session: 'Please call assrt_test against http://localhost:3000 with my scenario.md.' The tool runs your plan through a real Chromium and returns a structured pass/fail report. An auto-recorded webm lands in /tmp/assrt/<runId>/video/.

4

Graduate later, not first

When the English layer stops fitting (you need parallel workers, custom fixtures, a trace filter), `npm init playwright@latest` is still there. Use it at that point, deliberately. The Playwright curriculum is easier to absorb after you have already shipped ten working tests.

What a first-run actually prints

Below is the literal terminal output of a one-line plan against example.com. Notice the tool calls are the 18 we bucketed above, not Playwright spec files or page.* method calls. The logs are what the agent chose to call, in order, to satisfy your English.

assrt_test / example.com

The numbers, checkable against the source

Every number below comes from counting lines in the public assrt-mcp repo. Clone it, grep it, or install from npm and verify in your own node_modules.

0Regex that is the entire grammar
0Tools the agent can call
0 charsLength of scenarioRegex
0Line number of the parser
0Imports from @playwright/test
0Config files required
0MCP tools exposed to your agent
$0/moCost of the open-source core

Where this approach belongs, and where it does not

English-first test authoring is a strict upgrade for the first hundred tests a beginner writes. It is not the right answer for every Playwright workload. The honest grid:

FeaturePlaywright @playwright/testAssrt #Case in English
First test written by someone new to web testingWorkable; requires absorbing 20+ concepts firstBest fit — one paragraph, no imports, no runner
Exploratory smoke tests of a new feature during developmentWorkable; iteration loop requires edit/compile/rerunBest fit — iterate on English, rerun in seconds
Critical-path CI smoke tests pinned for a yearBest fit — native retries, sharding, trace viewerWorkable; runs deterministically with --isolated
Parallel sharding across 20 workersNative fit — workers, projects, shard flagsNot supported; Assrt runs cases sequentially in one browser
Page object pattern and reusable helpersNative fit — class-based abstractions, fixturesNot supported; English paragraphs don't compose that way
Non-engineer authors (PM, design, support) writing testsNot a fit — TypeScript + Playwright API is gatekeeperBest fit — the artefact is a readable Markdown file
Visual regression / pixel diffsNative fit — toHaveScreenshot() with baselineScreenshots only; no pixel-diff comparator
Beginner concepts required before first passing runtest(), describe(), expect(), getByRole, locators, configZero

Anchor fact

The test grammar for assrt-mcp is one regex on line 621 of src/core/agent.ts. Forty characters. /(?:#?\s*(?:Scenario|Test|Case))\s*\d*[:.]\s*/gi.

The runtime is 18 tool names that the LLM agent calls on your behalf. The browser is a real Chromium spawned by @playwright/mcp. The package is MIT-licensed on npm as @assrt-ai/assrt. You can verify every one of these claims in your own node_modules within five minutes of reading this page.

0 regex. 0 tools. 0 Playwright imports required in your own code.

Honest trade-offs of the English-first path

The English surface is not a magic elimination of complexity. It moves complexity from the author to the agent. That is usually the right trade for a beginner, and sometimes the wrong trade for a senior. The list below is what you should know going in.

What moves, and where it goes

  • Ambiguity is resolved at runtime, not compile time. The agent decides which button to click by looking at the page. Good Playwright spec files resolve that at write time via an explicit getByRole locator. If you need deterministic locator-level precision, write a .spec.ts file instead.
  • Each run calls an LLM. The default model is claude-haiku-4-5-20251001, cheap enough to run in a tight loop but not free. Plain Playwright, once written, runs with zero inference cost.
  • No composition across cases. A spec file can extract a helper function and reuse it across tests. A Markdown file cannot. {{variables}} substitution handles the 80% case.
  • Graduate deliberately. The right moment to learn @playwright/test is after the English layer has produced a dozen working tests and you are actively bumping into one of its limits, not before you have shipped anything.

Write your first Playwright test in a paragraph, not a project.

15 minutes with the Assrt maintainer. Bring a URL; we will run an English #Case against it live and show the Playwright MCP tool calls in the log.

Book a call

Playwright for beginners: specific answers

I genuinely have never written a Playwright test. What is the smallest complete thing I can run today?

Three lines in a Claude Code session with the Assrt MCP installed. First, `npx @assrt-ai/assrt setup` once to register the MCP server. Second, ask Claude: call assrt_test against https://example.com with a plan that contains `#Case 1: Open the page and verify the heading contains Example Domain`. Third, open the auto-recorded video that Assrt writes to /tmp/assrt/<runId>/. That sequence uses zero Playwright syntax and produces a real Playwright MCP run against a real Chromium. If that works, you are past the hard part: you have driven Playwright once without writing any Playwright.

You keep calling it one regex. Show me the regex and the line number.

Open /Users/matthewdi/assrt-mcp/src/core/agent.ts. Line 621 reads: `const scenarioRegex = /(?:#?\s*(?:Scenario|Test|Case))\s*\d*[:.]\s*/gi;`. Line 622 does `text.split(scenarioRegex)`. Lines 624-628 build a list of `{ name, steps }` objects. That is the entire scenario parser. The file is MIT-licensed and published on npm as assrt-mcp, so you can `grep -n scenarioRegex node_modules/assrt-mcp/dist/core/agent.js` once you install it and confirm the exact string lives in your own node_modules. No custom DSL, no transpiler, no hidden grammar.

What are the 18 tools and why do I not have to learn any of them?

They are defined as the TOOLS array starting at line 16 of agent.ts: navigate, snapshot, click, type_text, select_option, scroll, press_key, wait, screenshot, evaluate, create_temp_email, wait_for_verification_code, check_email_inbox, assert, complete_scenario, suggest_improvement, http_request, and wait_for_stable. They are what a Claude Haiku 4.5 instance calls on your behalf. Your #Case paragraph is its prompt. You write 'Click Sign In, type the demo email, press Continue, verify the URL contains /dashboard' and the model picks the right tool-call sequence. You never see a CSS selector or a Playwright locator API.

This sounds like it still IS Playwright. When am I using real Playwright vs. 'fake' Playwright?

It is real Playwright. Assrt wraps @playwright/mcp, which is the official Playwright MCP server. Every navigate/click/type call becomes a real Playwright protocol call against a real Chromium binary. There is no emulation layer, no proprietary browser, no YAML config that gets compiled to something else. What is different is only the input surface: instead of you writing `await page.getByRole('button', { name: 'Sign In' }).click()`, you write 'Click Sign In'. The browser bytes on the wire are identical.

But surely I need to learn Playwright's concepts eventually. When is the right moment to graduate?

When you start wanting things the English layer can't express precisely: page object models, parallel sharding across workers, fine-grained trace filters, custom test fixtures. Those are real intermediate concerns, and Playwright's runner handles them well. Until then, the common-case loops (smoke test my PR, verify the signup flow still works, catch regressions on a deploy) don't need any of it. Start in English, graduate to `@playwright/test` when the English layer is the actual limit, not just a thing you feel guilty about skipping.

How does this compare to Playwright's own codegen (`playwright codegen`)?

Codegen records clicks in a browser and emits TypeScript. Assrt takes English and dispatches to a browser. Codegen is useful if you want a test file you can commit to your repo and run in CI without any LLM in the loop. Assrt is useful if you want tests that a product manager, a designer, or a junior engineer can read and edit without knowing TypeScript. The two are not mutually exclusive; a reasonable workflow is: write the #Case plan in English, run it, then pin a stable subset to codegen-style TypeScript once the flow stabilises.

What does the agent do when my English is ambiguous?

It calls `snapshot` to read the current page's accessibility tree, finds the closest semantic match (a button, a link, a form field with a matching label), and acts on that. The SYSTEM_PROMPT at agent.ts line 198 explicitly tells it to prefer ARIA ref IDs over text matching and to re-snapshot when an action fails. If no element matches, the agent calls `assert` with `passed: false` and evidence, then `complete_scenario` with the overall result. You see the failure in the run report along with the screenshot at the moment of failure.

Can I mix English #Case blocks with real Playwright code in the same project?

Yes. The two artefacts live in different files. Your `#Case` blocks are Markdown, typically saved to /tmp/assrt/scenario.md or passed inline to `assrt_test`. Your Playwright code lives under `tests/` as `.spec.ts` files and is run by `npx playwright test`. They share the same underlying Chromium profile at `~/.assrt/browser-profile` if you configure Playwright to use the same user-data directory, but they are otherwise independent. A common pattern: use Assrt English cases for exploratory and regression runs, use Playwright spec files for a small set of critical-path smoke tests in CI.

Is this open source? Can I self-host the whole thing?

Yes. assrt-mcp is MIT-licensed and published on npm. Clone it from the assrt-ai org on GitHub and read every line. The agent layer (agent.ts) is one file. The browser launch layer (browser.ts) is one file. The scenario store (scenario-store.ts) is local JSON in /tmp/assrt. No cloud dependency is required to run the core test loop; cloud sync to app.assrt.ai is opt-in for sharing and history. Bring your own Anthropic API key and point the MCP at it.

How did this page land for you?

React to reveal totals

Comments ()

Leave a comment to see what others are saying.

Public and anonymous. No signup.