feat(firefox): support downloads (#1689)

This commit is contained in:
Yury Semikhatsky 2020-04-07 15:01:42 -07:00 committed by GitHub
parent 949dc7b514
commit a7ae205254
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 35 additions and 15 deletions

View file

@ -9,7 +9,7 @@
"main": "index.js", "main": "index.js",
"playwright": { "playwright": {
"chromium_revision": "754895", "chromium_revision": "754895",
"firefox_revision": "1072", "firefox_revision": "1074",
"webkit_revision": "1188" "webkit_revision": "1188"
}, },
"scripts": { "scripts": {

View file

@ -53,7 +53,7 @@ export abstract class BrowserBase extends EventEmitter implements Browser {
this._downloads.set(uuid, download); this._downloads.set(uuid, download);
} }
_downloadFinished(uuid: string, error: string) { _downloadFinished(uuid: string, error?: string) {
const download = this._downloads.get(uuid); const download = this._downloads.get(uuid);
if (!download) if (!download)
return; return;

View file

@ -81,7 +81,7 @@ export class Download {
await util.promisify(fs.unlink)(fileName).catch(e => {}); await util.promisify(fs.unlink)(fileName).catch(e => {});
} }
_reportFinished(error: string) { _reportFinished(error?: string) {
this._failure = error || null; this._failure = error || null;
this._finishedCallback(); this._finishedCallback();
} }

View file

@ -59,6 +59,8 @@ export class FFBrowser extends BrowserBase {
this._eventListeners = [ this._eventListeners = [
helper.addEventListener(this._connection, 'Browser.attachedToTarget', this._onAttachedToTarget.bind(this)), 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.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); this._firstPagePromise = new Promise(f => this._firstPageCallback = f);
} }
@ -96,7 +98,11 @@ export class FFBrowser extends BrowserBase {
viewport, viewport,
locale: options.locale, locale: options.locale,
timezoneId: options.timezoneId, timezoneId: options.timezoneId,
removeOnDetach: true removeOnDetach: true,
downloadOptions: {
behavior: options.acceptDownloads ? 'saveToDisk' : 'cancel',
downloadsDir: this._downloadsPath,
},
}); });
const context = new FFBrowserContext(this, browserContextId, options); const context = new FFBrowserContext(this, browserContextId, options);
await context._initialize(); 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() { _disconnect() {
helper.removeEventListeners(this._eventListeners); helper.removeEventListeners(this._eventListeners);
this._connection.close(); this._connection.close();

View file

@ -49,16 +49,17 @@ export class Firefox implements BrowserType<FFBrowser> {
async launch(options: LaunchOptions = {}): Promise<FFBrowser> { async launch(options: LaunchOptions = {}): Promise<FFBrowser> {
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead'); 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 => { const browser = await WebSocketTransport.connect(browserServer.wsEndpoint()!, transport => {
return FFBrowser.connect(transport, false, options.slowMo); return FFBrowser.connect(transport, false, options.slowMo);
}); });
browser._ownedServer = browserServer; browser._ownedServer = browserServer;
browser._downloadsPath = downloadsPath;
return browser; return browser;
} }
async launchServer(options: LaunchServerOptions = {}): Promise<BrowserServer> { async launchServer(options: LaunchServerOptions = {}): Promise<BrowserServer> {
return await this._launchServer(options, 'server'); return (await this._launchServer(options, 'server')).browserServer;
} }
async launchPersistentContext(userDataDir: string, options: LaunchOptions = {}): Promise<BrowserContext> { async launchPersistentContext(userDataDir: string, options: LaunchOptions = {}): Promise<BrowserContext> {
@ -66,17 +67,18 @@ export class Firefox implements BrowserType<FFBrowser> {
timeout = 30000, timeout = 30000,
slowMo = 0, slowMo = 0,
} = options; } = 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 => { const browser = await WebSocketTransport.connect(browserServer.wsEndpoint()!, transport => {
return FFBrowser.connect(transport, true, slowMo); return FFBrowser.connect(transport, true, slowMo);
}); });
browser._ownedServer = browserServer; browser._ownedServer = browserServer;
browser._downloadsPath = downloadsPath;
await helper.waitWithTimeout(browser._firstPagePromise, 'first page', timeout); await helper.waitWithTimeout(browser._firstPagePromise, 'first page', timeout);
const browserContext = browser._defaultContext; const browserContext = browser._defaultContext;
return browserContext; return browserContext;
} }
private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, userDataDir?: string): Promise<BrowserServer> { private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, userDataDir?: string): Promise<{ browserServer: BrowserServer, downloadsPath: string }> {
const { const {
ignoreDefaultArgs = false, ignoreDefaultArgs = false,
args = [], args = [],
@ -110,7 +112,7 @@ export class Firefox implements BrowserType<FFBrowser> {
if (!firefoxExecutable) if (!firefoxExecutable)
throw new Error(`No executable path is specified. Pass "executablePath" option directly.`); 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, executablePath: firefoxExecutable,
args: firefoxArguments, args: firefoxArguments,
env: os.platform() === 'linux' ? { env: os.platform() === 'linux' ? {
@ -146,7 +148,7 @@ export class Firefox implements BrowserType<FFBrowser> {
const webSocketWrapper = launchType === 'server' ? (await WebSocketTransport.connect(innerEndpoint, t => wrapTransportWithWebSocket(t, port))) : new WebSocketWrapper(innerEndpoint, []); const webSocketWrapper = launchType === 'server' ? (await WebSocketTransport.connect(innerEndpoint, t => wrapTransportWithWebSocket(t, port))) : new WebSocketWrapper(innerEndpoint, []);
browserWSEndpoint = webSocketWrapper.wsEndpoint; browserWSEndpoint = webSocketWrapper.wsEndpoint;
browserServer = new BrowserServer(launchedProcess, gracefullyClose, webSocketWrapper); browserServer = new BrowserServer(launchedProcess, gracefullyClose, webSocketWrapper);
return browserServer; return {browserServer, downloadsPath};
} }
async connect(options: ConnectOptions): Promise<FFBrowser> { async connect(options: ConnectOptions): Promise<FFBrowser> {

View file

@ -17,9 +17,9 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); 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) => { beforeEach(async(state) => {
state.server.setRoute('/download', (req, res) => { state.server.setRoute('/download', (req, res) => {
res.setHeader('Content-Type', 'application/octet-stream'); 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(path1)).toBeFalsy();
expect(fs.existsSync(path2)).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 browser = await browserType.launch(defaultBrowserOptions);
const page = await browser.newPage({ acceptDownloads: true }); const page = await browser.newPage({ acceptDownloads: true });
await page.setContent(`<a download=true href="${server.PREFIX}/download">download</a>`); await page.setContent(`<a download=true href="${server.PREFIX}/download">download</a>`);

View file

@ -30,8 +30,7 @@ const BROWSER_CONFIGS = [
...require('../lib/events').Events, ...require('../lib/events').Events,
...require('../lib/chromium/events').Events, ...require('../lib/chromium/events').Events,
}, },
missingCoverage: ['browserContext.setGeolocation', 'browserContext.setOffline', 'cDPSession.send', 'cDPSession.detach', 'page.emit("download")', missingCoverage: ['browserContext.setGeolocation', 'browserContext.setOffline', 'cDPSession.send', 'cDPSession.detach'],
'download.url', 'download.path', 'download.failure', 'download.createReadStream', 'download.delete'],
}, },
{ {
name: 'WebKit', name: 'WebKit',