api(download): Add saveAs helper (#2872)

This commit is contained in:
Ross Wollman 2020-07-22 14:55:27 -07:00 committed by GitHub
parent 4db035dff4
commit d8a17fb0ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 187 additions and 2 deletions

View file

@ -3182,6 +3182,7 @@ const path = await download.path();
- [download.delete()](#downloaddelete)
- [download.failure()](#downloadfailure)
- [download.path()](#downloadpath)
- [download.saveAs(path)](#downloadsaveaspath)
- [download.suggestedFilename()](#downloadsuggestedfilename)
- [download.url()](#downloadurl)
<!-- GEN:stop -->
@ -3206,6 +3207,12 @@ Returns download error if any.
Returns path to the downloaded file in case of successful download.
#### download.saveAs(path)
- `path` <[string]> Path where the download should be saved. The directory structure MUST exist as `saveAs` will not create it.
- returns: <[Promise]>
Saves the download to a user-specified path.
#### download.suggestedFilename()
- returns: <[string]>

View file

@ -27,6 +27,8 @@ export class Download {
private _uuid: string;
private _finishedCallback: () => void;
private _finishedPromise: Promise<void>;
private _saveAsRequests: { fulfill: () => void; reject: (error?: any) => void; path: string }[] = [];
private _loaded: boolean = false;
private _page: Page;
private _acceptDownloads: boolean;
private _failure: string | null = null;
@ -72,6 +74,26 @@ export class Download {
return fileName;
}
async saveAs(path: string) {
if (this._loaded) {
await this._saveAs(path);
return;
}
return new Promise((fulfill, reject) => this._saveAsRequests.push({fulfill, reject, path}));
}
async _saveAs(dlPath: string) {
if (!this._acceptDownloads)
throw new Error('Pass { acceptDownloads: true } when you are creating your browser context.');
const fileName = path.join(this._downloadsPath, this._uuid);
if (this._failure)
throw new Error('Download not found on disk. Check download.failure() for details.');
if (this._deleted)
throw new Error('Download already deleted. Save before deleting.');
await util.promisify(fs.copyFile)(fileName, dlPath);
}
async failure(): Promise<string | null> {
if (!this._acceptDownloads)
return 'Pass { acceptDownloads: true } when you are creating your browser context.';
@ -95,7 +117,26 @@ export class Download {
await util.promisify(fs.unlink)(fileName).catch(e => {});
}
_reportFinished(error?: string) {
async _reportFinished(error?: string) {
if (error) {
for (const { reject } of this._saveAsRequests) {
if (!this._acceptDownloads)
reject(new Error('Pass { acceptDownloads: true } when you are creating your browser context.'));
else
reject(error);
}
} else {
for (const { fulfill, reject, path } of this._saveAsRequests) {
try {
await this._saveAs(path);
fulfill();
} catch (err) {
reject(err);
}
}
}
this._loaded = true;
this._failure = error || null;
this._finishedCallback();
}

View file

@ -1530,6 +1530,7 @@ export type DownloadInitializer = {
};
export interface DownloadChannel extends Channel {
path(params?: DownloadPathParams): Promise<DownloadPathResult>;
saveAs(params: DownloadSaveAsParams): Promise<DownloadSaveAsResult>;
failure(params?: DownloadFailureParams): Promise<DownloadFailureResult>;
stream(params?: DownloadStreamParams): Promise<DownloadStreamResult>;
delete(params?: DownloadDeleteParams): Promise<DownloadDeleteResult>;
@ -1538,6 +1539,10 @@ export type DownloadPathParams = {};
export type DownloadPathResult = {
value?: string,
};
export type DownloadSaveAsParams = {
path: string,
};
export type DownloadSaveAsResult = void;
export type DownloadFailureParams = {};
export type DownloadFailureResult = {
error?: string,

View file

@ -40,6 +40,12 @@ export class Download extends ChannelOwner<DownloadChannel, DownloadInitializer>
return (await this._channel.path()).value || null;
}
async saveAs(path: string): Promise<void> {
return this._wrapApiCall('download.saveAs', async () => {
await this._channel.saveAs({ path });
});
}
async failure(): Promise<string | null> {
return (await this._channel.failure()).error || null;
}

View file

@ -1390,6 +1390,10 @@ interface Download
returns
value?: string
command saveAs
parameters
path: string
command failure
returns
error?: string
@ -1474,4 +1478,3 @@ interface ElectronApplication
handle: JSHandle
command close

View file

@ -32,6 +32,10 @@ export class DownloadDispatcher extends Dispatcher<Download, DownloadInitializer
return { value: path || undefined };
}
async saveAs(params: { path: string }): Promise<void> {
await this._object.saveAs(params.path);
}
async stream(): Promise<{ stream?: StreamChannel }> {
const stream = await this._object.createReadStream();
if (!stream)

View file

@ -16,8 +16,23 @@
const fs = require('fs');
const path = require('path');
const util = require('util');
const os = require('os');
const removeFolder = require('rimraf');
const mkdtempAsync = util.promisify(fs.mkdtemp);
const removeFolderAsync = util.promisify(removeFolder);
const {FFOX, CHROMIUM, WEBKIT, HEADLESS} = testOptions;
registerFixture('persistentDirectory', async ({}, test) => {
const persistentDirectory = await mkdtempAsync(path.join(os.tmpdir(), 'playwright-test-'));
try {
await test(persistentDirectory);
} finally {
await removeFolderAsync(persistentDirectory);
}
});
describe('Download', function() {
beforeEach(async ({server}) => {
server.setRoute('/download', (req, res) => {
@ -57,6 +72,110 @@ describe('Download', function() {
expect(fs.readFileSync(path).toString()).toBe('Hello world');
await page.close();
});
it('should save to user-specified path', async({persistentDirectory, browser, server}) => {
const page = await browser.newPage({ acceptDownloads: true });
await page.setContent(`<a href="${server.PREFIX}/download">download</a>`);
const [ download ] = await Promise.all([
page.waitForEvent('download'),
page.click('a')
]);
const userPath = path.join(persistentDirectory, "download.txt");
await download.saveAs(userPath);
expect(fs.existsSync(userPath)).toBeTruthy();
expect(fs.readFileSync(userPath).toString()).toBe('Hello world');
await page.close();
});
it('should save to user-specified path without updating original path', async({persistentDirectory, browser, server}) => {
const page = await browser.newPage({ acceptDownloads: true });
await page.setContent(`<a href="${server.PREFIX}/download">download</a>`);
const [ download ] = await Promise.all([
page.waitForEvent('download'),
page.click('a')
]);
const userPath = path.join(persistentDirectory, "download.txt");
await download.saveAs(userPath);
expect(fs.existsSync(userPath)).toBeTruthy();
expect(fs.readFileSync(userPath).toString()).toBe('Hello world');
const originalPath = await download.path();
expect(fs.existsSync(originalPath)).toBeTruthy();
expect(fs.readFileSync(originalPath).toString()).toBe('Hello world');
await page.close();
});
it('should save to two different paths with multiple saveAs calls', async({persistentDirectory, browser, server}) => {
const page = await browser.newPage({ acceptDownloads: true });
await page.setContent(`<a href="${server.PREFIX}/download">download</a>`);
const [ download ] = await Promise.all([
page.waitForEvent('download'),
page.click('a')
]);
const userPath = path.join(persistentDirectory, "download.txt");
await download.saveAs(userPath);
expect(fs.existsSync(userPath)).toBeTruthy();
expect(fs.readFileSync(userPath).toString()).toBe('Hello world');
const anotherUserPath = path.join(persistentDirectory, "download (2).txt");
await download.saveAs(anotherUserPath);
expect(fs.existsSync(anotherUserPath)).toBeTruthy();
expect(fs.readFileSync(anotherUserPath).toString()).toBe('Hello world');
await page.close();
});
it('should save to overwritten filepath', async({persistentDirectory, browser, server}) => {
const page = await browser.newPage({ acceptDownloads: true });
await page.setContent(`<a href="${server.PREFIX}/download">download</a>`);
const [ download ] = await Promise.all([
page.waitForEvent('download'),
page.click('a')
]);
const userPath = path.join(persistentDirectory, "download.txt");
await download.saveAs(userPath);
expect((await util.promisify(fs.readdir)(persistentDirectory)).length).toBe(1);
await download.saveAs(userPath);
expect((await util.promisify(fs.readdir)(persistentDirectory)).length).toBe(1);
expect(fs.existsSync(userPath)).toBeTruthy();
expect(fs.readFileSync(userPath).toString()).toBe('Hello world');
await page.close();
});
it('should error when saving to non-existent user-specified path', async({persistentDirectory, browser, server}) => {
const page = await browser.newPage({ acceptDownloads: true });
await page.setContent(`<a href="${server.PREFIX}/download">download</a>`);
const [ download ] = await Promise.all([
page.waitForEvent('download'),
page.click('a')
]);
const nonExistentUserPath = path.join(persistentDirectory, "does-not-exist","download.txt");
const { message } = await download.saveAs(nonExistentUserPath).catch(e => e);
expect(message).toContain('ENOENT');
expect(message).toContain('copyfile');
expect(message).toContain('no such file or directory');
expect(message).toContain('does-not-exist');
await page.close();
});
it('should error when saving with downloads disabled', async({persistentDirectory, browser, server}) => {
const page = await browser.newPage({ acceptDownloads: false });
await page.setContent(`<a href="${server.PREFIX}/download">download</a>`);
const [ download ] = await Promise.all([
page.waitForEvent('download'),
page.click('a')
]);
const userPath = path.join(persistentDirectory, "download.txt");
const { message } = await download.saveAs(userPath).catch(e => e);
expect(message).toContain('Pass { acceptDownloads: true } when you are creating your browser context');
await page.close();
});
it('should error when saving after deletion', async({persistentDirectory, browser, server}) => {
const page = await browser.newPage({ acceptDownloads: true });
await page.setContent(`<a href="${server.PREFIX}/download">download</a>`);
const [ download ] = await Promise.all([
page.waitForEvent('download'),
page.click('a')
]);
const userPath = path.join(persistentDirectory, "download.txt");
await download.delete();
const { message } = await download.saveAs(userPath).catch(e => e);
expect(message).toContain('Download already deleted. Save before deleting.');
await page.close();
});
it('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) => {