Claude
Skill vasyafomiuk/skills/java-playwright-cucumber-e2e/1.1.0/claude
Claude Code skill plugin marketplace — java-playwright-e2e: Java + Playwright + Cucumber (BDD) end-to-end test automation
npx -y skills add vasyafomiuk/skills --skill claudeAssembled 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
Scaffold and build end-to-end test automation projects using Java + Playwright + Cucumber (BDD). Use this skill when the user wants to: bootstrap a new test automation framework, turn Jira/acceptance criteria into Gherkin feature files, write UI / API / E2E tests, discover element locators with Playwright, design Page Objects, apply OOP and dependency injection, or wire up runners, hooks, config, parallelism, CI, and database/API validation. Project-agnostic reference — no application-specific assumptions.
SKILL.md
26.5 KB, as published. Nobody here has run it
Java + Playwright + Cucumber E2E Skill
A reusable blueprint for building maintainable end-to-end automation. The stack: Gherkin for the spec layer, Cucumber JVM for binding specs to code, Playwright (Java) for browser control, REST Assured for API calls, and Maven + (JUnit 5 platform suite or TestNG) for build and execution. Feature files are language-neutral, so the same Gherkin can describe a TypeScript or Java application — the runtime shown here is Java.
Guiding principles, from Playwright's own best-practices guide:
- Test user-visible behavior, not implementation details (function names, CSS classes, DOM shape).
- Isolate every test — each scenario runs with its own browser context (fresh cookies/storage).
- Let Playwright wait. Locators auto-wait and retry; web-first assertions auto-retry. Never
Thread.sleep. - Keep specs business-readable, glue thin, logic in services/pages.
WHEN TO USE
Trigger this skill when the user asks to:
- Scaffold / bootstrap a new automation project or module
- Convert acceptance criteria (Jira or otherwise) into feature files
- Write a UI, API, or end-to-end test
- Find or harden element locators with Playwright
- Design Page Objects, step layering, or dependency injection
- Add hooks, runners, tags, parallel execution, CI, config, or DB/API validation
STACK & VERSIONS (verified current as of 2026)
| Component | Version | Notes |
|---|---|---|
| JDK | 17+ | Playwright Java requires 17 minimum; build fails on 11 |
| Playwright Java | 1.5x – 1.6x | com.microsoft.playwright:playwright; pin and keep current |
| Cucumber JVM | 7.x (7.2x+) | cucumber-java + an engine (JUnit platform or TestNG) |
| Cucumber DI | cucumber-picocontainer | Lightest container; Spring is the heavier alternative |
| Test engine | JUnit 5 junit-platform-suite or cucumber-testng | Pick one |
| API | REST Assured 5.x | API + hybrid scenarios |
| Build | Maven 3.9+, maven-surefire-plugin 3.5+ | Node.js present (Playwright uses it internally) |
Keep Playwright current — new versions track the latest browser builds and catch regressions before they ship to users.
CORE WORKFLOW: Acceptance Criteria → Passing Test
Do one story/feature per iteration to keep context focused.
1. Read AC → extract each acceptance criterion as a discrete, testable statement
2. Write feature → one Scenario per AC (Gherkin); reuse existing steps first
3. Find locators → Playwright codegen / inspector / MCP snapshot (role/label first)
4. Build pages → Page Object with locators + web-first assertions
5. Wire steps → thin glue → service method → page object
6. Verify → run the scenario; assert UI + API + DB agree
7. Clean up → remove test data created during the run
Rules for the loop:
- One Scenario per AC; cover all of that AC in one flow (setup → action → assertions). Don't split one AC across scenarios (duplicate data, noise).
- Beyond AC, add negative/edge scenarios to find bugs: invalid/empty/null input, boundaries, special chars, injection, unauthorized access, concurrency, error handling.
- Validate truthfully. Never weaken an assertion to make a test pass. A failing test is a signal — investigate the cause (or the AC understanding), not the assertion.
- No conditional assertions. Don't wrap
assertinif (data != null). Provision the data in setup so the assertion always runs.
PROJECT SKELETON
Multi-module Maven keeps the reusable framework separate from per-suite tests. Start with two modules; add suites as siblings.
automation/
├── pom.xml # parent (aggregator + dependency mgmt)
├── framework/ # reusable library — no tests here
│ ├── pom.xml
│ └── src/main/java/com/example/automation/
│ ├── annotation/ # @FindBy, @VerifyAt, @VerifyBy
│ ├── driver/ # BrowserFactory, BrowserManager, BrowserSupport
│ ├── pageobject/ # CommonPage (base class)
│ ├── context/ # ScenarioContext (state sharing)
│ ├── config/ # Config, Environment, secret access
│ ├── steps/ # reusable common steps + services (ui/api/db)
│ ├── api/ # REST Assured wrappers
│ ├── db/ # repositories / query builders
│ └── util/ # helpers (dates, strings, files, input)
└── ui-suite/ # one test suite (repeat per app/area)
├── pom.xml
└── src/test/
├── java/com/example/automation/
│ ├── pages/ # suite Page Objects (extend CommonPage)
│ ├── steps/ # suite-specific steps + services
│ ├── hooks/ # @Before / @After
│ └── runners/ # Cucumber + JUnit/TestNG runner
└── resources/
├── features/ # *.feature (ui/, api/, e2e/)
├── junit-platform.properties # glue/plugin config (JUnit 5)
└── application-*.properties # per-environment config
Why split framework vs suite: the framework (driver, annotations, common steps, config) is built once and reused by every suite as a Maven dependency. Suites contain only Gherkin, suite pages, and suite-specific glue.
Dependencies (suite/framework pom.xml)
<dependency><groupId>com.microsoft.playwright</groupId><artifactId>playwright</artifactId><version>${playwright.version}</version></dependency>
<dependency><groupId>io.cucumber</groupId><artifactId>cucumber-java</artifactId><version>${cucumber.version}</version></dependency>
<!-- DI for step/page collaborators (constructor injection) -->
<dependency><groupId>io.cucumber</groupId><artifactId>cucumber-picocontainer</artifactId><version>${cucumber.version}</version></dependency>
<!-- Engine: choose JUnit 5 platform suite ... -->
<dependency><groupId>io.cucumber</groupId><artifactId>cucumber-junit-platform-engine</artifactId><version>${cucumber.version}</version><scope>test</scope></dependency>
<dependency><groupId>org.junit.platform</groupId><artifactId>junit-platform-suite</artifactId><scope>test</scope></dependency>
<!-- ... or TestNG: cucumber-testng (use one, not both) -->
<dependency><groupId>io.rest-assured</groupId><artifactId>rest-assured</artifactId><version>${restassured.version}</version></dependency>
<dependency><groupId>com.deque.html.axe-core</groupId><artifactId>playwright</artifactId><version>${axe.version}</version></dependency> <!-- optional ally -->
Install browsers once:
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install"
(use install --with-deps on Linux/CI to pull system libraries).
WRITING FEATURE FILES FROM ACCEPTANCE CRITERIA
Gherkin is the contract. Keep it declarative (describe what, not how).
@ui @regression
Feature: Internal notes on a record
Background:
Given User is set to "admin"
And User loads "TaskDetails" page
@AC1
Scenario: AC1 — User can add an internal note
When User enters following info on "TaskDetails" page:
| ElementName | Value |
| noteTitle | Quarterly review |
| noteContent | Reviewed and ok |
And User click on "saveNote" button on "TaskDetails" page
Then Verify "successToast" element is visible on "TaskDetails" page
And Verify note "Quarterly review" persisted for the current record
@AC2 @negative
Scenario Outline: AC2 — Saving is blocked for invalid titles
When User enters "<title>" on "noteTitle" input on "TaskDetails" page
And User click on "saveNote" button on "TaskDetails" page
Then Verify "<error>" element is visible on "TaskDetails" page
Examples:
| title | error |
| error | titleRequired |
| <xss> | titleInvalidChar |
Guidelines:
- One Scenario per AC; tag with the AC id (
@AC1) and type (@ui/@api/@e2e). Backgroundfor shared setup;Scenario Outline+Examplesfor data-driven variants.- Reuse existing common steps before writing new ones — search the steps package first.
- Keep steps business-level.
When User click on "saveNote" buttonis good; leaking#btn-1234into Gherkin is bad. - Add
@negativescenarios for empty/null/boundary/special-char/injection/unauthorized cases.
LOCATORS — discovery & strategy
Locators find elements at the moment of interaction and come with auto-waiting + retry, which removes most timing-based flakiness. They are the single largest source of flaky tests when chosen poorly, so prefer user-facing attributes that survive DOM churn.
Discover with Playwright tooling (never guess)
- Codegen records actions and emits resilient locators, prioritizing role/text/test-id:
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="codegen <url>" - Inspector /
PWDEBUG=1steps through a run and highlights locators live. - Playwright MCP (when available): navigate,
browser_snapshotfor the accessibility tree with refs, thenbrowser_close.
Priority order (XPath last resort)
getByRole(name=...)— buttons, links, headings (doubles as an a11y check)getByLabel(...)— form fieldsgetByText(...)— visible static textgetByTestId(...)—data-testid- scoped CSS /
[aria-label="..."] - XPath — only when nothing else works
Chain and filter instead of brittle compound selectors
// scope to the right row, 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();
Strict mode: if a locator matches multiple elements, an action throws — narrow
with filter / nth / role+name rather than silently acting on the first match.
PAGE OBJECTS (annotation-driven)
Every page extends a common base and declares locators as annotated fields. Three annotations carry the metadata generic steps need.
@Retention(RUNTIME) @Target(FIELD)
public @interface FindBy { // how to locate
String role() default ""; // preferred
String label() default "";
String text() default "";
String testId() default "";
String css() default "";
String xpath() default ""; // discouraged
boolean all() default false; // true for a list of matches
}
@Retention(RUNTIME) @Target(FIELD)
public @interface VerifyAt { UrlType value(); } // page URL (FULL / PARTIAL / CONTAINS)
@Retention(RUNTIME) @Target(FIELD)
public @interface VerifyBy { } // selector proving the page rendered
public abstract class CommonPage { /* shared locators + helpers */ }
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", text = "Save") private Locator saveNote;
@FindBy(css = "[role='alert']") private Locator successToast;
}
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 them by field name. @VerifyAt+@VerifyBylet a singlewaitForPageToLoad(page)confirm the right page rendered before acting.- For multi-field actions, expose a method that takes a typed input object, not many primitive params.
WEB-FIRST ASSERTIONS (the key reliability practice)
Use Playwright's auto-retrying assertions, not boolean getters. They poll until the condition holds or the timeout expires — this is what removes flaky "element not ready yet" failures.
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
// GOOD — waits & retries until visible
assertThat(page.locator("[role='alert']")).isVisible();
assertThat(saveNote).isEnabled();
assertThat(statusLabel).hasText("Approved");
// BAD — evaluates once, no waiting, flaky
assertTrue(page.locator("[role='alert']").isVisible()); // boolean getter, returns immediately
- Per-assertion timeout:
assertThat(locator).isVisible(new LocatorAssertions.IsVisibleOptions().setTimeout(15000)), or set a global default viaPlaywrightAssertions.setDefaultAssertionTimeout(...). Prefer per-assertion over a large global timeout. - Soft assertions when you want several checks to all report before failing the scenario — collect failures, assert at the end.
- Reserve boolean getters (
isVisible()) for control flow, never for the actual verification.
TEST ISOLATION
Each scenario gets its own browser context (fresh cookies, storage, cache — like a new incognito window). This prevents cascading failures and makes runs reproducible and parallel-safe.
- Create a new
BrowserContext+Pageper scenario in@Before; close them in@After. - Share one
Browser/Playwrightinstance per thread; create a context per scenario. - Reuse auth instead of logging in every scenario: log in once, save
context.storageState(...), and seed new contexts withnew Browser.NewContextOptions().setStorageStatePath(...). Far faster than UI login per test. - Seed data through the API, not the UI, when a scenario only needs preconditions — quicker and less flaky.
DEPENDENCY INJECTION & STATE SHARING
Two complementary mechanisms keep step classes clean and parallel-safe.
1. Constructor injection (PicoContainer) for collaborators
With cucumber-picocontainer on the classpath, Cucumber builds and shares one
instance of each collaborator per scenario and injects via constructors.
Steps stay thin; pages/services/hooks are injected, not new-ed. Because each
scenario gets fresh instances, this is inherently parallel-safe.
public class TaskDetailsSteps {
private final TaskDetailsService taskService; // injected
private final ScenarioContext context; // shared per scenario
public TaskDetailsSteps(TaskDetailsService taskService, ScenarioContext context) {
this.taskService = taskService;
this.context = context;
}
@When("User adds an internal note")
public void userAddsNote(InternalNote note) { // typed input from DataTable
taskService.addNote(note);
}
}
public record InternalNote(String title, String content, boolean confidential) {}
A BrowserContext/Page holder is also a natural injectable: the hook creates
the page and stores it on a per-scenario PageHolder, which the services
receive by injection — so every collaborator shares the same scenario's page.
Spring alternative: annotate the runner with
@CucumberContextConfiguration
@ContextConfiguration, mark steps/services as beans. Heavier; only worth it if you already need the Spring context. Pick one container; don't mix.
2. ScenarioContext for cross-step data
A small holder carries data between steps (current user, last API response,
parsed DTOs, DB results). Prefer an injected, scenario-scoped object over
global static state. If you must support legacy static access in parallel,
back it with InheritableThreadLocal and clear it in @After.
Simplicity note: the heaviest reference frameworks resolve pages/services by name via classpath reflection so one generic step drives every page. It is powerful but adds indirection. For new projects prefer explicit DI of page objects; reach for reflection-driven generic steps only when you have many near-identical pages and the payoff is clear.
STEP LAYERING
Three layers, one responsibility each:
Feature (Gherkin) → Step (glue) → Service (logic + assertions) → Page / BrowserSupport / API
- Step: matches the Gherkin line, parses params, delegates. No logic.
- Service: orchestrates the action and owns meaningful assertions. Reusable.
- Page / BrowserSupport / API: lowest level — locators, Playwright/REST calls.
public class TaskDetailsService {
private final PageHolder pages; // injected
public void addNote(InternalNote note) {
TaskDetailsPage page = pages.taskDetails();
page.noteTitle().fill(note.title());
page.noteContent().fill(note.content());
if (note.confidential()) page.confidential().check();
page.saveNote().click();
assertThat(page.successToast()).isVisible(); // web-first
}
}
BROWSER LIFECYCLE & HOOKS
public final class BrowserFactory {
public static Browser launch(Playwright pw) {
boolean headless = Config.HEADLESS; // headless on CI, headed locally
return switch (Config.BROWSER) {
case FIREFOX -> pw.firefox().launch(opts(headless));
case WEBKIT -> pw.webkit().launch(opts(headless));
case EDGE -> pw.chromium().launch(opts(headless).setChannel("msedge"));
default -> pw.chromium().launch(opts(headless).setChannel("chrome"));
};
}
}
public class Hooks {
private final PageHolder pages; // injected per scenario
public Hooks(PageHolder pages) { this.pages = pages; }
@Before("@ui or @e2e")
public void startBrowser(Scenario s) {
pages.open(); // new context + page for THIS scenario
}
@After
public void tearDown(Scenario s) {
if (s.isFailed() && pages.hasPage())
s.attach(pages.page().screenshot(), "image/png", "failure");
pages.close(); // close context/page — avoid leaks & state bleed
}
}
- One context per scenario; close it in
@After. Never share aPage/Browseracross threads. - Attach a screenshot on failure; for deeper CI debugging, capture a trace and open it in the Playwright trace viewer (timeline, DOM snapshots, network).
RUNNER & EXECUTION
JUnit 5 platform suite (modern default):
@Suite
@IncludeEngines("cucumber")
@SelectClasspathResource("features")
@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "com.example.automation.steps,com.example.automation.hooks")
public class TestRunner { }
src/test/resources/junit-platform.properties:
cucumber.glue=com.example.automation.steps,com.example.automation.hooks
cucumber.plugin=pretty, html:target/cucumber.html, json:target/cucumber.json
cucumber.execution.parallel.enabled=true
cucumber.execution.parallel.config.strategy=dynamic
TestNG alternative: extend AbstractTestNGCucumberTests and override
scenarios() with @DataProvider(parallel = true).
mvn test -Denv=dev # select environment
mvn test -Dcucumber.filter.tags="@ui and @AC1" # filter by tags
mvn test -Dbrowser=firefox # cross-browser
Parallelism rules: enable parallel scenarios via the engine, but each
thread must own its BrowserContext/Page — never share them. PicoContainer's
per-scenario instances make this safe by default. For multiple machines, shard
the suite across CI runners.
Tags organize execution: @ui, @api, @e2e, @regression, @smoke,
@negative, @security, plus AC ids (@AC1).
API TESTS & MOCKING
REST Assured for API-level scenarios
@When("User POST {string} with body {string}")
public void userPosts(String endpoint, String bodyKey) {
Response r = given().header("Authorization", token())
.contentType(JSON).body(load(bodyKey))
.when().post(Config.url(endpoint));
context.setResponse(r);
}
@Then("Verify status code is {int}")
public void verifyStatus(int expected) {
assertEquals(context.getResponse().statusCode(), expected);
}
- Cover every AC, even "UI-only" ones — the API still returns the data the UI renders; assert the value and confirm it matches the DB.
- Three-way validation for create/update: input → API response → DB all agree.
assertEquals(api.status, input.status); // API reflects what we sent assertEquals(api.status, dbRecord.getStatus()); // API reflects the DB assertEquals(dbRecord.getStatus(), input.status);// DB persisted what we sent - Validate exact error messages from the AC. Normalize dates before comparing (timezone/format drift causes flakiness).
Mock third-party / unstable dependencies with page.route
Don't test systems you don't control. Intercept and fulfill:
page.route("**/api/third-party/**", route ->
route.fulfill(new Route.FulfillOptions().setStatus(200).setBody(stubJson)));
Use the same route() API to mock, modify, abort, or capture requests for
deterministic UI scenarios.
DATABASE VALIDATION
Verify persistence after any data-changing action — a UI success toast is not proof the data was saved.
- Avoid raw SQL. Use an ORM/entity layer or a repository abstraction; keep reusable queries in a repository class and entities in one place.
- Provision required test data in setup using helpers so assertions never need null-guards. Use past dates for seeded data to avoid "date cannot precede last status" style validation errors.
- Control the data: test against a stable environment you own.
TaskNote saved = taskRepo.findNoteByTitle(recordId, "Quarterly review");
assertNotNull(saved);
assertEquals(saved.getContent(), "Reviewed and ok");
assertEquals(saved.getCreatedBy(), context.getUser());
CONFIG, ENVIRONMENTS & SECRETS
- Per-environment
application-<env>.properties; select with-Denv=<env>. - One
Configclass loads properties and exposes typed accessors (base URLs, browser, headless, timeouts); reference by key name, never commit values. - Never hardcode secrets. Read from environment variables or a secret manager at runtime; reference by key name, never commit values.
- Keep base URLs and credentials out of feature files and page objects — inject via config.
CI
- Run on every commit/PR; use Linux runners (cheaper, consistent).
- Install only needed browsers;
playwright install chromium --with-deps. - Run headless in CI; headed only for local development.
- Cache
~/.m2; upload the Cucumber HTML report and traces as artifacts withif: always()so they exist when tests fail. - Capture traces on first retry (not always-on — it is heavy) and debug failures in the trace viewer.
- Shard across runners for large suites.
TEST INTEGRITY & OOP PRACTICES
Integrity
- No "green light" tests written only to pass — tests must genuinely validate the AC.
- No conditional assertions that skip when data is absent — provision the data.
- Don't assert data types ("is defined",
instanceof); assert real values against the source of truth (API/DB). - When a test fails, report and find the root cause. Only change a test if the AC was genuinely misunderstood.
OOP & clean code
- SRP: a page does locators; a service does logic; a step does binding. Don't blend them.
- DRY: reuse common steps/services; extract shared helpers. Search before adding a new step.
- Encapsulation: locators private in pages; shared state behind an injected
ScenarioContext. - Composition: build complex pages from smaller card/section components.
- Typed inputs: pass input objects/records, not long primitive parameter lists.
- Naming: intention-revealing; scenario/step/method names describe behavior and expected outcome.
- No inline comments / emojis: Javadoc public methods; keep methods small and single-purpose.
QUICK REFERENCE
| Task | Action |
|---|---|
| New project | Parent + framework + suite modules; add Playwright/Cucumber/engine/REST Assured; playwright install |
| New feature | One Scenario per AC, tag @type @ACn, reuse common steps, add negatives |
| Find locator | Playwright codegen / inspector / MCP; prefer role/label; chain+filter |
| New page | Extend CommonPage; annotate locators; add @VerifyAt/@VerifyBy |
| New step | Thin glue → service → page; inject collaborators (PicoContainer) |
| Assert UI | assertThat(locator) web-first (auto-retry); never boolean isVisible() |
| Isolate | New BrowserContext per scenario; reuse auth via storageState |
| UI test | Tag @ui; context in @Before("@ui"); assert UI + DB |
| API test | REST Assured; store response in context; three-way validation |
| Mock dep | page.route("**/api/...", route -> route.fulfill(...)) |
| DB check | Repository/ORM (no raw SQL); verify persistence + audit fields |
| Run | mvn test -Denv=<env> -Dcucumber.filter.tags="@..." -Dbrowser=<b> |
| CI | Linux, headless, install --with-deps, traces on retry, upload report |
CHECKLIST (before declaring a test done)
- Every AC has exactly one scenario, tagged with its AC id
- Negative/edge scenarios added to hunt for bugs
- Locators discovered with Playwright, kept in page objects, role/label-based
- Verifications use web-first
assertThat(locator)— no boolean getters, noThread.sleep - Steps are thin; logic and assertions in services
- Collaborators injected (PicoContainer); state via injected
ScenarioContext - New browser context per scenario; auth reused via
storageStatewhere possible - Third-party/unstable deps mocked via
page.route - UI assertions confirmed against API and/or DB (no type-only checks)
- Exact error messages and normalized dates validated
- Test data provisioned in setup; cleaned up after
- Context closed in
@After; screenshot/trace on failure - Parallel-safe (no shared Page/Browser); headless on CI
- No hardcoded secrets, URLs, or proprietary references