Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: implement fill for locators #10220

Merged
merged 1 commit into from May 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
110 changes: 110 additions & 0 deletions packages/puppeteer-core/src/api/Locator.ts
Expand Up @@ -358,6 +358,116 @@ export class Locator extends EventEmitter {
);
}

/**
* Fills out the input identified by the locator using the provided value. The
* type of the input is determined at runtime and the appropriate fill-out
* method is chosen based on the type. contenteditable, selector, inputs are
* supported.
*/
async fill(
value: string,
fillOptions?: {signal?: AbortSignal}
): Promise<void> {
return await this.#run(
async element => {
const input = element as ElementHandle<HTMLElement>;
const inputType = await input.evaluate(el => {
if (el instanceof HTMLSelectElement) {
return 'select';
}
if (el instanceof HTMLInputElement) {
if (
new Set([
'textarea',
'text',
'url',
'tel',
'search',
'password',
'number',
'email',
]).has(el.type)
) {
return 'typeable-input';
} else {
return 'other-input';
}
}

if (el.isContentEditable) {
return 'contenteditable';
}

return 'unknown';
});

switch (inputType) {
case 'select':
await input.select(value);
break;
case 'contenteditable':
case 'typeable-input':
const textToType = await (
input as ElementHandle<HTMLInputElement>
).evaluate((input, newValue) => {
const currentValue = input.isContentEditable
? input.innerText
: input.value;

// Clear the input if the current value does not match the filled
// out value.
if (
newValue.length <= currentValue.length ||
!newValue.startsWith(input.value)
) {
if (input.isContentEditable) {
input.innerText = '';
} else {
input.value = '';
}
return newValue;
}
const originalValue = input.isContentEditable
? input.innerText
: input.value;

// If the value is partially filled out, only type the rest. Move
// cursor to the end of the common prefix.
if (input.isContentEditable) {
input.innerText = '';
input.innerText = originalValue;
} else {
input.value = '';
input.value = originalValue;
}
return newValue.substring(originalValue.length);
}, value);
await input.type(textToType);
break;
case 'other-input':
await input.focus();
await input.evaluate((input, value) => {
(input as HTMLInputElement).value = value;
input.dispatchEvent(new Event('input', {bubbles: true}));
input.dispatchEvent(new Event('change', {bubbles: true}));
}, value);
break;
case 'unknown':
throw new Error(`Element cannot be filled out.`);
}
},
{
signal: fillOptions?.signal,
conditions: [
this.#ensureElementIsInTheViewport,
this.#waitForVisibility,
this.#waitForEnabled,
this.#waitForStableBoundingBox,
],
}
);
}

async hover(hoverOptions?: {signal?: AbortSignal}): Promise<void> {
return await this.#run(
async element => {
Expand Down
115 changes: 115 additions & 0 deletions test/src/locator.spec.ts
Expand Up @@ -279,4 +279,119 @@ describe('Locator', function () {
expect(scrolled).toBe(true);
});
});

describe('Locator.change', function () {
it('should work for selects', async () => {
const {page} = getTestState();

await page.setContent(`
<select>
<option value="value1">Option 1</option>
<option value="value2">Option 2</option>
<select>
`);
let filled = false;
await page
.locator('select')
.on(LocatorEmittedEvents.Action, () => {
filled = true;
})
.fill('value2');
expect(
await page.evaluate(() => {
return document.querySelector('select')?.value === 'value2';
})
).toBe(true);
expect(filled).toBe(true);
});

it('should work for inputs', async () => {
const {page} = getTestState();
await page.setContent(`
<input>
`);
await page.locator('input').fill('test');
expect(
await page.evaluate(() => {
return document.querySelector('input')?.value === 'test';
})
).toBe(true);
});

it('should work if the input becomes enabled later', async () => {
const {page} = getTestState();

await page.setContent(`
<input disabled>
`);
const input = await page.$('input');
const result = page.locator('input').fill('test');
expect(
await input?.evaluate(el => {
return el.value;
})
).toBe('');
await input?.evaluate(el => {
el.disabled = false;
});
await result;
expect(
await input?.evaluate(el => {
return el.value;
})
).toBe('test');
});

it('should work for contenteditable', async () => {
const {page} = getTestState();
await page.setContent(`
<div contenteditable="true">
`);
await page.locator('div').fill('test');
expect(
await page.evaluate(() => {
return document.querySelector('div')?.innerText === 'test';
})
).toBe(true);
});

it('should work for pre-filled inputs', async () => {
const {page} = getTestState();
await page.setContent(`
<input value="te">
`);
await page.locator('input').fill('test');
expect(
await page.evaluate(() => {
return document.querySelector('input')?.value === 'test';
})
).toBe(true);
});

it('should override pre-filled inputs', async () => {
const {page} = getTestState();
await page.setContent(`
<input value="wrong prefix">
`);
await page.locator('input').fill('test');
expect(
await page.evaluate(() => {
return document.querySelector('input')?.value === 'test';
})
).toBe(true);
});

it('should work for non-text inputs', async () => {
const {page} = getTestState();
await page.setContent(`
<input type="color">
`);
await page.locator('input').fill('#333333');
expect(
await page.evaluate(() => {
return document.querySelector('input')?.value === '#333333';
})
).toBe(true);
});
});
});