chore: tracing over bidi+ (#10370)
Co-authored-by: Nikolay Vitkov <34244704+Lightning00Blade@users.noreply.github.com>
This commit is contained in:
parent
c2d3488ad8
commit
9473d740e7
@ -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> {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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({
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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\"",
|
||||||
|
@ -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();
|
||||||
|
Loading…
Reference in New Issue
Block a user