- Skill ID
- hieutrtr/ai1-skills/e2e-testing
- Publisher
- hieutrtr
- Repository
- ai1-skills
- Installs
- 0
- Files
- 5
- License
- MIT
- Requires
- Playwright 1.40+, Node.js 20+
- Synced
- Sep 23, 2026
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
hieutrtr/ai1-skills/e2e-testingInstalls these files- SKILL.md
- references/e2e-test-template.ts
- references/page-object-template.ts
- references/playwright-config-example.ts
- scripts/run-e2e-with-report.sh
What this skill tells the agent
E2E Testing
When to Use
Activate this skill when:
- Writing E2E tests for complete user workflows (login, CRUD operations, multi-page flows)
- Creating critical path regression tests that validate the full stack
- Testing cross-browser compatibility (Chromium, Firefox, WebKit)
- Validating authentication flows end-to-end
- Testing file upload/download workflows
- Writing smoke tests for deployment verification
Do NOT use this skill for:
- React component unit tests (use
react-testing-patterns) - Python backend unit/integration tests (use
pytest-patterns) - TDD workflow enforcement (use
tdd-workflow) - API contract testing without a browser (use
pytest-patternswith httpx)
Instructions
Test Structure
e2e/
├── playwright.config.ts # Global Playwright configuration
├── fixtures/
│ ├── auth.fixture.ts # Authentication state setup
│ └── test-data.fixture.ts # Test data creation/cleanup
├── pages/
│ ├── base.page.ts # Base page object with shared methods
│ ├── login.page.ts # Login page object
│ ├── users.page.ts # Users list page object
│ └── user-detail.page.ts # User detail page object
├── tests/
│ ├── auth/
│ │ ├── login.spec.ts
│ │ └── logout.spec.ts
│ ├── users/
│ │ ├── create-user.spec.ts
│ │ ├── edit-user.spec.ts
│ │ └── list-users.spec.ts
│ └── smoke/
│ └── critical-paths.spec.ts
└── utils/
├── api-helpers.ts # Direct API calls for test setup
└── test-constants.ts # Shared constantsNaming conventions:
- Test files:
<feature>.spec.ts - Page objects:
<page-name>.page.ts - Fixtures:
<concern>.fixture.ts - Test names: human-readable sentences describing the user action and expected outcome
Page Object Model
Every page gets a page object class that encapsulates selectors and actions. Tests never interact with selectors directly.
Base page object:
// e2e/pages/base.page.ts
import { type Page, type Locator } from "@playwright/test";
export abstract class BasePage {
constructor(protected readonly page: Page) {}
/** Navigate to the page's URL. */
abstract goto(): Promise<void>;
/** Wait for the page to be fully loaded. */
async waitForLoad(): Promise<void> {
await this.page.waitForLoadState("networkidle");
}
/** Get a toast/notification message. */
get toast(): Locator {
return this.page.getByRole("alert");
}
/** Get the page heading. */
get heading(): Locator {
return this.page.getByRole("heading", { level: 1 });
}
}Concrete page object:
// e2e/pages/users.page.ts
import { type Page, type Locator } from "@playwright/test";
import { BasePage } from "./base.page";
export class UsersPage extends BasePage {
// ─── Locators ─────────────────────────────────────────
readonly createButton: Locator;
readonly searchInput: Locator;
readonly userTable: Locator;
constructor(page: Page) {
super(page);
this.createButton = page.getByTestId("create-user-btn");
this.searchInput = page.getByRole("searchbox", { name: /search users/i });
this.userTable = page.getByRole("table");
}
// ─── Actions ──────────────────────────────────────────
async goto(): Promise<void> {
await this.page.goto("/users");
await this.waitForLoad();
}
async searchFor(query: string): Promise<void> {
await this.searchInput.fill(query);
// Wait for search results to update (debounced)
await this.page.waitForResponse("**/api/v1/users?*");
}
async clickCreateUser(): Promise<void> {
await this.createButton.click();
}
async getUserRow(email: string): Promise<Locator> {
return this.userTable.getByRole("row").filter({ hasText: email });
}
async getUserCount(): Promise<number> {
// Subtract 1 for header row
return (await this.userTable.getByRole("row").count()) - 1;
}
}Rules for page objects:
- One page object per page or major UI section
- Locators are public readonly properties
- Actions are async methods
- Page objects never contain assertions -- tests assert
- Page objects handle waits internally after actions
Selector Strategy
Priority order (highest to lowest):
| Priority | Selector | Example | When to Use |
|---|---|---|---|
| 1 | data-testid | getByTestId("submit-btn") | Interactive elements, dynamic content |
| 2 | Role | getByRole("button", { name: /save/i }) | Buttons, links, headings, inputs |
| 3 | Label | getByLabel("Email") | Form inputs with labels |
| 4 | Placeholder | getByPlaceholder("Search...") | Search inputs |
| 5 | Text | getByText("Welcome back") | Static text content |
NEVER use:
- CSS selectors (
.class-name,#id) -- brittle, break on styling changes - XPath (
//div[@class="foo"]) -- unreadable, extremely brittle - DOM structure selectors (
div > span:nth-child(2)) -- break on layout changes
Adding data-testid attributes:
// In React components -- add data-testid to interactive elements
<button data-testid="create-user-btn" onClick={handleCreate}>
Create User
</button>
// Convention: kebab-case, descriptive
// Pattern: <action>-<entity>-<element-type>
// Examples: create-user-btn, user-email-input, delete-confirm-dialogWait Strategies
NEVER use hardcoded waits:
// BAD: Hardcoded wait -- flaky, slow
await page.waitForTimeout(3000);
// BAD: Sleep
await new Promise((resolve) => setTimeout(resolve, 2000));Use explicit wait conditions:
// GOOD: Wait for a specific element to appear
await page.getByRole("heading", { name: "Dashboard" }).waitFor();
// GOOD: Wait for navigation
await page.waitForURL("/dashboard");
// GOOD: Wait for API response
await page.waitForResponse(
(response) =>
response.url().includes("/api/v1/users") && response.status() === 200,
);
// GOOD: Wait for network to settle
await page.waitForLoadState("networkidle");
// GOOD: Wait for element state
await page.getByTestId("submit-btn").waitFor({ state: "visible" });
await page.getByTestId("loading-spinner").waitFor({ state: "hidden" });Auto-waiting: Playwright auto-waits for elements to be actionable before clicking, filling, etc. Explicit waits are needed only for assertions or complex state transitions.
Auth State Reuse
Avoid logging in before every test. Save auth state and reuse it.
Setup auth state once:
// e2e/fixtures/auth.fixture.ts
import { test as base } from "@playwright/test";
import path from "path";
const AUTH_STATE_PATH = path.resolve("e2e/.auth/user.json");
export const setup = base.extend({});
setup("authenticate", async ({ page }) => {
// Perform real login
await page.goto("/login");
await page.getByLabel("Email").fill("testuser@example.com");
await page.getByLabel("Password").fill("TestPassword123!");
await page.getByRole("button", { name: /sign in/i }).click();
// Wait for auth to complete
await page.waitForURL("/dashboard");
// Save signed-in state
await page.context().storageState({ path: AUTH_STATE_PATH });
});Reuse in tests:
// playwright.config.ts
export default defineConfig({
projects: [
// Setup project runs first and saves auth state
{ name: "setup", testDir: "./e2e/fixtures", testMatch: "auth.fixture.ts" },
{
name: "chromium",
use: {
storageState: "e2e/.auth/user.json", // Reuse auth state
},
dependencies: ["setup"],
},
],
});Test Data Management
Principles:
- Tests create their own data (never depend on pre-existing data)
- Tests clean up after themselves (or use API to reset)
- Use API calls for setup, not UI interactions (faster, more reliable)
API helpers for test data:
// e2e/utils/api-helpers.ts
import { type APIRequestContext } from "@playwright/test";
export class TestDataAPI {More from hieutrtr
Use this skill in a project
Open any RiverX project, search the Skills panel for hieutrtr/ai1-skills/e2e-testing, and install it in one click.
