From d4f297569266698d158e569a7f853e8d86b8e2ae Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 3 Apr 2020 09:48:49 -0700 Subject: [PATCH] feat(firefox): support downloads --- package.json | 2 +- src/browser.ts | 2 +- src/download.ts | 2 +- src/firefox/ffBrowser.ts | 21 ++++++++++++++++++++- src/server/firefox.ts | 14 ++++++++------ test/download.spec.js | 6 +++--- 6 files changed, 34 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 498d846c7de71..0d1a98474d9a7 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "main": "index.js", "playwright": { "chromium_revision": "754895", - "firefox_revision": "1072", + "firefox_revision": "1074", "webkit_revision": "1188" }, "scripts": { diff --git a/src/browser.ts b/src/browser.ts index 79f8511ee9bb8..4d110fa508b69 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -53,7 +53,7 @@ export abstract class BrowserBase extends EventEmitter implements Browser { this._downloads.set(uuid, download); } - _downloadFinished(uuid: string, error: string) { + _downloadFinished(uuid: string, error?: string) { const download = this._downloads.get(uuid); if (!download) return; diff --git a/src/download.ts b/src/download.ts index f67732cfd54c1..4f03fb60a76a9 100644 --- a/src/download.ts +++ b/src/download.ts @@ -81,7 +81,7 @@ export class Download { await util.promisify(fs.unlink)(fileName).catch(e => {}); } - _reportFinished(error: string) { + _reportFinished(error?: string) { this._failure = error || null; this._finishedCallback(); } diff --git a/src/firefox/ffBrowser.ts b/src/firefox/ffBrowser.ts index f14e69c3b7e36..33eeebd56d7ce 100644 --- a/src/firefox/ffBrowser.ts +++ b/src/firefox/ffBrowser.ts @@ -59,6 +59,8 @@ export class FFBrowser extends BrowserBase { this._eventListeners = [ helper.addEventListener(this._connection, 'Browser.attachedToTarget', this._onAttachedToTarget.bind(this)), helper.addEventListener(this._connection, 'Browser.detachedFromTarget', this._onDetachedFromTarget.bind(this)), + helper.addEventListener(this._connection, 'Browser.downloadCreated', this._onDownloadCreated.bind(this)), + helper.addEventListener(this._connection, 'Browser.downloadFinished', this._onDownloadFinished.bind(this)), ]; this._firstPagePromise = new Promise(f => this._firstPageCallback = f); } @@ -96,7 +98,11 @@ export class FFBrowser extends BrowserBase { viewport, locale: options.locale, timezoneId: options.timezoneId, - removeOnDetach: true + removeOnDetach: true, + downloadOptions: { + behavior: options.acceptDownloads ? 'saveToDisk' : 'cancel', + downloadsDir: this._downloadsPath, + }, }); const context = new FFBrowserContext(this, browserContextId, options); await context._initialize(); @@ -135,6 +141,19 @@ export class FFBrowser extends BrowserBase { }); } + _onDownloadCreated(payload: Protocol.Browser.downloadCreatedPayload) { + const ffPage = this._ffPages.get(payload.pageTargetId)!; + assert(ffPage); + if (!ffPage) + return; + this._downloadCreated(ffPage._page, payload.uuid, payload.url); + } + + _onDownloadFinished(payload: Protocol.Browser.downloadFinishedPayload) { + const error = payload.canceled ? 'canceled' : payload.error; + this._downloadFinished(payload.uuid, error); + } + _disconnect() { helper.removeEventListeners(this._eventListeners); this._connection.close(); diff --git a/src/server/firefox.ts b/src/server/firefox.ts index d1181ce0dbe6a..f5ff763bf6465 100644 --- a/src/server/firefox.ts +++ b/src/server/firefox.ts @@ -49,16 +49,17 @@ export class Firefox implements BrowserType { async launch(options: LaunchOptions = {}): Promise { assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead'); - const browserServer = await this._launchServer(options, 'local'); + const {browserServer, downloadsPath} = await this._launchServer(options, 'local'); const browser = await WebSocketTransport.connect(browserServer.wsEndpoint()!, transport => { return FFBrowser.connect(transport, false, options.slowMo); }); browser._ownedServer = browserServer; + browser._downloadsPath = downloadsPath; return browser; } async launchServer(options: LaunchServerOptions = {}): Promise { - return await this._launchServer(options, 'server'); + return (await this._launchServer(options, 'server')).browserServer; } async launchPersistentContext(userDataDir: string, options: LaunchOptions = {}): Promise { @@ -66,17 +67,18 @@ export class Firefox implements BrowserType { timeout = 30000, slowMo = 0, } = options; - const browserServer = await this._launchServer(options, 'persistent', userDataDir); + const {browserServer, downloadsPath} = await this._launchServer(options, 'persistent', userDataDir); const browser = await WebSocketTransport.connect(browserServer.wsEndpoint()!, transport => { return FFBrowser.connect(transport, true, slowMo); }); browser._ownedServer = browserServer; + browser._downloadsPath = downloadsPath; await helper.waitWithTimeout(browser._firstPagePromise, 'first page', timeout); const browserContext = browser._defaultContext; return browserContext; } - private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, userDataDir?: string): Promise { + private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, userDataDir?: string): Promise<{ browserServer: BrowserServer, downloadsPath: string }> { const { ignoreDefaultArgs = false, args = [], @@ -110,7 +112,7 @@ export class Firefox implements BrowserType { if (!firefoxExecutable) throw new Error(`No executable path is specified. Pass "executablePath" option directly.`); - const { launchedProcess, gracefullyClose } = await launchProcess({ + const { launchedProcess, gracefullyClose, downloadsPath } = await launchProcess({ executablePath: firefoxExecutable, args: firefoxArguments, env: os.platform() === 'linux' ? { @@ -146,7 +148,7 @@ export class Firefox implements BrowserType { const webSocketWrapper = launchType === 'server' ? (await WebSocketTransport.connect(innerEndpoint, t => wrapTransportWithWebSocket(t, port))) : new WebSocketWrapper(innerEndpoint, []); browserWSEndpoint = webSocketWrapper.wsEndpoint; browserServer = new BrowserServer(launchedProcess, gracefullyClose, webSocketWrapper); - return browserServer; + return {browserServer, downloadsPath}; } async connect(options: ConnectOptions): Promise { diff --git a/test/download.spec.js b/test/download.spec.js index fdd84b7e954f3..14dfbe82c400e 100644 --- a/test/download.spec.js +++ b/test/download.spec.js @@ -17,9 +17,9 @@ const fs = require('fs'); const path = require('path'); -module.exports.describe = function({browserType, CHROMIUM, WEBKIT, FFOX, WIN, MAC}) { +module.exports.describe = function({browserType, defaultBrowserOptions, CHROMIUM, WEBKIT, FFOX, WIN, MAC}) { - describe.fail(FFOX)('Download', function() { + describe('Download', function() { beforeEach(async(state) => { state.server.setRoute('/download', (req, res) => { res.setHeader('Content-Type', 'application/octet-stream'); @@ -97,7 +97,7 @@ module.exports.describe = function({browserType, CHROMIUM, WEBKIT, FFOX, WIN, MA expect(fs.existsSync(path1)).toBeFalsy(); expect(fs.existsSync(path2)).toBeFalsy(); }); - it('should delete downloads on browser gone', async ({ server, defaultBrowserOptions }) => { + it('should delete downloads on browser gone', async ({ server }) => { const browser = await browserType.launch(defaultBrowserOptions); const page = await browser.newPage({ acceptDownloads: true }); await page.setContent(`download`);