Skip to content

Commit

Permalink
api(setInputFiles): introduce page/frame helpers, document, break com…
Browse files Browse the repository at this point in the history
…pat (#1818)
  • Loading branch information
pavelfeldman committed Apr 16, 2020
1 parent 58bb874 commit 2280126
Show file tree
Hide file tree
Showing 13 changed files with 340 additions and 45 deletions.
109 changes: 100 additions & 9 deletions docs/api.md
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 `<input type=file>`. 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 `<input type=file>`. 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');
});
```

Expand Down Expand Up @@ -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**
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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.
Expand All @@ -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]<null|[string]>> Resolves to the `node.textContent`.
Expand Down Expand Up @@ -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');
});
```

<!-- GEN:toc -->
- [fileChooser.element()](#filechooserelement)
- [fileChooser.isMultiple()](#filechooserismultiple)
- [fileChooser.page()](#filechooserpage)
- [fileChooser.setFiles(files[, options])](#filechoosersetfilesfiles-options)
<!-- GEN:stop -->

#### 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.
Expand Down
102 changes: 102 additions & 0 deletions docs/uploadDownload.md
@@ -0,0 +1,102 @@
# Uploading and downloading files

## Upload a file

```js
// <input id=upload type=file>

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)

<br/>
<br/>

## 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)

<br/>
<br/>

## 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)

<br/>
<br/>
3 changes: 2 additions & 1 deletion src/api.ts
Expand Up @@ -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';
Expand Down
3 changes: 2 additions & 1 deletion src/chromium/crPage.ts
Expand Up @@ -265,7 +265,8 @@ export class CRPage implements PageDelegate {
}

async setInputFiles(handle: dom.ElementHandle<HTMLInputElement>, files: types.FilePayload[]): Promise<void> {
await handle.evaluate(dom.setFileInputFunction, files);
await handle._evaluateInUtility(({ injected, node }, files) =>
injected.setInputFiles(node, files), dom.toFileTransferPayload(files));
}

async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> {
Expand Down
21 changes: 8 additions & 13 deletions src/dom.ts
Expand Up @@ -315,8 +315,8 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
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 {
Expand Down Expand Up @@ -439,15 +439,10 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}
}

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

0 comments on commit 2280126

Please sign in to comment.