feat(fill): allow filling based on the label selector (#4342)

This enables filling the input based on the connected label:

```html
<label for=target>Name</label><input id=target>
```

```js
await page.fill('text=Name', 'Alice');
```
This commit is contained in:
Dmitry Gozman 2020-11-05 05:22:49 -08:00 committed by GitHub
parent 5d39eae509
commit c384313058
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 59 additions and 23 deletions

View File

@ -2,7 +2,7 @@
<!-- GEN:toc-top-level -->
- [Text input](#text-input)
- [Checkboxes](#checkboxes)
- [Checkboxes and radio buttons](#checkboxes-and-radio-buttons)
- [Select options](#select-options)
- [Mouse click](#mouse-click)
- [Type characters](#type-characters)
@ -15,7 +15,7 @@
## Text input
This is the easiest way to fill out the form fields. It focuses the element and triggers an `input` event with the entered text. It works for `<input>`, `<textarea>` and `[contenteditable]` elements.
This is the easiest way to fill out the form fields. It focuses the element and triggers an `input` event with the entered text. It works for `<input>`, `<textarea>`, `[contenteditable]` and `<label>` associated with an input or textarea.
```js
// Text input
@ -29,6 +29,9 @@ await page.fill('#time', '13-15');
// Local datetime input
await page.fill('#local', '2020-03-02T05:15');
// Input through label
await page.fill('text=First Name', 'Peter');
```
#### API reference
@ -39,9 +42,9 @@ await page.fill('#local', '2020-03-02T05:15');
<br/>
## Checkboxes
## Checkboxes and radio buttons
This is the easiest way to check and uncheck a checkbox. This method can be used on the `input[type=checkbox]` and on the `label` associated with that input.
This is the easiest way to check and uncheck a checkbox or a radio button. This method can be used with `input[type=checkbox]`, `input[type=radio]`, `[role=checkbox]` or `label` associated with checkbox or radio button.
```js
// Check the checkbox
@ -49,6 +52,9 @@ await page.check('#agree');
// Uncheck by input <label>.
await page.uncheck('#subscribe-label');
// Select the radio button
await page.check('text=XL');
```
#### API reference

View File

@ -261,10 +261,10 @@ export class InjectedScript {
return this.pollRaf((progress, continuePolling) => {
if (node.nodeType !== Node.ELEMENT_NODE)
return 'error:notelement';
const element = node as Element;
if (!element.isConnected)
const element = this.findLabelTarget(node as Element);
if (element && !element.isConnected)
return 'error:notconnected';
if (!this.isVisible(element)) {
if (!element || !this.isVisible(element)) {
progress.logRepeating(' element is not visible - waiting...');
return continuePolling;
}
@ -438,27 +438,22 @@ export class InjectedScript {
return 'done';
}
findLabelTarget(element: Element): Element | undefined {
return element.nodeName === 'LABEL' ? (element as HTMLLabelElement).control || undefined : element;
}
isCheckboxChecked(node: Node) {
if (node.nodeType !== Node.ELEMENT_NODE)
throw new Error('Not a checkbox or radio button');
let element: Element | undefined = node as Element;
const element = node as Element;
if (element.getAttribute('role') === 'checkbox')
return element.getAttribute('aria-checked') === 'true';
if (element.nodeName === 'LABEL') {
const forId = element.getAttribute('for');
if (forId && element.ownerDocument)
element = element.ownerDocument.querySelector(`input[id="${forId}"]`) || undefined;
else
element = element.querySelector('input[type=checkbox],input[type=radio]') || undefined;
}
if (element && element.nodeName === 'INPUT') {
const type = element.getAttribute('type');
if (type && (type.toLowerCase() === 'checkbox' || type.toLowerCase() === 'radio'))
return (element as HTMLInputElement).checked;
}
throw new Error('Not a checkbox');
const input = this.findLabelTarget(element);
if (!input || input.nodeName !== 'INPUT')
throw new Error('Not a checkbox or radio button');
if (!['radio', 'checkbox'].includes((input as HTMLInputElement).type.toLowerCase()))
throw new Error('Not a checkbox or radio button');
return (input as HTMLInputElement).checked;
}
setInputFiles(node: Node, payloads: { name: string, mimeType: string, buffer: string }[]) {

View File

@ -58,6 +58,23 @@ it('should check the box inside label w/o id', async ({page}) => {
expect(await page.evaluate(() => window['checkbox'].checked)).toBe(true);
});
it('should check the box outside shadow dom label', async ({page}) => {
await page.setContent('<div></div>');
await page.$eval('div', div => {
const root = div.attachShadow({ mode: 'open' });
const label = document.createElement('label');
label.setAttribute('for', 'target');
label.textContent = 'Click me';
root.appendChild(label);
const input = document.createElement('input');
input.setAttribute('type', 'checkbox');
input.setAttribute('id', 'target');
root.appendChild(input);
});
await page.check('label');
expect(await page.$eval('input', input => input.checked)).toBe(true);
});
it('should check radio', async ({page}) => {
await page.setContent(`
<input type='radio'>one</input>

View File

@ -34,6 +34,24 @@ it('should fill input', async ({page, server}) => {
expect(await page.evaluate(() => window['result'])).toBe('some value');
});
it('should fill input with label', async ({page}) => {
await page.setContent(`<label for=target>Fill me</label><input id=target>`);
await page.fill('text=Fill me', 'some value');
expect(await page.$eval('input', input => input.value)).toBe('some value');
});
it('should fill input with label 2', async ({page}) => {
await page.setContent(`<label>Fill me<input id=target></label>`);
await page.fill('text=Fill me', 'some value');
expect(await page.$eval('input', input => input.value)).toBe('some value');
});
it('should fill textarea with label', async ({page}) => {
await page.setContent(`<label for=target>Fill me</label><textarea id=target>hey</textarea>`);
await page.fill('text=Fill me', 'some value');
expect(await page.$eval('textarea', textarea => textarea.value)).toBe('some value');
});
it('should throw on unsupported inputs', async ({page, server}) => {
await page.goto(server.PREFIX + '/input/textarea.html');
for (const type of ['color', 'file']) {