mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(setInputFiles): support label retargeting (#7364)
This way `page.setInputFiles('label')` works, similarly to other input actions.
This commit is contained in:
parent
014c224db6
commit
530523cb67
@ -24,6 +24,7 @@ export type FatalDOMError =
|
||||
'error:notinput' |
|
||||
'error:notinputvalue' |
|
||||
'error:notselect' |
|
||||
'error:notcheckbox';
|
||||
'error:notcheckbox' |
|
||||
'error:notmultiplefileinput';
|
||||
|
||||
export type RetargetableDOMError = 'error:notconnected';
|
||||
|
||||
@ -16,7 +16,6 @@
|
||||
|
||||
import * as channels from '../protocol/channels';
|
||||
import * as frames from './frames';
|
||||
import { assert } from '../utils/utils';
|
||||
import type { ElementStateWithoutStable, InjectedScript, InjectedScriptPoll } from './injected/injectedScript';
|
||||
import * as injectedScriptSource from '../generated/injectedScriptSource';
|
||||
import * as js from './javascript';
|
||||
@ -552,19 +551,22 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
if (!payload.mimeType)
|
||||
payload.mimeType = mime.getType(payload.name) || 'application/octet-stream';
|
||||
}
|
||||
const multiple = throwFatalDOMError(await this.evaluateInUtility(([injected, node]): 'error:notinput' | 'error:notconnected' | boolean => {
|
||||
if (node.nodeType !== Node.ELEMENT_NODE || (node as Node as Element).tagName !== 'INPUT')
|
||||
const retargeted = await this.evaluateHandleInUtility(([injected, node, multiple]): FatalDOMError | 'error:notconnected' | Element => {
|
||||
const element = injected.retarget(node, 'follow-label');
|
||||
if (!element)
|
||||
return 'error:notconnected';
|
||||
if (element.tagName !== 'INPUT')
|
||||
return 'error:notinput';
|
||||
const input = node as Node as HTMLInputElement;
|
||||
return input.multiple;
|
||||
}, {}));
|
||||
if (typeof multiple === 'string')
|
||||
return multiple;
|
||||
assert(multiple || files.length <= 1, 'Non-multiple file input can only accept single file!');
|
||||
if (multiple && !(element as HTMLInputElement).multiple)
|
||||
return 'error:notmultiplefileinput';
|
||||
return element;
|
||||
}, files.length > 1);
|
||||
if (!retargeted._objectId)
|
||||
return throwFatalDOMError(retargeted.rawValue() as FatalDOMError | 'error:notconnected');
|
||||
await progress.beforeInputAction(this);
|
||||
await this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
|
||||
progress.throwIfAborted(); // Avoid action that has side-effects.
|
||||
await this._page._delegate.setInputFiles(this as any as ElementHandle<HTMLInputElement>, files as types.FilePayload[]);
|
||||
await this._page._delegate.setInputFiles(retargeted as ElementHandle<HTMLInputElement>, files as types.FilePayload[]);
|
||||
});
|
||||
await this._page._doSlowMo();
|
||||
return 'done';
|
||||
@ -887,6 +889,8 @@ export function throwFatalDOMError<T>(result: T | FatalDOMError): T {
|
||||
throw new Error('Element is not a <select> element.');
|
||||
if (result === 'error:notcheckbox')
|
||||
throw new Error('Not a checkbox or radio button');
|
||||
if (result === 'error:notmultiplefileinput')
|
||||
throw new Error('Non-multiple file input can only accept single file');
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@ -326,7 +326,7 @@ export class InjectedScript {
|
||||
return { left: parseInt(style.borderLeftWidth || '', 10), top: parseInt(style.borderTopWidth || '', 10) };
|
||||
}
|
||||
|
||||
private _retarget(node: Node, behavior: 'follow-label' | 'no-follow-label'): Element | null {
|
||||
retarget(node: Node, behavior: 'follow-label' | 'no-follow-label'): Element | null {
|
||||
let element = node.nodeType === Node.ELEMENT_NODE ? node as Element : node.parentElement;
|
||||
if (!element)
|
||||
return null;
|
||||
@ -369,7 +369,7 @@ export class InjectedScript {
|
||||
continue;
|
||||
}
|
||||
|
||||
const element = this._retarget(node, 'no-follow-label');
|
||||
const element = this.retarget(node, 'no-follow-label');
|
||||
if (!element)
|
||||
return 'error:notconnected';
|
||||
|
||||
@ -411,7 +411,7 @@ export class InjectedScript {
|
||||
}
|
||||
|
||||
checkElementState(node: Node, state: ElementStateWithoutStable): boolean | 'error:notconnected' | FatalDOMError {
|
||||
const element = this._retarget(node, ['stable', 'visible', 'hidden'].includes(state) ? 'no-follow-label' : 'follow-label');
|
||||
const element = this.retarget(node, ['stable', 'visible', 'hidden'].includes(state) ? 'no-follow-label' : 'follow-label');
|
||||
if (!element || !element.isConnected) {
|
||||
if (state === 'hidden')
|
||||
return true;
|
||||
@ -447,7 +447,7 @@ export class InjectedScript {
|
||||
|
||||
selectOptions(optionsToSelect: (Node | { value?: string, label?: string, index?: number })[],
|
||||
node: Node, progress: InjectedScriptProgress, continuePolling: symbol): string[] | 'error:notconnected' | FatalDOMError | symbol {
|
||||
const element = this._retarget(node, 'follow-label');
|
||||
const element = this.retarget(node, 'follow-label');
|
||||
if (!element)
|
||||
return 'error:notconnected';
|
||||
if (element.nodeName.toLowerCase() !== 'select')
|
||||
@ -493,7 +493,7 @@ export class InjectedScript {
|
||||
}
|
||||
|
||||
fill(value: string, node: Node, progress: InjectedScriptProgress): FatalDOMError | 'error:notconnected' | 'needsinput' | 'done' {
|
||||
const element = this._retarget(node, 'follow-label');
|
||||
const element = this.retarget(node, 'follow-label');
|
||||
if (!element)
|
||||
return 'error:notconnected';
|
||||
if (element.nodeName.toLowerCase() === 'input') {
|
||||
@ -530,7 +530,7 @@ export class InjectedScript {
|
||||
}
|
||||
|
||||
selectText(node: Node): 'error:notconnected' | 'done' {
|
||||
const element = this._retarget(node, 'follow-label');
|
||||
const element = this.retarget(node, 'follow-label');
|
||||
if (!element)
|
||||
return 'error:notconnected';
|
||||
if (element.nodeName.toLowerCase() === 'input') {
|
||||
|
||||
@ -42,6 +42,13 @@ it('should work', async ({page, asset}) => {
|
||||
expect(await page.$eval('input', input => input.files[0].name)).toBe('file-to-upload.txt');
|
||||
});
|
||||
|
||||
it('should work with label', async ({page, asset}) => {
|
||||
await page.setContent(`<label for=target>Choose a file</label><input id=target type=file>`);
|
||||
await page.setInputFiles('text=Choose a file', asset('file-to-upload.txt'));
|
||||
expect(await page.$eval('input', input => input.files.length)).toBe(1);
|
||||
expect(await page.$eval('input', input => input.files[0].name)).toBe('file-to-upload.txt');
|
||||
});
|
||||
|
||||
it('should set from memory', async ({page}) => {
|
||||
await page.setContent(`<input type=file>`);
|
||||
await page.setInputFiles('input', {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user