Edge cases · must-have checklist
The bugs juniors miss and seniors hunt for
Twelve recruiter-facing edge cases with one-line Playwright assertions. Use them as a smoke checklist before your next release — or as interview-grade talking points.
Empty states
Every list/table must render a deliberate empty surface — not a blank screen — when filtered to zero rows.
1
await expect(page.getByText(/no users matched/i)).toBeVisible();Boundary values
Min, min+1, max-1, max — the four amounts that catch off-by-one errors in caps, lengths, and quotas.
123
await page.getByLabel(/withdraw/i).fill('5000');
await page.getByRole('button', { name: /withdraw/i }).click();
await expect(page.getByText(/posted/i).first()).toBeVisible();Slow API
Skeletons, loaders and disabled CTAs must hold for a 3-second backend without losing user input.
1234
await page.route('**/api/**', async (r) => {
await new Promise((res) => setTimeout(res, 3000));
await r.continue();
});Duplicate clicks
Idempotency keys + disabled-on-pending CTAs prevent double-charged orders and duplicate transactions.
12345
await Promise.all([
page.getByRole('button', { name: /place order/i }).click(),
page.getByRole('button', { name: /place order/i }).click(),
]);
await expect(page.locator('[data-testid="order-row"]')).toHaveCount(1);File upload wrong format
Server-side MIME check must reject a renamed .exe; client-side check is not a security boundary.
12
await page.getByTestId('upload-input').setInputFiles('fixtures/malware.png');
await expect(page.getByTestId('upload-error')).toContainText(/unsupported/i);Token expiration
Expired JWTs must redirect to /login with a deep link, not crash the route.
123
await page.evaluate(() => localStorage.removeItem('vl-lab-auth'));
await page.goto('https://lab.hakdogan.com/wallet');
await expect(page).toHaveURL(/\/login\?next=%2Fwallet$/);Invalid currency precision
Amounts with more than 2 decimal places must be rejected at the boundary, not silently rounded.
123
await page.getByLabel(/^deposit$/i).fill('250.123');
await page.getByRole('button', { name: /deposit funds/i }).click();
await expect(page.getByRole('alert')).toContainText(/two decimal/i);Modal overlay blocks click
An invisible overlay can swallow clicks; pointer-events + z-index must be tested explicitly.
12
await page.getByRole('button', { name: /open modal/i }).click();
await expect(page.locator('[data-testid="modal-overlay"]')).toHaveCSS('pointer-events', 'auto');Hidden vs. visible race
An element can be in the DOM but visibility:hidden — assertions must use toBeVisible(), not toBeAttached().
1
await expect(page.getByTestId('lazy-panel')).toBeVisible({ timeout: 5_000 });Pagination reset after filter
Applying a filter while on page 5 should not leave the user on an empty page 5 of the new dataset.
12
await page.getByLabel(/search users/i).fill('xx');
await expect(page.getByTestId('pager-current')).toHaveText('1');RBAC unauthorized access
Direct navigation to /admin as a non-admin must reject — and the test must verify no admin payload was fetched.
123456
const adminCalls: string[] = [];
page.on('request', (r) => {
if (r.url().includes('/api/admin/')) adminCalls.push(r.url());
});
await page.goto('https://lab.hakdogan.com/admin');
expect(adminCalls).toHaveLength(0);Network offline recovery
Disabling the network mid-session must produce a recoverable banner, not stuck spinners.
1234
await context.setOffline(true);
await page.getByRole('button', { name: /refresh/i }).click();
await expect(page.getByRole('alert')).toContainText(/offline/i);
await context.setOffline(false);