feat(download): adding a new Download._cancel method (#6236)
This commit is contained in:
parent
2b8ea73048
commit
5f6d4a7b73
|
|
@ -62,6 +62,14 @@ downloaded content. If [`option: acceptDownloads`] is not set, download events a
|
||||||
not performed and user has no access to the downloaded files.
|
not performed and user has no access to the downloaded files.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
## async method: Download._cancel
|
||||||
|
|
||||||
|
**Chromium-only** Cancels a download.
|
||||||
|
Will not fail if the download is already finished or canceled.
|
||||||
|
Upon successful cancellations, `download.failure()` would resolve to `'canceled'`.
|
||||||
|
|
||||||
|
Currently **experimental** and may subject to further changes.
|
||||||
|
|
||||||
## async method: Download.createReadStream
|
## async method: Download.createReadStream
|
||||||
* langs: java, js, csharp
|
* langs: java, js, csharp
|
||||||
- returns: <[null]|[Readable]>
|
- returns: <[null]|[Readable]>
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,12 @@ export class Artifact extends ChannelOwner<channels.ArtifactChannel, channels.Ar
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async cancel(): Promise<void> {
|
||||||
|
return this._wrapApiCall(`${this._apiName}.cancel`, async (channel: channels.ArtifactChannel) => {
|
||||||
|
return channel.cancel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async delete(): Promise<void> {
|
async delete(): Promise<void> {
|
||||||
return this._wrapApiCall(`${this._apiName}.delete`, async (channel: channels.ArtifactChannel) => {
|
return this._wrapApiCall(`${this._apiName}.delete`, async (channel: channels.ArtifactChannel) => {
|
||||||
return channel.delete();
|
return channel.delete();
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,10 @@ export class Download implements api.Download {
|
||||||
return this._artifact.createReadStream();
|
return this._artifact.createReadStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _cancel(): Promise<void> {
|
||||||
|
return this._artifact.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
async delete(): Promise<void> {
|
async delete(): Promise<void> {
|
||||||
return this._artifact.delete();
|
return this._artifact.delete();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,10 @@ export class ArtifactDispatcher extends Dispatcher<Artifact, channels.ArtifactIn
|
||||||
return { error: error || undefined };
|
return { error: error || undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async cancel(): Promise<void> {
|
||||||
|
await this._object.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
async delete(): Promise<void> {
|
async delete(): Promise<void> {
|
||||||
await this._object.delete();
|
await this._object.delete();
|
||||||
this._dispose();
|
this._dispose();
|
||||||
|
|
|
||||||
|
|
@ -2480,6 +2480,7 @@ export interface ArtifactChannel extends Channel {
|
||||||
saveAsStream(params?: ArtifactSaveAsStreamParams, metadata?: Metadata): Promise<ArtifactSaveAsStreamResult>;
|
saveAsStream(params?: ArtifactSaveAsStreamParams, metadata?: Metadata): Promise<ArtifactSaveAsStreamResult>;
|
||||||
failure(params?: ArtifactFailureParams, metadata?: Metadata): Promise<ArtifactFailureResult>;
|
failure(params?: ArtifactFailureParams, metadata?: Metadata): Promise<ArtifactFailureResult>;
|
||||||
stream(params?: ArtifactStreamParams, metadata?: Metadata): Promise<ArtifactStreamResult>;
|
stream(params?: ArtifactStreamParams, metadata?: Metadata): Promise<ArtifactStreamResult>;
|
||||||
|
cancel(params?: ArtifactCancelParams, metadata?: Metadata): Promise<ArtifactCancelResult>;
|
||||||
delete(params?: ArtifactDeleteParams, metadata?: Metadata): Promise<ArtifactDeleteResult>;
|
delete(params?: ArtifactDeleteParams, metadata?: Metadata): Promise<ArtifactDeleteResult>;
|
||||||
}
|
}
|
||||||
export type ArtifactPathAfterFinishedParams = {};
|
export type ArtifactPathAfterFinishedParams = {};
|
||||||
|
|
@ -2509,6 +2510,9 @@ export type ArtifactStreamOptions = {};
|
||||||
export type ArtifactStreamResult = {
|
export type ArtifactStreamResult = {
|
||||||
stream?: StreamChannel,
|
stream?: StreamChannel,
|
||||||
};
|
};
|
||||||
|
export type ArtifactCancelParams = {};
|
||||||
|
export type ArtifactCancelOptions = {};
|
||||||
|
export type ArtifactCancelResult = void;
|
||||||
export type ArtifactDeleteParams = {};
|
export type ArtifactDeleteParams = {};
|
||||||
export type ArtifactDeleteOptions = {};
|
export type ArtifactDeleteOptions = {};
|
||||||
export type ArtifactDeleteResult = void;
|
export type ArtifactDeleteResult = void;
|
||||||
|
|
|
||||||
|
|
@ -2043,6 +2043,8 @@ Artifact:
|
||||||
returns:
|
returns:
|
||||||
stream: Stream?
|
stream: Stream?
|
||||||
|
|
||||||
|
cancel:
|
||||||
|
|
||||||
delete:
|
delete:
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -960,6 +960,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||||
scheme.ArtifactSaveAsStreamParams = tOptional(tObject({}));
|
scheme.ArtifactSaveAsStreamParams = tOptional(tObject({}));
|
||||||
scheme.ArtifactFailureParams = tOptional(tObject({}));
|
scheme.ArtifactFailureParams = tOptional(tObject({}));
|
||||||
scheme.ArtifactStreamParams = tOptional(tObject({}));
|
scheme.ArtifactStreamParams = tOptional(tObject({}));
|
||||||
|
scheme.ArtifactCancelParams = tOptional(tObject({}));
|
||||||
scheme.ArtifactDeleteParams = tOptional(tObject({}));
|
scheme.ArtifactDeleteParams = tOptional(tObject({}));
|
||||||
scheme.StreamReadParams = tObject({
|
scheme.StreamReadParams = tObject({
|
||||||
size: tOptional(tNumber),
|
size: tOptional(tNumber),
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,16 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import { assert } from '../utils/utils';
|
||||||
import { SdkObject } from './instrumentation';
|
import { SdkObject } from './instrumentation';
|
||||||
|
|
||||||
type SaveCallback = (localPath: string, error?: string) => Promise<void>;
|
type SaveCallback = (localPath: string, error?: string) => Promise<void>;
|
||||||
|
type CancelCallback = () => Promise<void>;
|
||||||
|
|
||||||
export class Artifact extends SdkObject {
|
export class Artifact extends SdkObject {
|
||||||
private _localPath: string;
|
private _localPath: string;
|
||||||
private _unaccessibleErrorMessage: string | undefined;
|
private _unaccessibleErrorMessage: string | undefined;
|
||||||
|
private _cancelCallback: CancelCallback | undefined;
|
||||||
private _finishedCallback: () => void;
|
private _finishedCallback: () => void;
|
||||||
private _finishedPromise: Promise<void>;
|
private _finishedPromise: Promise<void>;
|
||||||
private _saveCallbacks: SaveCallback[] = [];
|
private _saveCallbacks: SaveCallback[] = [];
|
||||||
|
|
@ -29,10 +32,11 @@ export class Artifact extends SdkObject {
|
||||||
private _deleted = false;
|
private _deleted = false;
|
||||||
private _failureError: string | null = null;
|
private _failureError: string | null = null;
|
||||||
|
|
||||||
constructor(parent: SdkObject, localPath: string, unaccessibleErrorMessage?: string) {
|
constructor(parent: SdkObject, localPath: string, unaccessibleErrorMessage?: string, cancelCallback?: CancelCallback) {
|
||||||
super(parent, 'artifact');
|
super(parent, 'artifact');
|
||||||
this._localPath = localPath;
|
this._localPath = localPath;
|
||||||
this._unaccessibleErrorMessage = unaccessibleErrorMessage;
|
this._unaccessibleErrorMessage = unaccessibleErrorMessage;
|
||||||
|
this._cancelCallback = cancelCallback;
|
||||||
this._finishedCallback = () => {};
|
this._finishedCallback = () => {};
|
||||||
this._finishedPromise = new Promise(f => this._finishedCallback = f);
|
this._finishedPromise = new Promise(f => this._finishedCallback = f);
|
||||||
}
|
}
|
||||||
|
|
@ -76,6 +80,11 @@ export class Artifact extends SdkObject {
|
||||||
return this._failureError;
|
return this._failureError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async cancel(): Promise<void> {
|
||||||
|
assert(this._cancelCallback !== undefined);
|
||||||
|
return this._cancelCallback();
|
||||||
|
}
|
||||||
|
|
||||||
async delete(): Promise<void> {
|
async delete(): Promise<void> {
|
||||||
if (this._unaccessibleErrorMessage)
|
if (this._unaccessibleErrorMessage)
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,7 @@ export abstract class BrowserContext extends SdkObject {
|
||||||
abstract _doUpdateRequestInterception(): Promise<void>;
|
abstract _doUpdateRequestInterception(): Promise<void>;
|
||||||
abstract _doClose(): Promise<void>;
|
abstract _doClose(): Promise<void>;
|
||||||
abstract _onClosePersistent(): Promise<void>;
|
abstract _onClosePersistent(): Promise<void>;
|
||||||
|
abstract _doCancelDownload(uuid: string): Promise<void>;
|
||||||
|
|
||||||
async cookies(urls: string | string[] | undefined = []): Promise<types.NetworkCookie[]> {
|
async cookies(urls: string | string[] | undefined = []): Promise<types.NetworkCookie[]> {
|
||||||
if (urls && !Array.isArray(urls))
|
if (urls && !Array.isArray(urls))
|
||||||
|
|
|
||||||
|
|
@ -479,6 +479,16 @@ export class CRBrowserContext extends BrowserContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _doCancelDownload(guid: string) {
|
||||||
|
// The upstream CDP method is implemented in a way that no explicit error would be given
|
||||||
|
// regarding the requested `guid`, even if the download is in a state not suitable for
|
||||||
|
// cancellation (finished, cancelled, etc.) or the guid is invalid at all.
|
||||||
|
await this._browser._session.send('Browser.cancelDownload', {
|
||||||
|
guid: guid,
|
||||||
|
browserContextId: this._browserContextId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
backgroundPages(): Page[] {
|
backgroundPages(): Page[] {
|
||||||
const result: Page[] = [];
|
const result: Page[] = [];
|
||||||
for (const backgroundPage of this._browser._backgroundPages.values()) {
|
for (const backgroundPage of this._browser._backgroundPages.values()) {
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,9 @@ export class Download {
|
||||||
|
|
||||||
constructor(page: Page, downloadsPath: string, uuid: string, url: string, suggestedFilename?: string) {
|
constructor(page: Page, downloadsPath: string, uuid: string, url: string, suggestedFilename?: string) {
|
||||||
const unaccessibleErrorMessage = !page._browserContext._options.acceptDownloads ? 'Pass { acceptDownloads: true } when you are creating your browser context.' : undefined;
|
const unaccessibleErrorMessage = !page._browserContext._options.acceptDownloads ? 'Pass { acceptDownloads: true } when you are creating your browser context.' : undefined;
|
||||||
this.artifact = new Artifact(page, path.join(downloadsPath, uuid), unaccessibleErrorMessage);
|
this.artifact = new Artifact(page, path.join(downloadsPath, uuid), unaccessibleErrorMessage, () => {
|
||||||
|
return this._page._browserContext._doCancelDownload(uuid);
|
||||||
|
});
|
||||||
this._page = page;
|
this._page = page;
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this._suggestedFilename = suggestedFilename;
|
this._suggestedFilename = suggestedFilename;
|
||||||
|
|
|
||||||
|
|
@ -330,6 +330,11 @@ export class FFBrowserContext extends BrowserContext {
|
||||||
await this._browser._connection.send('Browser.removeBrowserContext', { browserContextId: this._browserContextId });
|
await this._browser._connection.send('Browser.removeBrowserContext', { browserContextId: this._browserContextId });
|
||||||
this._browser._contexts.delete(this._browserContextId);
|
this._browser._contexts.delete(this._browserContextId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _doCancelDownload(uuid: string) {
|
||||||
|
// TODO: Have this implemented
|
||||||
|
throw new Error('Download cancellation not yet implemented in Firefox');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toJugglerProxyOptions(proxy: types.ProxySettings) {
|
function toJugglerProxyOptions(proxy: types.ProxySettings) {
|
||||||
|
|
|
||||||
|
|
@ -326,4 +326,9 @@ export class WKBrowserContext extends BrowserContext {
|
||||||
await this._browser._browserSession.send('Playwright.deleteContext', { browserContextId: this._browserContextId });
|
await this._browser._browserSession.send('Playwright.deleteContext', { browserContextId: this._browserContextId });
|
||||||
this._browser._contexts.delete(this._browserContextId);
|
this._browser._contexts.delete(this._browserContextId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _doCancelDownload(uuid: string) {
|
||||||
|
// TODO: Have this implemented
|
||||||
|
throw new Error('Download cancellation not yet implemented in WebKit');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ if (browserName !== 'chromium') {
|
||||||
api.delete('coverage.startCSSCoverage');
|
api.delete('coverage.startCSSCoverage');
|
||||||
api.delete('coverage.stopCSSCoverage');
|
api.delete('coverage.stopCSSCoverage');
|
||||||
api.delete('page.pdf');
|
api.delete('page.pdf');
|
||||||
|
api.delete('download._cancel');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some permissions tests are disabled in webkit. See permissions.jest.js
|
// Some permissions tests are disabled in webkit. See permissions.jest.js
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,14 @@ it.describe('download event', () => {
|
||||||
res.setHeader('Content-Disposition', 'attachment; filename=file.txt');
|
res.setHeader('Content-Disposition', 'attachment; filename=file.txt');
|
||||||
res.end(`Hello world`);
|
res.end(`Hello world`);
|
||||||
});
|
});
|
||||||
|
server.setRoute('/downloadWithDelay', (req, res) => {
|
||||||
|
res.setHeader('Content-Type', 'application/octet-stream');
|
||||||
|
res.setHeader('Content-Disposition', 'attachment; filename=file.txt');
|
||||||
|
// Chromium requires a large enough payload to trigger the download event soon enough
|
||||||
|
res.write('a'.repeat(4096));
|
||||||
|
res.write('foo');
|
||||||
|
res.uncork();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should report downloads with acceptDownloads: false', async ({browser, server}) => {
|
it('should report downloads with acceptDownloads: false', async ({browser, server}) => {
|
||||||
|
|
@ -461,6 +469,41 @@ it.describe('download event', () => {
|
||||||
await page.close();
|
await page.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should be able to cancel pending downloads', async ({browser, server, browserName, browserVersion}) => {
|
||||||
|
// The exact upstream change is in b449b5c, which still does not appear in the first few 91.* tags until 91.0.4437.0.
|
||||||
|
it.fixme(browserName === 'chromium' && Number(browserVersion.split('.')[0]) < 91, 'The upstream Browser.cancelDownload command is not available before Chrome 91');
|
||||||
|
it.fixme(browserName !== 'chromium', 'Download cancellation currently implemented for only Chromium');
|
||||||
|
const page = await browser.newPage({ acceptDownloads: true });
|
||||||
|
await page.setContent(`<a href="${server.PREFIX}/downloadWithDelay">download</a>`);
|
||||||
|
const [ download ] = await Promise.all([
|
||||||
|
page.waitForEvent('download'),
|
||||||
|
page.click('a')
|
||||||
|
]);
|
||||||
|
await download._cancel();
|
||||||
|
const failure = await download.failure();
|
||||||
|
expect(failure).toBe('canceled');
|
||||||
|
await page.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not fail explicitly to cancel a download even if that is already finished', async ({browser, server, browserName, browserVersion}) => {
|
||||||
|
// The exact upstream change is in b449b5c, which still does not appear in the first few 91.* tags until 91.0.4437.0.
|
||||||
|
it.fixme(browserName === 'chromium' && Number(browserVersion.split('.')[0]) < 91, 'The upstream Browser.cancelDownload command is not available before Chrome 91');
|
||||||
|
it.fixme(browserName !== 'chromium', 'Download cancellation currently implemented for only Chromium');
|
||||||
|
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 path = await download.path();
|
||||||
|
expect(fs.existsSync(path)).toBeTruthy();
|
||||||
|
expect(fs.readFileSync(path).toString()).toBe('Hello world');
|
||||||
|
await download._cancel();
|
||||||
|
const failure = await download.failure();
|
||||||
|
expect(failure).toBe(null);
|
||||||
|
await page.close();
|
||||||
|
});
|
||||||
|
|
||||||
it('should report downloads with interception', async ({browser, server}) => {
|
it('should report downloads with interception', async ({browser, server}) => {
|
||||||
const page = await browser.newPage({ acceptDownloads: true });
|
const page = await browser.newPage({ acceptDownloads: true });
|
||||||
await page.route(/.*/, r => r.continue());
|
await page.route(/.*/, r => r.continue());
|
||||||
|
|
@ -474,5 +517,4 @@ it.describe('download event', () => {
|
||||||
expect(fs.readFileSync(path).toString()).toBe('Hello world');
|
expect(fs.readFileSync(path).toString()).toBe('Hello world');
|
||||||
await page.close();
|
await page.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
8
types/types.d.ts
vendored
8
types/types.d.ts
vendored
|
|
@ -9495,6 +9495,14 @@ export interface Dialog {
|
||||||
* performed and user has no access to the downloaded files.
|
* performed and user has no access to the downloaded files.
|
||||||
*/
|
*/
|
||||||
export interface Download {
|
export interface Download {
|
||||||
|
/**
|
||||||
|
* **Chromium-only** Cancels a download. Will not fail if the download is already finished or canceled. Upon successful
|
||||||
|
* cancellations, `download.failure()` would resolve to `'canceled'`.
|
||||||
|
*
|
||||||
|
* Currently **experimental** and may subject to further changes.
|
||||||
|
*/
|
||||||
|
_cancel(): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns readable stream for current download or `null` if download failed.
|
* Returns readable stream for current download or `null` if download failed.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,9 @@ function paramsForMember(member) {
|
||||||
return new Set(member.argsArray.map(a => a.alias));
|
return new Set(member.argsArray.map(a => a.alias));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Including experimental method names (with a leading underscore) that would be otherwise skipped
|
||||||
|
const allowExperimentalMethods = new Set([ 'Download._cancel' ]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string[]} rootNames
|
* @param {string[]} rootNames
|
||||||
*/
|
*/
|
||||||
|
|
@ -109,6 +112,20 @@ function listMethods(rootNames, apiFileName) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} className
|
||||||
|
* @param {string} methodName
|
||||||
|
*/
|
||||||
|
function shouldSkipMethodByName(className, methodName) {
|
||||||
|
if (allowExperimentalMethods.has(`${className}.${methodName}`))
|
||||||
|
return false;
|
||||||
|
if (methodName.startsWith('_') || methodName === 'T' || methodName === 'toString')
|
||||||
|
return true;
|
||||||
|
if (/** @type {any} */(EventEmitter).prototype.hasOwnProperty(methodName))
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} className
|
* @param {string} className
|
||||||
* @param {!ts.Type} classType
|
* @param {!ts.Type} classType
|
||||||
|
|
@ -120,9 +137,7 @@ function listMethods(rootNames, apiFileName) {
|
||||||
apiMethods.set(className, methods);
|
apiMethods.set(className, methods);
|
||||||
}
|
}
|
||||||
for (const [name, member] of /** @type {any[]} */(classType.symbol.members || [])) {
|
for (const [name, member] of /** @type {any[]} */(classType.symbol.members || [])) {
|
||||||
if (name.startsWith('_') || name === 'T' || name === 'toString')
|
if (shouldSkipMethodByName(className, name))
|
||||||
continue;
|
|
||||||
if (/** @type {any} */(EventEmitter).prototype.hasOwnProperty(name))
|
|
||||||
continue;
|
continue;
|
||||||
const memberType = checker.getTypeOfSymbolAtLocation(member, member.valueDeclaration);
|
const memberType = checker.getTypeOfSymbolAtLocation(member, member.valueDeclaration);
|
||||||
const signature = signatureForType(memberType);
|
const signature = signatureForType(memberType);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue