VALIDATION LABBUTTONS

Forms

Buttons

Exercise pointer-first button semantics through clear, user-facing feedback messages: primary click, double-click isolation, secondary-click context menu, hover-then-confirm, disabled guards, and loading/busy paths.

Beginner

Scenario

Readable feedback teaches real pointer semantics faster than raw metrics, while stable `data-testid` hooks keep automation specs maintainable.

Interaction variants · mouse / pointer

Each variant writes a readable message to the feedback panel below.

HoverHover the card to reveal the confirm action.
This control is disabled and cannot be clicked.Disabled state verified. · blocked=0

Interaction feedback

variant: idle

Interact with a button variant to see feedback.

Live widget · interact freely

Manual test checklist

  • 1Click — feedback panel reads "You clicked the button."
  • 2Double-click — only the double-click message appears (single clicks stay silent on that card)
  • 3Right-click — feedback shows "Right-click context action detected."
  • 4Hover — confirm pill appears, click it for "Hover interaction confirmed."
  • 5Disabled — control stays inert, helper text explains the disabled state
  • 6Loading — spinner appears, duplicate clicks blocked, completion message shown

Expected result

Each interaction shows a clear user-facing feedback message in the Interaction feedback panel. Disabled controls remain inactive. Loading controls enter a busy state, block duplicate clicks, and show completion after the delay.

Automation challenge

Assert `btn-feedback-message` after each interaction. Right-click `btn-variant-context` with `{ button: 'right' }`. Double-click `btn-variant-dblclick` and confirm only the double-click copy appears. Click `btn-variant-disabled-wrapper` and assert `btn-variant-disabled` stays disabled with `blocked=0` on `btn-count-disabled`. Start `btn-variant-loading`, spam-click while busy, and assert the flow completes once with "Loading completed successfully."

Stable selectors

  • Variant: Click[data-testid="btn-variant-click"]
  • Variant: Double click[data-testid="btn-variant-dblclick"]
  • Variant: Right click[data-testid="btn-variant-context"]
  • Variant: Hover[data-testid="btn-variant-hover"]
  • Hover confirm pill[data-testid="btn-hover-confirm"]
  • Variant: Disabled[data-testid="btn-variant-disabled"]
  • Disabled wrapper[data-testid="btn-variant-disabled-wrapper"]
  • Variant: Loading[data-testid="btn-variant-loading"]
  • Feedback panel[data-testid="btn-feedback-panel"]
  • Feedback message[data-testid="btn-feedback-message"]
  • Per-card status: click[data-testid="btn-status-click"]
  • Per-card status: dblclick[data-testid="btn-status-dblclick"]
  • Per-card status: context[data-testid="btn-status-context"]
  • Per-card status: hover[data-testid="btn-status-hover"]
  • Per-card status: disabled[data-testid="btn-status-disabled"]
  • Per-card status: loading[data-testid="btn-status-loading"]
  • Disabled helper line[data-testid="btn-count-disabled"]

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. Target interactive controls by stable id when each variant is unique.

1
2
3
4
await page.goto('https://lab.hakdogan.com/practice/buttons');

await page.locator('#btn-variant-click').click();
await page.locator('#btn-variant-context').click({ button: 'right' });

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

buttons.spec.ts
ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import { test, expect } from '@playwright/test';

test('button QA lab — message-based feedback', async ({ page }) => {
  await page.goto('https://lab.hakdogan.com/practice/buttons');

  // Initial state — feedback panel announces the prompt.
  await expect(page.getByTestId('btn-feedback-message'))
    .toHaveText('Interact with a button variant to see feedback.');

  // Click — readable feedback message.
  await page.getByTestId('btn-variant-click').click();
  await expect(page.getByTestId('btn-feedback-message'))
    .toHaveText('You clicked the button.');
  await expect(page.getByTestId('btn-status-click'))
    .toHaveText('You clicked the button.');

  // Double-click — only the dblclick message appears.
  await page.getByTestId('btn-variant-dblclick').dblclick();
  await expect(page.getByTestId('btn-feedback-message'))
    .toHaveText('You double-clicked the button.');

  // Right-click — context message.
  await page.getByTestId('btn-variant-context').click({ button: 'right' });
  await expect(page.getByTestId('btn-feedback-message'))
    .toHaveText('Right-click context action detected.');

  // Hover then confirm — two-step interaction.
  await page.getByTestId('btn-variant-hover').hover();
  await page.getByTestId('btn-hover-confirm').click();
  await expect(page.getByTestId('btn-feedback-message'))
    .toHaveText('Hover interaction confirmed.');

  // Disabled — never increments and stays inert.
  await expect(page.getByTestId('btn-variant-disabled')).toBeDisabled();
  await expect(page.getByTestId('btn-count-disabled')).toContainText('blocked=0');

  // Loading — second click during busy phase is ignored, completion message follows.
  const loading = page.getByTestId('btn-variant-loading');
  await loading.click();
  await expect(page.getByTestId('btn-feedback-message')).toHaveText('Loading…');
  await expect(loading).toBeDisabled();
  await expect(page.getByTestId('btn-feedback-message'))
    .toHaveText('Loading completed successfully.', { timeout: 5_000 });
});

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.

Mixed: fill() for stable fields, type() / press() where keyboard semantics matter.

Idlebuttons.spec.ts
Failure demos
https://lab.hakdogan.com/practice/buttons
playwright · headed · chromium0.00s
# awaiting Run Test · terminal scrolls automatically
Steps
  1. Click primary button
  2. Double-click isolation target
  3. Right-click context variant
  4. Hover-enter tracking
  5. Verify counters vs audit targets
  6. Pass
StatusIdle
BrowserChromium
FrameworkPlaywright + TypeScript
Elapsed (live)--
Specbuttons.spec.ts