agentleFS
lgtm-hqlgtm-hq/ai-skills/skills/test-ui/SKILL.md

test-ui

Skill1 starsChanged 10 days agoMITView on GitHub

Playwright E2E testing best practices. Use when writing browser tests, visual regression, or accessibility tests in any project. Enforces user-facing locators, auto-waiting, web-first assertions, and Page Object Model.

3 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

7 rules. About 0 are common, 4 are distinctive, and 3 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈4

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Write and maintain browser E2E tests following Playwright best practices.

    line 8
  2. Very specific:

    Never use waitForTimeout().

    line 39
  3. Distinctive:

    Use Playwright's auto-retrying assertions.

    line 68
  4. Very specific:

    Use a BasePage → SpecializedPage hierarchy with composed components.

    line 87
  5. Very specific:

    Use forEach loops over arrays/objects for data-driven parameterization

    line 133
  6. Distinctive:

    Test names describe the scenario and expected outcome

    line 134
  7. Distinctive:

    Import UI labels, error messages, and identifiers from constants/enums — never hard-code strings in tests

    line 137

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Playwright E2E Testing Standards

Write and maintain browser E2E tests following Playwright best practices. These rules are project-agnostic; project-specific conventions (fixtures, test IDs, config) live in the project's own skill — for the QSF suite, follow the test-ui-qsf skill alongside this one.

Locators (Priority Order)

Use user-facing locators.

// BEST: Semantic locators
page.getByRole("button", { name: "Submit" });
page.getByRole("tab", { name: "Dashboard" });
page.getByLabel("Email");
page.getByPlaceholder("Search...");
page.getByText("Welcome");
page.getByTitle("Document title");

// GOOD: Test IDs (attribute set by testIdAttribute in playwright.config.ts)
page.getByTestId("delete-row-btn");

// ACCEPTABLE: CSS locators for structural queries
page.locator('input[type="file"][multiple]');

// AVOID: Fragile CSS selectors
page.locator("#submit-btn");
page.locator("div > button.primary");

Auto-Waiting

Never use waitForTimeout(). Playwright auto-waits for elements.

// WRONG: Manual timeouts
await page.waitForTimeout(1000);
await button.click();

// CORRECT: Auto-waiting assertions
await expect(button).toBeVisible();
await button.click();

// CORRECT: Poll for async state
await expect
  .poll(async () => page.evaluate(() => localStorage.getItem("theme")))
  .toBe("dark");

// CORRECT: Wait for specific conditions
await page.waitForLoadState("networkidle");
await page.waitForURL(/\/dashboard/);

// ACCEPTABLE: toPass() for polling complex async operations
await expect(async () => {
  await dashboardPage.open();
  await dashboardPage.assertPresent();
}).toPass({ timeout: 30_000 });

Web-First Assertions

Use Playwright's auto-retrying assertions. Keep assertions in page objects where possible.

// CORRECT: Web-first (auto-retries)
await expect(page.getByRole("heading")).toHaveText("Dashboard");
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
await expect(button).toBeEnabled();

// CORRECT: Page object assertion methods
await dashboardPage.assertSelectedCheckboxes(["Odd", "Free"]);

// AVOID: Manual checks (no retry)
const text = await heading.textContent();
expect(text).toBe("Dashboard");

Page Object Model

Use a BasePage → SpecializedPage hierarchy with composed components.

// pageObjects/BasePage.ts — all pages extend this
export default class BasePage {
  public navigation = new Navigation(this.page);

  constructor(readonly page: Page) {}

  async openTab(tabName: string): Promise<void> {
    await this.page.getByRole("tab", { name: tabName }).click();
  }

  async assertUrl(path: string | RegExp): Promise<void> {
    await expect(this.page).toHaveURL(path);
  }
}

// pageObjects/DetailPage.ts — specialized page
export default class DetailPage extends BasePage {
  public fileUploadComponent = new FileUpload(this.page);

  readonly errorLabel: Locator = this.page.getByRole("alert");

  async assertError(message?: string): Promise<void> {
    await expect(this.errorLabel).toBeVisible();
    if (message) await expect(this.errorLabel).toHaveText(message);
  }
}

Rules

  • One class per page/component — keep files focused
  • Locators as readonly properties — defined in constructor scope, not in methods
  • Assertions belong in page objects — prefix with assert
  • Actions return Promise<void> — no chaining
  • Compose via child components — this.navigation, this.fileUploadComponent, etc.
  • Split large element libraries — keep shared element classes under ~300 lines, one file per element type

Test Design

  • Tests are independent — no shared mutable state between tests
  • Use forEach loops over arrays/objects for data-driven parameterization
  • Test names describe the scenario and expected outcome
  • Extract beforeEach navigation to shared helpers when duplicated 3+ times
  • No console.log debug output — use test.step() annotations
  • Import UI labels, error messages, and identifiers from constants/enums — never hard-code strings in tests

Known Anti-Patterns

1. Mutable global state across tests

// BAD: shared array mutated across iterations — creates order dependency
const passedTests: string[] = [];
for (const tc of testcases) {
  test(`test ${tc}`, async () => {
    passedTests.push(tc);
  });
}

// GOOD: each test is self-contained
testcases.forEach((tc) => {
  test(`test ${tc}`, async ({ page }) => {
    // no shared mutable state
  });
});

2. Test name doesn't match behavior

Name the test after what it actually asserts, not the component you started with.

3. Visibility-only assertions ("nothing burger" tests)

// BAD: only checks the element exists — passes even if broken
await detailPage.assertBadgeVisible("Theme");

// GOOD: verify content and interaction
await detailPage.assertBadgeText("Theme", "Default");
await detailPage.selectBadge("Theme", "Dark");
await detailPage.assertBadgeText("Theme", "Dark");

Network Interception

// Mock API responses
await page.route("**/api/user", (route) => {
  route.fulfill({ json: { name: "Test User" } });
});

// Simulate failures
await page.route("**/*.css", (route) => route.abort("failed"));

// Cleanup after test — unroute every mock you registered
await page.unroute("**/api/user");
await page.unroute("**/*.css");

Checklist

  • Locators use getByRole, getByTestId, getByLabel, getByTitle (not fragile CSS)
  • No waitForTimeout() calls — auto-waiting assertions only
  • Assertions are web-first (expect() auto-retrying), meaningful beyond visibility checks
  • Page objects extend the base page, compose components, keep locators as readonly properties, and own the assert* methods
  • Tests independent; forEach parameterization; descriptive names
  • No debug logging; constants imported, not hard-coded

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.