From 530523cb67d0f7c953e7af6e8d08ffaf9d407e91 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 28 Jun 2021 14:18:01 -0700 Subject: [PATCH] feat(setInputFiles): support label retargeting (#7364) This way `page.setInputFiles('label')` works, similarly to other input actions. --- src/server/common/domErrors.ts | 3 ++- src/server/dom.ts | 24 ++++++++++++++---------- src/server/injected/injectedScript.ts | 12 ++++++------ tests/page/page-set-input-files.spec.ts | 7 +++++++ 4 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/server/common/domErrors.ts b/src/server/common/domErrors.ts index a646314673..fc374066bb 100644 --- a/src/server/common/domErrors.ts +++ b/src/server/common/domErrors.ts @@ -24,6 +24,7 @@ export type FatalDOMError = 'error:notinput' | 'error:notinputvalue' | 'error:notselect' | - 'error:notcheckbox'; + 'error:notcheckbox' | + 'error:notmultiplefileinput'; export type RetargetableDOMError = 'error:notconnected'; diff --git a/src/server/dom.ts b/src/server/dom.ts index e10f195970..9dbe8be5c2 100644 --- a/src/server/dom.ts +++ b/src/server/dom.ts @@ -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 extends js.JSHandle { 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, files as types.FilePayload[]); + await this._page._delegate.setInputFiles(retargeted as ElementHandle, files as types.FilePayload[]); }); await this._page._doSlowMo(); return 'done'; @@ -887,6 +889,8 @@ export function throwFatalDOMError(result: T | FatalDOMError): T { throw new Error('Element is not a `); + 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(``); await page.setInputFiles('input', {