From playwright-test-gen
Converts Given/When/Then manual test cases (in CSV format) into Playwright TypeScript tests using the Page Object Model. Use this skill whenever the user wants to automate manual test cases, convert GWT scripts to Playwright, turn a test CSV into code, or generate automated tests from an existing test script. Also triggers when the user says "automate these tests", "convert to Playwright", "write Playwright tests from my CSV", or pastes GWT-style test steps and asks for code. Produces POM locators, page methods, test fixtures, and spec files — scoped to only what is needed based on the input.
How this skill is triggered — by the user, by Claude, or both
Slash command
/playwright-test-gen:playwright-test-genThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Converts Given/When/Then manual test cases (CSV format) into Playwright TypeScript tests. Produces well-structured Page Object Model code — only generating what is actually needed — following all established Playwright best practices.
Converts Given/When/Then manual test cases (CSV format) into Playwright TypeScript tests. Produces well-structured Page Object Model code — only generating what is actually needed — following all established Playwright best practices.
New to these skills? See the Playwright Skills Workflow Guide to understand how playwright-test-gen fits into the broader testing lifecycle, and when to use playwright-cli for exploration first. Expects a CSV with these columns:
Case, Preconditions, Steps (text), Expected, Folder
Frontend or CMSCMS-folder rows (Optimizely/Storyblok editor actions) are skipped by default. Flag if the user wants CMS tests included — these require a separate discussion about editor automation approach.
Read the CSV and group rows by the pages or features they touch. Identify:
If the user shares existing page objects or a project structure, check for:
Before writing code, present a brief plan:
Pages identified: LoginPage, DashboardPage
New locators needed: 4 (LoginPage), 2 (DashboardPage)
New methods needed: fillLoginForm, submitLogin, verifyDashboardLoaded
Existing methods reused: (none / list them)
New fixture registrations: loginPage, dashboardPage
Test file: tests/login.spec.ts — N test cases
Get a thumbs-up before generating if the plan is non-trivial (more than ~3 pages or ~10 tests).
Produce in order:
testFixtures/base.ts additions onlytests/<feature>.spec.tsThis is the most important rule in this skill. Do not invent locators. Follow this hierarchy:
These can be inferred from GWT language without seeing the DOM:
getByLabel('First name') — when the step references a labelled form field by its visible label textgetByRole('button', { name: 'Submit' }) — when the step references a button by its visible textgetByRole('heading', { name: '...' }) — when asserting a headinggetByRole('link', { name: '...' }) — when clicking a named linkgetByTestId('...') — only when the test case or spec explicitly names a test IDWhen the interaction is clear but the exact selector is unknown:
// TODO: confirm selector — replace with getByTestId or getByRole once DOM is available
readonly submitButton = this.page.locator('[data-testid="submit-btn"]'); // PLACEHOLDER
Use a // PLACEHOLDER comment on every line where a selector was inferred rather than confirmed.
.btn-primary) — fragiledata-testid values not referenced in the test casesIf you would have to guess what the DOM looks like to write the selector — placeholder it.
Split compound "When" steps into separate methods. Each method does one thing.
| Given/When/Then step | Method(s) |
|---|---|
| "When I fill in my email and password and click Sign in" | fillEmail(), fillPassword(), submitLoginForm() — or fillLoginForm(email, password) + submitLoginForm() |
| "When I search for 'accountant' and apply the Finance filter" | searchFor(term), applyFilter(label) |
| "When I click the first result card" | clickResultCard(index) |
Prefer small, composable methods over large monolithic ones. A method should map to a single user gesture or assertion.
Method naming: verb + noun, camelCase. Examples: fillSearchInput, submitForm, dismissCookieBanner, verifySuccessMessage, selectDropdownOption.
All assertions use web-first await expect(). Never use expect(await element.isVisible()).toBe(true).
Map "Then" statements to assertions:
| Then statement pattern | Playwright assertion |
|---|---|
| "...is visible / is displayed / appears" | await expect(locator).toBeVisible() |
| "...is not visible / is hidden" | await expect(locator).toBeHidden() |
| "...contains text X" | await expect(locator).toContainText('X') |
| "...shows text X" / "...reads X" | await expect(locator).toHaveText('X') |
| "...URL contains /path" | await expect(page).toHaveURL(/path/) |
| "...field contains value X" | await expect(locator).toHaveValue('X') |
| "...button is disabled" | await expect(locator).toBeDisabled() |
| "...N results are shown" | await expect(locator).toHaveCount(N) |
| "...is checked" | await expect(locator).toBeChecked() |
Assertions that cannot be cleanly mapped get a // TODO: write assertion — confirm expected behaviour comment.
tests/<featureName>.spec.ts — use camelCase derived from the CSV Folder or the dominant page/feature.
import { test, expect } from '../testFixtures/base';
test.describe('<Feature Name>', () => {
test.beforeEach(async ({ <pageName> }) => {
await <pageName>.goto('<path>');
// Handle any prerequisite state (e.g. dismiss cookie banner)
});
test('should <case name in sentence case>', async ({ <pageName> }) => {
// Arrange
// (any setup beyond beforeEach)
// Act
await <pageName>.<method>();
// Assert
await expect(<pageName>.<locator>).<assertion>();
});
});
Convert the CSV Case value to should <case in sentence case>. Examples:
Filter dropdown interaction → should show filter options when dropdown is clickedLogin with valid credentials → should redirect to dashboard when valid credentials are submitted
If the CSV name is already a "should..." statement, use it as-is.| Precondition type | Where it goes |
|---|---|
| Navigation to a URL | beforeEach via page.goto() or POM goto() method |
| Logged-in state | beforeEach fixture or auth setup (flag as TODO if auth approach is unknown) |
| Specific CMS content published | Comment as // Precondition: <content> must be published in CMS |
| Cookie/consent banner dismissed | beforeEach — dismissCookieBanner() method on the page object |
Present code in clearly labelled blocks in this order:
// objects/pages/<pageName>.ts
// NEW FILE ← or → // ADDITIONS TO EXISTING FILE
Show the full file if new. Show only the new locators + methods (with a comment indicating where they slot in) if adding to an existing file.
// testFixtures/base.ts — ADD THESE LINES
Only show the new imports and fixture entries. Do not reproduce the whole file.
// tests/<feature>.spec.ts
Full file.
After the code, list every // PLACEHOLDER locator in a summary table:
| Page object | Property name | What's needed |
|---|---|---|
LoginPage | errorMessage | Selector for the login error message — check DOM for data-testid or role |
This gives the engineer a clear hit-list to resolve before running the tests.
waitForLoadState('networkidle') — use element-based waitswaitForTimeout() — use web-first assertionsif/else in tests — all tests must be linear and deterministic'[email protected]')'[email protected]' and 'Password123!'Case: "Login with valid credentials"
Preconditions: "Given I am on the login page"
Steps (text): "When I enter a valid email address\nAnd I enter a valid password\nAnd I click the Sign in button"
Expected: "Then I am redirected to the dashboard\nAnd a welcome message is displayed"
Folder: "Frontend"
// objects/pages/loginPage.ts — ADDITIONS TO EXISTING FILE
// Add to constructor:
readonly emailInput = this.page.getByLabel('Email address'); // PLACEHOLDER — confirm label text
readonly passwordInput = this.page.getByLabel('Password');
readonly signInButton = this.page.getByRole('button', { name: 'Sign in' });
readonly welcomeMessage = this.page.getByTestId('welcome-message'); // PLACEHOLDER — confirm data-testid
// Add methods:
async fillEmail(email: string) {
await this.emailInput.fill(email);
}
async fillPassword(password: string) {
await this.passwordInput.fill(password);
}
async submitLoginForm() {
await this.signInButton.click();
await expect(this.page).toHaveURL(/dashboard/);
}
// tests/login.spec.ts
import { test, expect } from '../testFixtures/base';
test.describe('Login', () => {
test.beforeEach(async ({ loginPage }) => {
await loginPage.goto('/login');
});
test('should redirect to dashboard when valid credentials are submitted', async ({ loginPage }) => {
// Arrange
const email = '[email protected]';
const password = 'Password123';
// Act
await loginPage.fillEmail(email);
await loginPage.fillPassword(password);
await loginPage.submitLoginForm();
// Assert
await expect(loginPage.page).toHaveURL(/dashboard/);
await expect(loginPage.welcomeMessage).toBeVisible();
});
});
| Page object | Property | What's needed |
|---|---|---|
LoginPage | emailInput | Confirm the visible label text for the email field |
LoginPage | welcomeMessage | Find the data-testid or role for the welcome message element |
| Scenario | How to Handle |
|---|---|
CSV row has no Folder value | Treat as Frontend and flag the assumption in a comment |
| Multiple test cases share the same page | Generate one page object shared across all relevant specs |
| Precondition requires authenticated state | Stub with // TODO: set up auth fixture — confirm approach with team |
| Step references a component with no visible label or role | Use Tier 2 placeholder locator and note in the placeholder summary |
| CMS-folder row included in the CSV | Skip and list skipped cases at the top of the output with a note |
| Existing POM already has a matching locator | Reuse the existing property — do not duplicate |
Expected column has no assertable outcome (e.g. "Test passes") | Add // TODO: write assertion — confirm expected behaviour |
Generated test files are production code. Apply the same engineering standards as any other change.
All commits containing generated test files must follow the Conventional Commits specification with the test type:
test(<scope>): <description in imperative mood>
[optional body — what and why, not how]
Examples:
test(login): add Playwright spec for valid credentials flow
test(search): automate filter dropdown interaction tests
test/<feature-name> (e.g. test/add-login-playwright-tests)mainFrontend folder cases converted (CMS skipped or flagged)// PLACEHOLDERawait expect() web-first patternwaitForTimeout or networkidletest(<scope>): ...)test/<feature-name> conventionProvides a checklist for code reviews covering functionality, security, performance, maintainability, tests, and quality. Use for pull requests, audits, team standards, and developer training.
npx claudepluginhub tomrobinson26/qa-skills --plugin playwright-test-gen