← All practice topics
Automation Practice · Playwright

Inputs & Forms

The elements almost every test touches. Write a test for each block, then open the solution to compare.

Every solution on this page starts with this hook. With baseURL set to https://hiraanachwani.com in your playwright.config.ts, it opens this page before each test.

test.beforeEach(async ({ page }) => {
  await page.goto('/practice/forms');
});

Text inputs

Typing, clearing and reading values, plus fields you can't edit.

Type your name above

Try this

  1. Type a name and check the greeting below the fields updates.
  2. Clear the name and check the field is empty and its placeholder is e.g. Ada Lovelace.
  3. Fill the password and age, and write two lines in the bio.
  4. Check Employee ID is read-only and Country code is disabled.

Locator tip: every field has a visible label, so page.getByLabel('Full name') finds it the way a user would. Prefer it over CSS like #full-name, which breaks when developers rename IDs. Labels match on part of the text, so getByLabel('Age') also finds Languages further down; add { exact: true } when that happens.

Show Playwright solution
test('text inputs', async ({ page }) => {
  const name = page.getByLabel('Full name');

  await name.fill('Ada Lovelace');
  await expect(name).toHaveValue('Ada Lovelace');
  await expect(page.getByTestId('name-preview')).toHaveText('Hello, Ada Lovelace!');

  await name.clear();
  await expect(name).toHaveValue('');
  await expect(name).toHaveAttribute('placeholder', 'e.g. Ada Lovelace');

  await page.getByLabel('Password').fill('s3cret!');
  await page.getByLabel('Age', { exact: true }).fill('30');
  await page.getByLabel('Bio').fill('Line one\nLine two');
  await expect(page.getByLabel('Bio')).toHaveValue('Line one\nLine two');

  await expect(page.getByLabel('Employee ID')).toHaveValue('EMP-1024');
  await expect(page.getByLabel('Employee ID')).not.toBeEditable();
  await expect(page.getByLabel('Country code')).toBeDisabled();
});

Checkboxes

Checking, unchecking and reading state, including a box that starts checked and one you can't touch.

Tools you use
Selected: Playwright

Try this

  1. Check Selenium and uncheck Playwright, then check the result says Selected: Selenium.
  2. Check that Appium is disabled.
  3. Count how many checkboxes are in the group.

Locator tip: use check() and uncheck() rather than click(). They only act if the box isn't already in the state you want, so the test can't flip it the wrong way.

Show Playwright solution
test('checkboxes', async ({ page }) => {
  const playwright = page.getByRole('checkbox', { name: 'Playwright' });
  await expect(playwright).toBeChecked();

  await page.getByRole('checkbox', { name: 'Selenium' }).check();
  await playwright.uncheck();
  await expect(playwright).not.toBeChecked();
  await expect(page.getByTestId('skills-result')).toHaveText('Selected: Selenium');

  await expect(page.getByRole('checkbox', { name: 'Appium' })).toBeDisabled();
  await expect(page.getByRole('group', { name: 'Tools you use' }).getByRole('checkbox')).toHaveCount(4);
});

Radio buttons

Only one option in a group can be selected at a time.

Experience level
Level: none

Try this

  1. Check no option is selected when the page loads.
  2. Select Intermediate, then Expert, and check Intermediate is no longer selected.
  3. Check the result says Level: Expert.

Locator tip: page.getByRole('radio', { name: 'Expert' }) finds a radio button by its label text. All three share name="level", which is what makes them one group.

Show Playwright solution
test('radio buttons', async ({ page }) => {
  const group = page.getByRole('group', { name: 'Experience level' });
  for (const radio of await group.getByRole('radio').all()) {
    await expect(radio).not.toBeChecked();
  }

  await page.getByRole('radio', { name: 'Intermediate' }).check();
  await page.getByRole('radio', { name: 'Expert' }).check();

  await expect(page.getByRole('radio', { name: 'Intermediate' })).not.toBeChecked();
  await expect(page.getByRole('radio', { name: 'Expert' })).toBeChecked();
  await expect(page.getByTestId('level-result')).toHaveText('Level: Expert');
});

Custom dropdown

Most modern apps build dropdowns from buttons and lists instead of <select>. You open it, then click an option.

Browser
Browser: none

Try this

  1. Check the option list is hidden until you open the dropdown.
  2. Open it and pick Firefox. Check the button now shows Firefox and the list has closed.
  3. Open it again and press Escape. Check the list closes without changing the choice.

Locator tip: selectOption() fails here because there is no <select>. Click the combobox, then the option by role and name, the same two steps a user takes.

Show Playwright solution
test('custom dropdown', async ({ page }) => {
  const browser = page.getByRole('combobox', { name: 'Browser' });
  await expect(page.getByRole('listbox', { name: 'Browser' })).toBeHidden();

  await browser.click();
  await page.getByRole('option', { name: 'Firefox' }).click();

  await expect(browser).toHaveText('Firefox');
  await expect(page.getByRole('listbox', { name: 'Browser' })).toBeHidden();
  await expect(page.getByTestId('browser-result')).toHaveText('Browser: Firefox');

  await browser.click();
  await expect(page.getByRole('listbox', { name: 'Browser' })).toBeVisible();
  await page.keyboard.press('Escape');
  await expect(page.getByRole('listbox', { name: 'Browser' })).toBeHidden();
  await expect(browser).toHaveText('Firefox');
});

Form validation

A sign-up form with required fields. Nothing is sent anywhere; it only shows a message when it's valid.

    Try this

    1. Submit the empty form and check three errors appear.
    2. Enter a two-letter username and a bad email, submit, and check the error messages.
    3. Fill everything in correctly, submit, and check the result says Welcome, <username>!

    Locator tip: the errors are a list, so getByTestId('signup-errors').getByRole('listitem') gives you every message. toHaveText([...]) then checks them all, in order, in one line.

    Show Playwright solution
    test('form validation', async ({ page }) => {
      const signUp = page.getByRole('button', { name: 'Sign up' });
      const errors = page.getByTestId('signup-errors').getByRole('listitem');
    
      await signUp.click();
      await expect(errors).toHaveText([
        'Username is required.',
        'Email is required.',
        'You must accept the terms.',
      ]);
    
      await page.getByLabel('Username').fill('ad');
      await page.getByLabel('Work email').fill('not-an-email');
      await signUp.click();
      await expect(errors).toHaveText([
        'Username must be at least 3 characters.',
        'Enter a valid email address.',
        'You must accept the terms.',
      ]);
    
      await page.getByLabel('Username').fill('ada');
      await page.getByLabel('Work email').fill('ada@example.com');
      await page.getByLabel('I accept the terms').check();
      await signUp.click();
      await expect(errors).toHaveCount(0);
      await expect(page.getByTestId('signup-result')).toHaveText('Welcome, ada!');
    });