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
- facebook/immersive-web-sdktheir own version, first seen Oct 8, 2026
- bishal-pal/New-Skill-Testtheir own version, first seen Jun 24, 2026
- jie023/skillstheir own version, first seen Jun 3, 2026
Other versions3 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 3
- facebook/immersive-web-sdk★ 350same name
- jie023/skills★ 1same name
- bishal-pal/New-Skill-Test★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository41 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- analyze-codeSkill
- analyze-projectSkill
- analyze-testsSkill
- audit-mergesSkill
- babysit-prSkill
- backlogSkill
- branchSkill
- coderabbitSkill
- commitSkill
- greptileSkill
- implement-issuesSkill
- issueSkill
- jiraSkill
- lintro-addSkill
- lintro-reviewSkill
- lintro-verifySkill
- lintSkill
- properizeSkill
- pr-raycastSkill
- prSkill
- raycastSkill
- rebaseSkill
- reconcileSkill
- scorecardSkill
- stand-ciSkill
- stand-generalSkill
- stand-odinSkill
- stand-pySkill
- stand-rustSkill
- stand-tsSkill
- sweep-prsSkill
- test-apiSkill
- test-shellSkill
- testSkill
- test-ui-qsfSkill
- turbo-addSkill
- turbo-testSkill
- turbo-verifySkill
- which-prSkill
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
- Distinctive: line 8
Write and maintain browser E2E tests following Playwright best practices.
- Very specific: line 39
Never use waitForTimeout().
- Distinctive: line 68
Use Playwright's auto-retrying assertions.
- Very specific: line 87
Use a BasePage → SpecializedPage hierarchy with composed components.
- Very specific: line 133
Use forEach loops over arrays/objects for data-driven parameterization
- Distinctive: line 134
Test names describe the scenario and expected outcome
- Distinctive: line 137
Import UI labels, error messages, and identifiers from constants/enums — never hard-code strings in tests
≈ “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.
Files
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
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
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.

