chore: implement fill for locators (#10220)

This commit is contained in:
Alex Rudenko 2023-05-23 13:55:12 +02:00 committed by GitHub
parent 81f73a55f3
commit 9ca9bbf259
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 225 additions and 0 deletions

View File

@ -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 => {

View File

@ -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);
});
});
}); });