chore: tracing over bidi+ (#10370)

Co-authored-by: Nikolay Vitkov <34244704+Lightning00Blade@users.noreply.github.com>
This commit is contained in:
Alex Rudenko 2023-06-13 11:25:32 +02:00 committed by GitHub
parent c2d3488ad8
commit 9473d740e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 153 additions and 48 deletions

View File

@ -184,7 +184,25 @@ export class CDPPage extends Page {
this.#timeoutSettings this.#timeoutSettings
); );
this.#emulationManager = new EmulationManager(client); this.#emulationManager = new EmulationManager(client);
this.#tracing = new Tracing(client); this.#tracing = new Tracing({
read: opts => {
return this.#client.send('IO.read', opts);
},
close: opts => {
return this.#client.send('IO.close', opts);
},
start: opts => {
return client.send('Tracing.start', opts);
},
stop: async () => {
const deferred = Deferred.create();
this.#client.once('Tracing.tracingComplete', event => {
deferred.resolve(event);
});
await this.#client.send('Tracing.end');
return deferred.valueOrThrow() as Promise<Protocol.Tracing.TracingCompleteEvent>;
},
});
this.#coverage = new Coverage(client); this.#coverage = new Coverage(client);
this.#screenshotTaskQueue = screenshotTaskQueue; this.#screenshotTaskQueue = screenshotTaskQueue;
this.#viewport = null; this.#viewport = null;
@ -1476,7 +1494,17 @@ export class CDPPage extends Page {
} }
assert(result.stream, '`stream` is missing from `Page.printToPDF'); assert(result.stream, '`stream` is missing from `Page.printToPDF');
return getReadableFromProtocolStream(this.#client, result.stream); return getReadableFromProtocolStream(
{
read: opts => {
return this.#client.send('IO.read', opts);
},
close: opts => {
return this.#client.send('IO.close', opts);
},
},
result.stream
);
} }
override async pdf(options: PDFOptions = {}): Promise<Buffer> { override async pdf(options: PDFOptions = {}): Promise<Buffer> {

View File

@ -13,12 +13,15 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import {assert} from '../util/assert.js'; import Protocol from 'devtools-protocol';
import {Deferred} from '../util/Deferred.js';
import {isErrorLike} from '../util/ErrorLike.js';
import {CDPSession} from './Connection.js'; import {assert} from '../util/assert.js';
import {getReadableAsBuffer, getReadableFromProtocolStream} from './util.js';
import {
getReadableAsBuffer,
getReadableFromProtocolStream,
ProtocolReadable,
} from './util.js';
/** /**
* @public * @public
@ -29,6 +32,14 @@ export interface TracingOptions {
categories?: string[]; categories?: string[];
} }
/**
* @internal
*/
export interface TracingSource extends ProtocolReadable {
start(opts: Protocol.Tracing.StartRequest): Promise<void>;
stop(): Promise<Protocol.Tracing.TracingCompleteEvent>;
}
/** /**
* The Tracing class exposes the tracing audit interface. * The Tracing class exposes the tracing audit interface.
* @remarks * @remarks
@ -46,15 +57,15 @@ export interface TracingOptions {
* @public * @public
*/ */
export class Tracing { export class Tracing {
#client: CDPSession; #source: TracingSource;
#recording = false; #recording = false;
#path?: string; #path?: string;
/** /**
* @internal * @internal
*/ */
constructor(client: CDPSession) { constructor(source: TracingSource) {
this.#client = client; this.#source = source;
} }
/** /**
@ -102,7 +113,7 @@ export class Tracing {
this.#path = path; this.#path = path;
this.#recording = true; this.#recording = true;
await this.#client.send('Tracing.start', { await this.#source.start({
transferMode: 'ReturnAsStream', transferMode: 'ReturnAsStream',
traceConfig: { traceConfig: {
excludedCategories, excludedCategories,
@ -116,25 +127,13 @@ export class Tracing {
* @returns Promise which resolves to buffer with trace data. * @returns Promise which resolves to buffer with trace data.
*/ */
async stop(): Promise<Buffer | undefined> { async stop(): Promise<Buffer | undefined> {
const contentDeferred = Deferred.create<Buffer | undefined>(); const result = await this.#source.stop();
this.#client.once('Tracing.tracingComplete', async event => {
try {
const readable = await getReadableFromProtocolStream( const readable = await getReadableFromProtocolStream(
this.#client, this.#source,
event.stream result.stream!
); );
const buffer = await getReadableAsBuffer(readable, this.#path); const buffer = await getReadableAsBuffer(readable, this.#path);
contentDeferred.resolve(buffer ?? undefined);
} catch (error) {
if (isErrorLike(error)) {
contentDeferred.reject(error);
} else {
contentDeferred.reject(new Error(`Unknown error: ${error}`));
}
}
});
await this.#client.send('Tracing.end');
this.#recording = false; this.#recording = false;
return contentDeferred.valueOrThrow(); return buffer ?? undefined;
} }
} }

View File

@ -35,7 +35,13 @@ import {debugError} from './utils.js';
* @internal * @internal
*/ */
export class Browser extends BrowserBase { export class Browser extends BrowserBase {
static readonly subscribeModules = ['browsingContext', 'network', 'log']; static readonly subscribeModules = [
'browsingContext',
'network',
'log',
'cdp',
];
#browserName = ''; #browserName = '';
#browserVersion = ''; #browserVersion = '';
@ -55,11 +61,16 @@ export class Browser extends BrowserBase {
browserName = result.capabilities.browserName ?? ''; browserName = result.capabilities.browserName ?? '';
browserVersion = result.capabilities.browserVersion ?? ''; browserVersion = result.capabilities.browserVersion ?? '';
} catch (err) { } catch (err) {
// Chrome does not support session.new.
debugError(err); debugError(err);
} }
await opts.connection.send('session.subscribe', { await opts.connection.send('session.subscribe', {
events: Browser.subscribeModules as Bidi.Message.EventNames[], events: (browserName.toLocaleLowerCase().includes('firefox')
? Browser.subscribeModules.filter(module => {
return !['cdp'].includes(module);
})
: Browser.subscribeModules) as Bidi.Message.EventNames[],
}); });
return new Browser({ return new Browser({

View File

@ -59,6 +59,7 @@ export class BrowsingContext extends EventEmitter {
#timeoutSettings: TimeoutSettings; #timeoutSettings: TimeoutSettings;
#id: string; #id: string;
#url = 'about:blank'; #url = 'about:blank';
#cdpSessionId?: string;
constructor( constructor(
connection: Connection, connection: Connection,
@ -95,6 +96,10 @@ export class BrowsingContext extends EventEmitter {
return this.#id; return this.#id;
} }
get cdpSessionId(): string | undefined {
return this.#cdpSessionId;
}
async goto( async goto(
url: string, url: string,
options: { options: {
@ -285,14 +290,17 @@ export class BrowsingContext extends EventEmitter {
method: T, method: T,
params: ProtocolMapping.Commands[T]['paramsType'][0] = {} params: ProtocolMapping.Commands[T]['paramsType'][0] = {}
): Promise<ProtocolMapping.Commands[T]['returnType']> { ): Promise<ProtocolMapping.Commands[T]['returnType']> {
if (!this.#cdpSessionId) {
const session = await this.connection.send('cdp.getSession', { const session = await this.connection.send('cdp.getSession', {
context: this.#id, context: this.#id,
}); });
const sessionId = session.result.cdpSession; const sessionId = session.result.cdpSession;
this.#cdpSessionId = sessionId;
}
const result = await this.connection.send('cdp.sendCommand', { const result = await this.connection.send('cdp.sendCommand', {
cdpMethod: method, cdpMethod: method,
cdpParams: params, cdpParams: params,
cdpSession: sessionId, cdpSession: this.#cdpSessionId,
}); });
return result.result; return result.result;
} }

View File

@ -210,6 +210,16 @@ export class Connection extends EventEmitter {
// `log.entryAdded` specific context // `log.entryAdded` specific context
} else if ('source' in event.params && event.params.source.context) { } else if ('source' in event.params && event.params.source.context) {
context = this.#browsingContexts.get(event.params.source.context); context = this.#browsingContexts.get(event.params.source.context);
} else if (event.method === 'cdp.eventReceived') {
// TODO: this is not a good solution and we need to find a better one.
// Perhaps we need to have a dedicated CDP event emitter or emulate
// the CDPSession interface with BiDi?.
const cdpSessionId = event.params.cdpSession;
for (const context of this.#browsingContexts.values()) {
if (context.cdpSessionId === cdpSessionId) {
context?.emit(event.params.cdpMethod, event.params.cdpParams);
}
}
} }
context?.emit(event.method, event.params); context?.emit(event.method, event.params);
} }

View File

@ -17,6 +17,7 @@
import type {Readable} from 'stream'; import type {Readable} from 'stream';
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import Protocol from 'devtools-protocol';
import { import {
Page as PageBase, Page as PageBase,
@ -36,6 +37,7 @@ import {NetworkManagerEmittedEvents} from '../NetworkManager.js';
import {PDFOptions} from '../PDFOptions.js'; import {PDFOptions} from '../PDFOptions.js';
import {Viewport} from '../PuppeteerViewport.js'; import {Viewport} from '../PuppeteerViewport.js';
import {TimeoutSettings} from '../TimeoutSettings.js'; import {TimeoutSettings} from '../TimeoutSettings.js';
import {Tracing} from '../Tracing.js';
import {EvaluateFunc, HandleFor} from '../types.js'; import {EvaluateFunc, HandleFor} from '../types.js';
import { import {
debugError, debugError,
@ -117,6 +119,7 @@ export class Page extends PageBase {
}, },
], ],
]); ]);
#tracing: Tracing;
constructor(browserContext: BrowserContext, info: {context: string}) { constructor(browserContext: BrowserContext, info: {context: string}) {
super(); super();
@ -151,12 +154,38 @@ export class Page extends PageBase {
.sendCDPCommand('Accessibility.getFullAXTree'); .sendCDPCommand('Accessibility.getFullAXTree');
}, },
}); });
this.#tracing = new Tracing({
read: opts => {
return this.mainFrame().context().sendCDPCommand('IO.read', opts);
},
close: opts => {
return this.mainFrame().context().sendCDPCommand('IO.close', opts);
},
start: opts => {
return this.mainFrame().context().sendCDPCommand('Tracing.start', opts);
},
stop: async () => {
const deferred = Deferred.create();
this.mainFrame()
.context()
.once('Tracing.tracingComplete', event => {
deferred.resolve(event);
});
await this.mainFrame().context().sendCDPCommand('Tracing.end');
return deferred.valueOrThrow() as Promise<Protocol.Tracing.TracingCompleteEvent>;
},
});
} }
override get accessibility(): Accessibility { override get accessibility(): Accessibility {
return this.#accessibility; return this.#accessibility;
} }
override get tracing(): Tracing {
return this.#tracing;
}
override browser(): Browser { override browser(): Browser {
return this.#browserContext.browser(); return this.#browserContext.browser();
} }

