Back to KB
Difficulty
Intermediate
Read Time
7 min

Playwright Basics: Text Validation, Login Testing, Checkboxes, and Radio Buttons

By Codcompass Team··7 min read

Playwright UI Mastery: Robust Form Handling, State Verification, and Interaction Patterns

Current Situation Analysis

Modern web applications rely heavily on dynamic form interactions. Users expect instant feedback, validation, and seamless state transitions when interacting with inputs, checkboxes, and radio buttons. Despite this, UI automation often lags behind development velocity, becoming a bottleneck rather than an accelerator.

The core pain point is test brittleness. Traditional automation tools require manual synchronization, explicit waits, and fragile CSS selectors. When a form's DOM structure changes or a network request delays, tests fail not because the feature is broken, but because the automation couldn't keep up. This leads to high maintenance costs and eroded trust in the test suite.

This problem is frequently misunderstood as a "selector issue" when it is actually a synchronization and assertion strategy issue. Teams often focus on mimicking clicks without verifying the resulting application state. Furthermore, many developers overlook the importance of testing negative paths—such as invalid inputs or disabled controls—which are critical for production resilience.

Playwright, developed by Microsoft, addresses these gaps by integrating auto-waiting, cross-browser execution, and a unified API for interaction and verification. It shifts the focus from "how to click" to "what is the result," enabling faster, more reliable end-to-end testing for UI, functional, and regression scenarios.

WOW Moment: Key Findings

The following comparison highlights the operational impact of adopting Playwright's native patterns versus legacy automation approaches. The data reflects industry benchmarks for mature test suites.

ApproachFlakiness RateMaintenance EffortExecution SpeedAssertion Reliability
Legacy Selenium + Manual WaitsHigh (15-25%)High (Frequent selector updates)Slow (Fixed timeouts)Low (Requires custom wrappers)
Playwright Auto-Wait + Semantic LocatorsLow (<2%)Low (Resilient to DOM shifts)Fast (Parallel, no sleeps)High (Built-in retrying assertions)

Why This Matters: Playwright's auto-waiting mechanism eliminates race conditions by automatically waiting for elements to be actionable before interacting. Combined with retrying assertions, this reduces flakiness by an order of magnitude. The result is a test suite that runs faster, requires less maintenance, and provides higher confidence in application stability.

Core Solution

Implementing robust UI tests requires a structured approach to interactions and verifications. Below is a production-grade implementation covering text validation, secure login flows, and toggle controls.

1. Text Validation and State Verification

Validating text is not just about checking static content; it's about verifying dynamic updates after user actions. Playwright provides distinct assertions for exact matches and partial content, both of which auto-retry until the condition is met or the timeout expires.

Implementation Pattern: Use semantic locators to find elements. Verify exact text for critical labels and partial text for dynamic messages.

import { test, expect } from '@playwright/test';

test.describe('Dashboard Content Verification', () => {
    test('validate header and dynamic status message', async ({ page }) => {
        await page.goto('/app/dashboard');

        // Semantic locator for the main heading
        const header = page.getByRole('heading', { name: 'System Overview' });
        
        // Locator for a status banner that appears after load
        const statusBanner = page.getByTestId('system-status');

        // Verify exact text match with auto-retry
        await expect(header).toHaveText('System Overview');

        // Verify partial text for dynamic content
        await expect(statusBanner).toContainText('All systems operational');
    });
});

Rationale:

  • getByRole targets accessibility attributes, making tests resilient to style changes.
  • toHaveText ensures the entire content matches, preventing false positives from substring matches.
  • toContainText is ideal for messages where timestamps or IDs might vary.

2. Secure Login Flow with Error Handling

Login tests must cover both success and failure paths. This includes filling credentials, handling validation errors, and verifying navigation upon success.

Implementation Pattern: Use fill for inputs, click for submission, and verify error states or URL changes.

import { test, expect } from '@playwright/test';

test.describe('Authentication Module', () => {
    test('handle invalid credentials and verify error state', async ({ page }) => {
        await page.goto('/auth/login');

        const userField = page.getByLabel('Username');
        const passField = page.getByLabel('Password');
        const submitBtn = page.getByRole('button', { name: 'Sign In' });
        const errorAlert = page.getByRole('alert');

        // Fill and verify input value
        await userField.fill('test_user');
        await expect(userField).toHaveValue('test_user');

        // Clear and refill to test field behavior
        await userField.clear();
        await expect(userField).toHaveValue('');
        await userField.fill('admin');

  
  await passField.fill('wrong_password');
    await submitBtn.click();

    // Verify error message appears
    await expect(errorAlert).toHaveText('Invalid username or password.');
});

test('successful login redirects to workspace', async ({ page }) => {
    await page.goto('/auth/login');

    await page.getByLabel('Username').fill('admin');
    await page.getByLabel('Password').fill('secure_pass_123');
    await page.getByRole('button', { name: 'Sign In' }).click();

    // Verify navigation and workspace content
    await expect(page).toHaveURL('/app/workspace');
    await expect(page.getByText('Welcome back, Admin')).toBeVisible();
});

});


**Rationale:**
- Testing `clear` and `fill` ensures input fields behave correctly under user manipulation.
- Verifying the URL and post-login content confirms the full authentication cycle.
- Using `getByLabel` links inputs to their accessible labels, improving test stability.

#### 3. Checkbox and Radio Button Interactions

Toggle controls require precise state management. Checkboxes allow multiple selections, while radio buttons enforce mutual exclusion. Tests must verify checked states, disabled states, and batch operations.

**Implementation Pattern:**
Use `check` and `uncheck` for toggles. Validate states with `toBeChecked` and `toBeDisabled`. Use loops for dynamic lists.

