Test ui
Canonical Agent Skills library for Claude Code, Cursor, Codex, and other agents.
npx -y skills add lgtm-hq/ai-skills --skill test-uiAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
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.
SKILL.md
6.1 KB, ~1.4k tokens by cl100k_base, as published. Nobody here has run it
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
readonlyproperties — 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
forEachloops over arrays/objects for data-driven parameterization - Test names describe the scenario and expected outcome
- Extract
beforeEachnavigation to shared helpers when duplicated 3+ times - No
console.logdebug output — usetest.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
readonlyproperties, and own theassert*methods - Tests independent;
forEachparameterization; descriptive names - No debug logging; constants imported, not hard-coded