View File

@ -574,11 +574,19 @@ export async function getReadableAsBuffer(
} }
} }
/**
* @internal
*/
export interface ProtocolReadable {
read(opts: {handle: string; size: number}): Promise<Protocol.IO.ReadResponse>;
close(opts: {handle: string}): Promise<void>;
}
/** /**
* @internal * @internal
*/ */
export async function getReadableFromProtocolStream( export async function getReadableFromProtocolStream(
client: CDPSession, source: ProtocolReadable,
handle: string handle: string
): Promise<Readable> { ): Promise<Readable> {
// TODO: Once Node 18 becomes the lowest supported version, we can migrate to // TODO: Once Node 18 becomes the lowest supported version, we can migrate to
@ -597,11 +605,11 @@ export async function getReadableFromProtocolStream(
} }
try { try {
const response = await client.send('IO.read', {handle, size}); const response = await source.read({handle, size});
this.push(response.data, response.base64Encoded ? 'base64' : undefined); this.push(response.data, response.base64Encoded ? 'base64' : undefined);
if (response.eof) { if (response.eof) {
eof = true; eof = true;
await client.send('IO.close', {handle}); await source.close({handle});
this.push(null); this.push(null);
} }
} catch (error) { } catch (error) {

View File

@ -269,12 +269,6 @@
"parameters": ["webDriverBiDi"], "parameters": ["webDriverBiDi"],
"expectations": ["PASS"] "expectations": ["PASS"]
}, },
{
"testIdPattern": "[browser.spec] Browser specs Browser.version should return version",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["PASS"]
},
{ {
"testIdPattern": "[chromiumonly.spec] *", "testIdPattern": "[chromiumonly.spec] *",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -857,12 +851,24 @@
"parameters": ["cdp", "firefox"], "parameters": ["cdp", "firefox"],
"expectations": ["FAIL", "SKIP"] "expectations": ["FAIL", "SKIP"]
}, },
{
"testIdPattern": "[tracing.spec] *",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
},
{ {
"testIdPattern": "[tracing.spec] *", "testIdPattern": "[tracing.spec] *",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
"parameters": ["cdp", "firefox"], "parameters": ["cdp", "firefox"],
"expectations": ["SKIP"] "expectations": ["SKIP"]
}, },
{
"testIdPattern": "[tracing.spec] Tracing should throw if tracing on two pages",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
},
{ {
"testIdPattern": "[waittask.spec] waittask specs Frame.waitForTimeout waits for the given timeout before resolving", "testIdPattern": "[waittask.spec] waittask specs Frame.waitForTimeout waits for the given timeout before resolving",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -899,6 +905,12 @@
"parameters": ["chrome", "webDriverBiDi"], "parameters": ["chrome", "webDriverBiDi"],
"expectations": ["FAIL"] "expectations": ["FAIL"]
}, },
{
"testIdPattern": "[browser.spec] Browser specs Browser.version should return version",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["PASS"]
},
{ {
"testIdPattern": "[browsercontext.spec] BrowserContext should fire target events", "testIdPattern": "[browsercontext.spec] BrowserContext should fire target events",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -2439,7 +2451,7 @@
"testIdPattern": "[queryhandler.spec] Query handler tests P selectors should work for ARIA selectors in multiple isolated worlds", "testIdPattern": "[queryhandler.spec] Query handler tests P selectors should work for ARIA selectors in multiple isolated worlds",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
"parameters": ["cdp", "firefox"], "parameters": ["cdp", "firefox"],
"expectations": ["FAIL"] "expectations": ["FAIL", "TIMEOUT"]
}, },
{ {
"testIdPattern": "[requestinterception-experimental.spec] request interception \"after each\" hook in \"request interception\"", "testIdPattern": "[requestinterception-experimental.spec] request interception \"after each\" hook in \"request interception\"",

View File

@ -40,9 +40,9 @@ describe('Tracing', function () {
fs.unlinkSync(outputFile); fs.unlinkSync(outputFile);
} }
}); });
it('should output a trace', async () => { it('should output a trace', async () => {
const {server, page} = testState; const {server, page} = testState;
await page.tracing.start({screenshots: true, path: outputFile}); await page.tracing.start({screenshots: true, path: outputFile});
await page.goto(server.PREFIX + '/grid.html'); await page.goto(server.PREFIX + '/grid.html');
await page.tracing.stop(); await page.tracing.stop();