From d5bd459986e9374f7314080247b54ce63072ee76 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 14 Jul 2020 10:51:37 -0700 Subject: [PATCH] chore(rpc): remove some paths from the channel (#2934) --- src/rpc/channels.ts | 15 ++++++--- src/rpc/client/connection.ts | 4 +++ src/rpc/client/download.ts | 9 +++-- src/rpc/client/frame.ts | 14 +++++++- src/rpc/client/page.ts | 13 +++++++- src/rpc/client/stream.ts | 50 ++++++++++++++++++++++++++++ src/rpc/server/downloadDispatcher.ts | 11 +++++- src/rpc/server/frameDispatcher.ts | 4 +-- src/rpc/server/streamDispatcher.ts | 30 +++++++++++++++++ test/download.spec.js | 1 - 10 files changed, 137 insertions(+), 14 deletions(-) create mode 100644 src/rpc/client/stream.ts create mode 100644 src/rpc/server/streamDispatcher.ts diff --git a/src/rpc/channels.ts b/src/rpc/channels.ts index 1aa152ec22..1dd1186920 100644 --- a/src/rpc/channels.ts +++ b/src/rpc/channels.ts @@ -22,7 +22,6 @@ export type Binary = string; export interface Channel extends EventEmitter { } - export interface PlaywrightChannel extends Channel { } export type PlaywrightInitializer = { @@ -181,8 +180,8 @@ export interface FrameChannel extends Channel { evalOnSelector(params: { selector: string; expression: string, isFunction: boolean, arg: any} & PageAttribution): Promise; evalOnSelectorAll(params: { selector: string; expression: string, isFunction: boolean, arg: any} & PageAttribution): Promise; - addScriptTag(params: { url?: string | undefined, path?: string | undefined, content?: string | undefined, type?: string | undefined} & PageAttribution): Promise; - addStyleTag(params: { url?: string | undefined, path?: string | undefined, content?: string | undefined} & PageAttribution): Promise; + addScriptTag(params: { url?: string, content?: string, type?: string } & PageAttribution): Promise; + addStyleTag(params: { url?: string, content?: string } & PageAttribution): Promise; check(params: { selector: string, force?: boolean, noWaitAfter?: boolean } & types.TimeoutOptions & PageAttribution): Promise; click(params: { selector: string, force?: boolean, noWaitAfter?: boolean } & types.PointerActionOptions & types.MouseClickOptions & types.TimeoutOptions & PageAttribution): Promise; content(): Promise; @@ -352,6 +351,7 @@ export type DialogInitializer = { export interface DownloadChannel extends Channel { path(): Promise; failure(): Promise; + stream(): Promise; delete(): Promise; } export type DownloadInitializer = { @@ -360,6 +360,13 @@ export type DownloadInitializer = { }; +export interface StreamChannel extends Channel { + read(params: { size?: number }): Promise; +} +export type StreamInitializer = { +} + + // Chromium-specific. export interface CDPSessionChannel extends Channel { on(event: 'event', callback: (params: { method: string, params?: Object }) => void): this; @@ -370,7 +377,6 @@ export interface CDPSessionChannel extends Channel { } export type CDPSessionInitializer = {}; - export type PDFOptions = { scale?: number, displayHeaderFooter?: boolean, @@ -384,7 +390,6 @@ export type PDFOptions = { height?: string, preferCSSPageSize?: boolean, margin?: {top?: string, bottom?: string, left?: string, right?: string}, - path?: string, }; diff --git a/src/rpc/client/connection.ts b/src/rpc/client/connection.ts index ff52e34eb4..8a78a0162e 100644 --- a/src/rpc/client/connection.ts +++ b/src/rpc/client/connection.ts @@ -37,6 +37,7 @@ import { Channel } from '../channels'; import { ChromiumBrowser } from './chromiumBrowser'; import { ChromiumBrowserContext } from './chromiumBrowserContext'; import { Selectors } from './selectors'; +import { Stream } from './stream'; class Root extends ChannelOwner { constructor(connection: Connection) { @@ -201,6 +202,9 @@ export class Connection { case 'request': result = new Request(parent, type, guid, initializer); break; + case 'stream': + result = new Stream(parent, type, guid, initializer); + break; case 'response': result = new Response(parent, type, guid, initializer); break; diff --git a/src/rpc/client/download.ts b/src/rpc/client/download.ts index 56179dbbfc..5247eae9b1 100644 --- a/src/rpc/client/download.ts +++ b/src/rpc/client/download.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import * as fs from 'fs'; import { DownloadChannel, DownloadInitializer } from '../channels'; import { ChannelOwner } from './channelOwner'; import { Readable } from 'stream'; +import { Stream } from './stream'; export class Download extends ChannelOwner { static from(download: DownloadChannel): Download { @@ -45,8 +45,11 @@ export class Download extends ChannelOwner } async createReadStream(): Promise { - const fileName = await this.path(); - return fileName ? fs.createReadStream(fileName) : null; + const s = await this._channel.stream(); + if (!s) + return null; + const stream = Stream.from(s); + return stream.stream(); } async delete(): Promise { diff --git a/src/rpc/client/frame.ts b/src/rpc/client/frame.ts index 16e779b9a0..3e3453fb74 100644 --- a/src/rpc/client/frame.ts +++ b/src/rpc/client/frame.ts @@ -22,13 +22,17 @@ import { BrowserContext } from './browserContext'; import { ChannelOwner } from './channelOwner'; import { ElementHandle, convertSelectOptionValues, convertInputFiles } from './elementHandle'; import { JSHandle, Func1, FuncOn, SmartHandle, serializeArgument, parseResult } from './jsHandle'; +import * as fs from 'fs'; import * as network from './network'; +import * as util from 'util'; import { Page } from './page'; import { EventEmitter } from 'events'; import { Waiter } from './waiter'; import { Events } from '../../events'; import { TimeoutError } from '../../errors'; +const fsReadFileAsync = util.promisify(fs.readFile.bind(fs)); + export type GotoOptions = types.NavigateOptions & { referer?: string, }; @@ -174,10 +178,18 @@ export class Frame extends ChannelOwner { } async addScriptTag(options: { url?: string, path?: string, content?: string, type?: string }): Promise { - return ElementHandle.from(await this._channel.addScriptTag({ ...options, isPage: this._page!._isPageCall })); + const copy = { ...options }; + if (copy.path) { + copy.content = (await fsReadFileAsync(copy.path)).toString(); + copy.content += '//# sourceURL=' + copy.path.replace(/\n/g, ''); + } + return ElementHandle.from(await this._channel.addScriptTag({ ...copy, isPage: this._page!._isPageCall })); } async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise { + const copy = { ...options }; + if (copy.path) + copy.content = (await fsReadFileAsync(copy.path)).toString(); return ElementHandle.from(await this._channel.addStyleTag({ ...options, isPage: this._page!._isPageCall })); } diff --git a/src/rpc/client/page.ts b/src/rpc/client/page.ts index 3a22c74e35..5d28f8e977 100644 --- a/src/rpc/client/page.ts +++ b/src/rpc/client/page.ts @@ -39,6 +39,11 @@ import { Buffer } from 'buffer'; import { Coverage } from './coverage'; import { Waiter } from './waiter'; +import * as fs from 'fs'; +import * as util from 'util'; + +const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs)); + export class Page extends ChannelOwner { private _browserContext: BrowserContext; _ownedContext: BrowserContext | undefined; @@ -494,7 +499,10 @@ export class Page extends ChannelOwner { } async _pdf(options: types.PDFOptions = {}): Promise { + const path = options.path; const transportOptions: PDFOptions = { ...options } as PDFOptions; + if (path) + delete (transportOptions as any).path; if (transportOptions.margin) transportOptions.margin = { ...transportOptions.margin }; if (typeof options.width === 'number') @@ -507,7 +515,10 @@ export class Page extends ChannelOwner { transportOptions.margin![index] = transportOptions.margin![index] + 'px'; } const binary = await this._channel.pdf(transportOptions); - return Buffer.from(binary, 'base64'); + const buffer = Buffer.from(binary, 'base64'); + if (path) + await fsWriteFileAsync(path, buffer); + return buffer; } } diff --git a/src/rpc/client/stream.ts b/src/rpc/client/stream.ts new file mode 100644 index 0000000000..b6148de7fd --- /dev/null +++ b/src/rpc/client/stream.ts @@ -0,0 +1,50 @@ +/** + * 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 { Readable } from 'stream'; +import { StreamChannel, StreamInitializer } from '../channels'; +import { ChannelOwner } from './channelOwner'; + +export class Stream extends ChannelOwner { + static from(Stream: StreamChannel): Stream { + return (Stream as any)._object; + } + + constructor(parent: ChannelOwner, type: string, guid: string, initializer: StreamInitializer) { + super(parent, type, guid, initializer); + } + + stream(): Readable { + return new StreamImpl(this._channel); + } +} + +class StreamImpl extends Readable { + private _channel: StreamChannel; + + constructor(channel: StreamChannel) { + super(); + this._channel = channel; + } + + async _read(size: number) { + const data = await this._channel.read({ size }); + if (data) + this.push(Buffer.from(data, 'base64')); + else + this.push(null); + } +} diff --git a/src/rpc/server/downloadDispatcher.ts b/src/rpc/server/downloadDispatcher.ts index 97460678ba..f6eee1ab6c 100644 --- a/src/rpc/server/downloadDispatcher.ts +++ b/src/rpc/server/downloadDispatcher.ts @@ -15,8 +15,9 @@ */ import { Download } from '../../download'; -import { DownloadChannel, DownloadInitializer } from '../channels'; +import { DownloadChannel, DownloadInitializer, StreamChannel } from '../channels'; import { Dispatcher, DispatcherScope } from './dispatcher'; +import { StreamDispatcher } from './streamDispatcher'; export class DownloadDispatcher extends Dispatcher implements DownloadChannel { constructor(scope: DispatcherScope, download: Download) { @@ -30,6 +31,14 @@ export class DownloadDispatcher extends Dispatcher { + const stream = await this._object.createReadStream(); + if (!stream) + return null; + await new Promise(f => stream.on('readable', f)); + return new StreamDispatcher(this._scope, stream); + } + async failure(): Promise { return this._object.failure(); } diff --git a/src/rpc/server/frameDispatcher.ts b/src/rpc/server/frameDispatcher.ts index 12ca6fc514..ed9472de27 100644 --- a/src/rpc/server/frameDispatcher.ts +++ b/src/rpc/server/frameDispatcher.ts @@ -110,12 +110,12 @@ export class FrameDispatcher extends Dispatcher impleme await target.setContent(params.html, params); } - async addScriptTag(params: { url?: string | undefined, path?: string | undefined, content?: string | undefined, type?: string | undefined } & PageAttribution): Promise { + async addScriptTag(params: { url?: string, content?: string, type?: string } & PageAttribution): Promise { const target = params.isPage ? this._frame._page : this._frame; return new ElementHandleDispatcher(this._scope, await target.addScriptTag(params)); } - async addStyleTag(params: { url?: string | undefined, path?: string | undefined, content?: string | undefined } & PageAttribution): Promise { + async addStyleTag(params: { url?: string, content?: string } & PageAttribution): Promise { const target = params.isPage ? this._frame._page : this._frame; return new ElementHandleDispatcher(this._scope, await target.addStyleTag(params)); } diff --git a/src/rpc/server/streamDispatcher.ts b/src/rpc/server/streamDispatcher.ts new file mode 100644 index 0000000000..011243f23d --- /dev/null +++ b/src/rpc/server/streamDispatcher.ts @@ -0,0 +1,30 @@ +/** + * 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 { StreamChannel, StreamInitializer } from '../channels'; +import { Dispatcher, DispatcherScope } from './dispatcher'; +import * as stream from 'stream'; + +export class StreamDispatcher extends Dispatcher implements StreamChannel { + constructor(scope: DispatcherScope, stream: stream.Readable) { + super(scope, stream, 'stream', {}); + } + + async read(params: { size?: number }): Promise { + const buffer = this._object.read(Math.min(this._object.readableLength, params.size || this._object.readableLength)); + return buffer ? buffer.toString('base64') : ''; + } +} diff --git a/test/download.spec.js b/test/download.spec.js index 2de0499696..92d9f27618 100644 --- a/test/download.spec.js +++ b/test/download.spec.js @@ -162,7 +162,6 @@ describe('Download', function() { stream.on('data', data => content += data.toString()); await new Promise(f => stream.on('end', f)); expect(content).toBe('Hello world'); - stream.close(); await page.close(); }); it('should delete downloads on context destruction', async({browser, server}) => {