feat(downloads): support downloads on cr and wk (#1632)
This commit is contained in:
parent
3d6d9db44a
commit
75571e8eb8
63
docs/api.md
63
docs/api.md
|
|
@ -14,6 +14,7 @@
|
|||
- [class: JSHandle](#class-jshandle)
|
||||
- [class: ConsoleMessage](#class-consolemessage)
|
||||
- [class: Dialog](#class-dialog)
|
||||
- [class: Download](#class-download)
|
||||
- [class: Keyboard](#class-keyboard)
|
||||
- [class: Mouse](#class-mouse)
|
||||
- [class: Request](#class-request)
|
||||
|
|
@ -191,6 +192,7 @@ Indicates that the browser is connected.
|
|||
|
||||
#### browser.newContext([options])
|
||||
- `options` <[Object]>
|
||||
- `acceptDownloads` <[boolean]> Whether to automatically download all the attachments. Defaults to `false` where all the downloads are canceled.
|
||||
- `ignoreHTTPSErrors` <[boolean]> Whether to ignore HTTPS errors during navigation. Defaults to `false`.
|
||||
- `bypassCSP` <[boolean]> Toggles bypassing page's Content-Security-Policy.
|
||||
- `viewport` <?[Object]> Sets a consistent viewport for each page. Defaults to an 1280x720 viewport. `null` disables the default viewport.
|
||||
|
|
@ -230,6 +232,7 @@ Creates a new browser context. It won't share cookies/cache with other browser c
|
|||
|
||||
#### browser.newPage([options])
|
||||
- `options` <[Object]>
|
||||
- `acceptDownloads` <[boolean]> Whether to automatically download all the attachments. Defaults to `false` where all the downloads are canceled.
|
||||
- `ignoreHTTPSErrors` <[boolean]> Whether to ignore HTTPS errors during navigation. Defaults to `false`.
|
||||
- `bypassCSP` <[boolean]> Toggles bypassing page's Content-Security-Policy.
|
||||
- `viewport` <?[Object]> Sets a consistent viewport for each page. Defaults to an 1280x720 viewport. `null` disables the default viewport.
|
||||
|
|
@ -629,6 +632,7 @@ page.removeListener('request', logRequest);
|
|||
- [event: 'console'](#event-console)
|
||||
- [event: 'dialog'](#event-dialog)
|
||||
- [event: 'domcontentloaded'](#event-domcontentloaded)
|
||||
- [event: 'download'](#event-download)
|
||||
- [event: 'filechooser'](#event-filechooser)
|
||||
- [event: 'frameattached'](#event-frameattached)
|
||||
- [event: 'framedetached'](#event-framedetached)
|
||||
|
|
@ -729,6 +733,11 @@ Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` o
|
|||
|
||||
Emitted when the JavaScript [`DOMContentLoaded`](https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded) event is dispatched.
|
||||
|
||||
#### event: 'download'
|
||||
- <[Download]>
|
||||
|
||||
Emitted when attachment is downloaded. User can access basic file operations on downloaded content via the passed [Download] instance. Browser context must be created with the `acceptDownloads` set to `true` when user needs access to the downloaded content. If `acceptDownloads` is not set or set to `false`, download events are emitted, but the actual download is not performed and user has no access to the downloaded files.
|
||||
|
||||
#### event: 'filechooser'
|
||||
- <[Object]>
|
||||
- `element` <[ElementHandle]> handle to the input element that was clicked
|
||||
|
|
@ -2971,6 +2980,58 @@ const { chromium } = require('playwright'); // Or 'firefox' or 'webkit'.
|
|||
- returns: <[string]> Dialog's type, can be one of `alert`, `beforeunload`, `confirm` or `prompt`.
|
||||
|
||||
|
||||
### class: Download
|
||||
|
||||
[Download] objects are dispatched by page via the ['download'](#event-download) event.
|
||||
|
||||
Note that browser context must be created with the `acceptDownloads` set to `true` when user needs access to the downloaded content. If `acceptDownloads` is not set or set to `false`, download events are emitted, but the actual download is not performed and user has no access to the downloaded files.
|
||||
|
||||
All the downloaded files belonging to the browser context are deleted when the browser context is closed. All downloaded files are deleted when the browser closes.
|
||||
|
||||
An example of using `Download` class:
|
||||
```js
|
||||
const [ download ] = await Promise.all([
|
||||
page.waitForEvent('download'),
|
||||
page.click('a')
|
||||
]);
|
||||
const path = await download.path();
|
||||
...
|
||||
```
|
||||
|
||||
<!-- GEN:toc -->
|
||||
- [download.createReadStream()](#downloadcreatereadstream)
|
||||
- [download.delete()](#downloaddelete)
|
||||
- [download.failure()](#downloadfailure)
|
||||
- [download.path()](#downloadpath)
|
||||
- [download.url()](#downloadurl)
|
||||
<!-- GEN:stop -->
|
||||
|
||||
#### download.createReadStream()
|
||||
- returns: <[Promise]<null|[Readable]>>
|
||||
|
||||
Returns readable stream for current download or `null` if download failed.
|
||||
|
||||
#### download.delete()
|
||||
- returns: <[Promise]>
|
||||
|
||||
Deletes the downloaded file.
|
||||
|
||||
#### download.failure()
|
||||
- returns: <[Promise]<null|[string]>>
|
||||
|
||||
Returns download error if any.
|
||||
|
||||
#### download.path()
|
||||
- returns: <[Promise]<null|[string]>>
|
||||
|
||||
Returns path to the downloaded file in case of successful download.
|
||||
|
||||
#### download.url()
|
||||
- returns: <[string]>
|
||||
|
||||
Returns downloaded url.
|
||||
|
||||
|
||||
### class: Keyboard
|
||||
|
||||
Keyboard provides an api for managing a virtual keyboard. The high level api is [`keyboard.type`](#keyboardtypetext-options), which takes raw characters and generates proper keydown, keypress/input, and keyup events on your page.
|
||||
|
|
@ -4112,6 +4173,6 @@ const { chromium } = require('playwright');
|
|||
[number]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type "Number"
|
||||
[origin]: https://developer.mozilla.org/en-US/docs/Glossary/Origin "Origin"
|
||||
[selector]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors "selector"
|
||||
[stream.Readable]: https://nodejs.org/api/stream.html#stream_class_stream_readable "stream.Readable"
|
||||
[Readable]: https://nodejs.org/api/stream.html#stream_class_stream_readable "Readable"
|
||||
[string]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type "String"
|
||||
[xpath]: https://developer.mozilla.org/en-US/docs/Web/XPath "xpath"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
"playwright": {
|
||||
"chromium_revision": "754895",
|
||||
"firefox_revision": "1069",
|
||||
"webkit_revision": "1185"
|
||||
"webkit_revision": "1186"
|
||||
},
|
||||
"scripts": {
|
||||
"ctest": "cross-env BROWSER=chromium node test/test.js",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export { Browser } from './browser';
|
|||
export { BrowserContext } from './browserContext';
|
||||
export { ConsoleMessage } from './console';
|
||||
export { Dialog } from './dialog';
|
||||
export { Download } from './download';
|
||||
export { ElementHandle } from './dom';
|
||||
export { TimeoutError } from './errors';
|
||||
export { Frame } from './frames';
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
import { BrowserContext, BrowserContextOptions } from './browserContext';
|
||||
import { Page } from './page';
|
||||
import { EventEmitter } from 'events';
|
||||
import { Download } from './download';
|
||||
import { debugProtocol } from './transport';
|
||||
|
||||
export interface Browser extends EventEmitter {
|
||||
newContext(options?: BrowserContextOptions): Promise<BrowserContext>;
|
||||
|
|
@ -25,14 +27,38 @@ export interface Browser extends EventEmitter {
|
|||
isConnected(): boolean;
|
||||
close(): Promise<void>;
|
||||
_disconnect(): Promise<void>;
|
||||
_setDebugFunction(debugFunction: (message: string) => void): void;
|
||||
}
|
||||
|
||||
export async function createPageInNewContext(browser: Browser, options?: BrowserContextOptions): Promise<Page> {
|
||||
const context = await browser.newContext(options);
|
||||
const page = await context.newPage();
|
||||
page._ownedContext = context;
|
||||
return page;
|
||||
export abstract class BrowserBase extends EventEmitter implements Browser {
|
||||
_downloadsPath: string = '';
|
||||
private _downloads = new Map<string, Download>();
|
||||
_debugProtocol = debugProtocol;
|
||||
|
||||
abstract newContext(options?: BrowserContextOptions): Promise<BrowserContext>;
|
||||
abstract contexts(): BrowserContext[];
|
||||
abstract isConnected(): boolean;
|
||||
abstract close(): Promise<void>;
|
||||
abstract _disconnect(): Promise<void>;
|
||||
|
||||
async newPage(options?: BrowserContextOptions): Promise<Page> {
|
||||
const context = await this.newContext(options);
|
||||
const page = await context.newPage();
|
||||
page._ownedContext = context;
|
||||
return page;
|
||||
}
|
||||
|
||||
_downloadCreated(page: Page, uuid: string, url: string) {
|
||||
const download = new Download(page, this._downloadsPath, uuid, url);
|
||||
this._downloads.set(uuid, download);
|
||||
}
|
||||
|
||||
_downloadFinished(uuid: string, error: string) {
|
||||
const download = this._downloads.get(uuid);
|
||||
if (!download)
|
||||
return;
|
||||
download._reportFinished(error);
|
||||
this._downloads.delete(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
export type LaunchType = 'local' | 'server' | 'persistent';
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { TimeoutSettings } from './timeoutSettings';
|
|||
import * as types from './types';
|
||||
import { Events } from './events';
|
||||
import { ExtendedEventEmitter } from './extendedEventEmitter';
|
||||
import { Download } from './download';
|
||||
|
||||
export type BrowserContextOptions = {
|
||||
viewport?: types.Size | null,
|
||||
|
|
@ -38,7 +39,8 @@ export type BrowserContextOptions = {
|
|||
httpCredentials?: types.Credentials,
|
||||
deviceScaleFactor?: number,
|
||||
isMobile?: boolean,
|
||||
hasTouch?: boolean
|
||||
hasTouch?: boolean,
|
||||
acceptDownloads?: boolean
|
||||
};
|
||||
|
||||
export interface BrowserContext {
|
||||
|
|
@ -71,6 +73,7 @@ export abstract class BrowserContextBase extends ExtendedEventEmitter implements
|
|||
private readonly _closePromise: Promise<Error>;
|
||||
private _closePromiseFulfill: ((error: Error) => void) | undefined;
|
||||
readonly _permissions = new Map<string, string[]>();
|
||||
readonly _downloads = new Set<Download>();
|
||||
|
||||
constructor(options: BrowserContextOptions) {
|
||||
super();
|
||||
|
|
@ -89,13 +92,16 @@ export abstract class BrowserContextBase extends ExtendedEventEmitter implements
|
|||
_browserClosed() {
|
||||
for (const page of this.pages())
|
||||
page._didClose();
|
||||
this._didCloseInternal();
|
||||
this._didCloseInternal(true);
|
||||
}
|
||||
|
||||
_didCloseInternal() {
|
||||
async _didCloseInternal(omitDeleteDownloads = false) {
|
||||
this._closed = true;
|
||||
this.emit(Events.BrowserContext.Close);
|
||||
this._closePromiseFulfill!(new Error('Context closed'));
|
||||
if (!omitDeleteDownloads)
|
||||
await Promise.all([...this._downloads].map(d => d.delete()));
|
||||
this._downloads.clear();
|
||||
}
|
||||
|
||||
// BrowserContext methods.
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Browser, createPageInNewContext } from '../browser';
|
||||
import { BrowserBase } from '../browser';
|
||||
import { assertBrowserContextIsNotOwned, BrowserContext, BrowserContextBase, BrowserContextOptions, validateBrowserContextOptions, verifyGeolocation } from '../browserContext';
|
||||
import { Events as CommonEvents } from '../events';
|
||||
import { assert, debugError, helper } from '../helper';
|
||||
|
|
@ -29,10 +29,9 @@ import { readProtocolStream } from './crProtocolHelper';
|
|||
import { Events } from './events';
|
||||
import { Protocol } from './protocol';
|
||||
import { CRExecutionContext } from './crExecutionContext';
|
||||
import { EventEmitter } from 'events';
|
||||
import type { BrowserServer } from '../server/browserServer';
|
||||
|
||||
export class CRBrowser extends EventEmitter implements Browser {
|
||||
export class CRBrowser extends BrowserBase {
|
||||
readonly _connection: CRConnection;
|
||||
_session: CRSession;
|
||||
private _clientRootSessionPromise: Promise<CRSession> | null = null;
|
||||
|
|
@ -104,10 +103,6 @@ export class CRBrowser extends EventEmitter implements Browser {
|
|||
return Array.from(this._contexts.values());
|
||||
}
|
||||
|
||||
async newPage(options?: BrowserContextOptions): Promise<Page> {
|
||||
return createPageInNewContext(this, options);
|
||||
}
|
||||
|
||||
_onAttachedToTarget({targetInfo, sessionId, waitingForDebugger}: Protocol.Target.attachedToTargetPayload) {
|
||||
const session = this._connection.session(sessionId)!;
|
||||
const context = (targetInfo.browserContextId && this._contexts.has(targetInfo.browserContextId)) ?
|
||||
|
|
@ -250,10 +245,6 @@ export class CRBrowser extends EventEmitter implements Browser {
|
|||
this._clientRootSessionPromise = this._connection.createBrowserSession();
|
||||
return this._clientRootSessionPromise;
|
||||
}
|
||||
|
||||
_setDebugFunction(debugFunction: debug.IDebugger) {
|
||||
this._connection._debugProtocol = debugFunction;
|
||||
}
|
||||
}
|
||||
|
||||
class CRServiceWorker extends Worker {
|
||||
|
|
@ -284,12 +275,20 @@ export class CRBrowserContext extends BrowserContextBase {
|
|||
}
|
||||
|
||||
async _initialize() {
|
||||
const promises: Promise<any>[] = [
|
||||
this._browser._session.send('Browser.setDownloadBehavior', {
|
||||
behavior: this._options.acceptDownloads ? 'allowAndName' : 'deny',
|
||||
browserContextId: this._browserContextId || undefined,
|
||||
downloadPath: this._browser._downloadsPath
|
||||
})
|
||||
];
|
||||
if (this._options.permissions)
|
||||
await this.grantPermissions(this._options.permissions);
|
||||
promises.push(this.grantPermissions(this._options.permissions));
|
||||
if (this._options.offline)
|
||||
await this.setOffline(this._options.offline);
|
||||
promises.push(this.setOffline(this._options.offline));
|
||||
if (this._options.httpCredentials)
|
||||
await this.setHTTPCredentials(this._options.httpCredentials);
|
||||
promises.push(this.setHTTPCredentials(this._options.httpCredentials));
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
pages(): Page[] {
|
||||
|
|
@ -435,7 +434,7 @@ export class CRBrowserContext extends BrowserContextBase {
|
|||
}
|
||||
await this._browser._session.send('Target.disposeBrowserContext', { browserContextId: this._browserContextId });
|
||||
this._browser._contexts.delete(this._browserContextId);
|
||||
this._didCloseInternal();
|
||||
await this._didCloseInternal();
|
||||
}
|
||||
|
||||
backgroundPages(): Page[] {
|
||||
|
|
|
|||
|
|
@ -16,8 +16,7 @@
|
|||
*/
|
||||
|
||||
import { assert } from '../helper';
|
||||
import * as debug from 'debug';
|
||||
import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
|
||||
import { ConnectionTransport, ProtocolRequest, ProtocolResponse, debugProtocol } from '../transport';
|
||||
import { Protocol } from './protocol';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
|
|
@ -35,7 +34,6 @@ export class CRConnection extends EventEmitter {
|
|||
private readonly _sessions = new Map<string, CRSession>();
|
||||
readonly rootSession: CRSession;
|
||||
_closed = false;
|
||||
_debugProtocol: debug.IDebugger;
|
||||
|
||||
constructor(transport: ConnectionTransport) {
|
||||
super();
|
||||
|
|
@ -44,8 +42,6 @@ export class CRConnection extends EventEmitter {
|
|||
this._transport.onclose = this._onClose.bind(this);
|
||||
this.rootSession = new CRSession(this, '', 'browser', '');
|
||||
this._sessions.set('', this.rootSession);
|
||||
this._debugProtocol = debug('pw:protocol');
|
||||
(this._debugProtocol as any).color = '34';
|
||||
}
|
||||
|
||||
static fromSession(session: CRSession): CRConnection {
|
||||
|
|
@ -61,15 +57,15 @@ export class CRConnection extends EventEmitter {
|
|||
const message: ProtocolRequest = { id, method, params };
|
||||
if (sessionId)
|
||||
message.sessionId = sessionId;
|
||||
if (this._debugProtocol.enabled)
|
||||
this._debugProtocol('SEND ► ' + rewriteInjectedScriptEvaluationLog(message));
|
||||
if (debugProtocol.enabled)
|
||||
debugProtocol('SEND ► ' + rewriteInjectedScriptEvaluationLog(message));
|
||||
this._transport.send(message);
|
||||
return id;
|
||||
}
|
||||
|
||||
async _onMessage(message: ProtocolResponse) {
|
||||
if (this._debugProtocol.enabled)
|
||||
this._debugProtocol('◀ RECV ' + JSON.stringify(message));
|
||||
if (debugProtocol.enabled)
|
||||
debugProtocol('◀ RECV ' + JSON.stringify(message));
|
||||
if (message.id === kBrowserCloseMessageId)
|
||||
return;
|
||||
if (message.method === 'Target.attachedToTarget') {
|
||||
|
|
|
|||
|
|
@ -94,6 +94,8 @@ export class CRPage implements PageDelegate {
|
|||
helper.addEventListener(this._client, 'Page.frameStoppedLoading', event => this._onFrameStoppedLoading(event.frameId)),
|
||||
helper.addEventListener(this._client, 'Page.javascriptDialogOpening', event => this._onDialog(event)),
|
||||
helper.addEventListener(this._client, 'Page.navigatedWithinDocument', event => this._onFrameNavigatedWithinDocument(event.frameId, event.url)),
|
||||
helper.addEventListener(this._client, 'Page.downloadWillBegin', event => this._onDownloadWillBegin(event)),
|
||||
helper.addEventListener(this._client, 'Page.downloadProgress', event => this._onDownloadProgress(event)),
|
||||
helper.addEventListener(this._client, 'Runtime.bindingCalled', event => this._onBindingCalled(event)),
|
||||
helper.addEventListener(this._client, 'Runtime.consoleAPICalled', event => this._onConsoleAPI(event)),
|
||||
helper.addEventListener(this._client, 'Runtime.exceptionThrown', exception => this._handleException(exception.exceptionDetails)),
|
||||
|
|
@ -168,7 +170,6 @@ export class CRPage implements PageDelegate {
|
|||
promises.push(this._firstNonInitialNavigationCommittedPromise);
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
didClose() {
|
||||
helper.removeEventListeners(this._eventListeners);
|
||||
this._networkManager.dispose();
|
||||
|
|
@ -356,6 +357,17 @@ export class CRPage implements PageDelegate {
|
|||
this._page._onFileChooserOpened(handle);
|
||||
}
|
||||
|
||||
_onDownloadWillBegin(payload: Protocol.Page.downloadWillBeginPayload) {
|
||||
this._browserContext._browser._downloadCreated(this._page, payload.guid, payload.url);
|
||||
}
|
||||
|
||||
_onDownloadProgress(payload: Protocol.Page.downloadProgressPayload) {
|
||||
if (payload.state === 'completed')
|
||||
this._browserContext._browser._downloadFinished(payload.guid, '');
|
||||
if (payload.state === 'canceled')
|
||||
this._browserContext._browser._downloadFinished(payload.guid, 'canceled');
|
||||
}
|
||||
|
||||
async updateExtraHTTPHeaders(): Promise<void> {
|
||||
const headers = network.mergeHeaders([
|
||||
this._browserContext._options.extraHTTPHeaders,
|
||||
|
|
|
|||
88
src/download.ts
Normal file
88
src/download.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as util from 'util';
|
||||
import { Page } from './page';
|
||||
import { Events } from './events';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
export class Download {
|
||||
private _downloadsPath: string;
|
||||
private _uuid: string;
|
||||
private _finishedCallback: () => void;
|
||||
private _finishedPromise: Promise<void>;
|
||||
private _page: Page;
|
||||
private _acceptDownloads: boolean;
|
||||
private _failure: string | null = null;
|
||||
private _deleted = false;
|
||||
private _url: string;
|
||||
|
||||
constructor(page: Page, downloadsPath: string, uuid: string, url: string) {
|
||||
this._page = page;
|
||||
this._downloadsPath = downloadsPath;
|
||||
this._uuid = uuid;
|
||||
this._url = url;
|
||||
this._finishedCallback = () => {};
|
||||
this._finishedPromise = new Promise(f => this._finishedCallback = f);
|
||||
this._page.emit(Events.Page.Download, this);
|
||||
page._browserContext._downloads.add(this);
|
||||
this._acceptDownloads = !!this._page._browserContext._options.acceptDownloads;
|
||||
}
|
||||
|
||||
url(): string {
|
||||
return this._url;
|
||||
}
|
||||
|
||||
async path(): Promise<string | null> {
|
||||
if (!this._acceptDownloads)
|
||||
throw new Error('Pass { acceptDownloads: true } when you are creating your browser context.');
|
||||
const fileName = path.join(this._downloadsPath, this._uuid);
|
||||
await this._finishedPromise;
|
||||
if (this._failure)
|
||||
return null;
|
||||
return fileName;
|
||||
}
|
||||
|
||||
async failure(): Promise<string | null> {
|
||||
if (!this._acceptDownloads)
|
||||
return 'Pass { acceptDownloads: true } when you are creating your browser context.';
|
||||
await this._finishedPromise;
|
||||
return this._failure;
|
||||
}
|
||||
|
||||
async createReadStream(): Promise<Readable | null> {
|
||||
const fileName = await this.path();
|
||||
return fileName ? fs.createReadStream(fileName) : null;
|
||||
}
|
||||
|
||||
async delete(): Promise<void> {
|
||||
if (!this._acceptDownloads)
|
||||
return;
|
||||
const fileName = await this.path();
|
||||
if (this._deleted)
|
||||
return;
|
||||
this._deleted = true;
|
||||
if (fileName)
|
||||
await util.promisify(fs.unlink)(fileName).catch(e => {});
|
||||
}
|
||||
|
||||
_reportFinished(error: string) {
|
||||
this._failure = error || null;
|
||||
this._finishedCallback();
|
||||
}
|
||||
}
|
||||
|
|
@ -33,6 +33,7 @@ export const Events = {
|
|||
Close: 'close',
|
||||
Console: 'console',
|
||||
Dialog: 'dialog',
|
||||
Download: 'download',
|
||||
FileChooser: 'filechooser',
|
||||
DOMContentLoaded: 'domcontentloaded',
|
||||
// Can't use just 'error' due to node.js special treatment of error events.
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Browser, createPageInNewContext } from '../browser';
|
||||
import { BrowserBase } from '../browser';
|
||||
import { assertBrowserContextIsNotOwned, BrowserContext, BrowserContextBase, BrowserContextOptions, validateBrowserContextOptions, verifyGeolocation } from '../browserContext';
|
||||
import { Events } from '../events';
|
||||
import { assert, helper, RegisteredListener } from '../helper';
|
||||
|
|
@ -27,10 +27,9 @@ import { ConnectionEvents, FFConnection } from './ffConnection';
|
|||
import { headersArray } from './ffNetworkManager';
|
||||
import { FFPage } from './ffPage';
|
||||
import { Protocol } from './protocol';
|
||||
import { EventEmitter } from 'events';
|
||||
import type { BrowserServer } from '../server/browserServer';
|
||||
|
||||
export class FFBrowser extends EventEmitter implements Browser {
|
||||
export class FFBrowser extends BrowserBase {
|
||||
_connection: FFConnection;
|
||||
readonly _ffPages: Map<string, FFPage>;
|
||||
readonly _defaultContext: FFBrowserContext;
|
||||
|
|
@ -111,10 +110,6 @@ export class FFBrowser extends EventEmitter implements Browser {
|
|||
return Array.from(this._contexts.values());
|
||||
}
|
||||
|
||||
async newPage(options?: BrowserContextOptions): Promise<Page> {
|
||||
return createPageInNewContext(this, options);
|
||||
}
|
||||
|
||||
_onDetachedFromTarget(payload: Protocol.Browser.detachedFromTargetPayload) {
|
||||
const ffPage = this._ffPages.get(payload.targetId)!;
|
||||
this._ffPages.delete(payload.targetId);
|
||||
|
|
@ -156,10 +151,6 @@ export class FFBrowser extends EventEmitter implements Browser {
|
|||
else
|
||||
await this._disconnect();
|
||||
}
|
||||
|
||||
_setDebugFunction(debugFunction: debug.IDebugger) {
|
||||
this._connection._debugProtocol = debugFunction;
|
||||
}
|
||||
}
|
||||
|
||||
export class FFBrowserContext extends BrowserContextBase {
|
||||
|
|
@ -320,6 +311,6 @@ export class FFBrowserContext extends BrowserContextBase {
|
|||
}
|
||||
await this._browser._connection.send('Browser.removeBrowserContext', { browserContextId: this._browserContextId });
|
||||
this._browser._contexts.delete(this._browserContextId);
|
||||
this._didCloseInternal();
|
||||
await this._didCloseInternal();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,10 +15,9 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as debug from 'debug';
|
||||
import { EventEmitter } from 'events';
|
||||
import { assert } from '../helper';
|
||||
import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
|
||||
import { ConnectionTransport, ProtocolRequest, ProtocolResponse, debugProtocol } from '../transport';
|
||||
import { Protocol } from './protocol';
|
||||
|
||||
export const ConnectionEvents = {
|
||||
|
|
@ -34,7 +33,6 @@ export class FFConnection extends EventEmitter {
|
|||
private _callbacks: Map<number, {resolve: Function, reject: Function, error: Error, method: string}>;
|
||||
private _transport: ConnectionTransport;
|
||||
readonly _sessions: Map<string, FFSession>;
|
||||
_debugProtocol = debug('pw:protocol');
|
||||
_closed: boolean;
|
||||
|
||||
on: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
|
||||
|
|
@ -59,7 +57,6 @@ export class FFConnection extends EventEmitter {
|
|||
this.off = super.removeListener;
|
||||
this.removeListener = super.removeListener;
|
||||
this.once = super.once;
|
||||
(this._debugProtocol as any).color = '34';
|
||||
}
|
||||
|
||||
async send<T extends keyof Protocol.CommandParameters>(
|
||||
|
|
@ -78,14 +75,14 @@ export class FFConnection extends EventEmitter {
|
|||
}
|
||||
|
||||
_rawSend(message: ProtocolRequest) {
|
||||
if (this._debugProtocol.enabled)
|
||||
this._debugProtocol('SEND ► ' + rewriteInjectedScriptEvaluationLog(message));
|
||||
if (debugProtocol.enabled)
|
||||
debugProtocol('SEND ► ' + rewriteInjectedScriptEvaluationLog(message));
|
||||
this._transport.send(message);
|
||||
}
|
||||
|
||||
async _onMessage(message: ProtocolResponse) {
|
||||
if (this._debugProtocol.enabled)
|
||||
this._debugProtocol('◀ RECV ' + JSON.stringify(message));
|
||||
if (debugProtocol.enabled)
|
||||
debugProtocol('◀ RECV ' + JSON.stringify(message));
|
||||
if (message.id === kBrowserCloseMessageId)
|
||||
return;
|
||||
if (message.sessionId) {
|
||||
|
|
|
|||
|
|
@ -47,9 +47,10 @@ export class Chromium implements BrowserType<CRBrowser> {
|
|||
|
||||
async launch(options: LaunchOptions = {}): Promise<CRBrowser> {
|
||||
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
|
||||
const { browserServer, transport } = await this._launchServer(options, 'local');
|
||||
const { browserServer, transport, downloadsPath } = await this._launchServer(options, 'local');
|
||||
const browser = await CRBrowser.connect(transport!, false, options.slowMo);
|
||||
browser._ownedServer = browserServer;
|
||||
browser._downloadsPath = downloadsPath;
|
||||
return browser;
|
||||
}
|
||||
|
||||
|
|
@ -69,7 +70,7 @@ export class Chromium implements BrowserType<CRBrowser> {
|
|||
return browser._defaultContext;
|
||||
}
|
||||
|
||||
private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, userDataDir?: string): Promise<{ browserServer: BrowserServer, transport?: ConnectionTransport }> {
|
||||
private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, userDataDir?: string): Promise<{ browserServer: BrowserServer, transport?: ConnectionTransport, downloadsPath: string }> {
|
||||
const {
|
||||
ignoreDefaultArgs = false,
|
||||
args = [],
|
||||
|
|
@ -100,7 +101,7 @@ export class Chromium implements BrowserType<CRBrowser> {
|
|||
const chromeExecutable = executablePath || this._executablePath;
|
||||
if (!chromeExecutable)
|
||||
throw new Error(`No executable path is specified. Pass "executablePath" option directly.`);
|
||||
const { launchedProcess, gracefullyClose } = await launchProcess({
|
||||
const { launchedProcess, gracefullyClose, downloadsPath } = await launchProcess({
|
||||
executablePath: chromeExecutable,
|
||||
args: chromeArguments,
|
||||
env,
|
||||
|
|
@ -129,7 +130,7 @@ export class Chromium implements BrowserType<CRBrowser> {
|
|||
let browserServer: BrowserServer | undefined = undefined;
|
||||
transport = new PipeTransport(launchedProcess.stdio[3] as NodeJS.WritableStream, launchedProcess.stdio[4] as NodeJS.ReadableStream);
|
||||
browserServer = new BrowserServer(launchedProcess, gracefullyClose, launchType === 'server' ? wrapTransportWithWebSocket(transport, port) : null);
|
||||
return { browserServer, transport };
|
||||
return { browserServer, transport, downloadsPath };
|
||||
}
|
||||
|
||||
async connect(options: ConnectOptions): Promise<CRBrowser> {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@
|
|||
|
||||
import * as childProcess from 'child_process';
|
||||
import * as debug from 'debug';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as readline from 'readline';
|
||||
import * as removeFolder from 'rimraf';
|
||||
import * as stream from 'stream';
|
||||
|
|
@ -25,6 +28,8 @@ import { TimeoutError } from '../errors';
|
|||
import { helper } from '../helper';
|
||||
|
||||
const removeFolderAsync = util.promisify(removeFolder);
|
||||
const mkdtempAsync = util.promisify(fs.mkdtemp);
|
||||
const DOWNLOADS_FOLDER = path.join(os.tmpdir(), 'playwright_downloads-');
|
||||
|
||||
export type LaunchProcessOptions = {
|
||||
executablePath: string,
|
||||
|
|
@ -43,7 +48,11 @@ export type LaunchProcessOptions = {
|
|||
onkill: (exitCode: number | null, signal: string | null) => void,
|
||||
};
|
||||
|
||||
type LaunchResult = { launchedProcess: childProcess.ChildProcess, gracefullyClose: () => Promise<void> };
|
||||
type LaunchResult = {
|
||||
launchedProcess: childProcess.ChildProcess,
|
||||
gracefullyClose: () => Promise<void>,
|
||||
downloadsPath: string
|
||||
};
|
||||
|
||||
let lastLaunchedId = 0;
|
||||
|
||||
|
|
@ -93,6 +102,8 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
|
|||
console.log(`\x1b[31m[err]\x1b[0m ${data}`); // eslint-disable-line no-console
|
||||
});
|
||||
|
||||
const downloadsPath = await mkdtempAsync(DOWNLOADS_FOLDER);
|
||||
|
||||
let processClosed = false;
|
||||
const waitForProcessToClose = new Promise((fulfill, reject) => {
|
||||
spawnedProcess.once('exit', (exitCode, signal) => {
|
||||
|
|
@ -101,13 +112,10 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
|
|||
helper.removeEventListeners(listeners);
|
||||
options.onkill(exitCode, signal);
|
||||
// Cleanup as processes exit.
|
||||
if (options.tempDir) {
|
||||
removeFolderAsync(options.tempDir)
|
||||
.catch((err: Error) => console.error(err))
|
||||
.then(fulfill);
|
||||
} else {
|
||||
fulfill();
|
||||
}
|
||||
Promise.all([
|
||||
removeFolderAsync(downloadsPath),
|
||||
options.tempDir ? removeFolderAsync(options.tempDir) : Promise.resolve()
|
||||
]).catch((err: Error) => console.error(err)).then(fulfill);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -162,7 +170,7 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
|
|||
} catch (e) { }
|
||||
}
|
||||
|
||||
return { launchedProcess: spawnedProcess, gracefullyClose };
|
||||
return { launchedProcess: spawnedProcess, gracefullyClose, downloadsPath };
|
||||
}
|
||||
|
||||
export function waitForLine(process: childProcess.ChildProcess, inputStream: stream.Readable, regex: RegExp, timeout: number, timeoutError: TimeoutError): Promise<RegExpMatchArray> {
|
||||
|
|
|
|||
|
|
@ -47,9 +47,10 @@ export class WebKit implements BrowserType<WKBrowser> {
|
|||
|
||||
async launch(options: LaunchOptions = {}): Promise<WKBrowser> {
|
||||
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
|
||||
const { browserServer, transport } = await this._launchServer(options, 'local');
|
||||
const { browserServer, transport, downloadsPath } = await this._launchServer(options, 'local');
|
||||
const browser = await WKBrowser.connect(transport!, options.slowMo, false, () => browserServer.close());
|
||||
browser._ownedServer = browserServer;
|
||||
browser._downloadsPath = downloadsPath;
|
||||
return browser;
|
||||
}
|
||||
|
||||
|
|
@ -68,7 +69,7 @@ export class WebKit implements BrowserType<WKBrowser> {
|
|||
return browser._defaultContext;
|
||||
}
|
||||
|
||||
private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, userDataDir?: string): Promise<{ browserServer: BrowserServer, transport?: ConnectionTransport }> {
|
||||
private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, userDataDir?: string): Promise<{ browserServer: BrowserServer, transport?: ConnectionTransport, downloadsPath: string }> {
|
||||
const {
|
||||
ignoreDefaultArgs = false,
|
||||
args = [],
|
||||
|
|
@ -100,7 +101,7 @@ export class WebKit implements BrowserType<WKBrowser> {
|
|||
if (!webkitExecutable)
|
||||
throw new Error(`No executable path is specified.`);
|
||||
|
||||
const { launchedProcess, gracefullyClose } = await launchProcess({
|
||||
const { launchedProcess, gracefullyClose, downloadsPath } = await launchProcess({
|
||||
executablePath: webkitExecutable,
|
||||
args: webkitArguments,
|
||||
env: { ...env, CURL_COOKIE_JAR_PATH: path.join(userDataDir, 'cookiejar.db') },
|
||||
|
|
@ -128,7 +129,7 @@ export class WebKit implements BrowserType<WKBrowser> {
|
|||
let browserServer: BrowserServer | undefined = undefined;
|
||||
transport = new PipeTransport(launchedProcess.stdio[3] as NodeJS.WritableStream, launchedProcess.stdio[4] as NodeJS.ReadableStream);
|
||||
browserServer = new BrowserServer(launchedProcess, gracefullyClose, launchType === 'server' ? wrapTransportWithWebSocket(transport, port || 0) : null);
|
||||
return { browserServer, transport };
|
||||
return { browserServer, transport, downloadsPath };
|
||||
}
|
||||
|
||||
async connect(options: ConnectOptions): Promise<WKBrowser> {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as debug from 'debug';
|
||||
import * as WebSocket from 'ws';
|
||||
import { helper } from './helper';
|
||||
|
||||
|
|
@ -43,7 +44,7 @@ export interface ConnectionTransport {
|
|||
onclose?: () => void,
|
||||
}
|
||||
|
||||
export class SlowMoTransport {
|
||||
export class SlowMoTransport implements ConnectionTransport {
|
||||
private readonly _delay: number;
|
||||
private readonly _delegate: ConnectionTransport;
|
||||
|
||||
|
|
@ -184,3 +185,41 @@ export class SequenceNumberMixer<V> {
|
|||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export class InterceptingTransport implements ConnectionTransport {
|
||||
private readonly _delegate: ConnectionTransport;
|
||||
private _interceptor: (message: ProtocolRequest) => ProtocolRequest;
|
||||
|
||||
onmessage?: (message: ProtocolResponse) => void;
|
||||
onclose?: () => void;
|
||||
|
||||
constructor(transport: ConnectionTransport, interceptor: (message: ProtocolRequest) => ProtocolRequest) {
|
||||
this._delegate = transport;
|
||||
this._interceptor = interceptor;
|
||||
this._delegate.onmessage = this._onmessage.bind(this);
|
||||
this._delegate.onclose = this._onClose.bind(this);
|
||||
}
|
||||
|
||||
private _onmessage(message: ProtocolResponse) {
|
||||
if (this.onmessage)
|
||||
this.onmessage(message);
|
||||
}
|
||||
|
||||
private _onClose() {
|
||||
if (this.onclose)
|
||||
this.onclose();
|
||||
this._delegate.onmessage = undefined;
|
||||
this._delegate.onclose = undefined;
|
||||
}
|
||||
|
||||
send(s: ProtocolRequest) {
|
||||
this._delegate.send(this._interceptor(s));
|
||||
}
|
||||
|
||||
close() {
|
||||
this._delegate.close();
|
||||
}
|
||||
}
|
||||
|
||||
export const debugProtocol = debug('pw:protocol');
|
||||
(debugProtocol as any).color = '34';
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Browser, createPageInNewContext } from '../browser';
|
||||
import { BrowserBase } from '../browser';
|
||||
import { assertBrowserContextIsNotOwned, BrowserContext, BrowserContextBase, BrowserContextOptions, validateBrowserContextOptions, verifyGeolocation } from '../browserContext';
|
||||
import { Events } from '../events';
|
||||
import { assert, helper, RegisteredListener } from '../helper';
|
||||
|
|
@ -26,12 +26,11 @@ import * as types from '../types';
|
|||
import { Protocol } from './protocol';
|
||||
import { kPageProxyMessageReceived, PageProxyMessageReceivedPayload, WKConnection, WKSession } from './wkConnection';
|
||||
import { WKPage } from './wkPage';
|
||||
import { EventEmitter } from 'events';
|
||||
import type { BrowserServer } from '../server/browserServer';
|
||||
|
||||
const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.4 Safari/605.1.15';
|
||||
|
||||
export class WKBrowser extends EventEmitter implements Browser {
|
||||
export class WKBrowser extends BrowserBase {
|
||||
private readonly _connection: WKConnection;
|
||||
private readonly _attachToDefaultContext: boolean;
|
||||
readonly _browserSession: WKSession;
|
||||
|
|
@ -65,6 +64,8 @@ export class WKBrowser extends EventEmitter implements Browser {
|
|||
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.windowOpen', this._onWindowOpen.bind(this)),
|
||||
helper.addEventListener(this._browserSession, 'Playwright.downloadCreated', this._onDownloadCreated.bind(this)),
|
||||
helper.addEventListener(this._browserSession, 'Playwright.downloadFinished', this._onDownloadFinished.bind(this)),
|
||||
helper.addEventListener(this._browserSession, kPageProxyMessageReceived, this._onPageProxyMessageReceived.bind(this)),
|
||||
];
|
||||
|
||||
|
|
@ -95,10 +96,6 @@ export class WKBrowser extends EventEmitter implements Browser {
|
|||
return Array.from(this._contexts.values());
|
||||
}
|
||||
|
||||
async newPage(options?: BrowserContextOptions): Promise<Page> {
|
||||
return createPageInNewContext(this, options);
|
||||
}
|
||||
|
||||
async _waitForFirstPageTarget(): Promise<void> {
|
||||
assert(!this._wkPages.size);
|
||||
return this._firstPagePromise;
|
||||
|
|
@ -108,6 +105,17 @@ export class WKBrowser extends EventEmitter implements Browser {
|
|||
this._popupOpeners.push(payload.pageProxyId);
|
||||
}
|
||||
|
||||
_onDownloadCreated(payload: Protocol.Playwright.downloadCreatedPayload) {
|
||||
const page = this._wkPages.get(payload.pageProxyId);
|
||||
if (!page)
|
||||
return;
|
||||
this._downloadCreated(page._page, payload.uuid, payload.url);
|
||||
}
|
||||
|
||||
_onDownloadFinished(payload: Protocol.Playwright.downloadFinishedPayload) {
|
||||
this._downloadFinished(payload.uuid, payload.error);
|
||||
}
|
||||
|
||||
_onPageProxyCreated(event: Protocol.Playwright.pageProxyCreatedPayload) {
|
||||
const { pageProxyInfo } = event;
|
||||
const pageProxyId = pageProxyInfo.pageProxyId;
|
||||
|
|
@ -196,10 +204,6 @@ export class WKBrowser extends EventEmitter implements Browser {
|
|||
else
|
||||
await this._disconnect();
|
||||
}
|
||||
|
||||
_setDebugFunction(debugFunction: debug.IDebugger) {
|
||||
this._connection._debugProtocol = debugFunction;
|
||||
}
|
||||
}
|
||||
|
||||
export class WKBrowserContext extends BrowserContextBase {
|
||||
|
|
@ -215,18 +219,27 @@ export class WKBrowserContext extends BrowserContextBase {
|
|||
}
|
||||
|
||||
async _initialize() {
|
||||
const browserContextId = this._browserContextId;
|
||||
const promises: Promise<any>[] = [
|
||||
this._browser._browserSession.send('Playwright.setDownloadBehavior', {
|
||||
behavior: this._options.acceptDownloads ? 'allow' : 'deny',
|
||||
downloadPath: this._browser._downloadsPath,
|
||||
browserContextId
|
||||
})
|
||||
];
|
||||
if (this._options.ignoreHTTPSErrors)
|
||||
await this._browser._browserSession.send('Playwright.setIgnoreCertificateErrors', { browserContextId: this._browserContextId, ignore: true });
|
||||
promises.push(this._browser._browserSession.send('Playwright.setIgnoreCertificateErrors', { browserContextId, ignore: true }));
|
||||
if (this._options.locale)
|
||||
await this._browser._browserSession.send('Playwright.setLanguages', { browserContextId: this._browserContextId, languages: [this._options.locale] });
|
||||
promises.push(this._browser._browserSession.send('Playwright.setLanguages', { browserContextId, languages: [this._options.locale] }));
|
||||
if (this._options.permissions)
|
||||
await this.grantPermissions(this._options.permissions);
|
||||
promises.push(this.grantPermissions(this._options.permissions));
|
||||
if (this._options.geolocation)
|
||||
await this.setGeolocation(this._options.geolocation);
|
||||
promises.push(this.setGeolocation(this._options.geolocation));
|
||||
if (this._options.offline)
|
||||
await this.setOffline(this._options.offline);
|
||||
promises.push(this.setOffline(this._options.offline));
|
||||
if (this._options.httpCredentials)
|
||||
await this.setHTTPCredentials(this._options.httpCredentials);
|
||||
promises.push(this.setHTTPCredentials(this._options.httpCredentials));
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
_wkPages(): WKPage[] {
|
||||
|
|
@ -344,6 +357,6 @@ export class WKBrowserContext extends BrowserContextBase {
|
|||
}
|
||||
await this._browser._browserSession.send('Playwright.deleteContext', { browserContextId: this._browserContextId });
|
||||
this._browser._contexts.delete(this._browserContextId);
|
||||
this._didCloseInternal();
|
||||
await this._didCloseInternal();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
import * as debug from 'debug';
|
||||
import { EventEmitter } from 'events';
|
||||
import { assert } from '../helper';
|
||||
import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
|
||||
import { ConnectionTransport, ProtocolRequest, ProtocolResponse, debugProtocol } from '../transport';
|
||||
import { Protocol } from './protocol';
|
||||
|
||||
// WKPlaywright uses this special id to issue Browser.close command which we
|
||||
|
|
@ -35,7 +35,6 @@ export class WKConnection {
|
|||
private readonly _onDisconnect: () => void;
|
||||
private _lastId = 0;
|
||||
private _closed = false;
|
||||
_debugProtocol = debug('pw:protocol');
|
||||
|
||||
readonly browserSession: WKSession;
|
||||
|
||||
|
|
@ -47,7 +46,6 @@ export class WKConnection {
|
|||
this.browserSession = new WKSession(this, '', 'Browser has been closed.', (message: any) => {
|
||||
this.rawSend(message);
|
||||
});
|
||||
(this._debugProtocol as any).color = '34';
|
||||
}
|
||||
|
||||
nextMessageId(): number {
|
||||
|
|
@ -55,14 +53,14 @@ export class WKConnection {
|
|||
}
|
||||
|
||||
rawSend(message: ProtocolRequest) {
|
||||
if (this._debugProtocol.enabled)
|
||||
this._debugProtocol('SEND ► ' + rewriteInjectedScriptEvaluationLog(message));
|
||||
if (debugProtocol.enabled)
|
||||
debugProtocol('SEND ► ' + rewriteInjectedScriptEvaluationLog(message));
|
||||
this._transport.send(message);
|
||||
}
|
||||
|
||||
private _dispatchMessage(message: ProtocolResponse) {
|
||||
if (this._debugProtocol.enabled)
|
||||
this._debugProtocol('◀ RECV ' + message);
|
||||
if (debugProtocol.enabled)
|
||||
debugProtocol('◀ RECV ' + JSON.stringify(message));
|
||||
if (message.id === kBrowserCloseMessageId)
|
||||
return;
|
||||
if (message.pageProxyId) {
|
||||
|
|
|
|||
|
|
@ -195,10 +195,6 @@ module.exports.describe = function({testRunner, expect, playwright, MAC, WIN, FF
|
|||
await page.setContent(`<a href='${httpsServer.EMPTY_PAGE}'>foobar</a>`);
|
||||
await page.click('a');
|
||||
});
|
||||
it.fail(FFOX)('clicking on download link', async({page, server, httpsServer}) => {
|
||||
await page.setContent(`<a href="${server.PREFIX}/wasm/table2.wasm" download=true>table2.wasm</a>`);
|
||||
await page.click('a');
|
||||
});
|
||||
it('calling window.stop async', async({page, server, httpsServer}) => {
|
||||
server.setRoute('/empty.html', async (req, res) => {});
|
||||
await page.evaluate((url) => {
|
||||
|
|
|
|||
125
test/download.spec.js
Normal file
125
test/download.spec.js
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
module.exports.describe = function({testRunner, expect, browserType, CHROMIUM, WEBKIT, FFOX, WIN, MAC}) {
|
||||
const {describe, xdescribe, fdescribe} = testRunner;
|
||||
const {it, fit, xit, dit} = testRunner;
|
||||
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
|
||||
|
||||
describe.fail(FFOX)('Download', function() {
|
||||
beforeEach(async(state) => {
|
||||
state.server.setRoute('/download', (req, res) => {
|
||||
res.setHeader('Content-Type', 'application/octet-stream');
|
||||
res.setHeader('Content-Disposition', 'attachment');
|
||||
res.end(`Hello world`);
|
||||
});
|
||||
});
|
||||
|
||||
it('should report downloads with acceptDownloads: false', async({page, server}) => {
|
||||
await page.setContent(`<a download=true href="${server.PREFIX}/download">download</a>`);
|
||||
const [ download ] = await Promise.all([
|
||||
page.waitForEvent('download'),
|
||||
page.click('a')
|
||||
]);
|
||||
let error;
|
||||
expect(download.url()).toBe(`${server.PREFIX}/download`);
|
||||
await download.path().catch(e => error = e);
|
||||
expect(await download.failure()).toContain('acceptDownloads');
|
||||
expect(error.message).toContain('acceptDownloads: true');
|
||||
});
|
||||
it('should report downloads with acceptDownloads: true', async({browser, server}) => {
|
||||
const page = await browser.newPage({ acceptDownloads: true });
|
||||
await page.setContent(`<a download=true 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 page.close();
|
||||
});
|
||||
it('should delete file', async({browser, server}) => {
|
||||
const page = await browser.newPage({ acceptDownloads: true });
|
||||
await page.setContent(`<a download=true 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();
|
||||
await download.delete();
|
||||
expect(fs.existsSync(path)).toBeFalsy();
|
||||
});
|
||||
it('should expose stream', async({browser, server}) => {
|
||||
const page = await browser.newPage({ acceptDownloads: true });
|
||||
await page.setContent(`<a download=true href="${server.PREFIX}/download">download</a>`);
|
||||
const [ download ] = await Promise.all([
|
||||
page.waitForEvent('download'),
|
||||
page.click('a')
|
||||
]);
|
||||
const stream = await download.createReadStream();
|
||||
let content = '';
|
||||
stream.on('data', data => content += data.toString());
|
||||
await new Promise(f => stream.on('end', f));
|
||||
expect(content).toBe('Hello world');
|
||||
stream.close();
|
||||
});
|
||||
it('should delete downloads on context destruction', async({browser, server}) => {
|
||||
const page = await browser.newPage({ acceptDownloads: true });
|
||||
await page.setContent(`<a download=true href="${server.PREFIX}/download">download</a>`);
|
||||
const [ download1 ] = await Promise.all([
|
||||
page.waitForEvent('download'),
|
||||
page.click('a')
|
||||
]);
|
||||
const [ download2 ] = await Promise.all([
|
||||
page.waitForEvent('download'),
|
||||
page.click('a')
|
||||
]);
|
||||
const path1 = await download1.path();
|
||||
const path2 = await download2.path();
|
||||
expect(fs.existsSync(path1)).toBeTruthy();
|
||||
expect(fs.existsSync(path2)).toBeTruthy();
|
||||
await page.context().close();
|
||||
expect(fs.existsSync(path1)).toBeFalsy();
|
||||
expect(fs.existsSync(path2)).toBeFalsy();
|
||||
});
|
||||
it('should delete downloads on browser gone', async ({ server, defaultBrowserOptions }) => {
|
||||
const browser = await browserType.launch(defaultBrowserOptions);
|
||||
const page = await browser.newPage({ acceptDownloads: true });
|
||||
await page.setContent(`<a download=true href="${server.PREFIX}/download">download</a>`);
|
||||
const [ download1 ] = await Promise.all([
|
||||
page.waitForEvent('download'),
|
||||
page.click('a')
|
||||
]);
|
||||
const [ download2 ] = await Promise.all([
|
||||
page.waitForEvent('download'),
|
||||
page.click('a')
|
||||
]);
|
||||
const path1 = await download1.path();
|
||||
const path2 = await download2.path();
|
||||
expect(fs.existsSync(path1)).toBeTruthy();
|
||||
expect(fs.existsSync(path2)).toBeTruthy();
|
||||
await browser.close();
|
||||
expect(fs.existsSync(path1)).toBeFalsy();
|
||||
expect(fs.existsSync(path2)).toBeFalsy();
|
||||
expect(fs.existsSync(path.join(path1, '..'))).toBeFalsy();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
@ -32,7 +32,8 @@ const BROWSER_CONFIGS = [
|
|||
...require('../lib/events').Events,
|
||||
...require('../lib/chromium/events').Events,
|
||||
},
|
||||
missingCoverage: ['browserContext.setGeolocation', 'browserContext.setOffline', 'cDPSession.send', 'cDPSession.detach'],
|
||||
missingCoverage: ['browserContext.setGeolocation', 'browserContext.setOffline', 'cDPSession.send', 'cDPSession.detach', 'page.emit("download")',
|
||||
'download.url', 'download.path', 'download.failure', 'download.createReadStream', 'download.delete'],
|
||||
},
|
||||
{
|
||||
name: 'WebKit',
|
||||
|
|
@ -180,12 +181,12 @@ module.exports.addPlaywrightTests = ({testRunner, platform, products, playwright
|
|||
state._stdout.on('line', dumpout);
|
||||
state._stderr.on('line', dumperr);
|
||||
if (dumpProtocolOnFailure)
|
||||
state.browser._setDebugFunction(data => test.output.push(`\x1b[32m[pw:protocol]\x1b[0m ${data}`));
|
||||
state.browser._debugProtocol.log = data => test.output.push(`\x1b[32m[pw:protocol]\x1b[0m ${data}`);
|
||||
state.tearDown = async () => {
|
||||
state._stdout.off('line', dumpout);
|
||||
state._stderr.off('line', dumperr);
|
||||
if (dumpProtocolOnFailure)
|
||||
state.browser._setDebugFunction(() => void 0);
|
||||
delete state.browser._debugProtocol.log;
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -218,6 +219,7 @@ module.exports.addPlaywrightTests = ({testRunner, platform, products, playwright
|
|||
loadTests('./click.spec.js');
|
||||
loadTests('./cookies.spec.js');
|
||||
loadTests('./dialog.spec.js');
|
||||
loadTests('./download.spec.js');
|
||||
loadTests('./elementhandle.spec.js');
|
||||
loadTests('./emulation.spec.js');
|
||||
loadTests('./evaluation.spec.js');
|
||||
|
|
|
|||
1
utils/generate_types/overrides.d.ts
vendored
1
utils/generate_types/overrides.d.ts
vendored
|
|
@ -16,6 +16,7 @@
|
|||
import { Protocol } from './protocol';
|
||||
import { ChildProcess } from 'child_process';
|
||||
import { EventEmitter } from 'events';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
/**
|
||||
* Can be converted to JSON
|
||||
|
|
|
|||
Loading…
Reference in a new issue