Loading...
Loading...
Use when building e2e tests with Playwright, under deadline pressure to ship incomplete coverage, or struggling with wait strategies and mock-vs-reality tradeoffs - provides patterns for edge case coverage, deterministic waits, and strategic mocking decisions
npx skill4agent add zenobi-us/dotfiles playwright-testingwaitForSelector()waitForTimeout()page.waitForSelector()// ❌ BAD: Fixed timeout, hides what you're waiting for
await page.waitForSelector('.loading', { timeout: 5000 });
await page.click('button');
// ✅ GOOD: Explicit condition, timeout is safety net
// Wait for loading spinner to appear, proving async work started
await page.locator('.loading').waitFor({ state: 'visible' });
// Wait for it to disappear, proving async work completed
await page.locator('.loading').waitFor({ state: 'hidden' });
await page.click('button');
// ✅ GOOD: Custom condition when standard waits don't fit
async function waitForApiCall(page: Page, method: string) {
let apiCalled = false;
page.on('response', (response) => {
if (response.request().method() === method) {
apiCalled = true;
}
});
// Keep checking until API was called
await page.waitForFunction(() => apiCalled);
}// Reasonable defaults: 5s for navigation, 10s for complex async
await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
await page.locator('[data-testid]').waitFor({ timeout: 10000 });// INTENT: Test UI logic, not API integration
// → Mock the API, test DOM updates
test('displays user data when loaded', async ({ page }) => {
await page.route('/api/user', route => {
route.abort(); // Simulate network failure
});
await page.goto('/profile');
await expect(page.locator('.error-message')).toContainText('Failed to load');
});
// INTENT: Test API integration, not UI
// → Hit staging, verify contract is correct
test('payment endpoint returns correct schema', async ({ page }) => {
// Hit real staging, prove response matches what UI expects
const response = await page.request.post(
`${process.env.STAGING_API}/payment`,
{ data: { amount: 100 } }
);
expect(response.ok()).toBeTruthy();
const json = await response.json();
expect(json).toHaveProperty('transactionId');
expect(json).toHaveProperty('status');
});
// INTENT: Test complete critical flow
// → Hybrid: mock non-critical paths, hit staging for critical ones
test('checkout flow succeeds end-to-end', async ({ page }) => {
// Mock product catalog (doesn't change)
await page.route('/api/products', route => {
route.continue({ response: mockProducts });
});
// Hit real staging for payment (critical + mature)
// Hit real staging for order confirmation (critical + stable)
// Results in fast + reliable + safe tests
});| Rationalization | Reality | Action |
|---|---|---|
| "I'll defer edge case tests to next sprint" | You won't. Edge cases always ship untested. | Ship with complete coverage or don't ship. No exceptions. |
| "Fixed timeouts are good enough" | They work locally, flake in CI. Condition-based is not harder. | Use condition-based waits. Not "later." Now. |
| "I'll refactor to condition-based waits as I go" | You won't. Fixed timeouts stay forever. CI flakes forever. | Write condition-based waits first. Not "next time." This time. |
| "Manual testing covers edge cases" | It doesn't. Manual testing doesn't prevent regression. | Automated edge case tests are mandatory. Both matter. |
| "Mocking everything is pragmatic" | Pragmatism means working tests. All-mocks hide integration bugs. | Test critical paths against staging. Mock the rest by intent. |
| "My setup is too complex to refactor now" | It's not. Condition-based wait = 2 lines. Complexity is pretense. | Write correct waits first. Change behavior for shipping, not convenience. |
page.locator()page.$()// Find element and wait for it to be visible
await page.locator('[data-testid="submit"]').waitFor({ state: 'visible' });
// Find and click in one step (waits for visibility first)
await page.locator('[data-testid="submit"]').click();// All error cases for this path
await page.route('/api/checkout/**', route => {
if (Math.random() > 0.8) route.abort(); // 20% failure rate
else route.continue();
});const requests: any[] = [];
await page.on('request', (request) => {
if (request.url().includes('/api')) {
requests.push({
url: request.url(),
method: request.method(),
postData: request.postData(),
});
}
});
// After test actions
expect(requests).toContainEqual(
expect.objectContaining({ method: 'POST', url: expect.stringContaining('/payment') })
);