feat(fill): wait for the element to be enabled/writable/visible (#2435)
This commit is contained in:
parent
bf67245de6
commit
a644f0a881
22
src/dom.ts
22
src/dom.ts
|
|
@ -351,13 +351,21 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"');
|
||||
const deadline = this._page._timeoutSettings.computeDeadline(options);
|
||||
await this._page._frameManager.waitForSignalsCreatedBy(async () => {
|
||||
const injectedResult = await this._evaluateInUtility(({ injected, node }, value) => injected.fill(node, value), value);
|
||||
const needsInput = handleInjectedResult(injectedResult);
|
||||
if (needsInput) {
|
||||
if (value)
|
||||
await this._page.keyboard.insertText(value);
|
||||
else
|
||||
await this._page.keyboard.press('Delete');
|
||||
const poll = await this._evaluateHandleInUtility(({ injected, node }, { value }) => {
|
||||
return injected.waitForEnabledAndFill(node, value);
|
||||
}, { value });
|
||||
try {
|
||||
const filledPromise = poll.evaluate(poll => poll.result);
|
||||
const injectedResult = await helper.waitWithDeadline(filledPromise, 'element to be visible and enabled', deadline, 'pw:input');
|
||||
const needsInput = handleInjectedResult(injectedResult);
|
||||
if (needsInput) {
|
||||
if (value)
|
||||
await this._page.keyboard.insertText(value);
|
||||
else
|
||||
await this._page.keyboard.press('Delete');
|
||||
}
|
||||
} finally {
|
||||
poll.evaluate(poll => poll.cancel()).catch(e => {}).then(() => poll.dispose());
|
||||
}
|
||||
}, deadline, options, true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -212,53 +212,55 @@ export default class InjectedScript {
|
|||
return { status: 'success', value: options.filter(option => option.selected).map(option => option.value) };
|
||||
}
|
||||
|
||||
fill(node: Node, value: string): types.InjectedScriptResult<boolean> {
|
||||
if (node.nodeType !== Node.ELEMENT_NODE)
|
||||
return { status: 'error', error: 'Node is not of type HTMLElement' };
|
||||
const element = node as HTMLElement;
|
||||
if (!element.isConnected)
|
||||
return { status: 'notconnected' };
|
||||
if (!this.isVisible(element))
|
||||
return { status: 'error', error: 'Element is not visible' };
|
||||
if (element.nodeName.toLowerCase() === 'input') {
|
||||
const input = element as HTMLInputElement;
|
||||
const type = (input.getAttribute('type') || '').toLowerCase();
|
||||
const kDateTypes = new Set(['date', 'time', 'datetime', 'datetime-local']);
|
||||
const kTextInputTypes = new Set(['', 'email', 'number', 'password', 'search', 'tel', 'text', 'url']);
|
||||
if (!kTextInputTypes.has(type) && !kDateTypes.has(type))
|
||||
return { status: 'error', error: 'Cannot fill input of type "' + type + '".' };
|
||||
if (type === 'number') {
|
||||
value = value.trim();
|
||||
if (isNaN(Number(value)))
|
||||
return { status: 'error', error: 'Cannot type text into input[type=number].' };
|
||||
waitForEnabledAndFill(node: Node, value: string): types.InjectedScriptPoll<types.InjectedScriptResult<boolean>> {
|
||||
return this.poll('raf', () => {
|
||||
if (node.nodeType !== Node.ELEMENT_NODE)
|
||||
return { status: 'error', error: 'Node is not of type HTMLElement' };
|
||||
const element = node as HTMLElement;
|
||||
if (!element.isConnected)
|
||||
return { status: 'notconnected' };
|
||||
if (!this.isVisible(element))
|
||||
return false;
|
||||
if (element.nodeName.toLowerCase() === 'input') {
|
||||
const input = element as HTMLInputElement;
|
||||
const type = (input.getAttribute('type') || '').toLowerCase();
|
||||
const kDateTypes = new Set(['date', 'time', 'datetime', 'datetime-local']);
|
||||
const kTextInputTypes = new Set(['', 'email', 'number', 'password', 'search', 'tel', 'text', 'url']);
|
||||
if (!kTextInputTypes.has(type) && !kDateTypes.has(type))
|
||||
return { status: 'error', error: 'Cannot fill input of type "' + type + '".' };
|
||||
if (type === 'number') {
|
||||
value = value.trim();
|
||||
if (isNaN(Number(value)))
|
||||
return { status: 'error', error: 'Cannot type text into input[type=number].' };
|
||||
}
|
||||
if (input.disabled)
|
||||
return false;
|
||||
if (input.readOnly)
|
||||
return false;
|
||||
if (kDateTypes.has(type)) {
|
||||
value = value.trim();
|
||||
input.focus();
|
||||
input.value = value;
|
||||
if (input.value !== value)
|
||||
return { status: 'error', error: `Malformed ${type} "${value}"` };
|
||||
element.dispatchEvent(new Event('input', { 'bubbles': true }));
|
||||
element.dispatchEvent(new Event('change', { 'bubbles': true }));
|
||||
return { status: 'success', value: false }; // We have already changed the value, no need to input it.
|
||||
}
|
||||
} else if (element.nodeName.toLowerCase() === 'textarea') {
|
||||
const textarea = element as HTMLTextAreaElement;
|
||||
if (textarea.disabled)
|
||||
return false;
|
||||
if (textarea.readOnly)
|
||||
return false;
|
||||
} else if (!element.isContentEditable) {
|
||||
return { status: 'error', error: 'Element is not an <input>, <textarea> or [contenteditable] element.' };
|
||||
}
|
||||
if (input.disabled)
|
||||
return { status: 'error', error: 'Cannot fill a disabled input.' };
|
||||
if (input.readOnly)
|
||||
return { status: 'error', error: 'Cannot fill a readonly input.' };
|
||||
if (kDateTypes.has(type)) {
|
||||
value = value.trim();
|
||||
input.focus();
|
||||
input.value = value;
|
||||
if (input.value !== value)
|
||||
return { status: 'error', error: `Malformed ${type} "${value}"` };
|
||||
element.dispatchEvent(new Event('input', { 'bubbles': true }));
|
||||
element.dispatchEvent(new Event('change', { 'bubbles': true }));
|
||||
return { status: 'success', value: false }; // We have already changed the value, no need to input it.
|
||||
}
|
||||
} else if (element.nodeName.toLowerCase() === 'textarea') {
|
||||
const textarea = element as HTMLTextAreaElement;
|
||||
if (textarea.disabled)
|
||||
return { status: 'error', error: 'Cannot fill a disabled textarea.' };
|
||||
if (textarea.readOnly)
|
||||
return { status: 'error', error: 'Cannot fill a readonly textarea.' };
|
||||
} else if (!element.isContentEditable) {
|
||||
return { status: 'error', error: 'Element is not an <input>, <textarea> or [contenteditable] element.' };
|
||||
}
|
||||
const result = this.selectText(node);
|
||||
if (result.status === 'success')
|
||||
return { status: 'success', value: true }; // Still need to input the value.
|
||||
return result;
|
||||
const result = this.selectText(node);
|
||||
if (result.status === 'success')
|
||||
return { status: 'success', value: true }; // Still need to input the value.
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
selectText(node: Node): types.InjectedScriptResult {
|
||||
|
|
|
|||
|
|
@ -1013,6 +1013,11 @@ describe('Page.select', function() {
|
|||
});
|
||||
|
||||
describe('Page.fill', function() {
|
||||
async function giveItAChanceToFill(page) {
|
||||
for (let i = 0; i < 5; i++)
|
||||
await page.evaluate(() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f))));
|
||||
}
|
||||
|
||||
it('should fill textarea', async({page, server}) => {
|
||||
await page.goto(server.PREFIX + '/input/textarea.html');
|
||||
await page.fill('textarea', 'some value');
|
||||
|
|
@ -1113,27 +1118,47 @@ describe('Page.fill', function() {
|
|||
await page.fill('textarea', 123).catch(e => error = e);
|
||||
expect(error.message).toContain('Value must be string.');
|
||||
});
|
||||
it('should throw on disabled and readonly elements', async({page, server}) => {
|
||||
it('should retry on disabled element', async({page, server}) => {
|
||||
await page.goto(server.PREFIX + '/input/textarea.html');
|
||||
await page.$eval('input', i => i.disabled = true);
|
||||
const disabledError = await page.fill('input', 'some value').catch(e => e);
|
||||
expect(disabledError.message).toBe('Cannot fill a disabled input.');
|
||||
let done = false;
|
||||
|
||||
const promise = page.fill('input', 'some value').then(() => done = true);
|
||||
await giveItAChanceToFill(page);
|
||||
expect(done).toBe(false);
|
||||
expect(await page.evaluate(() => result)).toBe('');
|
||||
|
||||
await page.$eval('input', i => i.disabled = false);
|
||||
await promise;
|
||||
expect(await page.evaluate(() => result)).toBe('some value');
|
||||
});
|
||||
it('should retry on readonly element', async({page, server}) => {
|
||||
await page.goto(server.PREFIX + '/input/textarea.html');
|
||||
await page.$eval('textarea', i => i.readOnly = true);
|
||||
const readonlyError = await page.fill('textarea', 'some value').catch(e => e);
|
||||
expect(readonlyError.message).toBe('Cannot fill a readonly textarea.');
|
||||
let done = false;
|
||||
|
||||
const promise = page.fill('textarea', 'some value').then(() => done = true);
|
||||
await giveItAChanceToFill(page);
|
||||
expect(done).toBe(false);
|
||||
expect(await page.evaluate(() => result)).toBe('');
|
||||
|
||||
await page.$eval('textarea', i => i.readOnly = false);
|
||||
await promise;
|
||||
expect(await page.evaluate(() => result)).toBe('some value');
|
||||
});
|
||||
it('should throw on hidden and invisible elements', async({page, server}) => {
|
||||
it('should retry on invisible element', async({page, server}) => {
|
||||
await page.goto(server.PREFIX + '/input/textarea.html');
|
||||
await page.$eval('input', i => i.style.display = 'none');
|
||||
const invisibleError = await page.fill('input', 'some value', { force: true }).catch(e => e);
|
||||
expect(invisibleError.message).toBe('Element is not visible');
|
||||
let done = false;
|
||||
|
||||
await page.goto(server.PREFIX + '/input/textarea.html');
|
||||
await page.$eval('input', i => i.style.visibility = 'hidden');
|
||||
const hiddenError = await page.fill('input', 'some value', { force: true }).catch(e => e);
|
||||
expect(hiddenError.message).toBe('Element is not visible');
|
||||
const promise = page.fill('input', 'some value').then(() => done = true);
|
||||
await giveItAChanceToFill(page);
|
||||
expect(done).toBe(false);
|
||||
expect(await page.evaluate(() => result)).toBe('');
|
||||
|
||||
await page.$eval('input', i => i.style.display = 'inline');
|
||||
await promise;
|
||||
expect(await page.evaluate(() => result)).toBe('some value');
|
||||
});
|
||||
it('should be able to fill the body', async({page}) => {
|
||||
await page.setContent(`<body contentEditable="true"></body>`);
|
||||
|
|
|
|||
Loading…
Reference in a new issue