mirror of
https://github.com/puppeteer/puppeteer
synced 2024-06-14 14:02:48 +00:00
chore: implement fill for locators (#10220)
This commit is contained in:
parent
81f73a55f3
commit
9ca9bbf259
@ -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> {
|
async hover(hoverOptions?: {signal?: AbortSignal}): Promise<void> {
|
||||||
return await this.#run(
|
return await this.#run(
|
||||||
async element => {
|
async element => {
|
||||||
|
@ -279,4 +279,119 @@ describe('Locator', function () {
|
|||||||
expect(scrolled).toBe(true);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user