VALIDATION LABLOADER / SPINNER

Async

Loader / Spinner

Trigger an async fetch, verify the spinner is visible during load, and confirm it disappears once the data arrives.

Beginner

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.

1
2
3
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.

1
2
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

loader.spec.ts
ts
1
2
3
4
5
6
7
8
9
10
11
12
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.

Idleloader.spec.ts
Failure demos
https://lab.hakdogan.com/practice/loader
playwright · headed · chromium0.00s
# awaiting Run Test · terminal scrolls automatically
Steps
  1. Interacting with loader / spinner widget
  2. Awaiting deterministic render
  3. Asserting expected state
  4. Cleaning up context
  5. Pass
StatusIdle
BrowserChromium
FrameworkPlaywright + TypeScript
Elapsed (live)--
Specloader.spec.ts