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.
Scenario
OTP fields combine asynchronous feedback, segmented inputs, auto-advance, paste handling, backspace navigation, and validation in one realistic authentication flow.
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.
12345
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.
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
1234567891011121314151617
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.
otp-login.spec.ts- Fill username
- Request demo OTP
- Enter six-digit code
- Submit verification
- Assert verified state