I have a form with many elements of the following pattern (ng-star-inserted
et al. removed):
<ui-input label="First Name">
<div>First Name</div>
<div>
<input type="text" placeholder="" autocomplete="off">
</div>
</ui-input>
I would like Playwright to type text into the corresponding box on the form. As I understand it, the Playwright school of thought is to find the inner <input>
element and await input.fill("Humbleton")
.
I've tried:
page.getByRole('textbox', { name: 'First Name' });
page.getByText('First Name').locator('input');
page.getByLabel('First Name').locator('input');
page.getByText('First Name').getByRole('textbox');
page.getByLabel('First Name').getByRole('textbox');
of which none work.
The go-to workaround is to insert a secret, unique-value attribute into the HTML and use the resulting unique XPath. However, I consider this to be cheating and against the spirit of e2e testing, clean code, propriety, and Playwright's philosophy. Is there a Playwrightesque solution?
Finally I used page.keyboard
to fill out the form using page.keyboard.press('Tab')
to navigate from one field to another, but I have a hunch that this is not as robust and user-oriented as can be.
I feel there must be some overlooked elegant, expressive way to tell Playwright to just fill out the damn 'First Name' box.
For your getByLabel
to work, you'd need to use aria-label
:
<ui-input aria-label="First Name">
Other of your locators don't work because the input
isn't actually a child of the <div>First Name</div>
element, which is what .getByText
would return.
Also, your <div>First Name</div>
doesn't appear to be acting as a clickable label. I'd expect modern, accessible HTML to be something more along the lines of:
<ui-input aria-label="First Name">
<div>
<label for="first-name">First Name</label>
</div>
<div>
<input
id="first-name"
placeholder="Enter your first name"
autocomplete="off"
aria-labelledby="first-name"
>
</div>
</ui-input>
If you can make your markup more like this, selection becomes straightforward.
But assuming it's out of your control, you can use CSS selectors to find the input by the label
attribute, or use .filter()
to select a ui-input
with particular text.
import {expect, test} from "@playwright/test"; // ^1.46.1
const html = `<!DOCTYPE html><html><body>
<ui-input label="First Name">
<div>First Name</div>
<div>
<input placeholder="" autocomplete="off">
</div>
</ui-input>
</body></html>`;
test("Input can be located by First Name", async ({page}) => {
await page.setContent(html);
await page.locator('[label="First Name"] input').waitFor()
// or, more "Playwright"-y:
await page
.locator("ui-input")
.filter({hasText: "First Name"})
.getByRole("textbox").waitFor();
});
Note that these may not work or even be best practice, depending on your full HTML context, which wasn't shown.
And yeah, don't use XPath under almost any circumstances. If you have to avoid user-visible locators for some reason, CSS selectors are cleaner than XPath. Compare the XPath given in this answer against its functionally equivalent CSS selector, and the choice is quite clear:
'//ui-input[@label="First Name"]/descendant::input'
'ui-input[label="First Name"] input'
Then again, neither is optimal. See the docs on best practices, "Prefer user-facing attributes to XPath or CSS selectors".