Migrating from Playwright

You don't have to migrate. Keep your specs; write new tests, or the ones that break most often, in plain English next to them. Every recorded test becomes a Playwright spec again.

Coming There is no automatic import of Playwright specs yet (it's planned). Today you rewrite a spec as a test by hand, using the table below; it's usually shorter than the spec.

What maps to what

Playwright code and the test-file equivalent
PlaywrightIn a test file
test("Checkout works", …)name: Checkout works in the frontmatter; one test per .test.md file
page.goto("/pricing")start: /pricing, or a step Go to /pricing
getByRole("button", { name: "Save" }).click()Click "Save", or exactly: Exact: click role=button[name="Save"]
getByLabel("Email").fill(email)Fill "Email" with {{data.email}}, or Exact: fill label="Email" with {{data.email}}
selectOption("Europe/London")Select "Europe/London" in "Time zone"
keyboard.press("Enter")Exact: press Enter
expect(heading).toHaveText(…)Expect: the page heading is "Welcome"
expect(page).toHaveURL(/dashboard/)Expect: the URL contains /dashboard
expect(locator).toBeVisible()Expect: "Saved" is visible
expect(rows).toHaveCount(5)Expect: the orders table shows 5 orders, or Exact: expect css=.order-row count 5
test.step / helper functionsa flow (kind: flow) included with Use: flows/login.test.md
storageState / global setup loginan auth profile: auth: admin logs in once and reuses the saved session
process.env.PASSWORD{{secret.PASSWORD}}, declared with the domains it may be typed into
test data, fakerdata: and {{unique.email}}, {{faker.name}}
page.route() mocksComing Network mocking isn't available yet
anything elsea ```ts code step, kept verbatim (runs through the generated spec, not healed)

A worked example

A spec:

import { expect, test } from "@playwright/test";

test("A new project is saved", async ({ page }) => {
  await page.goto("/login");
  await page.getByLabel("Email").fill("ada@example.com");
  await page.getByLabel("Password").fill(process.env.SHOP_PASSWORD!);
  await page.getByRole("button", { name: "Log in" }).click();
  await page.getByRole("button", { name: "Create project" }).click();
  await expect(page.getByRole("dialog", { name: "New project" })).toBeVisible();
  await page.getByLabel("Project name").fill("Q3 roadmap");
  await page.getByRole("button", { name: "Create" }).click();
  await expect(page.getByRole("status")).toContainText("Project created");
});

The same test in plain English. The locators, waits and assertions come from the first run's recording:

---
name: A new project is saved
start: /login
---

1. Fill "Email" with ada@example.com
2. Fill "Password" with {{secret.SHOP_PASSWORD}}
3. Click "Log in"
4. Click "Create project"
5. Expect: a dialog titled "New project" is open
6. Fill "Project name" with Q3 roadmap
7. Click "Create"
8. Expect: a message says "Project created"
npx optestra init             # keeps your Playwright config and specs as they are
npx optestra author tests/create-project.test.md
npx optestra run
npx optestra generate         # the Playwright spec again, in tests/.optestra/

init never touches an existing Playwright config or spec, and doctor warns if your config would also pick up the generated specs in tests/.optestra/, with the one-line fix. The generated spec uses role and label locators, a test.step per English step, and learned waits instead of timeouts: see Playwright export.

What changes, and what doesn't

  • Selectors stop being your job: a changed button is healed and shown to you for review, instead of breaking the test.
  • Each Expect: line is compiled into a real check and tested so it can fail.
  • Things with no English equivalent stay code: a ```ts step runs your Playwright code as written (but isn't healed).
  • Network mocking (page.route) has no equivalent yet. Coming