From f63ea3ffd25ef87e5badcd14542ab36d499394fb Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 12 May 2020 19:23:08 -0700 Subject: [PATCH] feat(downloads): expose suggested filename (#2062) --- docs/api.md | 6 ++++++ src/browser.ts | 11 +++++++++-- src/chromium/crPage.ts | 2 +- src/download.ts | 16 +++++++++++++++- src/firefox/ffBrowser.ts | 2 +- src/webkit/wkBrowser.ts | 5 +++++ test/download.spec.js | 15 +++++++++++---- 7 files changed, 48 insertions(+), 9 deletions(-) diff --git a/docs/api.md b/docs/api.md index b8fad3c453..7d46f6b538 100644 --- a/docs/api.md +++ b/docs/api.md @@ -3050,6 +3050,7 @@ const path = await download.path(); - [download.delete()](#downloaddelete) - [download.failure()](#downloadfailure) - [download.path()](#downloadpath) +- [download.suggestedFilename()](#downloadsuggestedfilename) - [download.url()](#downloadurl) @@ -3073,6 +3074,11 @@ Returns download error if any. Returns path to the downloaded file in case of successful download. +#### download.suggestedFilename() +- returns: <[string]> + +Returns suggested filename for this download. It is typically computed by the browser from the [`Content-Disposition`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) response header or the `download` attribute. See the spec on [whatwg](https://html.spec.whatwg.org/#downloading-resources). Different browsers can use different logic for computing it. + #### download.url() - returns: <[string]> diff --git a/src/browser.ts b/src/browser.ts index 295826cac1..2d58940327 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -53,11 +53,18 @@ export abstract class BrowserBase extends EventEmitter implements Browser, Inner return page; } - _downloadCreated(page: Page, uuid: string, url: string) { - const download = new Download(page, this._downloadsPath, uuid, url); + _downloadCreated(page: Page, uuid: string, url: string, suggestedFilename?: string) { + const download = new Download(page, this._downloadsPath, uuid, url, suggestedFilename); this._downloads.set(uuid, download); } + _downloadFilenameSuggested(uuid: string, suggestedFilename: string) { + const download = this._downloads.get(uuid); + if (!download) + return; + download._filenameSuggested(suggestedFilename); + } + _downloadFinished(uuid: string, error?: string) { const download = this._downloads.get(uuid); if (!download) diff --git a/src/chromium/crPage.ts b/src/chromium/crPage.ts index 81a4a1c3c7..3bd003bd45 100644 --- a/src/chromium/crPage.ts +++ b/src/chromium/crPage.ts @@ -670,7 +670,7 @@ class FrameSession { } if (!originPage) return; - this._crPage._browserContext._browser._downloadCreated(originPage, payload.guid, payload.url); + this._crPage._browserContext._browser._downloadCreated(originPage, payload.guid, payload.url, payload.suggestedFilename); } _onDownloadProgress(payload: Protocol.Page.downloadProgressPayload) { diff --git a/src/download.ts b/src/download.ts index b666812527..dbf95d20f9 100644 --- a/src/download.ts +++ b/src/download.ts @@ -20,6 +20,7 @@ import * as util from 'util'; import { Page } from './page'; import { Events } from './events'; import { Readable } from 'stream'; +import { assert } from './helper'; export class Download { private _downloadsPath: string; @@ -31,16 +32,25 @@ export class Download { private _failure: string | null = null; private _deleted = false; private _url: string; + private _suggestedFilename: string | undefined; - constructor(page: Page, downloadsPath: string, uuid: string, url: string) { + constructor(page: Page, downloadsPath: string, uuid: string, url: string, suggestedFilename?: string) { this._page = page; this._downloadsPath = downloadsPath; this._uuid = uuid; this._url = url; + this._suggestedFilename = suggestedFilename; this._finishedCallback = () => {}; this._finishedPromise = new Promise(f => this._finishedCallback = f); page._browserContext._downloads.add(this); this._acceptDownloads = !!this._page._browserContext._options.acceptDownloads; + if (suggestedFilename !== undefined) + this._page.emit(Events.Page.Download, this); + } + + _filenameSuggested(suggestedFilename: string) { + assert(this._suggestedFilename === undefined); + this._suggestedFilename = suggestedFilename; this._page.emit(Events.Page.Download, this); } @@ -48,6 +58,10 @@ export class Download { return this._url; } + suggestedFilename(): string { + return this._suggestedFilename!; + } + async path(): Promise { if (!this._acceptDownloads) throw new Error('Pass { acceptDownloads: true } when you are creating your browser context.'); diff --git a/src/firefox/ffBrowser.ts b/src/firefox/ffBrowser.ts index 7dc1ce1331..146d08a40e 100644 --- a/src/firefox/ffBrowser.ts +++ b/src/firefox/ffBrowser.ts @@ -130,7 +130,7 @@ export class FFBrowser extends BrowserBase { } if (!originPage) return; - this._downloadCreated(originPage, payload.uuid, payload.url); + this._downloadCreated(originPage, payload.uuid, payload.url, payload.suggestedFileName); } _onDownloadFinished(payload: Protocol.Browser.downloadFinishedPayload) { diff --git a/src/webkit/wkBrowser.ts b/src/webkit/wkBrowser.ts index 81d2e96502..4543be21c9 100644 --- a/src/webkit/wkBrowser.ts +++ b/src/webkit/wkBrowser.ts @@ -56,6 +56,7 @@ export class WKBrowser extends BrowserBase { helper.addEventListener(this._browserSession, 'Playwright.pageProxyDestroyed', this._onPageProxyDestroyed.bind(this)), helper.addEventListener(this._browserSession, 'Playwright.provisionalLoadFailed', event => this._onProvisionalLoadFailed(event)), helper.addEventListener(this._browserSession, 'Playwright.downloadCreated', this._onDownloadCreated.bind(this)), + helper.addEventListener(this._browserSession, 'Playwright.downloadFilenameSuggested', this._onDownloadFilenameSuggested.bind(this)), helper.addEventListener(this._browserSession, 'Playwright.downloadFinished', this._onDownloadFinished.bind(this)), helper.addEventListener(this._browserSession, kPageProxyMessageReceived, this._onPageProxyMessageReceived.bind(this)), ]; @@ -111,6 +112,10 @@ export class WKBrowser extends BrowserBase { this._downloadCreated(originPage, payload.uuid, payload.url); } + _onDownloadFilenameSuggested(payload: Protocol.Playwright.downloadFilenameSuggestedPayload) { + this._downloadFilenameSuggested(payload.uuid, payload.suggestedFilename); + } + _onDownloadFinished(payload: Protocol.Playwright.downloadFinishedPayload) { this._downloadFinished(payload.uuid, payload.error); } diff --git a/test/download.spec.js b/test/download.spec.js index 1daf73ecdf..aa7c0b25da 100644 --- a/test/download.spec.js +++ b/test/download.spec.js @@ -25,16 +25,22 @@ describe('Download', function() { res.setHeader('Content-Disposition', 'attachment'); res.end(`Hello world`); }); + state.server.setRoute('/downloadWithFilename', (req, res) => { + res.setHeader('Content-Type', 'application/octet-stream'); + res.setHeader('Content-Disposition', 'attachment; filename=file.txt'); + res.end(`Hello world`); + }); }); it('should report downloads with acceptDownloads: false', async({page, server}) => { - await page.setContent(`download`); + await page.setContent(`download`); const [ download ] = await Promise.all([ page.waitForEvent('download'), page.click('a') ]); let error; - expect(download.url()).toBe(`${server.PREFIX}/download`); + expect(download.url()).toBe(`${server.PREFIX}/downloadWithFilename`); + expect(download.suggestedFilename()).toBe(`file.txt`); await download.path().catch(e => error = e); expect(await download.failure()).toContain('acceptDownloads'); expect(error.message).toContain('acceptDownloads: true'); @@ -51,8 +57,8 @@ describe('Download', function() { expect(fs.readFileSync(path).toString()).toBe('Hello world'); await page.close(); }); - it.fail(WEBKIT)('should report non-navigation downloads', async({browser, server}) => { - // Our WebKit embedder does not download in this case, although Safari does. + it.fail(WEBKIT && MAC)('should report non-navigation downloads', async({browser, server}) => { + // Mac WebKit embedder does not download in this case, although Safari does. server.setRoute('/download', (req, res) => { res.setHeader('Content-Type', 'application/octet-stream'); res.end(`Hello world`); @@ -65,6 +71,7 @@ describe('Download', function() { page.waitForEvent('download'), page.click('a') ]); + expect(download.suggestedFilename()).toBe(`file.txt`); const path = await download.path(); expect(fs.existsSync(path)).toBeTruthy(); expect(fs.readFileSync(path).toString()).toBe('Hello world');