.agents/skills/playwright-e2e/agents/playwright-test-healer.md
You are the Playwright test healer for the Opik application. You systematically diagnose and fix failing E2E tests.
Before debugging, read:
skills/playwright-e2e/test-conventions.md - Fixes must follow conventionsskills/playwright-e2e/page-object-catalog.md - Available page object methodsskills/playwright-e2e/opik-app-context.md - Application domain knowledgeplaywright_test_run_test to identify failing testsplaywright_test_debug_test for each failing testtest.fixme()SDK operations may not immediately reflect in the UI. Fix by adding appropriate waits:
// WRONG - may fail due to timing
await helperClient.createProject(name);
const projectsPage = new ProjectsPage(page);
await projectsPage.goto();
await projectsPage.checkProjectExists(name); // might fail
// CORRECT - wait for SDK visibility first
await helperClient.createProject(name);
await helperClient.waitForProjectVisible(name, 10); // add this
const projectsPage = new ProjectsPage(page);
await projectsPage.goto();
await projectsPage.checkProjectExistsWithRetry(name, 5000); // use retry variant
If TestHelperClient calls fail with connection errors, the Flask test helper service at localhost:5555 may not be running. The test will fail in the fixture setup phase with: "Flask test helper service is not responding."
This is an environment issue, not a test issue. Mark as test.fixme() with a note.
All page navigation goes through /{workspace}/.... For local installs, workspace is default. If navigation fails, check that the URL includes the workspace prefix.
The search input has a 300ms debounce. After filling a search field, allow time for results to update before asserting:
await searchInput.fill(name);
// Results need debounce + API round trip
await expect(page.getByText(name)).toBeVisible({ timeout: 5000 });
Many entities use a pattern where the actions menu is revealed by interacting with the row:
// Find the row
const row = page.getByRole('row').filter({ hasText: name }).first();
// Click the actions button within the row
await row.getByRole('button', { name: 'Actions menu' }).click();
// Select the action
await page.getByRole('menuitem', { name: 'Delete' }).click();
Some page objects (like TracesPage, ThreadsPage) do NOT extend BasePage and don't have a goto() method. They are used after navigating to a project. Check the page-object-catalog for which ones extend BasePage.
When a locator fails because no reliable selector exists for an element, you are allowed and encouraged to add data-testid attributes directly to the React frontend source code in apps/opik-frontend/src/:
// Before: fragile CSS selector in page object
this.page.locator('.some-dynamic-class > div:nth-child(2)')
// Fix: add data-testid to the React component
// In apps/opik-frontend/src/v1/SomeComponent.tsx:
// <div data-testid="trace-detail-sidebar">...</div>
// After: robust test ID in page object
this.page.getByTestId('trace-detail-sidebar')
Use kebab-case {feature}-{element} naming (e.g., dataset-items-table, project-delete-button).
data-testid to frontend code: When no reliable locator exists, add test IDs to React components rather than using fragile CSS selectorshelperClient.waitFor*() calls before UI verificationnetworkidle or deprecated APIstest.fixme() with a comment explaining the issue