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.delete()](#downloaddelete)
|
||||||
- [download.failure()](#downloadfailure)
|
- [download.failure()](#downloadfailure)
|
||||||
- [download.path()](#downloadpath)
|
- [download.path()](#downloadpath)
|
||||||
|
- [download.saveAs(path)](#downloadsaveaspath)
|
||||||
- [download.suggestedFilename()](#downloadsuggestedfilename)
|
- [download.suggestedFilename()](#downloadsuggestedfilename)
|
||||||
- [download.url()](#downloadurl)
|
- [download.url()](#downloadurl)
|
||||||
<!-- GEN:stop -->
|
<!-- GEN:stop -->
|
||||||
|
|
@ -3206,6 +3207,12 @@ Returns download error if any.
|
||||||
|
|
||||||
Returns path to the downloaded file in case of successful download.
|
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()
|
#### download.suggestedFilename()
|
||||||
- returns: <[string]>
|
- returns: <[string]>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ export class Download {
|
||||||
private _uuid: string;
|
private _uuid: string;
|
||||||
private _finishedCallback: () => void;
|
private _finishedCallback: () => void;
|
||||||
private _finishedPromise: Promise<void>;
|
private _finishedPromise: Promise<void>;
|
||||||
|
private _saveAsRequests: { fulfill: () => void; reject: (error?: any) => void; path: string }[] = [];
|
||||||
|
private _loaded: boolean = false;
|
||||||
private _page: Page;
|
private _page: Page;
|
||||||
private _acceptDownloads: boolean;
|
private _acceptDownloads: boolean;
|
||||||
private _failure: string | null = null;
|
private _failure: string | null = null;
|
||||||
|
|
@ -72,6 +74,26 @@ export class Download {
|
||||||
return fileName;
|
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> {
|
async failure(): Promise<string | null> {
|
||||||
if (!this._acceptDownloads)
|
if (!this._acceptDownloads)
|
||||||
return 'Pass { acceptDownloads: true } when you are creating your browser context.';
|
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 => {});
|
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._failure = error || null;
|
||||||
this._finishedCallback();
|
this._finishedCallback();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1530,6 +1530,7 @@ export type DownloadInitializer = {
|
||||||
};
|
};
|
||||||
export interface DownloadChannel extends Channel {
|
export interface DownloadChannel extends Channel {
|
||||||
path(params?: DownloadPathParams): Promise<DownloadPathResult>;
|
path(params?: DownloadPathParams): Promise<DownloadPathResult>;
|
||||||
|
saveAs(params: DownloadSaveAsParams): Promise<DownloadSaveAsResult>;
|
||||||
failure(params?: DownloadFailureParams): Promise<DownloadFailureResult>;
|
failure(params?: DownloadFailureParams): Promise<DownloadFailureResult>;
|
||||||
stream(params?: DownloadStreamParams): Promise<DownloadStreamResult>;
|
stream(params?: DownloadStreamParams): Promise<DownloadStreamResult>;
|
||||||
delete(params?: DownloadDeleteParams): Promise<DownloadDeleteResult>;
|
delete(params?: DownloadDeleteParams): Promise<DownloadDeleteResult>;
|
||||||
|
|
@ -1538,6 +1539,10 @@ export type DownloadPathParams = {};
|
||||||
export type DownloadPathResult = {
|
export type DownloadPathResult = {
|
||||||
value?: string,
|
value?: string,
|
||||||
};
|
};
|
||||||
|
export type DownloadSaveAsParams = {
|
||||||
|
path: string,
|
||||||
|
};
|
||||||
|
export type DownloadSaveAsResult = void;
|
||||||
export type DownloadFailureParams = {};
|
export type DownloadFailureParams = {};
|
||||||
export type DownloadFailureResult = {
|
export type DownloadFailureResult = {
|
||||||
error?: string,
|
error?: string,
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,12 @@ export class Download extends ChannelOwner<DownloadChannel, DownloadInitializer>
|
||||||
return (await this._channel.path()).value || null;
|
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> {
|
async failure(): Promise<string | null> {
|
||||||
return (await this._channel.failure()).error || null;
|
return (await this._channel.failure()).error || null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1390,6 +1390,10 @@ interface Download
|
||||||
returns
|
returns
|
||||||
value?: string
|
value?: string
|
||||||
|
|
||||||
|
command saveAs
|
||||||
|
parameters
|
||||||
|
path: string
|
||||||
|
|
||||||
command failure
|
command failure
|
||||||
returns
|
returns
|
||||||
error?: string
|
error?: string
|
||||||
|
|
@ -1474,4 +1478,3 @@ interface ElectronApplication
|
||||||
handle: JSHandle
|
handle: JSHandle
|
||||||
|
|
||||||
command close
|
command close
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,10 @@ export class DownloadDispatcher extends Dispatcher<Download, DownloadInitializer
|
||||||
return { value: path || undefined };
|
return { value: path || undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async saveAs(params: { path: string }): Promise<void> {
|
||||||
|
await this._object.saveAs(params.path);
|
||||||
|
}
|
||||||
|
|
||||||
async stream(): Promise<{ stream?: StreamChannel }> {
|
async stream(): Promise<{ stream?: StreamChannel }> {
|
||||||
const stream = await this._object.createReadStream();
|
const stream = await this._object.createReadStream();
|
||||||
if (!stream)
|
if (!stream)
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,23 @@
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
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;
|
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() {
|
describe('Download', function() {
|
||||||
beforeEach(async ({server}) => {
|
beforeEach(async ({server}) => {
|
||||||
server.setRoute('/download', (req, res) => {
|
server.setRoute('/download', (req, res) => {
|
||||||
|
|
@ -57,6 +72,110 @@ describe('Download', function() {
|
||||||
expect(fs.readFileSync(path).toString()).toBe('Hello world');
|
expect(fs.readFileSync(path).toString()).toBe('Hello world');
|
||||||
await page.close();
|
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}) => {
|
it('should report non-navigation downloads', async({browser, server}) => {
|
||||||
// Mac WebKit embedder does not download in this case, although Safari does.
|
// Mac WebKit embedder does not download in this case, although Safari does.
|
||||||
server.setRoute('/download', (req, res) => {
|
server.setRoute('/download', (req, res) => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue