Defines the code generation guardrails, selector rules, and anti-patterns for transforming action logs into Playwright test scripts. Covers Angular Material-specific pitfalls, scoping rules, assertion generation, and CRUD verification patterns.
This skill defines the rules and guardrails for HOW to generate Playwright test scripts from action logs. Load this skill during script generation to enforce all code generation constraints.
For action log format, recording rules, script templates, and action-to-Playwright mappings, see #skill:script-recording.
When generating Playwright scripts, you NEVER:
testData key should be usedwaitForTimeout() calls — all timing is handled by Playwright auto-wait + config timeoutsgetByText() as a button selector — always prefer getByRole('button', { name: '...' })button:has(mat-icon:text(...)) — use button:has-text(...) scoped to the row insteadimg[alt="..."], img[src*="..."], or button:has(img[alt="..."]) for Material Icon buttons — these are elements with text content, not elements; use or scoped to the row instead<mat-icon><img>button:has-text("edit")button:has-text("delete")mat-row as a CSS tag selector — Angular Material table rows are <tr mat-row>, not <mat-row> elements; use tr:has-text("...") insteadtextbox, spinbutton, combobox) as CSS tag selectors — these are roles, not HTML tags; use getByRole('textbox', { name: '...' }) insteadbutton[aria-label="..."] for icon-only buttons unless the element has a literal aria-label HTML attribute in the snapshot — icon buttons derive their content from <mat-icon> text, not aria-label#mat-input-0 — use getByLabel() insteadinput[type="text"], input:first-of-type, or input[type="text"]:first-of-type for form fields — always use getByLabel('{fieldName}') scoped to the dialog/form container instead; the field's accessible name is in the elementDescription and page snapshotgetByText() — scope to mat-dialog-container first.click() immediately before .fill() on the same element — fill() focuses the element internally; the click is redundant and will fail on Angular Material MDC fields where mdc-notched-outline or mdc-floating-label overlays intercept pointer events (codegen records the click because the user clicked manually, but it must be suppressed in the generated script)mat-option:has-text('...') for mat-select option clicks — :has-text() is a substring match and causes strict-mode violations when option values overlap (e.g., '1' matches '0.01', '0.1', '1'). Always override to getByRole('option', { name: '...', exact: true })button:nth-child(N) or other index-based button selectors — these are fragile and may resolve to disabled or wrong elements. Override using the button's elementDescription text content (e.g., button:has-text('chevron_right'))keyboard.press('Escape') to dismiss Angular Material dialogs (mat-dialog-container) — these dialogs do not close via the Escape key. Always use page.locator('mat-dialog-container').getByRole('button', { name: 'Cancel' }).click() followed by page.locator('mat-dialog-container').waitFor({ state: 'hidden' }) instead. When the action log contains a press_key: Escape while a mat-dialog-container is open, override it to a Cancel button clickfill() with non-numeric text on input[type=number] fields (snapshot role spinbutton) — Playwright throws Cannot type text into input[type=number]. When the test intentionally enters non-numeric characters for validation testing, use pressSequentially() instead, which simulates keystrokes and lets the browser handle rejectiongetByRole('listbox').waitFor({ state: 'hidden' }) after mat-select option clicks — Angular Material's CDK overlay animates closed after option selection; without this wait, the next interaction (whether another mat-select, a button click like chevron_right or Save, or any other element) fires while the overlay is still closing, causing a timeout. This wait must be emitted after every mat-select option click, including the last one in a sequenceApply these rules when transforming action log entries into Playwright code:
page.goto() (strip baseURL prefix).await page.waitForLoadState('networkidle') after every navigate action.page.once('dialog', ...) handlers before the triggering click.page.locator('mat-dialog-container') when the snapshot context is a modal.await page.locator('mat-dialog-container').waitFor({ state: 'visible' }) after any click that opens a dialog (Add, Edit, Delete confirmation).page.locator('mat-dialog-container').getByRole('button', { name: 'Cancel' }).click() + waitFor({ state: 'hidden' }) — never use keyboard.press('Escape') as mat-dialogs do not respond to the Escape key.getByRole('button', { name: '...' }) for button-clicks — never use bare getByText(). Exception: buttons inside table column headers (Add, sort) must always be scoped via getByRole('columnheader', { name: '...' }).getByRole('button') — never use an unscoped getByRole('button', { name: 'Add' }) as it may match both the table-header button and dialog submit buttons, causing wrong-element clicks or strict-mode failures.getByLabel('...') for labeled inputs — never use auto-generated IDs.getByRole('columnheader', { name: '...' }).getByRole('button') — not th[aria-sort] button or button[aria-label="..."].page.frameLocator('{frameContext}').testData.{key} instead of inline strings for all fill/select values.testData.// Unique test data — may need regeneration on re-run for data with unique prefixes.For each action log entry with assertion: true, generate the corresponding await expect(...) call:
assert_url: await expect(page).toHaveURL(/{expected}/); — Use regex for resilience.assert_visible: await expect(page.locator('{selector}')).toBeVisible(); — If expected is provided, also emit await expect(page.locator('{selector}')).toContainText('{expected}');.assert_not_visible: await expect(page.locator('{selector}')).not.toBeVisible();assert_text: await expect(page.locator('{selector}')).toContainText('{expected}');assert_count: await expect(page.locator('{selector}')).toHaveCount({expected});assert_value: await expect(page.locator('{selector}')).toHaveValue('{expected}'); — If the expected value came from a fill action in the same case, reference testData.{key} instead of inlining.Place each assertion at its sequential position in the generated test — assertions verify the outcome of preceding actions and must appear in order. Add a descriptive comment before each assertion: // Verify: {elementDescription or human-readable assertion purpose}.
For CRUD test patterns, ensure the generated script includes these critical verifications:
assert_visible with the test data value)assert_not_visible on mat-dialog-container)assert_visible on error elements)