Skip to main content

loom-e2e-testing

End-to-end testing for web applications with Playwright, Cypress, Selenium, and Puppeteer.

Datos de origen

Repositorio
cosmix/loom
Última actividad en el origen
29 de agosto de 2026 a las 21:19
Idioma detectado de SKILL.md
inglés
Estrellas
61
Forks
4

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
name
loom-e2e-testing
description
End-to-end testing for web applications with Playwright, Cypress, Selenium, and Puppeteer.
triggers
["e2e","e2e testing","end-to-end","end-to-end tests","Playwright","Cypress","Selenium","Puppeteer","Page Object Model","page object","test fixtures","selectors","locator","locators","getByRole","data-testid","auto-wait","async tests","network interception","route mocking","visual regression","visual testing","screenshot","trace viewer","flaky tests","flakiness","browser testing","browser automation","UI test","UI testing","acceptance test","smoke test","[Truncated]"]
# E2E Testing ## Overview Browser E2E validates full user journeys. This file owns **selectors, auto-waiting, network interception, POM, browser flakiness, and trace/video debugging** (Playwright-first, Cypress noted where it differs). E2E is the *tip* of the pyramid — keep it thin and reserve it for critical paths; push logic down to unit/integration (`loom-test-strategy`). For test-double taxonomy and AAA see `loom-testing`. ## Framework choice Default to **Playwright** for new suites: true multi-browser (incl. WebKit/Safari), multi-tab/origin, out-of-process network interception, built-in parallelism and trace viewer, and non-JS bindings (Python/.NET/Java). Choose **Cypress** only when the team is already invested or wants its live time-travel debugger; its in-browser model limits multi-tab, cross-origin, and true WebKit. Selenium/Puppeteer: legacy or Chrome-only automation, not greenfield E2E. ```bash npm init playwright@latest # scaffolds config + CI + browsers ``` ```typescript // playwright.config.ts — the load-bearing knobs export default defineConfig({ testDir: "./e2e", fullyParallel: true, // parallel within files too forbidOnly: !!process.env.CI, // fail CI if a .only slips in retries: process.env.CI ? 2 : 0, // retry ONLY to surface flakes reporter: [["html"], ["junit", { outputFile: "results.xml" }]], use: { baseURL: "http://localhost:3000", trace: "on-first-retry", // trace the retry, not every run screenshot: "only-on-failure", video: "retain-on-failure", }, webServer: { // boots the app, waits for it command: "npm run dev", url: "http://localhost:3000", reuseExistingServer: !process.env.CI, }, projects: [ { name: "chromium", use: { ...devices["Desktop Chrome"] } }, { name: "firefox", use: { ...devices["Desktop Firefox"] } }, { name: "webkit", use: { ...devices["Desktop Safari"] } }, ], }); ``` ⚠ `retries` exists to **diagnose** flakes (a test that passes only on retry is flaky and must be fixed), never to hide them — a retry-masked bug ships. `webServer` is why `page.goto` doesn't race the server boot; without it you get connection-refused flakes. ## Selectors — user-facing first Priority, best → worst. Higher options survive refactors and assert accessibility for free; CSS/XPath couple tests to DOM structure and shatter on markup changes. 1. **Role + accessible name** — `getByRole("button", { name: "Sign in" })` 2. **Label / placeholder / text** — `getByLabel("Email")`, `getByText("Welcome")` 3. **`data-testid`** — for elements with no stable role/text 4. CSS — only for structural selection (`.modal-content`) 5. **XPath — avoid** (brittle, unreadable) ```typescript page.getByRole("textbox", { name: "Email" }); page.getByLabel("Password"); page.getByTestId("product-card-123"); // add data-testid in the component // chain/filter instead of nth-index (index shifts → flake) page.getByTestId("product-list") .getByRole("listitem").filter({ hasText: "Widget" }) .getByRole("button", { name: "Add to cart" }); ``` ⚠ Never select by index (`.nth(2)`, `items[2]`) or auto-generated CSS-module class hashes — both change silently. Add `data-testid` at the component and strip it in prod builds (`babel-plugin-react-remove-properties`). Cypress: use `@testing-library/cypress` (`cy.findByRole`) for the same priority. ## Auto-waiting vs explicit waits Playwright locator actions **auto-wait** for the element to be attached, visible, stable, and enabled before acting, and web-first assertions (`expect(locator).toBeVisible()`) **retry** until they pass or time out. This eliminates almost all manual waiting. ```typescript // RIGHT — assert the condition; it retries internally await page.getByRole("button", { name: "Submit" }).click(); await expect(page.getByText("Success")).toBeVisible(); // WRONG — arbitrary sleep: flaky if slow, wasteful if fast await page.click("#submit"); await page.waitForTimeout(2000); // ⛔ never ``` Use explicit waits only for things auto-wait can't see — navigation and network: ```typescript await page.waitForURL("/dashboard"); // after a click that navigates await page.getByTestId("spinner").waitFor({ state: "hidden" }); ``` ⚠ `waitForTimeout` is banned in real suites — it's the #1 source of both flakiness and slowness. `waitForLoadState("networkidle")` is discouraged by Playwright (racy on apps that poll); wait for a concrete UI condition instead. Cypress auto-retries assertions/queries but **not** raw values — re-query, don't cache elements. ## Network interception Stub the network to make E2E deterministic and to reach error states the UI can't otherwise hit. Register the route **before** the navigation that triggers it. ```typescript // Deterministic data — no dependence on backend state await page.route("**/api/products", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([{ id: 1, name: "Test Widget" }]) })); // Force an error path await page.route("**/api/checkout", (route) => route.fulfill({ status: 500 })); // Assert a request happened (wait for it, don't sleep) const resp = page.waitForResponse("**/api/order"); await page.getByRole("button", { name: "Place order" }).click(); expect((await resp).status()).toBe(201); ``` ⚠ Use glob/regex (`**/api/x`) — exact strings miss query params and absolute vs relative URLs. Decide deliberately: **stub** for speed/determinism/error-injection; hit the **real** backend for a handful of true full-stack smoke tests. Cypress uses `cy.intercept()` with an alias + `cy.wait("@alias")`. ## Page Object Model Encapsulate page structure so a UI change touches one file, not fifty tests. Expose **user intentions** (`login(email, pw)`), not raw locators. ```typescript // e2e/pages/LoginPage.ts export class LoginPage { constructor(private page: Page) {} readonly email = () => this.page.getByLabel("Email"); readonly submit = () => this.page.getByRole("button", { name: "Sign in" }); async goto() { await this.page.goto("/login"); } async login(email: string, pw: string) { await this.email().fill(email); await this.page.getByLabel("Password").fill(pw); await this.submit().click(); } } ``` ⚠ Keep **assertions out of page objects** (except small `expect` helpers) — a POM describes the page; the test owns the verification. Prefer Playwright **fixtures** over a monolithic `App` object for composing pages/state; they scope setup/teardown automatically. ## Fixtures & auth (speed) Logging in through the UI on every test is the biggest E2E time sink. Authenticate **once**, save `storageState`, and reuse it — cutting minutes off a suite. ```typescript // global-setup: log in once, persist cookies/localStorage await page.goto("/login"); /* ...fill+submit... */ await page.context().storageState({ path: "e2e/.auth/user.json" }); // then in config or a fixture: use: { storageState: "e2e/.auth/user.json" } ``` Custom fixtures give per-test isolated data with automatic cleanup: ```typescript export const test = base.extend<{ user: User }>({ user: async ({}, use) => { const u = await prisma.user.create({ data: UserFactory.create() }); await use(u); await prisma.user.delete({ where: { id: u.id } }); // teardown always runs }, }); ``` ⚠ Even faster: seed state via the **API request context** rather than clicking through setup — see hybrid pattern below. ## Preventing browser flakiness General flaky-test theory (clock, RNG, shared state) and the full cause table are in `loom-test-strategy`. Browser-specific rules: - **Wait for conditions, never timeouts** — covered above; this is 80% of E2E flake. - **Select by role/text/testid, never index** — DOM order shifts. - **Isolate per test** — Playwright gives each test a fresh browser context (clean cookies/storage) by default; don't defeat it with shared global state. Mint unique data (`test-${Date.now()}@x.com`). - **Freeze time & animations** for anything time- or motion-sensitive: ```typescript await page.clock.setFixedTime(new Date("2024-01-15T10:00:00Z")); await page.emulateMedia({ reducedMotion: "reduce" }); ``` - **Broken-window rule:** fix or quarantine a flaky test the day it appears; a tolerated flake trains the team to ignore red. ## Debugging: trace, video, screenshot ```bash npx playwright test --trace on # force a trace for a run npx playwright show-trace trace.zip # timeline, DOM snapshots, network, console npx playwright test --debug # step through with Inspector ``` Configure artifacts on failure only (`trace: "on-first-retry"`, `video/screenshot: retain/only-on-failure` — see config) so green runs stay fast. The **trace viewer** (time-travel DOM + network + console per action) is the fastest way to root-cause a CI-only failure — wire trace upload into CI artifacts. `await page.pause()` opens the Inspector mid-test for local poking. ## Visual regression ```typescript await expect(page).toHaveScreenshot("dashboard.png", { mask: [page.getByTestId("timestamp")], // hide dynamic regions maxDiffPixelRatio: 0.01, }); ``` ⚠ Screenshots are **OS/font/GPU-dependent** — generate and compare baselines in the *same* container you run CI in (`--update-snapshots` locally on a Mac then failing in Linux CI is the classic trap). Disable animations, mask dynamic content (dates, avatars, ads), and pin the viewport. Treat baselines as reviewed artifacts, not auto-accepted. ## Hybrid API + UI Do setup and teardown over HTTP (fast, reliable); reserve the browser for the user-facing assertion. Cuts runtime and removes setup-related flake. ```typescript test("cart shows seeded item", async ({ page, request }) => { await request.post("/api/cart/add", { data: { productId: "123", quantity: 2 } }); await page.goto("/cart"); await expect(page.getByTestId("cart-item")).toHaveCount(1); await expect(page.getByTestId("quantity")).toHaveText("2"); }); ``` ## Cross-browser scope Run the full matrix (Chromium/Firefox/WebKit + mobile viewports) on **critical paths only**; run the rest on Chromium to keep CI fast. Skip project-specifically rather than duplicating tests: ```typescript test.describe("Admin panel", () => { test.skip(({ browserName }) => browserName !== "chromium"); // Chrome-only }); ``` ## Smoke suite A tiny must-pass set gating every release — homepage renders, auth works, key APIs return < 500: ```typescript test("user can sign in", async ({ page }) => { await page.goto("/login"); await page.getByLabel("Email").fill("user@example.com"); await page.getByLabel("Password").fill("password123"); await page.getByRole("button", { name: "Sign in" }).click(); await expect(page).toHaveURL("/dashboard"); }); ``` ## Verify before done - [ ] Selectors are role/label/text/testid — no index, XPath, or generated-class-hash selectors - [ ] Zero `waitForTimeout`/`sleep`; waits target a condition, URL, or response - [ ] Auth via `storageState`; per-test data is unique; cleanup in fixture teardown - [ ] Network stubbed where determinism/error-injection is needed; real backend only for true smoke tests - [ ] `retries` used to *surface* flakes, not mask them; any retry-only pass is triaged - [ ] trace/video/screenshot retained on failure and uploaded as CI artifacts - [ ] E2E limited to critical journeys; logic-level cases pushed down (`loom-test-strategy`) - [ ] Visual baselines generated in the CI container, with dynamic regions masked - [ ] No `.only` left in the suite (`forbidOnly` on in CI)
Ver en GitHub