.cursor/skills/fix-pw-spec/SKILL.md
This skill fixes test code bugs — not product bugs. Use it when:
If the product itself is broken, use diagnose-pw-failure instead.
Read these three things:
app/client/playwright/results/<test-path>/trace.zipapp/client/playwright/results/<test-path>/*.png| Error pattern | Root cause | Fix |
|---|---|---|
Timeout waiting for locator("...") | Selector doesn't match any element | Inspect the actual page; update selector |
strict mode violation: locator resolved to N elements | Locator too broad | Add .first(), .nth(), or refine with chained locators |
expect(received).toHaveText("X") received "Y" | Wrong expected value OR wrong element | Verify which element you're targeting; update expected value if test data changed |
Target closed / page.goto: Navigation failed | Page navigated away before action completed | Add waitForResponse or waitForURL before the action |
locator.click: Element is not visible | Element exists but hidden (behind modal, off-screen) | Wait for the right state, scroll into view, or close blocking elements |
Cannot find module / TypeScript error | Import path wrong or missing export | Fix the import path; check that POM/helper exports the symbol |
ECONNREFUSED / net::ERR_* | Not a spec bug — deployment issue | Stop. This is not fixable in the spec. |
When fixing, follow the project conventions (from .cursor/rules/playwright.mdc):
Prefer this priority order when replacing a broken selector:
page.getByRole("button", { name: /submit/i }) — bestpage.getByPlaceholder("Search...") — for inputs without proper label associationpage.getByTestId("my-element") — explicit contractpage.locator(SELECTORS.widgetInDeployed("text")) — Appsmith widget selectors from constantsDo not: Guess selectors. If unsure what's on the page, add a temporary await page.pause() or read the component source to find the actual DOM structure.
Replace any hard waits or networkidle with specific conditions:
// Instead of waitForTimeout or networkidle, wait for the element:
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
// For mutations, wait for the API response:
const response = page.waitForResponse(r => r.url().includes(API.actionsExecute));
await page.getByRole("button", { name: "Save" }).click();
await response;
Ensure all assertions use the auto-retrying form:
// WRONG — non-retrying
expect(await locator.textContent()).toBe("value");
// RIGHT — auto-retrying
await expect(locator).toHaveText("value");
After fixing, re-run the specific test:
cd app/client && npx playwright test <spec-file> --project=<project>
If it still fails, go back to Step 2 with the new error output.
Budget: Max 3 fix attempts per failure. After 3, either:
diagnose-pw-failure)Before declaring the fix done, check:
networkidle introducedwaitForTimeout introducedexpectnpx eslint <file> --ext .ts cleanSummarize what was wrong and what was fixed:
## Spec Fix Summary
**File**: playwright/tests/sanity/widgets/table-filter.spec.ts
**Attempts**: 2
### Fix 1
- **Error**: Timeout waiting for locator `getByRole("button", { name: "Filter" })`
- **Cause**: Button text is "Add Filter", not "Filter"
- **Fix**: Updated to `getByRole("button", { name: /add filter/i })`
### Fix 2
- **Error**: strict mode violation on `.first()` table row
- **Cause**: Filter hadn't applied yet when assertion ran
- **Fix**: Added `waitForResponse` on the execute API before asserting row content
**Result**: PASS