Skip to main content

loom-e2e-testing

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

معلومات المصدر

المستودع
cosmix/loom
آخر نشاط في المصدر
٢٩ أغسطس ٢٠٢٦ في ٢١:١٩
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٦١
التفرعات
٤

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
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)
عرض على GitHub