VALIDATION LAB•OTP LOGIN

Forms

OTP Login

Practice a one-time-password login flow: request a code, enter six digits, verify the result, and cover resend and invalid-code behavior.

Intermediate

Scenario

OTP fields combine asynchronous feedback, segmented inputs, auto-advance, paste handling, backspace navigation, and validation in one realistic authentication flow.

One-time password

Request a code, then enter six digits.

Demo code: 123456

Waiting for an OTP request…

Live widget · interact freely

Manual test checklist

  • 1Enter a username and request the demo OTP
  • 2Paste 123456 into the first OTP field and confirm the digits distribute across the fields
  • 3Use Backspace to move to the previous OTP field
  • 4Submit an incomplete or invalid code and confirm the error is visible
  • 5Submit 123456 and confirm the verified state appears

Expected result

A complete six-digit code of 123456 shows a successful verification state; incomplete or invalid codes remain visible as clear validation errors.

Automation challenge

Request the OTP, fill the six fields as a group, then assert the success state. Add a negative test for an incomplete code and verify the error before retrying.

Stable selectors

  • OTP login form[data-testid="otp-login-form"]
  • Username[data-testid="otp-login-username"]
  • Fetch OTP[data-testid="otp-login-fetch"]
  • OTP digit 1[data-testid="otp-login-digit-0"]
  • OTP digit 6[data-testid="otp-login-digit-5"]
  • Submit[data-testid="otp-login-submit"]
  • Validation result[data-testid="otp-login-result"]

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.

Start with the visible labels and the stable test ids on the six repeated OTP fields.

1
2
3
4
5
await page.goto('https://qapractice.hakdogan.com/forms/otp-login');

await page.getByLabel('Username').fill('demo-user');
await page.getByRole('button', { name: 'Fetch OTP' }).click();
await page.getByTestId('otp-login-digit-0').fill('123456');

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

otp-login.spec.ts
ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { test, expect } from '@playwright/test';

test.describe('OTP login', () => {
  test('verifies the demo code', async ({ page }) => {
    await page.goto('https://qapractice.hakdogan.com/forms/otp-login');

    await page.getByLabel('Username').fill('demo-user');
    await page.getByRole('button', { name: 'Fetch OTP' }).click();
    await page.getByTestId('otp-login-digit-0').fill('123456');
    await page.getByTestId('otp-login-submit').click();

    await expect(page.getByTestId('otp-login-result')).toHaveAttribute(
      'data-state',
      'success',
    );
  });
});

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.

Idleotp-login.spec.ts
Failure demos
https://qapractice.hakdogan.com/forms/otp-login
playwright · headed · chromium0.00s
# awaiting Run Test · terminal scrolls automatically
Steps
  1. Fill username
  2. Request demo OTP
  3. Enter six-digit code
  4. Submit verification
  5. Assert verified state
StatusIdle
BrowserChromium
FrameworkPlaywright + TypeScript
Elapsed (live)--
Specotp-login.spec.ts