Skip to content

Commit c384313

Browse files
authored
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'); ```
1 parent 5d39eae commit c384313

File tree

4 files changed

+59
-23
lines changed

4 files changed

+59
-23
lines changed

docs/input.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<!-- GEN:toc-top-level -->
44
- [Text input](#text-input)
5-
- [Checkboxes](#checkboxes)
5+
- [Checkboxes and radio buttons](#checkboxes-and-radio-buttons)
66
- [Select options](#select-options)
77
- [Mouse click](#mouse-click)
88
- [Type characters](#type-characters)
@@ -15,7 +15,7 @@
1515

1616
## Text input
1717

18-
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.
18+
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.
1919

2020
```js
2121
// Text input
@@ -29,6 +29,9 @@ await page.fill('#time', '13-15');
2929

3030
// Local datetime input
3131
await page.fill('#local', '2020-03-02T05:15');
32+
33+
// Input through label
34+
await page.fill('text=First Name', 'Peter');
3235
```
3336

3437
#### API reference
@@ -39,16 +42,19 @@ await page.fill('#local', '2020-03-02T05:15');
3942

4043
<br/>
4144

42-
## Checkboxes
45+
## Checkboxes and radio buttons
4346

44-
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.
47+
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.
4548

4649
```js
4750
// Check the checkbox
4851
await page.check('#agree');
4952

5053
// Uncheck by input <label>.
5154
await page.uncheck('#subscribe-label');
55+
56+
// Select the radio button
57+
await page.check('text=XL');
5258
```
5359

5460
#### API reference

src/server/injected/injectedScript.ts

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -261,10 +261,10 @@ export class InjectedScript {
261261
return this.pollRaf((progress, continuePolling) => {
262262
if (node.nodeType !== Node.ELEMENT_NODE)
263263
return 'error:notelement';
264-
const element = node as Element;
265-
if (!element.isConnected)
264+
const element = this.findLabelTarget(node as Element);
265+
if (element && !element.isConnected)
266266
return 'error:notconnected';
267-
if (!this.isVisible(element)) {
267+
if (!element || !this.isVisible(element)) {
268268
progress.logRepeating(' element is not visible - waiting...');
269269
return continuePolling;
270270
}
@@ -438,27 +438,22 @@ export class InjectedScript {
438438
return 'done';
439439
}
440440

441+
findLabelTarget(element: Element): Element | undefined {
442+
return element.nodeName === 'LABEL' ? (element as HTMLLabelElement).control || undefined : element;
443+
}
444+
441445
isCheckboxChecked(node: Node) {
442446
if (node.nodeType !== Node.ELEMENT_NODE)
443447
throw new Error('Not a checkbox or radio button');
444-
445-
let element: Element | undefined = node as Element;
448+
const element = node as Element;
446449
if (element.getAttribute('role') === 'checkbox')
447450
return element.getAttribute('aria-checked') === 'true';
448-
449-
if (element.nodeName === 'LABEL') {
450-
const forId = element.getAttribute('for');
451-
if (forId && element.ownerDocument)
452-
element = element.ownerDocument.querySelector(`input[id="${forId}"]`) || undefined;
453-
else
454-
element = element.querySelector('input[type=checkbox],input[type=radio]') || undefined;
455-
}
456-
if (element && element.nodeName === 'INPUT') {
457-
const type = element.getAttribute('type');
458-
if (type && (type.toLowerCase() === 'checkbox' || type.toLowerCase() === 'radio'))
459-
return (element as HTMLInputElement).checked;
460-
}
461-
throw new Error('Not a checkbox');
451+
const input = this.findLabelTarget(element);
452+
if (!input || input.nodeName !== 'INPUT')
453+
throw new Error('Not a checkbox or radio button');
454+
if (!['radio', 'checkbox'].includes((input as HTMLInputElement).type.toLowerCase()))
455+
throw new Error('Not a checkbox or radio button');
456+
return (input as HTMLInputElement).checked;
462457
}
463458

464459
setInputFiles(node: Node, payloads: { name: string, mimeType: string, buffer: string }[]) {

test/check.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,23 @@ it('should check the box inside label w/o id', async ({page}) => {
5858
expect(await page.evaluate(() => window['checkbox'].checked)).toBe(true);
5959
});
6060

61+
it('should check the box outside shadow dom label', async ({page}) => {
62+
await page.setContent('<div></div>');
63+
await page.$eval('div', div => {
64+
const root = div.attachShadow({ mode: 'open' });
65+
const label = document.createElement('label');
66+
label.setAttribute('for', 'target');
67+
label.textContent = 'Click me';
68+
root.appendChild(label);
69+
const input = document.createElement('input');
70+
input.setAttribute('type', 'checkbox');
71+
input.setAttribute('id', 'target');
72+
root.appendChild(input);
73+
});
74+
await page.check('label');
75+
expect(await page.$eval('input', input => input.checked)).toBe(true);
76+
});
77+
6178
it('should check radio', async ({page}) => {
6279
await page.setContent(`
6380
<input type='radio'>one</input>

test/page-fill.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,24 @@ it('should fill input', async ({page, server}) => {
3434
expect(await page.evaluate(() => window['result'])).toBe('some value');
3535
});
3636

37+
it('should fill input with label', async ({page}) => {
38+
await page.setContent(`<label for=target>Fill me</label><input id=target>`);
39+
await page.fill('text=Fill me', 'some value');
40+
expect(await page.$eval('input', input => input.value)).toBe('some value');
41+
});
42+
43+
it('should fill input with label 2', async ({page}) => {
44+
await page.setContent(`<label>Fill me<input id=target></label>`);
45+
await page.fill('text=Fill me', 'some value');
46+
expect(await page.$eval('input', input => input.value)).toBe('some value');
47+
});
48+
49+
it('should fill textarea with label', async ({page}) => {
50+
await page.setContent(`<label for=target>Fill me</label><textarea id=target>hey</textarea>`);
51+
await page.fill('text=Fill me', 'some value');
52+
expect(await page.$eval('textarea', textarea => textarea.value)).toBe('some value');
53+
});
54+
3755
it('should throw on unsupported inputs', async ({page, server}) => {
3856
await page.goto(server.PREFIX + '/input/textarea.html');
3957
for (const type of ['color', 'file']) {

0 commit comments

Comments
 (0)