api(download): Add saveAs helper (#2872)
This commit is contained in:
parent
4db035dff4
commit
d8a17fb0ab
|
|
@ -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]>
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue