Cucumber step definitions
Skill vasyafomiuk/skills/java-playwright-e2e/skills/cucumber-step-definitions
Claude Code skill plugin marketplace — java-playwright-e2e: Java + Playwright + Cucumber (BDD) end-to-end test automation
npx -y skills add vasyafomiuk/skills --skill cucumber-step-definitionsAssembled 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
Implements Cucumber-JVM step definitions and the service layer behind them so Gherkin lines bind to real, parallel-safe Java automation code. Use this skill when the user wants to: write or fix step definition glue, map Gherkin steps to Java methods, wire up cucumber-picocontainer dependency injection, share state between steps with a ScenarioContext, build typed input objects or records from Cucumber DataTables, add @Before/@After hooks, manage the per-scenario browser context/page lifecycle, attach screenshots or traces on failure, reuse login via saved storageState, or refactor fat steps into a thin-glue + service-layer design. Also use it when steps leak state or are flaky under parallel execution, throw PicoContainer "cannot instantiate" errors, or leak state across scenarios. The runner and parallel-execution config itself live in e2e-framework-setup; this skill owns the hook bodies and per-scenario lifecycle. Project-agnostic — no application-specific assumptions.
SKILL.md
17.7 KB, as published. Nobody here has run it
Cucumber Step Definitions & Service Layer
This skill owns the glue that turns a Gherkin line into executing Java. It sits in
the middle of the acceptance-criteria -> passing-test workflow: a feature file
already exists (from create-test-scenarios) and page objects exist (from
playwright-page-objects); your job is to bind each step to a method, inject the
collaborators it needs, push real logic and assertions down into a service layer,
and manage the per-scenario lifecycle so the whole thing runs parallel-safe. For
cross-cutting rules (stack/versions, locator priority, web-first assertions,
isolation, naming/OOP, test-integrity) defer to the orchestrator
(java-playwright-e2e:orchestrator) — this skill states each in one line and spends
its words on the step/DI/hook how-to.
WHEN TO USE
Use this skill when the user asks to:
- Write or fix step definitions / glue mapping Gherkin to Java methods.
- Convert a feature's steps into a thin-glue + service-layer design.
- Set up or debug cucumber-picocontainer constructor injection.
- Share data across steps (current user, last response, parsed DTO) via a ScenarioContext.
- Build typed input objects/records from a Cucumber DataTable.
- Add @Before/@After hooks, browser context/page lifecycle, screenshot/trace-on-failure.
- Fix flaky-under-parallel steps, PicoContainer "cannot instantiate" errors, or state bleed between scenarios.
If a prerequisite is missing, hand off rather than invent it: no feature file ->
create-test-scenarios; no page object / locators -> playwright-page-objects;
no project scaffold, runner, or parallel config -> e2e-framework-setup.
THE THREE-LAYER RULE
Every step obeys exactly this chain. Each layer has one responsibility:
Feature (Gherkin) -> Step (glue) -> Service (logic + assertions) -> Page / BrowserSupport / API
- Step: matches the Gherkin line, parses/coerces params, delegates to a service. No branching, no assertions, no Playwright calls.
- Service: orchestrates the action and owns the meaningful assertions. Reusable across steps. This is where logic lives.
- Page / BrowserSupport / API: lowest level — locators and Playwright/REST calls. Owned by sibling skills, not here.
The test for a correct step: if you deleted the Gherkin, the step body should read
like a single sentence — "parse this, hand it to that service." If a step has an
if, a loop, or an assertThat, the logic belongs in a service.
@When("User adds an internal note")
public void userAddsNote(InternalNote note) { // typed param, see DataTables below
taskService.addNote(note); // delegate; no logic here
}
DEPENDENCY INJECTION (cucumber-picocontainer)
With cucumber-picocontainer on the classpath, Cucumber instantiates one instance
of each collaborator per scenario and injects them through constructors. Steps,
services, hooks, the page holder, and the ScenarioContext are all plain classes
with constructor parameters — never new-ed by you. Because instances are fresh
per scenario, this is inherently parallel-safe (the isolation rule lives in
the orchestrator).
public class TaskDetailsSteps {
private final TaskDetailsService taskService;
private final ScenarioContext context;
public TaskDetailsSteps(TaskDetailsService taskService, ScenarioContext context) {
this.taskService = taskService;
this.context = context;
}
// @When / @Then methods delegate to taskService ...
}
PicoContainer wiring rules — these are the ones that actually bite:
- Declare every collaborator as a constructor parameter. No field injection, no setters, no static lookups.
- A class may appear in many constructors; PicoContainer hands out the same per-scenario instance to each, so steps and services share one
PageHolder/ScenarioContext. - One public constructor per injectable class. Two constructors -> PicoContainer can't choose -> "cannot instantiate" at runtime.
- No cyclic dependencies (A needs B, B needs A) -> PicoContainer throws. Break the cycle by moving shared state into a third holder both depend on.
- Keep injectables in the glue path (the packages named in
cucumber.glue) so Cucumber discovers them. - Don't put scenario state in
staticfields; let the per-scenario instance carry it (see ScenarioContext).
Spring alternative: annotate the runner with
@CucumberContextConfiguration+@ContextConfigurationand mark steps/services as beans. Heavier — only worth it if you already run a Spring context. Pick one container; never mix Pico and Spring.
TYPED INPUTS FROM DATATABLES
Pass an input object or record, never a long primitive parameter list. Register a
@DataTableType once so any step receives the typed object directly. Prefer a
record for immutable input.
public record InternalNote(String title, String content, boolean confidential) {}
public class DataTableTransformers {
@DataTableType
public InternalNote internalNote(Map<String, String> row) {
return new InternalNote(
row.get("title"),
row.get("content"),
Boolean.parseBoolean(row.getOrDefault("confidential", "false")));
}
}
When User adds an internal note
| title | content | confidential |
| Quarterly review | Reviewed and ok | true |
Notes:
- A single-row table maps to one object; a multi-row table maps to
List<InternalNote>automatically once the type is registered. - Register transformers in a glue-path class so Cucumber finds them; they participate in DI like any other glue.
- For scalar coercions (enums, money, dates) use
@ParameterTypeto keep{string}out of step signatures and validation in one place. - Keep parsing in the transformer/step, not the service — the service receives clean typed objects.
SCENARIOCONTEXT FOR CROSS-STEP STATE
When one step produces data a later step consumes (the logged-in user, the last API response, a parsed DTO, a DB row), carry it in an injected, scenario-scoped holder — not global statics.
import io.restassured.response.Response;
import java.util.*;
/** Injected once per scenario (picocontainer); carries data between steps. A fresh
* instance per scenario means no @After cleanup of the context itself is needed. */
public class ScenarioContext {
private String currentUser;
private Response lastResponse;
private long recordId;
private final List<Long> createdNoteIds = new ArrayList<>();
private final Map<String, Object> bag = new HashMap<>(); // escape hatch for ad-hoc keys
public String currentUser() { return currentUser; }
public void setCurrentUser(String u) { this.currentUser = u; }
public Response getResponse() { return lastResponse; }
public void setResponse(Response r) { this.lastResponse = r; }
public long recordId() { return recordId; }
public void setRecordId(long id) { this.recordId = id; }
public List<Long> createdNoteIds() { return createdNoteIds; }
public void put(String k, Object v) { bag.put(k, v); }
@SuppressWarnings("unchecked")
public <T> T get(String k) { return (T) bag.get(k); }
}
- A fresh instance per scenario means no
@Aftercleanup needed and no bleed between scenarios — that is the whole point of preferring it over statics. - Prefer typed accessors (
setResponse/getResponse,setCurrentUser/currentUser) over the raw string-keyedbagwhen the set of shared keys is known; theput/getmap is the escape hatch for ad-hoc values. - If you must keep a legacy
staticAPI working under parallel execution, back it withInheritableThreadLocaland clear it in@After. New code: just injectScenarioContext.
// producer step
context.setResponse(response);
// consumer step in a later line
assertThat(context.getResponse().statusCode()).isEqualTo(201);
HOOKS & PER-SCENARIO LIFECYCLE
Hooks are glue too — they get DI. Inject a PageHolder (a scenario-scoped object
that owns the BrowserContext/Page); the hook opens it in @Before and closes it
in @After, so every injected service sees the same scenario's page.
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("@ui or @e2e")
public void tearDown(Scenario s) {
if (s.isFailed() && pages.hasPage()) {
s.attach(pages.page().screenshot(), "image/png", "failure");
}
pages.close(); // close context/page; prevent leaks + state bleed
}
}
Rules:
- Tag hooks (
@Before("@ui or @e2e")) so API-only scenarios don't pay to launch a browser; back them with a separate API-setup hook if needed. - Order with
orderwhen sequencing matters:@Before(order = 0)runs before@Before(order = 10);@Afterruns in reverse. Use it to seed data after the context exists. - Use a
@BeforeAll/@AfterAllstatic hook (or the runner) for once-per-JVM cost like launchingPlaywright/Browser; share oneBrowserper thread, one context per scenario. - Always
pages.close()in@Aftereven on failure — a leaked context is the most common cause of slow, flaky parallel runs. - Attach a screenshot on failure; for deeper debugging, record a Playwright trace and attach it on failure — see TRACING & TRACE-VIEWER DEBUGGING below, which owns the hook bodies. (
e2e-framework-setupkeeps only the CI artifact upload of the resulting zips.) - Per-scenario
storageStateseeding — a role-tagged scenario (@admin,@viewer) builds its context from a saved auth file instead of logging in through the UI:browser.newContext(new Browser.NewContextOptions().setStorageStatePath(Paths.get("auth/admin.json"))). The global login that writes thoseauth/*.jsonfiles once lives ine2e-framework-setup; here you only read them per scenario to reuse the session.
// PageHolder sketch — owns lifecycle, hands the same Page to every collaborator
public class PageHolder {
private final Browser browser; // injected (one per thread)
private BrowserContext ctx;
private Page page;
public PageHolder(Browser browser) { this.browser = browser; }
public void open() { ctx = browser.newContext(); page = ctx.newPage(); }
public Page page() { return page; }
public BrowserContext context() { return ctx; } // used by the tracing hooks
public TaskDetailsPage taskDetails() { return new TaskDetailsPage(page); } // page object for this scenario (real projects use a generic page registry)
public boolean hasPage() { return page != null; }
public void close() { if (ctx != null) ctx.close(); } // closing context closes its pages
}
TRACING & TRACE-VIEWER DEBUGGING
A Playwright trace is the highest-value artifact for diagnosing a failed scenario:
it captures a step-by-step timeline with DOM snapshots, network, and console. This
skill owns the tracing hook bodies; e2e-framework-setup only uploads the resulting
zips as CI artifacts.
Start the recording in @Before (after the context exists) and stop it in @After
only when the scenario failed — recording on green runs is pure overhead.
@Before("@ui or @e2e")
public void startTrace(Scenario s) {
pages.context().tracing().start(new Tracing.StartOptions()
.setScreenshots(true).setSnapshots(true).setSources(true));
}
@After("@ui or @e2e")
public void stopTrace(Scenario s) {
if (s.isFailed() && pages.hasPage()) {
Path zip = Paths.get("target/traces/" + safe(s.getName()) + ".zip");
pages.context().tracing().stop(new Tracing.StopOptions().setPath(zip));
s.attach(Files.readAllBytes(zip), "application/zip", "trace"); // surfaces in the report
} else {
pages.context().tracing().stop(); // discard on success
}
}
private static String safe(String name) { return name.replaceAll("[^a-zA-Z0-9-]", "_"); }
setSnapshots(true)enables the DOM snapshots that drive the viewer's time-travel;setSources(true)embeds the test source so each action links back to its line;setScreenshots(true)gives the filmstrip.- Open a trace at https://trace.playwright.dev (drag the zip in — runs locally, nothing uploaded), or from the CLI:
mvn exec:java -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="show-trace target/traces/x.zip". - What to read in the viewer: the timeline/filmstrip to find the action that stalled or failed; the DOM snapshot before/after that action to see what the page actually looked like (often a locator matched nothing or matched two); the network tab for a failed/slow request behind the symptom; the console tab for page-side JS errors.
- Keep this hook separate from (or ordered after) the screenshot hook so the trace stop runs on the same failed-scenario path.
THE SERVICE LAYER
The service is where the action is orchestrated and where assertions live. It
receives the page via the injected holder and uses web-first, auto-retrying
assertions — assertThat(locator), never boolean getters (full rule in
the orchestrator).
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
public class TaskDetailsService {
private final PageHolder pages; // injected — shared scenario page
public TaskDetailsService(PageHolder pages) { this.pages = pages; }
public void addNote(InternalNote note) {
TaskDetailsPage page = pages.taskDetails(); // page object from playwright-page-objects
page.waitForPageToLoad(); // confirm the page rendered first
page.addNote(note); // page owns the fill/click (Playwright calls)
assertThat(page.successToast()).isVisible(); // service owns the web-first assertion
}
}
Service rules:
- One service per cohesive area of behavior; reuse it across many steps. Don't put locators here — call page-object methods.
- Assertions are meaningful and unconditional: never wrap
assertThatinif (x != null)— provision the data so the assertion always runs (integrity rule in the orchestrator). - Use soft assertions when several checks should all report before the scenario fails; collect and assert at the end.
- API steps delegate to a REST-Assured service (
rest-assured-api-tests); persistence checks delegate to a repository service (database-validation). The step never calls those APIs directly.
REUSE BEFORE WRITING
DRY applies hardest to steps. Before adding a @When/@Then, search the glue
packages for an existing step that matches the Gherkin phrasing — duplicate steps
with near-identical regex are a common rot. Reuse a common step; if it's close,
generalize it (e.g. parameterize with {string}) rather than cloning. Keep
reusable, app-agnostic steps in the framework's common-steps package and
suite-specific steps in the suite. If you can't see the project's existing glue, ask the user
for an example step class and match its package layout, naming, and DI style before adding new steps.
HANDOFF
create-test-scenarios— produces the feature files your steps bind to. If a Gherkin line has no matching step, that skill (or this one) authors the step; if the scenario itself is wrong, fix it there.playwright-page-objects— owns locators and page-object methods. Your services call page methods; you never declare locators here. Missing page/locator -> hand off.rest-assured-api-tests— API steps delegate to its REST-Assured services andpage.routemocking; store the response inScenarioContextfor later steps.database-validation— persistence-verification steps delegate to its repository/ORM services for three-way (input/API/DB) validation.e2e-framework-setup— owns the runner,cucumber.glue, parallel config, the global login that writesauth/*.jsonstorage-state files, CI artifact upload of trace zips, and where injectable packages must live. Go there for "step not found / not glued" or parallelism. (Trace recording and trace-viewer usage live in this skill.)- the orchestrator (
java-playwright-e2e:orchestrator) — single source of truth for DI, isolation, web-first assertions, naming/OOP, and test-integrity conventions referenced one-line above.