test-fixer
Test Fixer
Debug and fix failing Playwright tests by visually inspecting the actual page state.
⚠️ CRITICAL: Browser Session Behavior
Each MCP call = new browser session. Browser CLOSES after each call. You CANNOT navigate in one call and interact in another. Use
browser_run_codefor ALL test debugging. If you need to return to a specific state (e.g., after login), you MUST redo ALL steps from scratch.
Workflow
- Parse the error - Extract failing test file, line number, selector, and error type
- Capture page state - Use
browser_run_codeto navigate AND interact in one session - Analyze the issue - Compare expected vs actual selectors/state
- Fix the code - Update page object and/or test spec
- Verify - Re-run the single failing test
Step 1: Parse Error
Extract from test output:
- File path: e.g.,
tests/olx-landing.spec.ts:45 - Error type: timeout, strict mode violation, element not found, assertion failed
- Failing selector: the locator that failed
- Expected vs received: for assertion errors
Step 2: Capture Page State
CRITICAL: Browser Session Behavior
Each MCP call creates a NEW browser session. For multi-step operations, use browser_run_code!
Option A: Single browser_run_code Call (Recommended)
Run all exploration steps in one session:
python .claude/skills/mcp-client/scripts/mcp_client.py call playwright browser_run_code '{
"code": "
// Navigate to page
await page.goto(\"https://www.olx.ro\");
// Handle cookies
const acceptBtn = page.getByRole(\"button\", { name: \"Accept\" });
if (await acceptBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await acceptBtn.click();
await page.waitForTimeout(500);
}
// Replicate test steps up to failure
const categoryLink = page.getByRole(\"link\", { name: /Auto, moto/i }).first();
await categoryLink.click();
await page.waitForTimeout(1500);
// Capture state at failure point
const snapshot = await page.accessibility.snapshot();
return JSON.stringify({
url: page.url(),
didNavigate: page.url().includes(\"auto\"),
snapshot: snapshot
}, null, 2);
"
}'
Option B: Simple Navigate (When No Interactions Needed)
browser_navigate returns both page AND snapshot in one call:
python .claude/skills/mcp-client/scripts/mcp_client.py call playwright browser_navigate '{"url": "https://example.com"}'
Option C: Run Test in Debug Mode
For complex scenarios requiring visual debugging:
# Run the specific failing test with headed browser and pause on failure
npx playwright test "tests/example.spec.ts:45" --headed --debug
Option D: Use Test Artifacts
Check the test-results folder for screenshots and traces:
# View trace from failed test
npx playwright show-trace test-results/*/trace.zip
Step 3: Analyze Issue
| Error Type | Analysis | Typical Fix |
|---|---|---|
strict mode violation |
Multiple elements match | Add .first(), use more specific selector |
element not visible |
Element exists but hidden | Wait for visibility, check cookie banners |
timeout waiting for selector |
Selector outdated | Update to match actual DOM |
toHaveURL failed |
Navigation didn't happen | Add waitForURL, check click target |
toContainText failed |
Wrong assumed text | Discover actual error message text |
click opens submenu, not page |
Multi-step interaction needed | Add click on "View all" or final nav link |
Common Issue: Wrong Assumed Text
If assertion fails on toContainText or toHaveText, the test probably assumed the wrong error message.
Fix: Discover the actual text:
python .claude/skills/mcp-client/scripts/mcp_client.py call playwright browser_run_code '{
"code": "
await page.goto(\"https://example.com/login\");
// Cookies
const acceptBtn = page.getByRole(\"button\", { name: /accept/i });
if (await acceptBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await acceptBtn.click();
}
// Trigger the error condition
await page.fill(\"input[type=email]\", \"wrong@test.com\");
await page.fill(\"input[type=password]\", \"wrongpass\");
await page.click(\"button[type=submit]\");
await page.waitForTimeout(3000);
// Capture ACTUAL error text
const errors = await page.locator(\"[class*=error], [role=alert]\").evaluateAll(els =>
els.filter(e => e.offsetParent !== null).map(e => ({
text: e.textContent?.trim(),
selector: e.className ? \".\" + e.className.split(\" \")[0] : \"[role=alert]\"
}))
);
return JSON.stringify({ errors }, null, 2);
"
}'
Then update test with actual text:
// Before (assumed)
await expect(page.locator('.error')).toContainText('Invalid credentials');
// After (discovered)
await expect(page.getByRole('alert')).toContainText('Email sau parolă incorectă');
Selector Priority (best to worst)
getByRole()- Accessible, stablegetByTestId()- Stable if devs maintain itgetByText()- Readable, somewhat stablelocator('[href="..."]')- For links- CSS selectors - Last resort
Understanding Element Behavior
Use browser_run_code to test what clicking does:
python .claude/skills/mcp-client/scripts/mcp_client.py call playwright browser_run_code '{
"code": "
await page.goto(\"https://www.olx.ro\");
// Dismiss cookies
const acceptBtn = page.getByRole(\"button\", { name: \"Accept\" });
if (await acceptBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await acceptBtn.click();
}
// Record initial state
const initialUrl = page.url();
// Click the element that failed
const element = page.getByRole(\"link\", { name: /Auto, moto/i }).first();
await element.click();
await page.waitForTimeout(1500);
// Analyze what happened
const finalUrl = page.url();
const didNavigate = finalUrl !== initialUrl;
// Look for submenus or dropdowns
const submenuVisible = await page.locator(\"[class*=submenu], [class*=dropdown], [class*=menu]\").first().isVisible().catch(() => false);
// Get visible links that might be \"View all\" type
const navLinks = await page.getByRole(\"link\").filter({ hasText: /vezi|view|all|toate/i }).allTextContents();
const snapshot = await page.accessibility.snapshot();
return JSON.stringify({
initialUrl,
finalUrl,
didNavigate,
submenuVisible,
navLinks,
snapshot
}, null, 2);
"
}'
Step 4: Fix the Code
Page Object Updates
When selectors break, update the page object locator:
// Before (broken)
this.searchButton = page.getByRole('button', { name: 'Search' });
// After (from snapshot showing actual name)
this.searchButton = page.getByRole('button', { name: /Căutare/i });
Test Updates
When assertions fail, update test logic:
// Before (navigation not waiting)
await categoryLink.click();
await expect(page).toHaveURL(/category/);
// After (proper navigation wait)
await Promise.all([
page.waitForURL(/category/),
categoryLink.click()
]);
Multi-Step Navigation Fixes
When click opens submenu instead of navigating:
// Before (assumes direct navigation)
async navigateToCategory(categoryName: string) {
await this.page.getByRole('link', { name: categoryName }).click();
}
// After (handles submenu)
async navigateToCategory(categoryName: string) {
// Click category to open submenu
const categoryLink = this.page.getByRole('link', { name: categoryName }).first();
await categoryLink.click();
// Click "View all" to actually navigate
const viewAllLink = this.page.getByRole('link', { name: /Vezi toate anunturile/i });
await viewAllLink.click();
}
Common Fixes Reference
Cookie Banner Blocking
// Add to page object
async acceptCookies() {
const banner = this.page.getByRole('dialog').filter({ hasText: /cookie|privacy/i });
if (await banner.isVisible({ timeout: 2000 }).catch(() => false)) {
await this.page.getByRole('button', { name: /accept|agree/i }).click();
}
}
Multiple Elements Match
// Use .first() for first match
await page.getByRole('link', { name: 'Category' }).first().click();
// Or use more specific parent context
await page.locator('nav').getByRole('link', { name: 'Category' }).click();
// Or use getByTestId if available
await page.getByTestId('login-submit-button').click();
Element Not Ready
// Wait for element to be actionable
await expect(element).toBeVisible();
await element.click();
// Or wait for network idle
await page.waitForLoadState('networkidle');
Navigation Not Completing
// Wait for URL change explicitly
await Promise.all([
page.waitForURL(/expected-path/),
triggerElement.click()
]);
Step 5: Verify
Re-run only the failing test:
npx playwright test "tests/olx-landing.spec.ts" -g "Category links navigate correctly"
Or run by line number:
npx playwright test tests/olx-landing.spec.ts:45
Checklist Before Fixing
- Read the failing test file
- Navigate to the URL the test uses (with
browser_run_code) - Accept cookies/dismiss popups if present
- Replicate test steps up to failure
- Capture fresh DOM snapshot
- Understand actual UI behavior (dropdowns, submenus, multi-step flows)
- Find the actual element in snapshot
- Update selector AND flow to match reality
- Re-run the single failing test to verify
Common Misunderstandings
| Assumption | Reality | Fix |
|---|---|---|
| Click navigates directly | Opens submenu first | Add step to click "View all" or similar |
| Element is immediately visible | Requires scroll or hover | Add scrollIntoViewIfNeeded() or hover action |
| Form submits on button click | Requires Enter key or specific trigger | Use correct submission method |
| Modal closes on outside click | Requires explicit close button | Click the close/X button |
| Page loads immediately | Redirect to different domain | Add waitForURL with correct domain pattern |
References
See references/error-patterns.md for detailed diagnosis and fixes for:
- Timeout errors (selector, URL)
- Strict mode violations
- Assertion failures (text, URL)
- Element state errors (not visible, disabled)
- Navigation issues
Quick Debug Script
For complex debugging, use this exploration script:
python .claude/skills/mcp-client/scripts/mcp_client.py call playwright browser_run_code '{
"code": "
// ====== CUSTOMIZE THIS SECTION ======
const URL = \"https://www.olx.ro/cont/\";
const WAIT_FOR_REDIRECT = /login\\.olx\\.ro/;
// =====================================
await page.goto(URL);
// Handle cookies
const acceptBtn = page.getByRole(\"button\", { name: /accept/i });
if (await acceptBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await acceptBtn.click();
await page.waitForTimeout(500);
}
// Wait for redirect if specified
if (WAIT_FOR_REDIRECT) {
await page.waitForURL(WAIT_FOR_REDIRECT, { timeout: 10000 });
}
// Get comprehensive element info
const inputs = await page.locator(\"input\").evaluateAll(els =>
els.map(e => ({
type: e.type,
name: e.name,
placeholder: e.placeholder,
testid: e.dataset.testid
}))
);
const buttons = await page.locator(\"button\").evaluateAll(els =>
els.map(e => ({
text: e.textContent?.trim(),
testid: e.dataset.testid,
type: e.type
}))
);
const links = await page.getByRole(\"link\").evaluateAll(els =>
els.slice(0, 20).map(e => ({
text: e.textContent?.trim()?.substring(0, 50),
href: e.href
}))
);
const snapshot = await page.accessibility.snapshot();
return JSON.stringify({
url: page.url(),
inputs,
buttons,
links,
snapshot
}, null, 2);
"
}'