VALIDATION LAB•LOADER / SPINNER
Async
Loader / Spinner
Trigger an async fetch, verify the spinner is visible during load, and confirm it disappears once the data arrives.
Scenario
Spinners that linger after data arrives, or never appear at all, are universal QA tickets. The flake 'sometimes I see a spinner, sometimes I don't' is almost always a missed loading state on cache hits.
Live widget · interact freely
Manual test checklist
- 1Click load — spinner appears, then content replaces it
- 2Click load while already loading — only one spinner at a time
- 3Spinner has `role='status'` for screen readers
- 4Disable the network — confirm a fallback error replaces the spinner
- 5Reload the page — does cached state skip the spinner?
Expected result
After clicking 'Load metrics' a spinner appears within 100ms, the data card hydrates 1.6s later, and the spinner is unmounted.
Automation challenge
Don't `waitForTimeout(1600)`. Use `await expect(spinner).toBeVisible()` immediately after the click, then `await expect(spinner).toBeHidden({ timeout: 5_000 })` followed by `await expect(card).toBeVisible()`.
Stable selectors
- Trigger load
[data-testid="loader-trigger"] - Spinner
[data-testid="loader-spinner"] - Data card
[data-testid="loader-card"]
Locator strategy
Three levels from simple IDs to scoped Playwright locators. IDs and names are easy to learn but are not always the best long-term choice when labels change or components repeat.
Use simple IDs and names to understand how locating elements works.
123
await page.goto('https://lab.hakdogan.com/practice/loader');
await page.locator('#loader-trigger').click();Avoid as primary strategies
XPath (unless there is no alternative), long CSS chains, Tailwind-style utility class selectors, generated or unstable IDs, and volatile framework internals break when layout, styling, or DOM structure shifts.
12
await page.locator('.w-full.rounded-xl.border.bg-blue-500').fill('demo@example.com');
await page.locator('//div[2]/form/div[1]/input').fill('demo@example.com');Reference Playwright spec
123456789101112
import { test, expect } from '@playwright/test';
test('loader appears and disappears', async ({ page }) => {
await page.goto('https://lab.hakdogan.com/practice/loader');
await page.getByTestId('loader-trigger').click();
await expect(page.getByTestId('loader-spinner')).toBeVisible();
await expect(page.getByTestId('loader-spinner')).toBeHidden({
timeout: 5_000,
});
await expect(page.getByTestId('loader-card')).toBeVisible();
});Headed Test Playback
Simulated headed-browser flow — no real browser is launched.
Automation-style playback (Playwright-shaped logs). No real browser; no commands run on your machine.
loader.spec.ts- Interacting with loader / spinner widget
- Awaiting deterministic render
- Asserting expected state
- Cleaning up context
- Pass