chore: Add Page.Console event to BiDi (#9700)

This commit is contained in:
Nikolay Vitkov 2023-02-20 13:00:29 +01:00 committed by GitHub
parent fb0d405ee3
commit 9b54365df5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 176 additions and 59 deletions

View File

@ -30,7 +30,7 @@ declare const __JSHandleSymbol: unique symbol;
/**
* @internal
*/
export class CDPJSHandle<T> extends JSHandle<T> {
export class CDPJSHandle<T = unknown> extends JSHandle<T> {
/**
* Used for nominally typing {@link JSHandle}.
*/

View File

@ -15,7 +15,6 @@
*/
import {Protocol} from 'devtools-protocol';
import {JSHandle} from '../api/JSHandle.js';
import {createDeferredPromise} from '../util/DeferredPromise.js';
import {CDPSession} from './Connection.js';
@ -31,7 +30,7 @@ import {debugError} from './util.js';
*/
export type ConsoleAPICalledCallback = (
eventType: ConsoleMessageType,
handles: JSHandle[],
handles: CDPJSHandle[],
trace: Protocol.Runtime.StackTrace
) => void;

View File

@ -36,10 +36,7 @@ export class Browser extends BrowserBase {
static async create(opts: Options): Promise<Browser> {
// TODO: await until the connection is established.
try {
// TODO: Add 'session.new' to BiDi types
(await opts.connection.send('session.new' as any, {})) as unknown as {
sessionId: string;
};
await opts.connection.send('session.new', {});
} catch {}
return new Browser(opts);
}

View File

@ -42,6 +42,7 @@ interface Commands {
params: Bidi.Script.DisownParameters;
returnType: Bidi.Script.DisownResult;
};
'browsingContext.create': {
params: Bidi.BrowsingContext.CreateParameters;
returnType: Bidi.BrowsingContext.CreateResult;
@ -50,10 +51,23 @@ interface Commands {
params: Bidi.BrowsingContext.CloseParameters;
returnType: Bidi.BrowsingContext.CloseResult;
};
'session.new': {
params: {capabilities?: Record<any, unknown>}; // TODO: Update Types in chromium bidi
returnType: {sessionId: string};
};
'session.status': {
params: {context: string}; // TODO: Update Types in chromium bidi
returnType: Bidi.Session.StatusResult;
};
'session.subscribe': {
params: Bidi.Session.SubscribeParameters;
returnType: Bidi.Session.SubscribeResult;
};
'session.unsubscribe': {
params: Bidi.Session.SubscribeParameters;
returnType: Bidi.Session.UnsubscribeResult;
};
}
/**

View File

@ -104,13 +104,9 @@ export class JSHandle<T = unknown> extends BaseJSHandle<T> {
}
override async jsonValue(): Promise<T> {
if (!('handle' in this.#remoteValue)) {
return BidiSerializer.deserialize(this.#remoteValue);
}
const value = await this.evaluate(object => {
return object;
});
if (value === undefined) {
const value = BidiSerializer.deserialize(this.#remoteValue);
if (this.#remoteValue.type !== 'undefined' && value === undefined) {
throw new Error('Could not serialize referenced object');
}
return value;
@ -130,8 +126,23 @@ export class JSHandle<T = unknown> extends BaseJSHandle<T> {
}
}
get isPrimitiveValue(): boolean {
switch (this.#remoteValue.type) {
case 'string':
case 'number':
case 'bigint':
case 'boolean':
case 'undefined':
case 'null':
return true;
default:
return false;
}
}
override toString(): string {
if (!('handle' in this.#remoteValue)) {
if (this.isPrimitiveValue) {
return 'JSHandle:' + BidiSerializer.deserialize(this.#remoteValue);
}

View File

@ -14,33 +14,90 @@
* limitations under the License.
*/
import {Page as PageBase} from '../../api/Page.js';
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {Page as PageBase, PageEmittedEvents} from '../../api/Page.js';
import {stringifyFunction} from '../../util/Function.js';
import {ConsoleMessage, ConsoleMessageLocation} from '../ConsoleMessage.js';
import type {EvaluateFunc, HandleFor} from '../types.js';
import {isString} from '../util.js';
import {Connection} from './Connection.js';
import {JSHandle} from './JSHandle.js';
import {BidiSerializer} from './Serializer.js';
import {Reference} from './types.js';
/**
* @internal
*/
export class Page extends PageBase {
#connection: Connection;
#subscribedEvents = [
'log.entryAdded',
] as Bidi.Session.SubscribeParameters['events'];
_contextId: string;
constructor(connection: Connection, contextId: string) {
super();
this.#connection = connection;
this._contextId = contextId;
// TODO: Investigate an implementation similar to CDPSession
this.connection.send('session.subscribe', {
events: this.#subscribedEvents,
contexts: [this._contextId],
});
this.connection.on('log.entryAdded', this.#onLogEntryAdded.bind(this));
}
#onLogEntryAdded(event: Bidi.Log.LogEntry): void {
if (isConsoleLogEntry(event)) {
const args = event.args.map(arg => {
return getBidiHandle(this, arg);
});
const text = args
.reduce((value, arg) => {
const parsedValue = arg.isPrimitiveValue
? BidiSerializer.deserialize(arg.bidiObject())
: arg.toString();
return `${value} ${parsedValue}`;
}, '')
.slice(1);
this.emit(
PageEmittedEvents.Console,
new ConsoleMessage(
event.method as any,
text,
args,
getStackTraceLocations(event.stackTrace)
)
);
} else if (isJavaScriptLogEntry(event)) {
this.emit(
PageEmittedEvents.Console,
new ConsoleMessage(
event.level as any,
event.text ?? '',
[],
getStackTraceLocations(event.stackTrace)
)
);
}
}
override async close(): Promise<void> {
await this.#connection.send('browsingContext.close', {
context: this._contextId,
});
this.connection.send('session.unsubscribe', {
events: this.#subscribedEvents,
contexts: [this._contextId],
});
this.connection.off('log.entryAdded', this.#onLogEntryAdded.bind(this));
}
get connection(): Connection {
@ -122,20 +179,49 @@ export class Page extends PageBase {
return returnByValue
? BidiSerializer.deserialize(result.result)
: getBidiHandle(this, result.result as Reference);
: getBidiHandle(this, result.result);
}
}
/**
* @internal
*/
export function getBidiHandle(context: Page, result: Reference): JSHandle {
// TODO: | ElementHandle<Node>
export function getBidiHandle(
context: Page,
result: Bidi.CommonDataTypes.RemoteValue
): JSHandle {
if (
(result.type === 'node' || result.type === 'window') &&
context._contextId
) {
throw new Error('ElementHandle not implemented');
// TODO: Implement ElementHandle
return new JSHandle(context, result);
}
return new JSHandle(context, result);
}
function isConsoleLogEntry(
event: Bidi.Log.LogEntry
): event is Bidi.Log.ConsoleLogEntry {
return event.type === 'console';
}
function isJavaScriptLogEntry(
event: Bidi.Log.LogEntry
): event is Bidi.Log.JavascriptLogEntry {
return event.type === 'javascript';
}
function getStackTraceLocations(stackTrace?: Bidi.Script.StackTrace) {
const stackTraceLocations: ConsoleMessageLocation[] = [];
if (stackTrace) {
for (const callFrame of stackTrace.callFrames) {
stackTraceLocations.push({
url: callFrame.url,
lineNumber: callFrame.lineNumber,
columnNumber: callFrame.columnNumber,
});
}
}
return stackTraceLocations;
}

View File

@ -1,6 +0,0 @@
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
export type Reference = Extract<
Bidi.CommonDataTypes.RemoteValue,
Bidi.CommonDataTypes.RemoteReference
>;

View File

@ -1068,7 +1068,7 @@
"expectations": ["FAIL"]
},
{
"testIdPattern": "[page.spec] Page Page.Events.Console should work for different console API calls",
"testIdPattern": "[page.spec] Page Page.Events.Console should work for different console API calls with logging functions",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox"],
"expectations": ["FAIL"]
@ -1763,24 +1763,12 @@
"parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[jshandle.spec] JSHandle Page.evaluateHandle should work",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[jshandle.spec] JSHandle Page.evaluateHandle should return the RemoteObject",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[jshandle.spec] JSHandle Page.evaluateHandle should use the same JS wrappers",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[jshandle.spec] JSHandle JSHandle.jsonValue should not work with dates",
"platforms": ["darwin", "linux", "win32"],
@ -1808,7 +1796,7 @@
{
"testIdPattern": "[jshandle.spec] JSHandle JSHandle.toString should work with different subtypes",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL"]
},
{
@ -1822,5 +1810,17 @@
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["SKIP", "FAIL"]
},
{
"testIdPattern": "[page.spec] Page Page.Events.Console should work",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[page.spec] Page Page.Events.Console should work for different console API calls with timing functions",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL"]
}
]

View File

@ -668,7 +668,39 @@ describe('Page', function () {
expect(await message.args()[1]!.jsonValue()).toEqual(5);
expect(await message.args()[2]!.jsonValue()).toEqual({foo: 'bar'});
});
it('should work for different console API calls', async () => {
it('should work for different console API calls with logging functions', async () => {
const {page} = getTestState();
const messages: any[] = [];
page.on('console', msg => {
return messages.push(msg);
});
// All console events will be reported before `page.evaluate` is finished.
await page.evaluate(() => {
console.trace('calling console.trace');
console.dir('calling console.dir');
console.warn('calling console.warn');
console.error('calling console.error');
console.log(Promise.resolve('should not wait until resolved!'));
});
expect(
messages.map(msg => {
return msg.type();
})
).toEqual(['trace', 'dir', 'warning', 'error', 'log']);
expect(
messages.map(msg => {
return msg.text();
})
).toEqual([
'calling console.trace',
'calling console.dir',
'calling console.warn',
'calling console.error',
'JSHandle@promise',
]);
});
it('should work for different console API calls with timing functions', async () => {
const {page} = getTestState();
const messages: any[] = [];
@ -680,29 +712,13 @@ describe('Page', function () {
// A pair of time/timeEnd generates only one Console API call.
console.time('calling console.time');
console.timeEnd('calling console.time');
console.trace('calling console.trace');
console.dir('calling console.dir');
console.warn('calling console.warn');
console.error('calling console.error');
console.log(Promise.resolve('should not wait until resolved!'));
});
expect(
messages.map(msg => {
return msg.type();
})
).toEqual(['timeEnd', 'trace', 'dir', 'warning', 'error', 'log']);
).toEqual(['timeEnd']);
expect(messages[0]!.text()).toContain('calling console.time');
expect(
messages.slice(1).map(msg => {
return msg.text();
})
).toEqual([
'calling console.trace',
'calling console.dir',
'calling console.warn',
'calling console.error',
'JSHandle@promise',
]);
});
it('should not fail for window object', async () => {
const {page} = getTestState();