QA How-To
Playwright getByLabel: Examples and Best Practices
Explore playwright getByLabel examples for login, checkout, preferences, uploads, repeated forms, and validation, with robust TypeScript best practices.
21 min read | 3,147 words
TL;DR
Strong Playwright getByLabel examples locate inputs through the names users understand, apply the action suited to each control, and assert a business result. Use semantic container scope for duplicate labels and preserve strictness so ambiguous forms fail clearly.
Key Takeaways
- Treat each label as part of a user-facing form contract and assert the outcome after entering data.
- Choose actions by control type: fill, check, selectOption, and setInputFiles express different intentions.
- Scope repeated address and editor fields through named groups, dialogs, or rows.
- Assert validation semantics such as aria-invalid, focused fields, and linked error messages.
- Use data-driven cases for input boundaries while keeping locators readable inside each test.
- Keep page objects centered on business workflows instead of generic label-to-value maps.
Playwright getByLabel examples become valuable when they go beyond filling a single email field. Real test suites must cover registration, preferences, addresses, uploads, validation, repeated editors, and data boundaries without turning form automation into a collection of fragile CSS selectors.
The examples in this guide are runnable with Playwright Test and focus on observable behavior. They use labels for fields, roles for controls and status messages, and semantic containers for repeated forms. Each pattern can be lifted into an application test while preserving the reason the test exists.
TL;DR
const form = page.getByRole('form', { name: 'Create account' });
await form.getByLabel('Work email').fill('engineer@example.com');
await form.getByLabel('Accept terms').check();
await form.getByRole('button', { name: 'Create account' }).click();
await expect(page.getByRole('status')).toHaveText('Account created');
| Control | Preferred action | Useful assertion |
|---|---|---|
| Text, email, password | fill | toHaveValue |
| Checkbox or radio | check or uncheck | toBeChecked |
| Native select | selectOption | toHaveValue |
| File input | setInputFiles | input value or submitted result |
| Invalid input | focus through normal submission | toHaveAttribute and toBeFocused |
The locator identifies the field. The action supplies user data. The assertion proves the application responded correctly.
1. Playwright getByLabel Examples with a Self-Contained Fixture
Use page.setContent for a compact locator exercise or component-level browser test. Production end-to-end tests should navigate through the real application, but the locator behavior remains the same.
import { test, expect } from '@playwright/test';
test('creates an account through labeled fields', async ({ page }) => {
await page.setContent(`
<form aria-label="Create account">
<label>Full name <input name="name"></label>
<label>Work email <input name="email" type="email"></label>
<label>Password <input name="password" type="password"></label>
<label><input name="terms" type="checkbox"> Accept terms</label>
<button>Create account</button>
<p role="status"></p>
</form>
<script>
document.querySelector('form').addEventListener('submit', event => {
event.preventDefault();
document.querySelector('[role=status]').textContent = 'Account created';
});
</script>
`);
const form = page.getByRole('form', { name: 'Create account' });
await form.getByLabel('Full name').fill('Morgan Lee');
await form.getByLabel('Work email').fill('morgan@example.com');
await form.getByLabel('Password').fill('local-test-value');
await form.getByLabel('Accept terms').check();
await form.getByRole('button', { name: 'Create account' }).click();
await expect(page.getByRole('status')).toHaveText('Account created');
});
Scoping to the named form documents where the controls belong and prevents an unrelated email field elsewhere on the page from creating ambiguity. Password values should come from safe test fixtures or environment configuration in a real suite, never from committed production credentials.
The final assertion is intentionally outside the input. A successful fill proves browser interaction, while Account created proves that the workflow reached its expected result. Add backend or persistence checks only when the test's risk requires them.
2. Test Sign-In, Clear, and Keyboard Submission
A sign-in example should verify more than two values. It can cover replacement of prefilled text, password confidentiality in logs, keyboard submission, and the authenticated state. Playwright's fill method replaces the current value, while press can exercise the form's keyboard behavior.
import { test, expect } from '@playwright/test';
test('signs in by pressing Enter from the password field', async ({ page }) => {
await page.setContent(`
<form aria-label="Sign in">
<label>Email <input type="email" value="old@example.com"></label>
<label>Password <input type="password"></label>
<p role="status"></p>
</form>
<script>
document.querySelector('form').addEventListener('submit', event => {
event.preventDefault();
document.querySelector('[role=status]').textContent = 'Signed in';
});
</script>
`);
const email = page.getByLabel('Email', { exact: true });
const password = page.getByLabel('Password');
await email.fill('qa@example.com');
await password.fill('safe-fixture-password');
await password.press('Enter');
await expect(email).toHaveValue('qa@example.com');
await expect(page.getByRole('status')).toHaveText('Signed in');
});
Use exact matching when a screen also contains Recovery email or Email preferences. Prefer press('Enter') only when keyboard submission is a requirement. Otherwise, clicking the named submit button describes the primary user path more clearly.
Never print passwords to test output or attach raw form state to reports. Redact request bodies in custom diagnostics, and use accounts created specifically for automation. Locator readability should not come at the cost of secret hygiene.
3. Cover Checkboxes, Radios, and Toggle Outcomes
Checkbox and radio tests are most stable when they express the desired state. check() leaves a checked control checked, unlike click(), which toggles based on the initial state. uncheck() provides the corresponding explicit operation for checkboxes.
import { test, expect } from '@playwright/test';
test('updates notification and billing preferences', async ({ page }) => {
await page.setContent(`
<fieldset>
<legend>Notifications</legend>
<label><input type="checkbox" checked> Product news</label>
<label><input type="checkbox"> Security alerts by SMS</label>
</fieldset>
<fieldset>
<legend>Billing cycle</legend>
<label><input type="radio" name="cycle" value="monthly" checked> Monthly</label>
<label><input type="radio" name="cycle" value="annual"> Annual</label>
</fieldset>
`);
await page.getByLabel('Product news').uncheck();
await page.getByLabel('Security alerts by SMS').check();
await page.getByLabel('Annual').check();
await expect(page.getByLabel('Product news')).not.toBeChecked();
await expect(page.getByLabel('Security alerts by SMS')).toBeChecked();
await expect(page.getByLabel('Annual')).toBeChecked();
await expect(page.getByLabel('Monthly')).not.toBeChecked();
});
The visible label can sit after the input because nesting creates the association. In an application, also verify the saved status or reload and confirm persistence. A state assertion immediately after the action is useful, but it may only prove client-side state.
If a visual switch uses a checkbox underneath, getByLabel and check may still be correct when the accessibility semantics remain checkbox. If it exposes role switch, getByRole('switch', { name: ... }) can better describe the component. Inspect what the browser exposes rather than assuming from its appearance.
4. Select Native Options, Dates, and Numeric Values
Native form controls have specialized value formats. selectOption works on select, date inputs accept the standardized YYYY-MM-DD form through fill, and number inputs still receive text through fill. Assertions can verify the normalized DOM value.
import { test, expect } from '@playwright/test';
test('completes scheduling fields', async ({ page }) => {
await page.setContent(`
<label for="team">Team</label>
<select id="team">
<option value="quality">Quality Engineering</option>
<option value="platform">Platform</option>
</select>
<label>Start date <input type="date"></label>
<label>Seats <input type="number" min="1" max="50"></label>
`);
await page.getByLabel('Team').selectOption('platform');
await page.getByLabel('Start date').fill('2026-07-13');
await page.getByLabel('Seats').fill('12');
await expect(page.getByLabel('Team')).toHaveValue('platform');
await expect(page.getByLabel('Start date')).toHaveValue('2026-07-13');
await expect(page.getByLabel('Seats')).toHaveValue('12');
});
selectOption can select by value, label, or other supported option descriptors. Prefer the stable business value when it is an intentional contract, and the visible option label when the displayed choice is what matters. Do not use it on a div-based custom listbox.
Date controls can render differently by browser and operating system, but the input value remains standardized. If the application wraps a custom date picker around an input, test both the intended picker workflow and the resulting labeled field value at the right layer.
5. Upload Files Through a Labeled Input
File inputs are often visually hidden behind a styled button, but they still need an accessible label or another clear interaction contract. setInputFiles can use a real fixture path or an in-memory payload. An in-memory file keeps this example portable.
import { test, expect } from '@playwright/test';
test('uploads a resume from a labeled file input', async ({ page }) => {
await page.setContent(`
<label for="resume">Upload resume</label>
<input id="resume" type="file" accept=".txt">
<p role="status"></p>
<script>
document.querySelector('input').addEventListener('change', event => {
const file = event.target.files[0];
document.querySelector('[role=status]').textContent = file.name + ' selected';
});
</script>
`);
await page.getByLabel('Upload resume').setInputFiles({
name: 'morgan-resume.txt',
mimeType: 'text/plain',
buffer: Buffer.from('SDET experience and skills')
});
await expect(page.getByRole('status')).toHaveText('morgan-resume.txt selected');
await expect(page.getByLabel('Upload resume')).toHaveValue(/morgan-resume\.txt$/);
});
In an end-to-end upload test, add a server-confirmed outcome such as a file row, parsing status, or persisted document after reload. The input value alone does not prove that the backend accepted the bytes.
Test file type and size boundaries with small purpose-built fixtures. Do not commit personal documents or real candidate data. Generate synthetic content, keep fixtures minimal, and make cleanup part of the environment lifecycle.
6. Scope Shipping and Billing Address Labels
Shipping and billing forms commonly repeat Street address, City, and Postal code. A fieldset with a legend gives each set a user-facing group name. Playwright can locate that group by role, then find its labeled fields.
import { test, expect } from '@playwright/test';
test('keeps billing and shipping values separate', async ({ page }) => {
await page.setContent(`
<fieldset>
<legend>Shipping address</legend>
<label>City <input></label>
<label>Postal code <input></label>
</fieldset>
<fieldset>
<legend>Billing address</legend>
<label>City <input></label>
<label>Postal code <input></label>
</fieldset>
`);
const shipping = page.getByRole('group', { name: 'Shipping address' });
const billing = page.getByRole('group', { name: 'Billing address' });
await shipping.getByLabel('City').fill('Seattle');
await shipping.getByLabel('Postal code').fill('98101');
await billing.getByLabel('City').fill('Portland');
await billing.getByLabel('Postal code').fill('97205');
await expect(shipping.getByLabel('City')).toHaveValue('Seattle');
await expect(billing.getByLabel('City')).toHaveValue('Portland');
});
This is more robust than page.getByLabel('City').nth(0) because it survives section reordering and unrelated fields. It also improves the page for keyboard and assistive technology users by making the address grouping explicit.
Named dialogs, table rows, and articles provide similar scope. For a repeated editor, identify the row by visible business data, then locate Edit or the labeled input within it. The Playwright getByRole guide covers container and control roles in more depth.
7. Verify Inline Validation and Accessible Error State
Validation has at least three layers: the rule, the message, and the connection between the message and the field. A strong browser test covers the user-visible text plus important semantics such as aria-invalid and focus.
import { test, expect } from '@playwright/test';
test('reports an invalid employee ID accessibly', async ({ page }) => {
await page.setContent(`
<form>
<label for="employee-id">Employee ID</label>
<input id="employee-id" aria-describedby="employee-error">
<p id="employee-error"></p>
<button>Continue</button>
</form>
<script>
document.querySelector('form').addEventListener('submit', event => {
event.preventDefault();
const input = document.querySelector('#employee-id');
if (!/^EMP-[0-9]{4}$/.test(input.value)) {
input.setAttribute('aria-invalid', 'true');
document.querySelector('#employee-error').textContent = 'Use the format EMP-1234';
input.focus();
}
});
</script>
`);
const employeeId = page.getByLabel('Employee ID');
await employeeId.fill('1234');
await page.getByRole('button', { name: 'Continue' }).click();
await expect(employeeId).toHaveAttribute('aria-invalid', 'true');
await expect(employeeId).toBeFocused();
await expect(page.getByText('Use the format EMP-1234')).toBeVisible();
});
Avoid selecting the error only through a styling class. Error colors and class names can change without altering behavior. Visible text and semantic state describe what the user receives. An accessibility audit can supplement this test, but the example keeps the business rule readable.
Also test valid boundaries. A suite that covers only malformed input can miss a rule that rejects correct values. Keep rule matrices in unit or service tests when there are many combinations, then select representative browser cases for integration confidence.
8. Handle Localization and Label Copy Changes Deliberately
User-facing locators naturally respond to copy changes. That is useful when wording is part of the experience, but it can create wide maintenance work in multilingual products. Decide which layer owns translation correctness.
A practical strategy is to run core journeys in the default locale with exact user-facing strings, then run targeted locale tests using locale-specific expected labels. Do not use a single regular expression that accepts words from every language. That can allow a page rendered in the wrong locale to pass.
import { test, expect } from '@playwright/test';
const cases = [
{ locale: 'en', label: 'Email address', value: 'reader@example.com' },
{ locale: 'es', label: 'Correo electrónico', value: 'reader@example.com' }
];
for (const item of cases) {
test('fills email in ' + item.locale, async ({ page }) => {
await page.setContent(
'<label>' + item.label + '<input type="email"></label>'
);
await page.getByLabel(item.label, { exact: true }).fill(item.value);
await expect(page.getByLabel(item.label, { exact: true })).toHaveValue(item.value);
});
}
The test data names the intended locale and label together. In a real application, navigate with the locale configuration and assert a landmark or heading too, so a stale translation fixture cannot make the wrong page look correct.
If content teams frequently revise copy, centralize only stable translation access, not vague fallback patterns. A label change can be a legitimate product change that requires test review. Treat it as signal rather than automatically bypassing it with a test ID.
9. Data-Driven Playwright getByLabel Examples for Boundaries
Data-driven tests are helpful for a concise validation matrix. Keep each case named, independent, and focused on one rule. Do not turn an end-to-end test into a hundred-case replacement for faster unit tests.
import { test, expect } from '@playwright/test';
const postalCases = [
{ name: 'too short', value: '123', valid: false },
{ name: 'five digits', value: '12345', valid: true },
{ name: 'contains a letter', value: '12A45', valid: false }
];
for (const postalCase of postalCases) {
test('postal code: ' + postalCase.name, async ({ page }) => {
await page.setContent(`
<label>Postal code <input pattern="[0-9]{5}"></label>
`);
const postalCode = page.getByLabel('Postal code');
await postalCode.fill(postalCase.value);
const isValid = await postalCode.evaluate(
element => (element as HTMLInputElement).checkValidity()
);
expect(isValid).toBe(postalCase.valid);
});
}
evaluate is justified here because native constraint validity is the subject of the test. For ordinary visible behavior, prefer locator assertions. In an application workflow, submitting the form and asserting the displayed error may provide stronger integration evidence than checking the DOM API directly.
Keep boundary cases deterministic and avoid random data unless the random seed and failing value are reported. A concise table in code review should make the covered partitions obvious.
10. Package Repeated Workflows Without Hiding Intent
Form abstractions should reduce repetition while keeping business language visible. A helper that accepts a generic object and fills every key by label looks convenient, but it can hide control types, required order, and sensitive values. Prefer a typed workflow with explicit actions.
import { type Locator, type Page } from '@playwright/test';
type Address = {
city: string;
postalCode: string;
};
export class CheckoutPage {
readonly shipping: Locator;
constructor(private readonly page: Page) {
this.shipping = page.getByRole('group', { name: 'Shipping address' });
}
async enterShipping(address: Address): Promise<void> {
await this.shipping.getByLabel('City').fill(address.city);
await this.shipping.getByLabel('Postal code').fill(address.postalCode);
}
async continue(): Promise<void> {
await this.page.getByRole('button', { name: 'Continue to payment' }).click();
}
}
The type documents required data, and the methods preserve the field vocabulary. Keep the payment and shipping workflows separate even if both contain similar labels. Their security, validation, and lifecycle risks differ.
Assertions may live in the test or in focused expectation methods. Avoid a page object that performs hidden assertions during every action, because negative tests and alternate outcomes become hard to express. For explicit automation contracts outside user-facing semantics, review the Playwright getByTestId patterns.
11. Add Negative Form Cases Without Duplicating the Suite
Negative browser cases should target integration risks that lower layers cannot prove alone. Examples include a server rejection mapped to the correct labeled field, an error summary moving focus appropriately, a duplicate submission being prevented, and user input surviving a retryable failure. Do not copy the complete happy-path test for every invalid character.
Build a small helper for stable setup, then keep the invalid action and assertion in the test. The reader should see which field receives which value, what the user does next, and what feedback appears. If several controls share the same message, scope the error to the form or assert its relationship through aria-describedby when that is part of the product contract.
Avoid intercepting the submit button click to manufacture an error entirely in test code. Mock the smallest service boundary or use a deterministic test environment so the real client behavior still runs. Confirm that sensitive values do not leak into alerts, URLs, or attachments.
12. Apply a Form Test Review Checklist
Before merge, confirm that every getByLabel call resolves through intentional markup and has semantic scope where labels repeat. Check that exact strings and regex patterns express approved wording, not accidental tolerance. Verify that control-specific actions match the rendered element rather than its visual appearance.
Review the assertion depth. A value assertion covers the control, while a status, error, URL, or persisted record covers the workflow. Choose the smallest combination that proves the risk without creating a brittle end-to-end script. Make parallel data unique and cleanup deterministic.
Finally, run the configured browser projects for components with native control differences, date inputs, file uploads, or custom widgets. Inspect traces for unexpected duplicate controls or hidden responsive variants. Review failures as possible markup and accessibility issues before adding test IDs, waits, or positional selection. This checklist keeps the examples readable as the form grows.
Review test data with the same care as selectors. Boundary values should be named, synthetic, and safe to retain in artifacts. Parallel runs need unique accounts or records, and workflows that create data need deterministic cleanup. When a test fails after submission, its trace should reveal which case ran without exposing secrets. Good data design prevents form locator examples from becoming flaky only after they reach CI scale.
Interview Questions and Answers
Q: Why are label locators valuable in form tests?
They express fields using the names users understand and validate the label-control relationship. They are less coupled to layout than CSS chains. Failures can also reveal accessibility regressions that an id selector would miss.
Q: Which action should you use for a checkbox?
Use check() when the required final state is checked and uncheck() for the opposite. These are more deterministic than click(), which toggles based on current state. Follow with toBeChecked when that state is important.
Q: How would you test two City fields?
I would locate named Shipping address and Billing address groups, then call getByLabel('City') within each. This survives section reordering and documents the business context. I would avoid nth() unless order itself is under test.
Q: How do you test a custom dropdown that has a label?
I inspect the component semantics. For a combobox, I locate it by role and accessible name, open it, and select an option by role. I do not call selectOption unless the rendered element is a native select.
Q: What should a validation test assert?
It should assert the rule's user-visible message and the important interaction result, such as blocked submission or focus on the invalid field. When implemented, aria-invalid and the error description relationship are useful semantic checks. I also include representative valid boundaries.
Q: When are data-driven browser tests appropriate?
They are useful for a small set of high-value integration partitions that share setup and assertions. Large validation combinations belong in faster unit or service tests. Every browser case should have a descriptive name and deterministic input.
Common Mistakes
- Using click() for checkboxes and radios when the expected final state is known.
- Selecting repeated labels with nth() instead of scoping to a named group, dialog, or row.
- Testing a native input value but never verifying server acceptance or persisted state.
- Calling selectOption on a custom div-based dropdown.
- Passing real resumes, customer records, credentials, or other sensitive data through fixtures.
- Making locale tests pass with a regex that accepts labels from every supported language.
- Building one generic form filler that hides control type, order, and business meaning.
- Moving an exhaustive validation matrix into slow browser tests instead of choosing risk-based integration cases.
Conclusion
These Playwright getByLabel examples show a consistent pattern: locate the field through its user-facing name, use the action designed for its control type, and assert the behavior that matters. Semantic scope keeps repeated forms clear, while strict matching exposes ambiguity instead of hiding it.
Choose one high-risk form in your suite and replace positional selectors with named groups and getByLabel. Then add an assertion for validation, submission, or persistence so the test proves more than data entry.
Interview Questions and Answers
Why would you use getByLabel in a form test?
It identifies the control by user-facing language and verifies a meaningful accessibility relationship. It is more resilient to layout changes than a DOM chain. The resulting test and failure are readable in business terms.
How do check and click differ for a checkbox?
click toggles whatever state exists, while check guarantees the final state is checked. check is idempotent and communicates intent more clearly. I use toBeChecked when the state is part of the expected result.
How do you automate repeated address labels?
I locate each fieldset or group by its accessible name, then find City and Postal code within that scope. This avoids dependence on order. It also encourages markup that communicates form structure to users.
How do you test accessible inline validation?
I submit representative invalid data through the labeled control and assert the visible error and blocked workflow. I also check semantic signals such as aria-invalid, described error text, and focus when the product implements them. Valid boundaries receive coverage too.
How would you design label-based page objects?
I use typed fields or workflow methods named after business actions. I keep control-specific actions explicit and avoid a generic label-value map. Assertions stay focused so tests can express success, validation, and alternate outcomes.
What is your approach to localized label locators?
I run core journeys with exact expected strings in a chosen locale and add targeted cases for supported locales. I verify locale context as well as the field. I do not use a multilingual catch-all regex because it can hide the wrong locale.
Frequently Asked Questions
What are good Playwright getByLabel examples?
Good examples cover labeled text fields, checkboxes, radio buttons, native selects, file inputs, and validation. They also verify a visible or persisted outcome after the action.
How do I fill an input with getByLabel?
Create the locator with page.getByLabel and call fill with the desired string. Use toHaveValue for the field value and add a workflow assertion when submission behavior matters.
How do I select a checkbox by label in Playwright?
Call check on page.getByLabel with the checkbox label. Use uncheck for the opposite state and toBeChecked to verify the outcome.
Can getByLabel upload a file?
Yes, when the file input has a proper label. Call setInputFiles with a fixture path or an in-memory file payload, then assert the application upload result.
How should I handle duplicate field labels?
Scope to the smallest meaningful container, such as a named fieldset, dialog, form, or row. Then call getByLabel from that container instead of using a positional selector.
Should form validation use getByLabel?
Use getByLabel to act on and inspect the invalid field. Also assert the visible error, semantic invalid state, focus behavior, and whether submission was correctly blocked.
Can getByLabel be used in data-driven tests?
Yes. Keep the case matrix small, deterministic, and named by the covered boundary. Use unit or service tests for large combinatorial rule sets.
Related Guides
- Playwright drag and drop: Examples and Best Practices
- Playwright getByPlaceholder: Examples and Best Practices
- Playwright mock date and time: Examples and Best Practices
- Playwright popup and new tab handling: Examples and Best Practices
- Playwright tag and grep filters: Examples and Best Practices
- Playwright APIRequestContext: Examples and Best Practices