From ea28cccfe021a22d1772ee4abfdf67d646aa6c17 Mon Sep 17 00:00:00 2001 From: Andrey Lushnikov Date: Mon, 22 Jul 2019 21:30:49 -0700 Subject: [PATCH] feat(page): introduce file chooser interception (#4653) This patch introduces a page.waitForFileChooser() method that adds a watchdog to wait for file chooser dialogs. This lets Puppeteer users to capture file chooser requests and fulfill/cancel them if necessary. Fixes #2946 --- docs/api.md | 138 +++++++++++++++++++-------- lib/Page.js | 102 ++++++++++++++++++-- lib/api.js | 1 + lib/helper.js | 7 +- test/chromiumonly.spec.js | 17 ++++ test/input.spec.js | 191 +++++++++++++++++++++++++++++++++++++- 6 files changed, 405 insertions(+), 51 deletions(-) diff --git a/docs/api.md b/docs/api.md index 604527679fd..d0324f39794 100644 --- a/docs/api.md +++ b/docs/api.md @@ -152,6 +152,7 @@ * [page.url()](#pageurl) * [page.viewport()](#pageviewport) * [page.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])](#pagewaitforselectororfunctionortimeout-options-args) + * [page.waitForFileChooser([options])](#pagewaitforfilechooseroptions) * [page.waitForFunction(pageFunction[, options[, ...args]])](#pagewaitforfunctionpagefunction-options-args) * [page.waitForNavigation([options])](#pagewaitfornavigationoptions) * [page.waitForRequest(urlOrPredicate[, options])](#pagewaitforrequesturlorpredicate-options) @@ -182,6 +183,10 @@ - [class: Tracing](#class-tracing) * [tracing.start([options])](#tracingstartoptions) * [tracing.stop()](#tracingstop) +- [class: FileChooser](#class-filechooser) + * [fileChooser.accept(filePaths)](#filechooseracceptfilepaths) + * [fileChooser.cancel()](#filechoosercancel) + * [fileChooser.isMultiple()](#filechooserismultiple) - [class: Dialog](#class-dialog) * [dialog.accept([promptText])](#dialogacceptprompttext) * [dialog.defaultValue()](#dialogdefaultvalue) @@ -1727,6 +1732,7 @@ This setting will change the default maximum time for the following methods and - [page.reload([options])](#pagereloadoptions) - [page.setContent(html[, options])](#pagesetcontenthtml-options) - [page.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])](#pagewaitforselectororfunctionortimeout-options-args) +- [page.waitForFileChooser([options])](#pagewaitforfilechooseroptions) - [page.waitForFunction(pageFunction[, options[, ...args]])](#pagewaitforfunctionpagefunction-options-args) - [page.waitForNavigation([options])](#pagewaitfornavigationoptions) - [page.waitForRequest(urlOrPredicate[, options])](#pagewaitforrequesturlorpredicate-options) @@ -1914,6 +1920,28 @@ await page.waitFor(selector => !!document.querySelector(selector), {}, selector) Shortcut for [page.mainFrame().waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])](#framewaitforselectororfunctionortimeout-options-args). +#### page.waitForFileChooser([options]) +- `options` <[Object]> Optional waiting parameters + - `timeout` <[number]> Maximum wait time in milliseconds, defaults to 30 seconds, pass `0` to disable the timeout. The default value can be changed by using the [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) method. +- returns: <[Promise]<[FileChooser]>> A promise that resolves after a page requests a file picker. + +> **NOTE** In non-headless Chromium, this method results in the native file picker dialog **not showing up** for the user. + +This method is typically coupled with an action that triggers file choosing. +The following example clicks a button that issues a file chooser, and then +responds with `/tmp/myfile.pdf` as if a user has selected this file. + +```js +const [fileChooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('#upload-file-button'), // some button that triggers file selection +]); +await fileChooser.accept(['/tmp/myfile.pdf']); +``` + +> **NOTE** This must be called *before* the file chooser is launched. It will not return a currently active file chooser. + + #### page.waitForFunction(pageFunction[, options[, ...args]]) - `pageFunction` <[function]|[string]> Function to be evaluated in browser context - `options` <[Object]> Optional waiting parameters @@ -2351,6 +2379,37 @@ Only one trace can be active at a time per browser. #### tracing.stop() - returns: <[Promise]<[Buffer]>> Promise which resolves to buffer with trace data. +### class: FileChooser + +[FileChooser] objects are returned via the ['page.waitForFileChooser'](#pagewaitforfilechooseroptions) method. + +File choosers let you react to the page requesting for a file. + +An example of using [FileChooser]: + +```js +const [fileChooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('#upload-file-button'), // some button that triggers file selection +]); +await fileChooser.accept(['/tmp/myfile.pdf']); +``` + +> **NOTE** In browsers, only one file chooser can be opened at a time. +> All file choosers must be accepted or canceled. Not doing so will prevent subsequent file choosers from appearing. + +#### fileChooser.accept(filePaths) +- `filePaths` <[Array]<[string]>> Accept the file chooser request with given paths. 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). +- returns: <[Promise]> + +#### fileChooser.cancel() +- returns: <[Promise]> + +Closes the file chooser without selecting any files. + +#### fileChooser.isMultiple() +- returns: <[boolean]> Whether file chooser allow for [multiple](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#attr-multiple) file selection. + ### class: Dialog [Dialog] objects are dispatched by page via the ['dialog'](#event-dialog) event. @@ -3626,50 +3685,51 @@ TimeoutError is emitted whenever certain operations are terminated due to timeou +[AXNode]: #accessibilitysnapshotoptions "AXNode" +[Accessibility]: #class-accessibility "Accessibility" [Array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array "Array" -[boolean]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type "Boolean" +[Body]: #class-body "Body" +[BrowserContext]: #class-browsercontext "BrowserContext" +[BrowserFetcher]: #class-browserfetcher "BrowserFetcher" +[Browser]: #class-browser "Browser" [Buffer]: https://nodejs.org/api/buffer.html#buffer_class_buffer "Buffer" -[function]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function "Function" -[number]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type "Number" +[CDPSession]: #class-cdpsession "CDPSession" +[ChildProcess]: https://nodejs.org/api/child_process.html "ChildProcess" +[ConnectionTransport]: ../lib/WebSocketTransport.js "ConnectionTransport" +[ConsoleMessage]: #class-consolemessage "ConsoleMessage" +[Coverage]: #class-coverage "Coverage" +[Dialog]: #class-dialog "Dialog" +[ElementHandle]: #class-elementhandle "ElementHandle" +[Element]: https://developer.mozilla.org/en-US/docs/Web/API/element "Element" +[Error]: https://nodejs.org/api/errors.html#errors_class_error "Error" +[ExecutionContext]: #class-executioncontext "ExecutionContext" +[FileChooser]: #class-filechooser "FileChooser" +[Frame]: #class-frame "Frame" +[JSHandle]: #class-jshandle "JSHandle" +[Keyboard]: #class-keyboard "Keyboard" +[Map]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map "Map" +[Mouse]: #class-mouse "Mouse" [Object]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object "Object" -[origin]: https://developer.mozilla.org/en-US/docs/Glossary/Origin "Origin" [Page]: #class-page "Page" [Promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise "Promise" -[string]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type "String" -[stream.Readable]: https://nodejs.org/api/stream.html#stream_class_stream_readable "stream.Readable" -[CDPSession]: #class-cdpsession "CDPSession" -[BrowserFetcher]: #class-browserfetcher "BrowserFetcher" -[BrowserContext]: #class-browsercontext "BrowserContext" -[Error]: https://nodejs.org/api/errors.html#errors_class_error "Error" -[Frame]: #class-frame "Frame" -[ConsoleMessage]: #class-consolemessage "ConsoleMessage" -[ChildProcess]: https://nodejs.org/api/child_process.html "ChildProcess" -[Coverage]: #class-coverage "Coverage" -[iterator]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols "Iterator" -[Response]: #class-response "Response" [Request]: #class-request "Request" -[Browser]: #class-browser "Browser" -[TimeoutError]: #class-timeouterror "TimeoutError" -[Body]: #class-body "Body" -[Element]: https://developer.mozilla.org/en-US/docs/Web/API/element "Element" -[Keyboard]: #class-keyboard "Keyboard" -[Dialog]: #class-dialog "Dialog" -[JSHandle]: #class-jshandle "JSHandle" -[ExecutionContext]: #class-executioncontext "ExecutionContext" -[Mouse]: #class-mouse "Mouse" -[Map]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map "Map" -[selector]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors "selector" -[Tracing]: #class-tracing "Tracing" -[ElementHandle]: #class-elementhandle "ElementHandle" -[UIEvent.detail]: https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail "UIEvent.detail" -[Serializable]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Description "Serializable" -[Touchscreen]: #class-touchscreen "Touchscreen" -[Target]: #class-target "Target" -[USKeyboardLayout]: ../lib/USKeyboardLayout.js "USKeyboardLayout" -[xpath]: https://developer.mozilla.org/en-US/docs/Web/XPath "xpath" -[UnixTime]: https://en.wikipedia.org/wiki/Unix_time "Unix Time" +[Response]: #class-response "Response" [SecurityDetails]: #class-securitydetails "SecurityDetails" +[Serializable]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Description "Serializable" +[Target]: #class-target "Target" +[TimeoutError]: #class-timeouterror "TimeoutError" +[Touchscreen]: #class-touchscreen "Touchscreen" +[Tracing]: #class-tracing "Tracing" +[UIEvent.detail]: https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail "UIEvent.detail" +[USKeyboardLayout]: ../lib/USKeyboardLayout.js "USKeyboardLayout" +[UnixTime]: https://en.wikipedia.org/wiki/Unix_time "Unix Time" [Worker]: #class-worker "Worker" -[Accessibility]: #class-accessibility "Accessibility" -[AXNode]: #accessibilitysnapshotoptions "AXNode" -[ConnectionTransport]: ../lib/WebSocketTransport.js "ConnectionTransport" +[boolean]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type "Boolean" +[function]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function "Function" +[iterator]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols "Iterator" +[number]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type "Number" +[origin]: https://developer.mozilla.org/en-US/docs/Glossary/Origin "Origin" +[selector]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors "selector" +[stream.Readable]: https://nodejs.org/api/stream.html#stream_class_stream_readable "stream.Readable" +[string]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type "String" +[xpath]: https://developer.mozilla.org/en-US/docs/Web/XPath "xpath" diff --git a/lib/Page.js b/lib/Page.js index 80d960dc3b0..0a8acb5f858 100644 --- a/lib/Page.js +++ b/lib/Page.js @@ -15,6 +15,7 @@ */ const fs = require('fs'); +const path = require('path'); const EventEmitter = require('events'); const mime = require('mime'); const {Events} = require('./Events'); @@ -43,12 +44,7 @@ class Page extends EventEmitter { */ static async create(client, target, ignoreHTTPSErrors, defaultViewport, screenshotTaskQueue) { const page = new Page(client, target, ignoreHTTPSErrors, screenshotTaskQueue); - await Promise.all([ - page._frameManager.initialize(), - client.send('Target.setAutoAttach', {autoAttach: true, waitForDebuggerOnStart: false, flatten: true}), - client.send('Performance.enable', {}), - client.send('Log.enable', {}), - ]); + await page._initialize(); if (defaultViewport) await page.setViewport(defaultViewport); return page; @@ -115,6 +111,8 @@ class Page extends EventEmitter { networkManager.on(Events.NetworkManager.Response, event => this.emit(Events.Page.Response, event)); networkManager.on(Events.NetworkManager.RequestFailed, event => this.emit(Events.Page.RequestFailed, event)); networkManager.on(Events.NetworkManager.RequestFinished, event => this.emit(Events.Page.RequestFinished, event)); + this._fileChooserInterceptionIsDisabled = false; + this._fileChooserInterceptors = new Set(); client.on('Page.domContentEventFired', event => this.emit(Events.Page.DOMContentLoaded)); client.on('Page.loadEventFired', event => this.emit(Events.Page.Load)); @@ -125,12 +123,59 @@ class Page extends EventEmitter { client.on('Inspector.targetCrashed', event => this._onTargetCrashed()); client.on('Performance.metrics', event => this._emitMetrics(event)); client.on('Log.entryAdded', event => this._onLogEntryAdded(event)); + client.on('Page.fileChooserOpened', event => this._onFileChooser(event)); this._target._isClosedPromise.then(() => { this.emit(Events.Page.Close); this._closed = true; }); } + async _initialize() { + await Promise.all([ + this._frameManager.initialize(), + this._client.send('Target.setAutoAttach', {autoAttach: true, waitForDebuggerOnStart: false, flatten: true}), + this._client.send('Performance.enable', {}), + this._client.send('Log.enable', {}), + this._client.send('Page.setInterceptFileChooserDialog', {enabled: true}).catch(e => { + this._fileChooserInterceptionIsDisabled = true; + }), + ]); + } + + /** + * @param {!Protocol.Page.fileChooserOpenedPayload} event + */ + _onFileChooser(event) { + if (!this._fileChooserInterceptors.size) { + this._client.send('Page.handleFileChooser', { action: 'fallback' }).catch(debugError); + return; + } + const interceptors = Array.from(this._fileChooserInterceptors); + this._fileChooserInterceptors.clear(); + const fileChooser = new FileChooser(this._client, event); + for (const interceptor of interceptors) + interceptor.call(null, fileChooser); + } + + /** + * @param {!{timeout?: number}=} options + * @return !Promise} + */ + async waitForFileChooser(options = {}) { + if (this._fileChooserInterceptionIsDisabled) + throw new Error('File chooser handling does not work with multiple connections to the same page'); + const { + timeout = this._timeoutSettings.timeout(), + } = options; + let callback; + const promise = new Promise(x => callback = x); + this._fileChooserInterceptors.add(callback); + return helper.waitWithTimeout(promise, 'waiting for file chooser', timeout).catch(e => { + this._fileChooserInterceptors.delete(callback); + throw e; + }); + } + /** * @param {!{longitude: number, latitude: number, accuracy: (number|undefined)}} options */ @@ -1258,5 +1303,48 @@ class ConsoleMessage { } } +class FileChooser { + /** + * @param {Puppeteer.CDPSession} client + * @param {!Protocol.Page.fileChooserOpenedPayload} event + */ + constructor(client, event) { + this._client = client; + this._multiple = event.mode !== 'selectSingle'; + this._handled = false; + } -module.exports = {Page, ConsoleMessage}; + /** + * @return {boolean} + */ + isMultiple() { + return this._multiple; + } + + /** + * @param {!Array} filePaths + * @return {!Promise} + */ + async accept(filePaths) { + assert(!this._handled, 'Cannot accept FileChooser which is already handled!'); + this._handled = true; + const files = filePaths.map(filePath => path.resolve(filePath)); + await this._client.send('Page.handleFileChooser', { + action: 'accept', + files, + }); + } + + /** + * @return {!Promise} + */ + async cancel() { + assert(!this._handled, 'Cannot cancel FileChooser which is already handled!'); + this._handled = true; + await this._client.send('Page.handleFileChooser', { + action: 'cancel', + }); + } +} + +module.exports = {Page, ConsoleMessage, FileChooser}; diff --git a/lib/api.js b/lib/api.js index 135a69eaf3e..afb0acb7653 100644 --- a/lib/api.js +++ b/lib/api.js @@ -25,6 +25,7 @@ module.exports = { Dialog: require('./Dialog').Dialog, ElementHandle: require('./JSHandle').ElementHandle, ExecutionContext: require('./ExecutionContext').ExecutionContext, + FileChooser: require('./Page').FileChooser, Frame: require('./FrameManager').Frame, JSHandle: require('./JSHandle').JSHandle, Keyboard: require('./Input').Keyboard, diff --git a/lib/helper.js b/lib/helper.js index e12d63dbece..217b04df5cc 100644 --- a/lib/helper.js +++ b/lib/helper.js @@ -214,11 +214,14 @@ class Helper { let reject; const timeoutError = new TimeoutError(`waiting for ${taskName} failed: timeout ${timeout}ms exceeded`); const timeoutPromise = new Promise((resolve, x) => reject = x); - const timeoutTimer = setTimeout(() => reject(timeoutError), timeout); + let timeoutTimer = null; + if (timeout) + timeoutTimer = setTimeout(() => reject(timeoutError), timeout); try { return await Promise.race([promise, timeoutPromise]); } finally { - clearTimeout(timeoutTimer); + if (timeoutTimer) + clearTimeout(timeoutTimer); } } diff --git a/test/chromiumonly.spec.js b/test/chromiumonly.spec.js index b19d3942a90..360934f4c5b 100644 --- a/test/chromiumonly.spec.js +++ b/test/chromiumonly.spec.js @@ -93,6 +93,23 @@ module.exports.addLauncherTests = function({testRunner, expect, defaultBrowserOp await disconnectedEventPromise; }); }); + + describe('Page.waitForFileChooser', () => { + it('should fail gracefully when trying to work with filechoosers within multiple connections', async() => { + // 1. Launch a browser and connect to all pages. + const originalBrowser = await puppeteer.launch(defaultBrowserOptions); + await originalBrowser.pages(); + // 2. Connect a remote browser and connect to first page. + const remoteBrowser = await puppeteer.connect({browserWSEndpoint: originalBrowser.wsEndpoint()}); + const [page] = await remoteBrowser.pages(); + // 3. Make sure |page.waitForFileChooser()| does not work with multiclient. + let error = null; + await page.waitForFileChooser().catch(e => error = e); + expect(error.message).toBe('File chooser handling does not work with multiple connections to the same page'); + originalBrowser.close(); + }); + + }); }); }; diff --git a/test/input.spec.js b/test/input.spec.js index aaca2d53279..90610f9b1df 100644 --- a/test/input.spec.js +++ b/test/input.spec.js @@ -16,14 +16,16 @@ const path = require('path'); -module.exports.addTests = function({testRunner, expect}) { - const {describe, xdescribe, fdescribe} = testRunner; +const FILE_TO_UPLOAD = path.join(__dirname, '/assets/file-to-upload.txt'); + +module.exports.addTests = function({testRunner, expect, puppeteer}) { + const {describe, xdescribe, fdescribe, describe_fails_ffox} = testRunner; const {it, fit, xit, it_fails_ffox} = testRunner; const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; describe('input', function() { it('should upload the file', async({page, server}) => { await page.goto(server.PREFIX + '/input/fileupload.html'); - const filePath = path.relative(process.cwd(), __dirname + '/assets/file-to-upload.txt'); + const filePath = path.relative(process.cwd(), FILE_TO_UPLOAD); const input = await page.$('input'); await input.uploadFile(filePath); expect(await page.evaluate(e => e.files[0].name, input)).toBe('file-to-upload.txt'); @@ -35,4 +37,187 @@ module.exports.addTests = function({testRunner, expect}) { }, input)).toBe('contents of the file'); }); }); + + describe_fails_ffox('Page.waitForFileChooser', function() { + it('should work when file input is attached to DOM', async({page, server}) => { + await page.setContent(``); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + expect(chooser).toBeTruthy(); + }); + it('should work when file input is not attached to DOM', async({page, server}) => { + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.evaluate(() => { + const el = document.createElement('input'); + el.type = 'file'; + el.click(); + }), + ]); + expect(chooser).toBeTruthy(); + }); + it('should respect timeout', async({page, server}) => { + let error = null; + await page.waitForFileChooser({timeout: 1}).catch(e => error = e); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should respect default timeout when there is no custom timeout', async({page, server}) => { + page.setDefaultTimeout(1); + let error = null; + await page.waitForFileChooser().catch(e => error = e); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should prioritize exact timeout over default timeout', async({page, server}) => { + page.setDefaultTimeout(0); + let error = null; + await page.waitForFileChooser({timeout: 1}).catch(e => error = e); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should work with no timeout', async({page, server}) => { + const [chooser] = await Promise.all([ + page.waitForFileChooser({timeout: 0}), + page.evaluate(() => setTimeout(() => { + const el = document.createElement('input'); + el.type = 'file'; + el.click(); + }, 50)) + ]); + expect(chooser).toBeTruthy(); + }); + it('should return the same file chooser when there are many watchdogs simultaneously', async({page, server}) => { + await page.setContent(``); + const [fileChooser1, fileChooser2] = await Promise.all([ + page.waitForFileChooser(), + page.waitForFileChooser(), + page.$eval('input', input => input.click()), + ]); + expect(fileChooser1 === fileChooser2).toBe(true); + }); + }); + + describe_fails_ffox('FileChooser.accept', function() { + it('should accept single file', async({page, server}) => { + await page.setContent(``); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + await Promise.all([ + chooser.accept([FILE_TO_UPLOAD]), + new Promise(x => page.once('metrics', x)), + ]); + 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 be able to read selected file', async({page, server}) => { + await page.setContent(``); + page.waitForFileChooser().then(chooser => chooser.accept([FILE_TO_UPLOAD])); + expect(await page.$eval('input', async picker => { + picker.click(); + await new Promise(x => picker.oninput = x); + const reader = new FileReader(); + const promise = new Promise(fulfill => reader.onload = fulfill); + reader.readAsText(picker.files[0]); + return promise.then(() => reader.result); + })).toBe('contents of the file'); + }); + it('should be able to reset selected files with empty file list', async({page, server}) => { + await page.setContent(``); + page.waitForFileChooser().then(chooser => chooser.accept([FILE_TO_UPLOAD])); + expect(await page.$eval('input', async picker => { + picker.click(); + await new Promise(x => picker.oninput = x); + return picker.files.length; + })).toBe(1); + page.waitForFileChooser().then(chooser => chooser.accept([])); + expect(await page.$eval('input', async picker => { + picker.click(); + await new Promise(x => picker.oninput = x); + return picker.files.length; + })).toBe(0); + }); + it('should not accept multiple files for single-file input', async({page, server}) => { + await page.setContent(``); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + let error = null; + await chooser.accept([ + path.relative(process.cwd(), __dirname + '/assets/file-to-upload.txt'), + path.relative(process.cwd(), __dirname + '/assets/pptr.png'), + ]).catch(e => error = e); + expect(error).not.toBe(null); + }); + it('should fail when accepting file chooser twice', async({page, server}) => { + await page.setContent(``); + const [fileChooser] = await Promise.all([ + page.waitForFileChooser(), + page.$eval('input', input => input.click()), + ]); + await fileChooser.accept([]); + let error = null; + await fileChooser.accept([]).catch(e => error = e); + expect(error.message).toBe('Cannot accept FileChooser which is already handled!'); + }); + }); + + describe_fails_ffox('FileChooser.cancel', function() { + it('should cancel dialog', async({page, server}) => { + // Consider file chooser canceled if we can summon another one. + // There's no reliable way in WebPlatform to see that FileChooser was + // canceled. + await page.setContent(``); + const [fileChooser1] = await Promise.all([ + page.waitForFileChooser(), + page.$eval('input', input => input.click()), + ]); + await fileChooser1.cancel(); + // If this resolves, than we successfully canceled file chooser. + await Promise.all([ + page.waitForFileChooser(), + page.$eval('input', input => input.click()), + ]); + }); + it('should fail when canceling file chooser twice', async({page, server}) => { + await page.setContent(``); + const [fileChooser] = await Promise.all([ + page.waitForFileChooser(), + page.$eval('input', input => input.click()), + ]); + await fileChooser.cancel(); + let error = null; + await fileChooser.cancel().catch(e => error = e); + expect(error.message).toBe('Cannot cancel FileChooser which is already handled!'); + }); + }); + + describe_fails_ffox('FileChooser.isMultiple', () => { + it('should work for single file pick', async({page, server}) => { + await page.setContent(``); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + expect(chooser.isMultiple()).toBe(false); + }); + it('should work for "multiple"', async({page, server}) => { + await page.setContent(``); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + expect(chooser.isMultiple()).toBe(true); + }); + it('should work for "webkitdirectory"', async({page, server}) => { + await page.setContent(``); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + expect(chooser.isMultiple()).toBe(true); + }); + }); };