web-testing-playwright-e2e-349e61 — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited web-testing-playwright-e2e-349e61 (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Quick Guide: Playwright drives a real browser through complete user journeys. Locators are lazy queries re-resolved on every use and expect(locator) matchers retry until they pass, so a correct test needs no sleeps and no stale-handle handling. Each test gets its own browser context; shared setup arrives through fixtures rather than module-level variables, because tests run in parallel workers.Detailed Resources:
fixtures
worker-scoped fixtures, polling assertions, accessibility assertions
changes, troubleshooting
<critical_requirements>
Reach for `getByRole()` first. It selects the element the way a user and a screen reader find it, so it survives markup changes and validates the accessible name as a side effect.
Assert with `expect(locator)` matchers. They retry until the condition holds or the timeout expires, which is what removes the reason to sleep at all — a fixed wait is either too short or too slow.
Register a route before the navigation that triggers the request. Routes are attached to the context and only affect requests made after they are set, so a page.route() after page.goto() never sees the call it was meant to intercept.
Give each test its own state through a fixture. Workers run in parallel and a module-level variable is shared inside one worker and absent from the next, so setup that lives in a fixture is the only setup that behaves the same in both.
</critical_requirements>
Auto-detection: Playwright, @playwright/test, playwright.config, page.goto, test.describe, test.extend, expect(page), getByRole, getByLabel, getByTestId, toBeVisible, toHaveURL, toHaveScreenshot, toMatchAriaSnapshot, page.route, route.fulfill, page.clock, toPass, devices
Applies to:
Handled elsewhere:
whatever owns component testing
not decide what that tree should be
<philosophy>
A locator is a query, not a handle. page.getByRole("button") resolves nothing until it is used, and resolves again on every use — so there is no stale element to guard against, and no reason to capture one early. Assertions inherit the same model: expect(locator).toBeVisible() polls until the timeout.
Everything else follows: the sleep, the CSS selector and the shared variable each throw away something the model already gives for free.
</philosophy>
<decision_framework>
Hooks or fixtures for setup:
describe block → beforeEach, which keeps the setup where it is read.globalSetup in the config. beforeAll runs once per _worker_,not once per run.
Stub the network or hit it:
and keep one such test per feature.
</decision_framework>
<patterns>
test.describe groups, beforeEach handles the navigation every test in the group repeats.
test.describe("Login", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/login");
});
test("successful login lands on the dashboard", async ({ page }) => {
await page.getByLabel(/email/i).fill(VALID_EMAIL);
await page.getByRole("button", { name: /sign in/i }).click();
await expect(page).toHaveURL("/dashboard");
});
});Full code: examples/core.md
Locators in the constructor, domain methods on the class. Worth it once several tests touch the same page; inline locators stay clearer for a one-off.
export class LoginPage {
readonly emailInput: Locator;
readonly signInButton: Locator;
constructor(page: Page) {
this.emailInput = page.getByLabel(/email/i);
this.signInButton = page.getByRole("button", { name: /sign in/i });
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
// ...
}
}Full code: examples/core.md — inheritance in examples/page-objects.md
Prefer the query that names what the element _is_; fall back to a test id only where no role or label exists.
await page.getByRole("button", { name: /submit/i });
await page.getByLabel(/email address/i);
await page.getByTestId("user-avatar"); // no semantic role available
// Narrow within a list instead of writing a fragile selector
await page
.getByRole("listitem")
.filter({ hasText: "Product A" })
.getByRole("button", { name: /add to cart/i })
.click();
// Exclude and combine (v1.33+)
page.getByRole("listitem").filter({ hasNot: page.getByText("Out of stock") });
page.getByRole("button").and(page.getByTitle("Subscribe"));Priority table and role mappings: reference.md
Every expect(locator) matcher retries, including negated ones.
await expect(page.getByText("Welcome")).toBeVisible();
await expect(page).toHaveURL(/\/dashboard/);
await expect(page.getByRole("progressbar")).not.toBeVisible();
// Soft assertions collect every failure in one run instead of stopping at the first
await expect.soft(page.getByTestId("avatar")).toBeVisible();
await expect.soft(page.getByText("Premium")).toBeVisible();Full assertion table: reference.md
page.route() intercepts by URL pattern; route.fulfill() answers, route.abort() fails the request.
await page.route("**/api/users", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ id: "user-123", name: "Jane Doe" }),
}),
);
await page.route("**/api/users", (route) => route.abort("failed")); // network failureFull code: examples/core.md — modifying a real response in examples/api-mocking.md
toHaveScreenshot() writes a baseline on first run and compares afterwards. Mask what changes on its own.
await expect(page).toHaveScreenshot("dashboard.png", {
mask: [page.getByTestId("current-time")],
animations: "disabled",
});Full code: examples/visual-testing.md
base.extend adds fixtures for page objects and setup. An auto fixture runs without being named by the test.
export const test = base.extend<{ loginPage: LoginPage; authenticated: void }>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
authenticated: [
async ({ context }, use) => {
await context.addCookies([SESSION_COOKIE]);
await use();
await context.clearCookies();
},
{ auto: true },
],
});Full code: examples/core.md — composition and seeding in examples/fixtures.md
Move time instead of waiting for it. clock.install() comes before every other clock call.
await page.clock.install({ time: new Date("2024-02-02T08:00:00") });
await page.goto("/dashboard");
await page.clock.fastForward("25:00");
await expect(page.getByText(/session expires/i)).toBeVisible();Full code: examples/advanced-features.md
Assert the accessible tree as a whole, in one readable block.
await expect(page.getByRole("navigation")).toMatchAriaSnapshot(`
- navigation:
- link "Home"
- link "Products"
`);Full code: examples/advanced-features.md
</patterns>
<red_flags>
Breaks at runtime:
page.waitForTimeout() before an assertion — either too short and flaky or too long and slow;the retrying matcher already covers it.
page.route() registered after the navigation that fires the request — the route nevermatches; set it up before goto, in beforeEach or in a fixture.
.btn-primary, #submit-btn) — they change with styling and markup; a roleor label query does not.
use a fixture.
? or [] in page.route() — unsupported since v1.52; use a regex.route.continue() overriding the Cookie header — refused since v1.52; usecontext.addCookies().
_react / _vue selector engines — removed in v1.58; use a role query or a test id.Surprising behaviour:
beforeAll runs once per worker, not once per run — one-time setup belongs in globalSetup.toBeVisible() waits for visibility while toBeAttached() only checks the DOM; a hidden elementsatisfies the second.
toBeEditable() throws on a non-editable element since v1.50 rather than returning false.produced in the same environment as CI.
on layout.
expect.timeout from the config, not the test timeout — a slow page fails theassertion first.
</red_flags>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.