1. Required fields
Beginner
Submit the form without filling anything in.
Expected: the status says Please fix the highlighted fields., each required field shows its own message (full name, email, password, country, experience, terms), invalid fields get aria-invalid="true", and the cursor jumps to Full name.
Show me how
import { test, expect } from '@playwright/test';
test('shows every required-field error', async ({ page }) => {
await page.goto('/forms.html');
await page.getByTestId('submit').click();
await expect(page.getByTestId('form-status')).toHaveText('Please fix the highlighted fields.');
// Map each error element to its message and check them all in one loop.
const errors = {
'full-name-error': 'Full name must be at least 2 characters.',
'email-error': 'Enter a valid email address.',
'password-error': 'Password needs 8+ characters and a number.',
'country-error': 'Please choose a country.',
'experience-error': 'Choose your experience level.',
'terms-error': 'You must accept the terms.',
};
for (const [testId, message] of Object.entries(errors)) {
await expect(page.getByTestId(testId)).toHaveText(message);
}
// Accessibility state is worth asserting too.
await expect(page.getByTestId('full-name')).toHaveAttribute('aria-invalid', 'true');
await expect(page.getByTestId('full-name')).toBeFocused();
});
2. Fill in every field type
Beginner
Fill every input (text, select, multi-select, radio, checkboxes, date, slider, colour, file and textarea) and submit.
Expected: the status thanks you by name, and the JSON under Result contains exactly the values you entered, e.g. "country": "PT", "languages": ["python", "javascript"] and "satisfaction": 8.
Show me how
import { test, expect } from '@playwright/test';
test('submits every field type', async ({ page }) => {
await page.goto('/forms.html');
await page.getByTestId('full-name').fill('Ana Silva');
await page.getByTestId('email').fill('[email protected]');
await page.getByTestId('phone').fill('+351 912 345 678');
await page.getByTestId('password').fill('Secret123');
await page.getByTestId('age').fill('30');
// Date inputs always take ISO format (yyyy-mm-dd), whatever the display locale.
await page.getByTestId('birth-date').fill('1995-05-17');
// selectOption() accepts the option value, or an array for multi-selects.
await page.getByTestId('country').selectOption('PT');
await page.getByTestId('languages').selectOption(['python', 'javascript']);
await page.getByTestId('exp-mid').check();
await page.getByTestId('tool-playwright').check();
await page.getByTestId('tool-cypress').check();
// fill() also works for range and colour inputs.
await page.getByTestId('satisfaction').fill('8');
await page.getByTestId('fav-color').fill('#ff6600');
// Upload a file from memory: no fixture file on disk needed.
await page.getByTestId('avatar').setInputFiles({
name: 'avatar.png',
mimeType: 'image/png',
buffer: Buffer.from('fake image bytes'),
});
await page.getByTestId('bio').fill('I automate all the things.');
await page.getByTestId('terms').check();
await page.getByTestId('submit').click();
await expect(page.getByTestId('form-status')).toContainText('Thanks Ana Silva');
// Parse the JSON result and compare it as an object, not as text.
const result = JSON.parse(await page.getByTestId('form-result').innerText());
expect(result).toMatchObject({
fullName: 'Ana Silva',
country: 'PT',
languages: ['python', 'javascript'],
experience: 'mid',
tools: ['playwright', 'cypress'],
satisfaction: 8,
favColor: '#ff6600',
avatar: 'avatar.png',
});
});
3. Validation boundaries
Intermediate
Find the exact edge of each rule: age must be 18–120, the phone 9–15 digits, the password 8+ characters with a number.
Expected: age 17 shows Age must be between 18 and 120. but 18 doesn't; 12345 is rejected as a phone; abcdefgh is rejected as a password but abcdefg1 isn't.
Show me how
import { test, expect } from '@playwright/test';
// Each row: field, value, the error element, and the message ('' = valid).
const cases = [
{ field: 'age', value: '17', error: 'age-error', message: 'Age must be between 18 and 120.' },
{ field: 'age', value: '18', error: 'age-error', message: '' },
{ field: 'age', value: '121', error: 'age-error', message: 'Age must be between 18 and 120.' },
{ field: 'phone', value: '12345', error: 'phone-error', message: 'Phone must be 9-15 digits.' },
{ field: 'phone', value: '912345678', error: 'phone-error', message: '' },
{ field: 'password', value: 'abcdefgh', error: 'password-error', message: 'Password needs 8+ characters and a number.' },
{ field: 'password', value: 'abcdefg1', error: 'password-error', message: '' },
];
for (const c of cases) {
test(`${c.field} = "${c.value}" is ${c.message ? 'rejected' : 'accepted'}`, async ({ page }) => {
await page.goto('/forms.html');
await page.getByTestId(c.field).fill(c.value);
// Validation runs on submit; other fields being empty doesn't matter here
// because we only look at this field's own error.
await page.getByTestId('submit').click();
await expect(page.getByTestId(c.error)).toHaveText(c.message);
});
}
4. File too large
Beginner
Upload an avatar just over 1 MB.
Expected: the file name and size appear under the input straight away, and submitting shows File is larger than 1 MB.
Show me how
import { test, expect } from '@playwright/test';
test('rejects an avatar over 1 MB', async ({ page }) => {
await page.goto('/forms.html');
// Buffer.alloc() creates a file of exactly the size we need.
await page.getByTestId('avatar').setInputFiles({
name: 'huge.png',
mimeType: 'image/png',
buffer: Buffer.alloc(1024 * 1024 + 1),
});
await expect(page.getByTestId('avatar-info')).toHaveText('huge.png (1024 KB)');
await page.getByTestId('submit').click();
await expect(page.getByTestId('avatar-error')).toHaveText('File is larger than 1 MB.');
});