Playwright page objects
Skill vasyafomiuk/skills/java-playwright-e2e/skills/playwright-page-objects
Claude Code skill plugin marketplace — java-playwright-e2e: Java + Playwright + Cucumber (BDD) end-to-end test automation
npx -y skills add vasyafomiuk/skills --skill playwright-page-objectsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Discovers real, stable Playwright (Java) locators for a Playwright+Cucumber suite and builds annotation-driven Page Objects from them. Use this skill when the user wants to: find or harden Playwright (Java) element locators for a page, decide between getByRole / getByLabel / getByText / getByTestId / CSS / XPath, fix strict-mode "resolved to N elements" errors, scope a locator to a row/card/dialog with filter or nth, snapshot a page with Playwright MCP or codegen to read the accessibility tree, read Playwright (Java) locators from a screenshot/HTML for the Playwright+Cucumber suite, design a Page Object with @FindBy / @VerifyAt / @VerifyBy and a CommonPage base, add axe-core accessibility assertions, or confirm the right page rendered before acting. Project-agnostic — no application-specific assumptions.
SKILL.md
15.0 KB, as published. Nobody here has run it
Playwright Page Objects
This skill owns the locator layer of the AC -> passing-test workflow: turning a
live page into resilient, user-facing locators and packaging them into
annotation-driven Page Objects. It sits between scenario authoring and step glue —
create-test-scenarios produces the Gherkin that names elements; you produce the
Page Objects that those element names resolve to; cucumber-step-definitions
services then consume your pages to drive the browser. The single largest source of
flaky tests is a badly chosen locator, so the whole job here is: discover from the
real page, prefer attributes that survive markup churn, and encapsulate.
Cross-cutting rules (stack/versions, web-first assertions, DI, test isolation,
naming/OOP) live in the orchestrator (java-playwright-e2e:orchestrator) — this skill
states each only as a one-line principle and points there.
WHEN TO USE
Use this skill when the user wants to:
- Find locators for a page, screen, or component ("get me the selectors for the login form").
- Choose between
getByRole,getByLabel,getByText,getByTestId, CSS, or XPath. - Fix a strict-mode error ("locator resolved to 3 elements", action throwing on ambiguous match).
- Scope a locator to a specific row / card / dialog, or chain & filter instead of a brittle compound selector.
- Snapshot a page via Playwright MCP, run codegen/inspector, or read locators from a screenshot/HTML.
- Build or refactor a Page Object:
@FindByfields, aCommonPagebase,@VerifyAt/@VerifyBy. - Confirm the correct page rendered before interacting with it.
This skill does not write feature files (see create-test-scenarios) or step
definitions/services (see cucumber-step-definitions). It stops at the page object.
STEP 1 — ACQUIRE THE PAGE (ordered fallback chain)
Never guess locators from memory. Get the real accessibility tree / DOM first. Try each option in order; fall to the next only when the current one is unavailable.
- Playwright MCP (preferred). Detect it: scan the available tools for Playwright
MCP entries (e.g.
browser_navigate,browser_snapshot,browser_close). If they are deferred, load them withToolSearch(query"playwright browser snapshot"or"select:browser_snapshot,browser_navigate,browser_close"). Then:browser_navigateto the URL ->browser_snapshotto read the accessibility tree (roles, accessible names, element refs) -> pick role/label/text locators from it ->browser_close. The snapshot's roles and names map directly ontogetByRole/getByLabel, which is exactly the priority you want. - Codegen / inspector against a live URL the user provides:
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="codegen <url>". Codegen records actions and emits role/text/test-id-first locators. For a running test,PWDEBUG=1opens the inspector and highlights locators live. - Screenshot the user provides — read visible roles (button/link/heading), field
labels, and static text directly off the image; translate to
getByRole/getByLabel/getByText. - Raw HTML source the user provides — parse it, but still extract user-facing
attributes (
role,aria-label,<label for>, visible text) rather thandivnesting. - Ask the user for the live URL or the page HTML when none of the above is available.
Always prefer user-facing attributes (role / accessible name / label / visible text) over DOM structure. They are what the user perceives and they survive class renames, wrapper
<div>churn, and component-library upgrades.
STEP 2 — CHOOSE THE LOCATOR (priority order)
Pick the highest option on this list that uniquely identifies the element. XPath is a last resort. This priority order is shared across the suite — see the orchestrator.
| # | Locator | Use for |
|---|---|---|
| 1 | getByRole(role, name=...) | buttons, links, headings, checkboxes — also doubles as an a11y check |
| 2 | getByLabel(...) | form fields with a visible/aria label |
| 3 | getByText(...) | visible static, non-interactive text |
| 4 | getByPlaceholder(...) | inputs identified only by placeholder |
| 5 | getByTestId(...) | when devs added data-testid and nothing user-facing is stable |
| 6 | scoped CSS / [aria-label="..."] | structural fallback, kept short and attribute-based |
| 7 | XPath | only when nothing above works; brittle, avoid |
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Save"));
page.getByLabel("Note Title");
page.getByText("Reviewed and ok");
page.getByTestId("task-details");
Notes:
getByRolematches on the accessible name; that is the snapshot'sname, not the DOM text. Prefer it for anything interactive.- Use
setExact(true)when a substring name would over-match ("Save"vs"Save and close"). - Reach for
getByTestIdonly after user-facing options fail — it tests an artifact devs added, not what the user sees, but it is far stabler than positional CSS/XPath.
STEP 3 — SCOPE, CHAIN & FILTER (strict mode)
Playwright locators run in strict mode: an action (click, fill, ...) throws if the
locator resolves to more than one element. Treat that as a signal to narrow, never to
grab the first match. Three tools, in order of preference:
Scope to a parent, then locate within it — the cleanest fix for "N matches":
// Act inside the dialog, not the whole page
Locator dialog = page.getByRole(AriaRole.DIALOG, new Page.GetByRoleOptions().setName("Confirm delete"));
dialog.getByRole(AriaRole.BUTTON, new Locator.GetByRoleOptions().setName("Delete")).click();
Filter by text or by a nested locator to pick the right repeated element (row/card):
// Scope to the row that contains "Product 2", then act within it
page.getByRole(AriaRole.LISTITEM)
.filter(new Locator.FilterOptions().setHasText("Product 2"))
.getByRole(AriaRole.BUTTON, new Locator.GetByRoleOptions().setName("Add to cart"))
.click();
// Filter by a nested control rather than raw text
page.getByRole(AriaRole.LISTITEM)
.filter(new Locator.FilterOptions().setHas(
page.getByRole(AriaRole.HEADING, new Page.GetByRoleOptions().setName("Product 2"))));
nth / first / last only when order is genuinely meaningful and stable — it is
positional, so use it last:
page.getByRole(AriaRole.LISTITEM).nth(2); // brittle if list order changes
Strict-mode triage when an action throws "resolved to N elements":
- Add a
getByRole(...).setName(...)to disambiguate by accessible name. - Scope to the nearest meaningful parent (dialog / row / card / section).
filter(setHasText / setHas)to pin the right repeated container.- Only then
nth(...)— and leave a note that order is load-bearing.
Chaining beats a single brittle compound CSS selector: div.list > li:nth-child(3) .btn
breaks on any layout change; the scoped/filtered role chain above does not.
STEP 4 — BUILD THE PAGE OBJECT (annotation-driven)
Every page extends a shared base and declares locators as private annotated fields. Three annotations carry the metadata that generic steps and page-load verification need. Encapsulation rule (locators private, no leaking getters/setters) is shared — see the orchestrator.
@Retention(RUNTIME) @Target(FIELD)
public @interface FindBy { // how to locate — mirrors the priority order
String role() default ""; // preferred
String name() default ""; // accessible name for an interactive role (supersedes monolith role+text)
String label() default "";
String text() default ""; // visible static text (non-interactive)
String testId() default "";
String css() default "";
String xpath() default ""; // discouraged
boolean all() default false;// true -> resolve to a list of matches
}
@Retention(RUNTIME) @Target(FIELD)
public @interface VerifyAt { UrlType value(); } // expected URL match: FULL / PARTIAL / CONTAINS
@Retention(RUNTIME) @Target(FIELD)
public @interface VerifyBy { } // a selector that proves the page rendered
/** Shared locators, page-load wait, and helpers for every page. */
public abstract class CommonPage {
protected final Page page;
protected CommonPage(Page page) { this.page = page; }
/** Confirms the right page rendered before any action runs. */
public abstract void waitForPageToLoad();
}
public class TaskDetailsPage extends CommonPage {
@VerifyAt(UrlType.CONTAINS)
public static final String URL = "/task-details";
@VerifyBy
public static final String ROOT = "[data-testid='task-details']";
@FindBy(label = "Note Title") private Locator noteTitle;
@FindBy(label = "Note Content") private Locator noteContent;
@FindBy(role = "button", name = "Save") private Locator saveNote;
@FindBy(css = "[role='alert']") private Locator successToast;
public TaskDetailsPage(Page page) { super(page); }
// waitForPageToLoad() is implemented in STEP 5 below (asserts @VerifyAt + @VerifyBy)
}
Rules:
- All locators live in page objects — never in feature files or step classes.
- Locators are
private; no_prefix, no getters/setters. Generic steps resolve fields by name; explicit-DI projects expose behavioral methods (addNote(...)), not the rawLocator. A page may expose one read-onlyLocatoraccessor (e.g.successToast()) for the service's web-first assertion — never expose a locator to a step or feature file. - One
@FindByper field, expressing a single chosen strategy from STEP 2. Setall = truefor a list-valued locator the caller will iterate or count. - Compose, don't inflate: model a repeated card/row as its own small component page and build the screen from those, rather than one page with a hundred fields.
- For a multi-field action, expose a method that takes a typed input object (a
record), not a long primitive parameter list. - Match existing page objects. If the project already has page objects, ask for / read one first and follow its base class, annotation usage, and naming rather than introducing a new style.
STEP 5 — VERIFY THE PAGE BEFORE ACTING
Confirm you are on the intended page before driving it; acting on a half-rendered or wrong
page is a top flakiness source. @VerifyAt + @VerifyBy give a single check both pieces
of evidence — the URL and a rendered anchor element:
@Override
public void waitForPageToLoad() {
assertThat(page).hasURL(Pattern.compile(".*" + Pattern.quote(URL))); // @VerifyAt
assertThat(page.locator(ROOT)).isVisible(); // @VerifyBy
}
Use Playwright's auto-retrying assertThat(...) here (it polls until the condition
holds), never a one-shot boolean getter. Web-first assertions are a shared convention —
full rule in the orchestrator. Call waitForPageToLoad() at the top of any service
flow that targets this page, before the first fill/click.
ACCESSIBILITY (axe-core)
Locators built from roles and accessible names already lean on the a11y tree; axe-core
turns that into an explicit assertion. Run a scan against the live page and fail the
test on any violation, using the same web-first style as page verification. The axe-core
Playwright dependency is pinned in e2e-framework-setup.
import com.deque.html.axecore.playwright.AxeBuilder;
import com.deque.html.axecore.results.AxeResults;
AxeResults results = new AxeBuilder(page).analyze();
assertTrue(results.getViolations().isEmpty(), results.getViolations().toString());
- Scope the scan to the region under test rather than the whole document:
AxeResults results = new AxeBuilder(page).include(List.of("main")).analyze(); - Suppress known, triaged issues instead of asserting on a noisy full result —
keep the suppression list short and reviewed:
AxeResults results = new AxeBuilder(page).disableRules(List.of("color-contrast")).analyze(); - Tag accessibility scenarios with
@a11yso they can be run (or skipped) as a slice. - Reuse the scan behind one generic step backed by a service, so feature files stay
declarative and the AxeBuilder wiring lives in one place:
The step resolves the named page and delegates to a service method that runs theThen User sees no accessibility violations on "TaskDetails" pageAxeBuilder(page).analyze()assertion above — no axe wiring in the step or feature file.
ANTI-PATTERNS
- Guessing selectors without acquiring the page (STEP 1) — leads to non-existent or ambiguous locators.
- Auto-generated absolute XPath (
/html/body/div[2]/div[3]/...) — breaks on the first layout change. - Compound positional CSS (
.col > div:nth-child(4) > button) — chain + filter instead. - Swallowing strict-mode by appending
.first()— narrow with role+name / scope / filter first. public Locator getSaveNote()leaking a locator to steps — expose behavior, keep locators private.- Locator strings hardcoded in step definitions or
.featurefiles — they belong in the page object only.
HANDOFF
- the orchestrator (
java-playwright-e2e:orchestrator) — the single source of truth for locator-priority, encapsulation, web-first assertions, DI, and isolation. Defer there for any cross-cutting rule. - create-test-scenarios — upstream. Its Gherkin names elements (
"saveNote","successToast"); your@FindByfield names should match those names so steps resolve cleanly. - cucumber-step-definitions — downstream consumer. Its services receive your page objects by DI and call the behavioral methods you expose; hand it pages whose locators are already scoped and verified.
- rest-assured-api-tests — when a scenario needs
page.routemocking or API seeding rather than UI locators, that work lives there, not here. - e2e-framework-setup — owns where the
annotation/andpageobject/packages sit in the Maven module layout your page objects compile into.