VALIDATION LABINPUTS

Forms

Inputs

Validate an email + password login form, surface a red error on invalid email, and assert the success state once both fields satisfy their rules.

Beginner

Scenario

Text inputs are the #1 source of production bugs: trim, max-length, special chars, autofill, paste, IME composition. A QA engineer who covers them well prevents the silent data-quality issues that pile up in any backend.

Your password must include:

  • At least 8 characters
  • One number
  • One uppercase letter
  • One lowercase letter
  • One special character(!"#$%&'()*+,-./:;<=>?@[]^_`{|}~)
Awaiting submission…

Live widget · interact freely

Manual test checklist

  • 1Type a malformed email and confirm the inline error fires before submit
  • 2Paste 'hasan.qa@hakdogan.com ' with trailing spaces — does the form trim?
  • 3Try a password under 8 chars and confirm the rule list highlights what's missing
  • 4Use the keyboard only (Tab / Shift-Tab) and confirm focus order is logical
  • 5Type emoji and unicode (héllo, 🚀) — does the field accept and submit cleanly?
  • 6Disable JavaScript and confirm the form still surfaces native validation

Expected result

Submitting an invalid email reveals 'Please enter a valid email address.'; submitting a valid email and a strong password reveals the green 'Login successful' state.

Automation challenge

Assert the inline error is present BEFORE submit on blur, then write a positive path that drives every password rule from red → green and lands on 'Login successful'. Avoid `waitForTimeout` — use role + state-based locators only.

Stable selectors

  • Login form[data-testid="login-form"]
  • Work email[data-testid="work-email"]
  • Password[data-testid="login-password"]
  • Email error[data-testid="email-error"]
  • Password rules[data-testid="input-password-rules"]
  • Sign in[data-testid="sign-in-button"]
  • Result row[data-testid="input-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.

Use simple IDs and names to understand how locating elements works. IDs and name attributes are readable on simple forms; pair them with labels in real apps.

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

await page.locator('#email').fill('demo@hakdogan.com');
await page.locator('#password').fill('Password123!');
await page.locator('#sign-in').click();

Also valid

1
2
await page.locator('[name="email"]').fill('demo@hakdogan.com');
await page.getByPlaceholder('abc@def.com').fill('demo@hakdogan.com');

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

inputs.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
import { test, expect } from '@playwright/test';

test.describe('inputs — login validation', () => {
  test('invalid email surfaces the inline error', async ({ page }) => {
    await page.goto('https://lab.hakdogan.com/practice/inputs');

    await page.getByLabel(/work email/i).fill('not-an-email');
    await page.getByLabel(/^password$/i).fill('Pass-w0rd!');
    await page.getByRole('button', { name: /sign in/i }).click();

    await expect(page.getByTestId('email-error')).toHaveText(
      'Please enter a valid email address.',
    );
    await expect(page.getByTestId('input-result')).toHaveAttribute(
      'data-state',
      'error',
    );
  });

  test('all password rules turn green and login succeeds', async ({ page }) => {
    await page.goto('https://lab.hakdogan.com/practice/inputs');

    await page.getByLabel(/work email/i).fill('hasan.qa@hakdogan.com');
    await page.getByLabel(/^password$/i).fill('Pass-w0rd!');

    for (const rule of ['length', 'number', 'upper', 'lower', 'special']) {
      await expect(
        page.getByTestId(`input-password-req-${rule}`),
      ).toHaveAttribute('data-state', 'ok');
    }

    await page.getByRole('button', { name: /sign in/i }).click();
    await expect(page.getByTestId('input-result')).toContainText(
      'Login successful',
    );
  });
});

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.

This flow uses fill() — fast and stable for standard forms (no per-key events).

Idleinputs.spec.ts
Failure demos
https://lab.hakdogan.com/login
playwright · headed · chromium0.00s
# awaiting Run Test · terminal scrolls automatically
Steps
  1. Show login page empty
  2. Fill Work Email
  3. Fill Password
  4. Click Sign in
  5. Navigate to Bug Playground
  6. Assert Bug Playground heading
  7. Pass
StatusIdle
BrowserChromium
FrameworkPlaywright + TypeScript
Elapsed (live)--
Specinputs.spec.ts