From 22801263447f1023bd3c8595569ce9312afd3e67 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 16 Apr 2020 10:25:28 -0700 Subject: [PATCH] api(setInputFiles): introduce page/frame helpers, document, break compat (#1818) --- docs/api.md | 109 +++++++++++++++++++++++++++++++++++---- docs/uploadDownload.md | 102 ++++++++++++++++++++++++++++++++++++ src/api.ts | 3 +- src/chromium/crPage.ts | 3 +- src/dom.ts | 21 +++----- src/fileChooser.ts | 47 +++++++++++++++++ src/firefox/ffPage.ts | 3 +- src/frames.ts | 7 +++ src/injected/injected.ts | 23 +++++++++ src/page.ts | 12 ++--- src/types.ts | 6 +++ src/webkit/wkPage.ts | 2 +- test/input.spec.js | 47 ++++++++++++----- 13 files changed, 340 insertions(+), 45 deletions(-) create mode 100644 docs/uploadDownload.md create mode 100644 src/fileChooser.ts diff --git a/docs/api.md b/docs/api.md index 340e8d5f31..7074290214 100644 --- a/docs/api.md +++ b/docs/api.md @@ -15,6 +15,7 @@ - [class: ConsoleMessage](#class-consolemessage) - [class: Dialog](#class-dialog) - [class: Download](#class-download) +- [class: FileChooser](#class-filechooser) - [class: Keyboard](#class-keyboard) - [class: Mouse](#class-mouse) - [class: Request](#class-request) @@ -698,6 +699,7 @@ page.removeListener('request', logRequest); - [page.setDefaultNavigationTimeout(timeout)](#pagesetdefaultnavigationtimeouttimeout) - [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) - [page.setExtraHTTPHeaders(headers)](#pagesetextrahttpheadersheaders) +- [page.setInputFiles(selector, files[, options])](#pagesetinputfilesselector-files-options) - [page.setViewportSize(viewportSize)](#pagesetviewportsizeviewportsize) - [page.title()](#pagetitle) - [page.type(selector, text[, options])](#pagetypeselector-text-options) @@ -757,15 +759,13 @@ Emitted when attachment download started. User can access basic file operations > **NOTE** Browser context **must** be created with the `acceptDownloads` set to `true` when user needs access to the downloaded content. If `acceptDownloads` is not set or set to `false`, download events are emitted, but the actual download is not performed and user has no access to the downloaded files. #### event: 'filechooser' -- <[Object]> - - `element` <[ElementHandle]> handle to the input element that was clicked - - `multiple` <[boolean]> Whether file chooser allow for [multiple](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#attr-multiple) file selection. +- <[FileChooser]> -Emitted when a file chooser is supposed to appear, such as after clicking the ``. Playwright can respond to it via setting the input files using [`elementHandle.setInputFiles`](#elementhandlesetinputfilesfiles-options) which can be uploaded in the end. +Emitted when a file chooser is supposed to appear, such as after clicking the ``. Playwright can respond to it via setting the input files using [`fileChooser.setFiles`](#filechoosersetfilesfiles-options) that can be uploaded after that. ```js -page.on('filechooser', async ({element, multiple}) => { - await element.setInputFiles('/tmp/myfile.pdf'); +page.on('filechooser', async (fileChooser) => { + await fileChooser.setFiles('/tmp/myfile.pdf'); }); ``` @@ -1592,6 +1592,27 @@ The extra HTTP headers will be sent with every request the page initiates. > **NOTE** page.setExtraHTTPHeaders does not guarantee the order of headers in the outgoing requests. +#### page.setInputFiles(selector, files[, options]) +- `selector` <[string]> A selector to search for element to click. If there are multiple elements satisfying the selector, the first will be clicked. +- `files` <[string]|[Array]<[string]>|[Object]|[Array]<[Object]>> + - `name` <[string]> [File] name **required** + - `mimeType` <[string]> [File] type **required** + - `buffer` <[Buffer]> File content **required** +- `options` <[Object]> + - `waitUntil` <"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|"nowait"> Actions that cause navigations are waiting for those navigations to fire `domcontentloaded` by default. This behavior can be changed to either wait for another load phase or to omit the waiting altogether using `nowait`: + - `'domcontentloaded'` - consider navigation to be finished when the `DOMContentLoaded` event is fired. + - `'load'` - consider navigation to be finished when the `load` event is fired. + - `'networkidle0'` - consider navigation to be finished when there are no more than 0 network connections for at least `500` ms. + - `'networkidle2'` - consider navigation to be finished when there are no more than 2 network connections for at least `500` ms. + - `'nowait'` - do not wait. + - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. +- returns: <[Promise]> + +This method expects `selector` to point to an [input element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). + +Sets the value of the file input to these file paths or files. If some of the `filePaths` are relative paths, then they are resolved relative to the [current working directory](https://nodejs.org/api/process.html#process_process_cwd). For empty array, clears the selected files. + + #### page.setViewportSize(viewportSize) - `viewportSize` <[Object]> - `width` <[number]> page width in pixels. **required** @@ -1936,6 +1957,7 @@ An example of getting text from an iframe element: - [frame.press(selector, key[, options])](#framepressselector-key-options) - [frame.selectOption(selector, values[, options])](#frameselectoptionselector-values-options) - [frame.setContent(html[, options])](#framesetcontenthtml-options) +- [frame.setInputFiles(selector, files[, options])](#framesetinputfilesselector-files-options) - [frame.title()](#frametitle) - [frame.type(selector, text[, options])](#frametypeselector-text-options) - [frame.uncheck(selector, [options])](#frameuncheckselector-options) @@ -2320,6 +2342,26 @@ frame.selectOption('select#colors', 'red', 'green', 'blue'); - `'networkidle2'` - consider setting content to be finished when there are no more than 2 network connections for at least `500` ms. - returns: <[Promise]> +#### frame.setInputFiles(selector, files[, options]) +- `selector` <[string]> A selector to search for element to click. If there are multiple elements satisfying the selector, the first will be clicked. +- `files` <[string]|[Array]<[string]>|[Object]|[Array]<[Object]>> + - `name` <[string]> [File] name **required** + - `mimeType` <[string]> [File] type **required** + - `buffer` <[Buffer]> File content **required** +- `options` <[Object]> + - `waitUntil` <"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|"nowait"> Actions that cause navigations are waiting for those navigations to fire `domcontentloaded` by default. This behavior can be changed to either wait for another load phase or to omit the waiting altogether using `nowait`: + - `'domcontentloaded'` - consider navigation to be finished when the `DOMContentLoaded` event is fired. + - `'load'` - consider navigation to be finished when the `load` event is fired. + - `'networkidle0'` - consider navigation to be finished when there are no more than 0 network connections for at least `500` ms. + - `'networkidle2'` - consider navigation to be finished when there are no more than 2 network connections for at least `500` ms. + - `'nowait'` - do not wait. + - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. +- returns: <[Promise]> + +This method expects `selector` to point to an [input element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). + +Sets the value of the file input to these file paths or files. If some of the `filePaths` are relative paths, then they are resolved relative to the [current working directory](https://nodejs.org/api/process.html#process_process_cwd). For empty array, clears the selected files. + #### frame.title() - returns: <[Promise]<[string]>> The page's title. @@ -2836,8 +2878,8 @@ This method focuses the element and selects all its text content. #### elementHandle.setInputFiles(files[, options]) - `files` <[string]|[Array]<[string]>|[Object]|[Array]<[Object]>> - `name` <[string]> [File] name **required** - - `type` <[string]> [File] type **required** - - `data` <[string]> Base64-encoded data **required** + - `mimeType` <[string]> [File] type **required** + - `buffer` <[Buffer]> File content **required** - `options` <[Object]> - `waitUntil` <"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|"nowait"> Actions that cause navigations are waiting for those navigations to fire `domcontentloaded` by default. This behavior can be changed to either wait for another load phase or to omit the waiting altogether using `nowait`: - `'domcontentloaded'` - consider navigation to be finished when the `DOMContentLoaded` event is fired. @@ -2850,7 +2892,7 @@ This method focuses the element and selects all its text content. This method expects `elementHandle` to point to an [input element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). -Sets the value of the file input to these file paths or files. If some of the `filePaths` are relative paths, then they are resolved relative to the [current working directory](https://nodejs.org/api/process.html#process_process_cwd). For empty array, clears the selected files. +Sets the value of the file input to these file paths or files. If some of the `filePaths` are relative paths, then they are resolved relative to the [current working directory](https://nodejs.org/api/process.html#process_process_cwd). For empty array, clears the selected files. #### elementHandle.textContent() - returns: <[Promise]> Resolves to the `node.textContent`. @@ -3122,6 +3164,55 @@ Returns path to the downloaded file in case of successful download. Returns downloaded url. +### class: FileChooser + +[FileChooser] objects are dispatched by the page in the ['filechooser'](#event-filechooser) event. + +```js +page.on('filechooser', async (fileChooser) => { + await fileChooser.setFiles('/tmp/myfile.pdf'); +}); +``` + + +- [fileChooser.element()](#filechooserelement) +- [fileChooser.isMultiple()](#filechooserismultiple) +- [fileChooser.page()](#filechooserpage) +- [fileChooser.setFiles(files[, options])](#filechoosersetfilesfiles-options) + + +#### fileChooser.element() +- returns: <[ElementHandle]> + +Returns input element associated with this file chooser. + +#### fileChooser.isMultiple() +- returns: <[boolean]> + +Returns whether this file chooser accepts multiple files. + +#### fileChooser.page() +- returns: <[Page]> + +Returns page this file chooser belongs to. + +#### fileChooser.setFiles(files[, options]) +- `files` <[string]|[Array]<[string]>|[Object]|[Array]<[Object]>> + - `name` <[string]> [File] name **required** + - `mimeType` <[string]> [File] type **required** + - `buffer` <[Buffer]> File content **required** +- `options` <[Object]> + - `waitUntil` <"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|"nowait"> Actions that cause navigations are waiting for those navigations to fire `domcontentloaded` by default. This behavior can be changed to either wait for another load phase or to omit the waiting altogether using `nowait`: + - `'domcontentloaded'` - consider navigation to be finished when the `DOMContentLoaded` event is fired. + - `'load'` - consider navigation to be finished when the `load` event is fired. + - `'networkidle0'` - consider navigation to be finished when there are no more than 0 network connections for at least `500` ms. + - `'networkidle2'` - consider navigation to be finished when there are no more than 2 network connections for at least `500` ms. + - `'nowait'` - do not wait. + - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. +- returns: <[Promise]> + +Sets the value of the file input this chooser is associated with. If some of the `filePaths` are relative paths, then they are resolved relative to the [current working directory](https://nodejs.org/api/process.html#process_process_cwd). For empty array, clears the selected files. + ### class: Keyboard Keyboard provides an api for managing a virtual keyboard. The high level api is [`keyboard.type`](#keyboardtypetext-options), which takes raw characters and generates proper keydown, keypress/input, and keyup events on your page. diff --git a/docs/uploadDownload.md b/docs/uploadDownload.md new file mode 100644 index 0000000000..2aff42a2c6 --- /dev/null +++ b/docs/uploadDownload.md @@ -0,0 +1,102 @@ +# Uploading and downloading files + +## Upload a file + +```js +// + +await page.setInputFiles('input#upload', 'myfile.pdf'); +``` + +You can select input files for upload using the `page.setInputFiles` method. It expects first arcument to point to an [input element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) with the type `"file"`. Multiple files can be passed in the array. If some of the file paths are relative, they are resolved relative to the [current working directory](https://nodejs.org/api/process.html#process_process_cwd). Empty array clears the selected files. + +#### Variations + +```js +// Select multiple files. +page.setInputFiles('input#upload', ['file1.txt', 'file2.txt']); + +// Upload buffer from memory, without reading from file. +page.setInputFiles('input#upload', { + name: 'file.txt', + mimeType: 'text/plain', + buffer: Buffer.from('this is test') +}); + +// Remove all the selected files +page.setInputFiles('input#upload', []); +``` + +#### API reference + +- [`page.setInputFiles(selector, files[, options])`](https://github.com/microsoft/playwright/blob/master/docs/api.md#pagesetinputfilesselector-value-options) +- [`frame.setInputFiles(selector, files[, options])`](https://github.com/microsoft/playwright/blob/master/docs/api.md#framesetinputfilesselector-value-options) +- [`elementHandle.setInputFiles(files[, options])`](https://github.com/microsoft/playwright/blob/master/docs/api.md#elementhandlesetinputfilesfiles-options) + +
+
+ +## Uploading file using dynamic input element + +Sometimes element that picks files appears dynamically. When this happens, [`"filechooser"`](https://github.com/microsoft/playwright/blob/master/docs/api.md#event-filechooser) event is emitted on the page. It contains the [`FileChooser`](https://github.com/microsoft/playwright/blob/master/docs/api.md#class-filechooser) object that can be used to select files: + +```js +const [ fileChooser ] = await Promise.all([ + page.waitForEvent('filechooser'), // <-- start waiting for the file chooser + page.click('button#delayed-select-files') // <-- perform the action that directly or indirectly initiates it. +]); +// Now that both operations resolved, we can use the returned value to select files. +await fileChooser.setFiles(['file1.txt', 'file2.txt']) +``` + +#### Variations + +If you have no idea what invokes the file chooser, you can still handle the event and select files from it: + +```js +page.on('filechooser', async (fileChooser) => { + await fileChooser.setFiles(['file1.txt', 'file2.txt']); +}); +``` + +Note that handling the event forks the control flow and makes script harder to follow. Your scenario might end while you are setting the files since your main control flow is not awaiting for this operation to resolve. + +#### API reference + +- [`FileChooser`](https://github.com/microsoft/playwright/blob/master/docs/api.md#class-filechooser) +- [`page.on('filechooser')`](https://github.com/microsoft/playwright/blob/master/docs/api.md#event-filechooser) +- [`page.waitForEvent(event)`](https://github.com/microsoft/playwright/blob/master/docs/api.md##pagewaitforeventevent-optionsorpredicate) + +
+
+ +## Handle file downloads + +```js +const [ dowload ] = await Promise.all([ + page.waitForEvent('dowload'), // <-- start waiting for the download + page.click('button#delayed-dowload') // <-- perform the action that directly or indirectly initiates it. +]); +const path = await download.path(); +``` + +For every attachment downloaded by the page, [`"download"`](https://github.com/microsoft/playwright/blob/master/docs/api.md#event-download) event is emitted. If you create a browser context with the `acceptDownloads: true`, all these attachments are going to be downloaded into a temporary folder. You can obtain the download url, file system path and payload stream using the [`Download`](https://github.com/microsoft/playwright/blob/master/docs/api.md#class-download) object from the event. + +#### Variations + +If you have no idea what initiates the download, you can still handle the event: + +```js +page.on('download', download => download.path().then(console.log)); +``` + +Note that handling the event forks the control flow and makes script harder to follow. Your scenario might end while you are downloading a file since your main control flow is not awaiting for this operation to resolve. + +#### API reference + +- [`Download`](https://github.com/microsoft/playwright/blob/master/docs/api.md#class-download) +- [`page.on('download')`](https://github.com/microsoft/playwright/blob/master/docs/api.md#event-download) +- [`page.waitForEvent(event)`](https://github.com/microsoft/playwright/blob/master/docs/api.md##pagewaitforeventevent-optionsorpredicate) + +
+
\ No newline at end of file diff --git a/src/api.ts b/src/api.ts index 070e7b5d74..08a49bcc5b 100644 --- a/src/api.ts +++ b/src/api.ts @@ -21,12 +21,13 @@ export { ConsoleMessage } from './console'; export { Dialog } from './dialog'; export { Download } from './download'; export { ElementHandle } from './dom'; +export { FileChooser } from './fileChooser'; export { TimeoutError } from './errors'; export { Frame } from './frames'; export { Keyboard, Mouse } from './input'; export { JSHandle } from './javascript'; export { Request, Response, Route } from './network'; -export { FileChooser, Page, Worker } from './page'; +export { Page, Worker } from './page'; export { Selectors } from './selectors'; export { CRBrowser as ChromiumBrowser } from './chromium/crBrowser'; diff --git a/src/chromium/crPage.ts b/src/chromium/crPage.ts index c64250f828..c8e6de810e 100644 --- a/src/chromium/crPage.ts +++ b/src/chromium/crPage.ts @@ -265,7 +265,8 @@ export class CRPage implements PageDelegate { } async setInputFiles(handle: dom.ElementHandle, files: types.FilePayload[]): Promise { - await handle.evaluate(dom.setFileInputFunction, files); + await handle._evaluateInUtility(({ injected, node }, files) => + injected.setInputFiles(node, files), dom.toFileTransferPayload(files)); } async adoptElementHandle(handle: dom.ElementHandle, to: dom.FrameExecutionContext): Promise> { diff --git a/src/dom.ts b/src/dom.ts index f8b60513b2..e4b2e0ba2c 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -315,8 +315,8 @@ export class ElementHandle extends js.JSHandle { if (typeof item === 'string') { const file: types.FilePayload = { name: path.basename(item), - type: mime.getType(item) || 'application/octet-stream', - data: await util.promisify(fs.readFile)(item, 'base64') + mimeType: mime.getType(item) || 'application/octet-stream', + buffer: await util.promisify(fs.readFile)(item) }; filePayloads.push(file); } else { @@ -439,15 +439,10 @@ export class ElementHandle extends js.JSHandle { } } -export const setFileInputFunction = async (element: HTMLInputElement, payloads: types.FilePayload[]) => { - const files = await Promise.all(payloads.map(async (file: types.FilePayload) => { - const result = await fetch(`data:${file.type};base64,${file.data}`); - return new File([await result.blob()], file.name, {type: file.type}); +export function toFileTransferPayload(files: types.FilePayload[]): types.FileTransferPayload[] { + return files.map(file => ({ + name: file.name, + type: file.mimeType, + data: file.buffer.toString('base64') })); - const dt = new DataTransfer(); - for (const file of files) - dt.items.add(file); - element.files = dt.files; - element.dispatchEvent(new Event('input', { 'bubbles': true })); - element.dispatchEvent(new Event('change', { 'bubbles': true })); -}; +} diff --git a/src/fileChooser.ts b/src/fileChooser.ts new file mode 100644 index 0000000000..6ab8617f9f --- /dev/null +++ b/src/fileChooser.ts @@ -0,0 +1,47 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ElementHandle } from './dom'; +import { Page } from './page'; +import * as types from './types'; + +export class FileChooser { + private _page: Page; + private _elementHandle: ElementHandle; + private _isMultiple: boolean; + + constructor(page: Page, elementHandle: ElementHandle, isMultiple: boolean) { + this._page = page; + this._elementHandle = elementHandle; + this._isMultiple = isMultiple; + } + + element(): ElementHandle { + return this._elementHandle; + } + + isMultiple(): boolean { + return this._isMultiple; + } + + page(): Page { + return this._page; + } + + async setFiles(files: string | types.FilePayload | string[] | types.FilePayload[], options?: types.NavigatingActionWaitOptions) { + return this._elementHandle.setInputFiles(files, options); + } +} diff --git a/src/firefox/ffPage.ts b/src/firefox/ffPage.ts index d14dc964ff..ca0f7a890a 100644 --- a/src/firefox/ffPage.ts +++ b/src/firefox/ffPage.ts @@ -433,7 +433,8 @@ export class FFPage implements PageDelegate { } async setInputFiles(handle: dom.ElementHandle, files: types.FilePayload[]): Promise { - await handle.evaluate(dom.setFileInputFunction, files); + await handle._evaluateInUtility(({ injected, node }, files) => + injected.setInputFiles(node, files), dom.toFileTransferPayload(files)); } async adoptElementHandle(handle: dom.ElementHandle, to: dom.FrameExecutionContext): Promise> { diff --git a/src/frames.ts b/src/frames.ts index e882718d89..e7aab1384c 100644 --- a/src/frames.ts +++ b/src/frames.ts @@ -718,6 +718,13 @@ export class Frame { return result; } + async setInputFiles(selector: string, files: string | types.FilePayload | string[] | types.FilePayload[], options?: types.NavigatingActionWaitOptions): Promise { + const { handle, deadline } = await this._waitForSelectorInUtilityContext(selector, options); + const result = await handle.setInputFiles(files, helper.optionsWithUpdatedTimeout(options, deadline)); + handle.dispose(); + return result; + } + async type(selector: string, text: string, options?: { delay?: number } & types.NavigatingActionWaitOptions) { const { handle, deadline } = await this._waitForSelectorInUtilityContext(selector, options); await handle.type(text, helper.optionsWithUpdatedTimeout(options, deadline)); diff --git a/src/injected/injected.ts b/src/injected/injected.ts index 80e822cff3..7f781b6405 100644 --- a/src/injected/injected.ts +++ b/src/injected/injected.ts @@ -248,6 +248,29 @@ class Injected { throw new Error('Not a checkbox'); } + async setInputFiles(node: Node, payloads: types.FileTransferPayload[]) { + if (node.nodeType !== Node.ELEMENT_NODE) + return 'Node is not of type HTMLElement'; + const element: Element | undefined = node as Element; + if (element.nodeName !== 'INPUT') + return 'Not an element'; + const input = element as HTMLInputElement; + const type = (input.getAttribute('type') || '').toLowerCase(); + if (type !== 'file') + return 'Not an input[type=file] element'; + + const files = await Promise.all(payloads.map(async file => { + const result = await fetch(`data:${file.type};base64,${file.data}`); + return new File([await result.blob()], file.name, {type: file.type}); + })); + const dt = new DataTransfer(); + for (const file of files) + dt.items.add(file); + input.files = dt.files; + input.dispatchEvent(new Event('input', { 'bubbles': true })); + input.dispatchEvent(new Event('change', { 'bubbles': true })); + } + async waitForDisplayedAtStablePosition(node: Node, timeout: number) { if (!node.isConnected) throw new Error('Element is not attached to the DOM'); diff --git a/src/page.ts b/src/page.ts index d2870da2e0..4d0772e40a 100644 --- a/src/page.ts +++ b/src/page.ts @@ -30,6 +30,7 @@ import { ConsoleMessage, ConsoleMessageLocation } from './console'; import * as accessibility from './accessibility'; import { ExtendedEventEmitter } from './extendedEventEmitter'; import { EventEmitter } from 'events'; +import { FileChooser } from './fileChooser'; export interface PageDelegate { readonly rawMouse: input.RawMouse; @@ -83,11 +84,6 @@ type PageState = { extraHTTPHeaders: network.Headers | null; }; -export type FileChooser = { - element: dom.ElementHandle, - multiple: boolean -}; - export class Page extends ExtendedEventEmitter { private _closed = false; private _closedCallback: () => void; @@ -174,7 +170,7 @@ export class Page extends ExtendedEventEmitter { handle.dispose(); return; } - const fileChooser: FileChooser = { element: handle, multiple }; + const fileChooser = new FileChooser(this, handle, multiple); this.emit(Events.Page.FileChooser, fileChooser); } @@ -458,6 +454,10 @@ export class Page extends ExtendedEventEmitter { return this.mainFrame().selectOption(selector, values, options); } + async setInputFiles(selector: string, files: string | types.FilePayload | string[] | types.FilePayload[], options?: types.NavigatingActionWaitOptions): Promise { + return this.mainFrame().setInputFiles(selector, files, options); + } + async type(selector: string, text: string, options?: { delay?: number } & types.NavigatingActionWaitOptions) { return this.mainFrame().type(selector, text, options); } diff --git a/src/types.ts b/src/types.ts index 79a82dd411..6d3c275f6f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -94,6 +94,12 @@ export type SelectOption = { }; export type FilePayload = { + name: string, + mimeType: string, + buffer: Buffer, +}; + +export type FileTransferPayload = { name: string, type: string, data: string, diff --git a/src/webkit/wkPage.ts b/src/webkit/wkPage.ts index c2be00600a..94d126e071 100644 --- a/src/webkit/wkPage.ts +++ b/src/webkit/wkPage.ts @@ -713,7 +713,7 @@ export class WKPage implements PageDelegate { async setInputFiles(handle: dom.ElementHandle, files: types.FilePayload[]): Promise { const objectId = toRemoteObject(handle).objectId!; - await this._session.send('DOM.setInputFiles', { objectId, files }); + await this._session.send('DOM.setInputFiles', { objectId, files: dom.toFileTransferPayload(files) }); } async adoptElementHandle(handle: dom.ElementHandle, to: dom.FrameExecutionContext): Promise> { diff --git a/test/input.spec.js b/test/input.spec.js index 3eed30a7be..00cb2fec08 100644 --- a/test/input.spec.js +++ b/test/input.spec.js @@ -38,6 +38,25 @@ describe('input', function() { }); }); +describe('Page.setInputFiles', function() { + it('should work', async({page}) => { + await page.setContent(``); + await page.setInputFiles('input', path.join(__dirname, '/assets/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', { + name: 'test.txt', + mimeType: 'text/plain', + buffer: Buffer.from('this is a test') + }); + expect(await page.$eval('input', input => input.files.length)).toBe(1); + expect(await page.$eval('input', input => input.files[0].name)).toBe('test.txt'); + }); +}); + describe('Page.waitForFileChooser', function() { it('should emit event', async({page, server}) => { await page.setContent(``); @@ -105,11 +124,13 @@ describe('Page.waitForFileChooser', function() { }); it('should accept single file', async({page, server}) => { await page.setContent(``); - const [{ element }] = await Promise.all([ + const [fileChooser] = await Promise.all([ page.waitForEvent('filechooser'), page.click('input'), ]); - await element.setInputFiles(FILE_TO_UPLOAD); + expect(fileChooser.page()).toBe(page); + expect(fileChooser.element()).toBeTruthy(); + await fileChooser.setFiles(FILE_TO_UPLOAD); 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'); }); @@ -151,7 +172,7 @@ describe('Page.waitForFileChooser', function() { it('should be able to read selected file', async({page, server}) => { await page.setContent(``); const [, content] = await Promise.all([ - page.waitForEvent('filechooser').then(({element}) => element.setInputFiles(FILE_TO_UPLOAD)), + page.waitForEvent('filechooser').then(fileChooser => fileChooser.setFiles(FILE_TO_UPLOAD)), page.$eval('input', async picker => { picker.click(); await new Promise(x => picker.oninput = x); @@ -166,7 +187,7 @@ describe('Page.waitForFileChooser', function() { it('should be able to reset selected files with empty file list', async({page, server}) => { await page.setContent(``); const [, fileLength1] = await Promise.all([ - page.waitForEvent('filechooser').then(({element}) => element.setInputFiles(FILE_TO_UPLOAD)), + page.waitForEvent('filechooser').then(fileChooser => fileChooser.setFiles(FILE_TO_UPLOAD)), page.$eval('input', async picker => { picker.click(); await new Promise(x => picker.oninput = x); @@ -175,7 +196,7 @@ describe('Page.waitForFileChooser', function() { ]); expect(fileLength1).toBe(1); const [, fileLength2] = await Promise.all([ - page.waitForEvent('filechooser').then(({element}) => element.setInputFiles([])), + page.waitForEvent('filechooser').then(fileChooser => fileChooser.setFiles([])), page.$eval('input', async picker => { picker.click(); await new Promise(x => picker.oninput = x); @@ -186,12 +207,12 @@ describe('Page.waitForFileChooser', function() { }); it('should not accept multiple files for single-file input', async({page, server}) => { await page.setContent(``); - const [{ element }] = await Promise.all([ + const [fileChooser] = await Promise.all([ page.waitForEvent('filechooser'), page.click('input'), ]); let error = null; - await element.setInputFiles([ + await fileChooser.setFiles([ path.relative(process.cwd(), __dirname + '/assets/file-to-upload.txt'), path.relative(process.cwd(), __dirname + '/assets/pptr.png') ]).catch(e => error = e); @@ -216,26 +237,26 @@ describe('Page.waitForFileChooser', function() { describe('Page.waitForFileChooser isMultiple', () => { it('should work for single file pick', async({page, server}) => { await page.setContent(``); - const [{ multiple }] = await Promise.all([ + const [fileChooser] = await Promise.all([ page.waitForEvent('filechooser'), page.click('input'), ]); - expect(multiple).toBe(false); + expect(fileChooser.isMultiple()).toBe(false); }); it('should work for "multiple"', async({page, server}) => { await page.setContent(``); - const [{ multiple }] = await Promise.all([ + const [fileChooser] = await Promise.all([ page.waitForEvent('filechooser'), page.click('input'), ]); - expect(multiple).toBe(true); + expect(fileChooser.isMultiple()).toBe(true); }); it('should work for "webkitdirectory"', async({page, server}) => { await page.setContent(``); - const [{ multiple }] = await Promise.all([ + const [fileChooser] = await Promise.all([ page.waitForEvent('filechooser'), page.click('input'), ]); - expect(multiple).toBe(true); + expect(fileChooser.isMultiple()).toBe(true); }); });