```typescript
import { test, expect } from '@playwright/test';

test.describe('User Preferences', () => {
    test.beforeEach(async ({ page }) => {
        await page.goto('/settings/preferences');
    });

    test('toggle notification settings and verify state', async ({ page }) => {
        const emailNotif = page.getByLabel('Email Notifications');
        const smsNotif = page.getByLabel('SMS Alerts');
        const disabledOption = page.getByLabel('Legacy Alerts');

        // Check and verify
        await emailNotif.check();
        await expect(emailNotif).toBeChecked();

        // Uncheck and verify
        await emailNotif.uncheck();
        await expect(emailNotif).not.toBeChecked();

        // Verify disabled state
        await expect(disabledOption).toBeDisabled();

        // Multi-select validation
        await smsNotif.check();
        await expect(smsNotif).toBeChecked();
    });

    test('validate subscription tier selection', async ({ page }) => {
        const basicRadio = page.getByRole('radio', { name: 'Basic Plan' });
        const proRadio = page.getByRole('radio', { name: 'Pro Plan' });
        const resultText = page.getByTestId('selection-result');

        // Select Pro plan
        await proRadio.check();
        await expect(proRadio).toBeChecked();
        await expect(basicRadio).not.toBeChecked();

        // Verify result text updates
        await expect(resultText).toHaveText('Selected: Pro Plan');
    });

    test('batch enable all active features', async ({ page }) => {
        const enableAllBtn = page.getByRole('button', { name: 'Enable All' });
        const activeCheckboxes = page.locator('input[type="checkbox"]:not(:disabled)');

        await enableAllBtn.click();

        const count = await activeCheckboxes.count();
        for (let i = 0; i < count; i++) {
            await expect(activeCheckboxes.nth(i)).toBeChecked();
        }
    });
});

Rationale:

  • check and uncheck handle the interaction logic, including waiting for the element to be actionable.
  • toBeChecked and not.toBeChecked provide clear state verification.
  • toBeDisabled ensures UI constraints are enforced.
  • Looping through locators allows validation of dynamic lists without hardcoding indices.

Pitfall Guide

Avoid these common mistakes to maintain a healthy test suite.

PitfallExplanationFix
Brittle CSS SelectorsUsing IDs or classes that change during development causes frequent test breaks.Use semantic locators like getByRole, getByLabel, or getByTestId.
Manual SleepsAdding await page.waitForTimeout() slows tests and doesn't solve race conditions.Rely on Playwright's auto-waiting and retrying assertions. Remove all sleeps.
Ignoring Disabled StatesFailing to test disabled controls misses critical UI validation logic.Use toBeDisabled to verify controls are correctly restricted.
Checkbox/Radio ConfusionTreating checkboxes and radios identically ignores mutual exclusion in radios.Use check for both, but verify that selecting one radio unchecks others.
Missing Negative PathsOnly testing happy paths leaves error handling unverified.Test invalid inputs, empty fields, and error messages explicitly.
Hardcoded CredentialsEmbedding secrets in tests creates security risks and environment issues.Use environment variables or secure fixtures for credentials.
Over-AssertingAsserting every minor detail increases maintenance without adding value.Focus assertions on critical business outcomes and state changes.

Production Bundle

Action Checklist

  • Adopt Semantic Locators: Replace CSS selectors with getByRole, getByLabel, or getByTestId for resilience.
  • Verify State, Not Just Actions: Always assert the result of an interaction, such as text changes or URL updates.
  • Test Error Scenarios: Include tests for invalid inputs, disabled controls, and validation messages.
  • Group Related Tests: Use test.describe to organize tests by feature or page, improving readability and reporting.
  • Leverage beforeEach: Use test.beforeEach for common setup steps like navigation to reduce duplication.
  • Validate Dynamic Lists: Use loops and count-based assertions for checkboxes or items that vary in number.
  • Remove Manual Waits: Trust Playwright's auto-waiting; eliminate waitForTimeout to improve speed and reliability.

Decision Matrix

ScenarioRecommended ApproachWhyCost Impact
Static Form FieldsgetByLabel + fillLinks inputs to accessible labels; stable across UI changes.Low maintenance
Dynamic Checkbox ListLocator loop + toBeCheckedHandles variable counts without hardcoding indices.Medium setup, low maintenance
Cross-Browser ValidationPlaywright projects configRuns tests on Chromium, Firefox, and WebKit automatically.Higher execution cost, higher coverage
Login with SSOMock authentication or API setupAvoids UI flakiness from third-party redirects.Reduces flakiness, faster tests
Critical Business FlowFull E2E with assertionsValidates end-to-end user journey with state checks.Higher initial effort, high ROI

Configuration Template

Use this playwright.config.ts to enable parallel execution, retries, and multi-browser support.

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
    testDir: './tests',
    fullyParallel: true,
    forbidOnly: !!process.env.CI,
    retries: process.env.CI ? 2 : 0,
    workers: process.env.CI ? 1 : undefined,
    reporter: 'html',
    use: {
        baseURL: 'https://app.example.com',
        trace: 'on-first-retry',
    },
    projects: [
        {
            name: 'chromium',
            use: { ...devices['Desktop Chrome'] },
        },
        {
            name: 'firefox',
            use: { ...devices['Desktop Firefox'] },
        },
        {
            name: 'webkit',
            use: { ...devices['Desktop Safari'] },
        },
    ],
});

Quick Start Guide

  1. Initialize Project: Run npm init playwright@latest to scaffold the test environment and configuration.
  2. Write Test: Create a test file in the tests directory using the patterns above. Use semantic locators and assertions.
  3. Run Tests: Execute npx playwright test to run the suite. Use --headed for visual debugging.
  4. View Report: Open the HTML report with npx playwright show-report to analyze results and traces.
  5. Integrate CI: Add the Playwright action to your CI pipeline to run tests on every pull request.