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 * @internal
*/ */
export class CDPJSHandle<T> extends JSHandle<T> { export class CDPJSHandle<T = unknown> extends JSHandle<T> {
/** /**
* Used for nominally typing {@link JSHandle}. * Used for nominally typing {@link JSHandle}.
*/ */

View File

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

View File

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

View File

@ -42,6 +42,7 @@ interface Commands {
params: Bidi.Script.DisownParameters; params: Bidi.Script.DisownParameters;
returnType: Bidi.Script.DisownResult; returnType: Bidi.Script.DisownResult;
}; };
'browsingContext.create': { 'browsingContext.create': {
params: Bidi.BrowsingContext.CreateParameters; params: Bidi.BrowsingContext.CreateParameters;
returnType: Bidi.BrowsingContext.CreateResult; returnType: Bidi.BrowsingContext.CreateResult;
@ -50,10 +51,23 @@ interface Commands {
params: Bidi.BrowsingContext.CloseParameters; params: Bidi.BrowsingContext.CloseParameters;
returnType: Bidi.BrowsingContext.CloseResult; returnType: Bidi.BrowsingContext.CloseResult;
}; };
'session.new': {
params: {capabilities?: Record<any, unknown>}; // TODO: Update Types in chromium bidi
returnType: {sessionId: string};
};
'session.status': { 'session.status': {
params: {context: string}; // TODO: Update Types in chromium bidi params: {context: string}; // TODO: Update Types in chromium bidi
returnType: Bidi.Session.StatusResult; 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> { override async jsonValue(): Promise<T> {
if (!('handle' in this.#remoteValue)) { const value = BidiSerializer.deserialize(this.#remoteValue);
return BidiSerializer.deserialize(this.#remoteValue);
} if (this.#remoteValue.type !== 'undefined' && value === undefined) {
const value = await this.evaluate(object => {
return object;
});
if (value === undefined) {
throw new Error('Could not serialize referenced object'); throw new Error('Could not serialize referenced object');
} }
return value; 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 { override toString(): string {
if (!('handle' in this.#remoteValue)) { if (this.isPrimitiveValue) {
return 'JSHandle:' + BidiSerializer.deserialize(this.#remoteValue); return 'JSHandle:' + BidiSerializer.deserialize(this.#remoteValue);
} }

View File

@ -14,33 +14,90 @@
* limitations under the License. * 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 {stringifyFunction} from '../../util/Function.js';
import {ConsoleMessage, ConsoleMessageLocation} from '../ConsoleMessage.js';
import type {EvaluateFunc, HandleFor} from '../types.js'; import type {EvaluateFunc, HandleFor} from '../types.js';
import {isString} from '../util.js'; import {isString} from '../util.js';
import {Connection} from './Connection.js'; import {Connection} from './Connection.js';
import {JSHandle} from './JSHandle.js'; import {JSHandle} from './JSHandle.js';
import {BidiSerializer} from './Serializer.js'; import {BidiSerializer} from './Serializer.js';
import {Reference} from './types.js';
/** /**
* @internal * @internal
*/ */
export class Page extends PageBase { export class Page extends PageBase {
#connection: Connection; #connection: Connection;
#subscribedEvents = [
'log.entryAdded',
] as Bidi.Session.SubscribeParameters['events'];
_contextId: string; _contextId: string;
constructor(connection: Connection, contextId: string) { constructor(connection: Connection, contextId: string) {
super(); super();
this.#connection = connection; this.#connection = connection;
this._contextId = contextId; 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> { override async close(): Promise<void> {
await this.#connection.send('browsingContext.close', { await this.#connection.send('browsingContext.close', {
context: this._contextId, 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 { get connection(): Connection {
@ -122,20 +179,49 @@ export class Page extends PageBase {
return returnByValue return returnByValue
? BidiSerializer.deserialize(result.result) ? BidiSerializer.deserialize(result.result)
: getBidiHandle(this, result.result as Reference); : getBidiHandle(this, result.result);
} }
} }
/** /**
* @internal * @internal
*/ */
export function getBidiHandle(context: Page, result: Reference): JSHandle { export function getBidiHandle(
// TODO: | ElementHandle<Node> context: Page,
result: Bidi.CommonDataTypes.RemoteValue
): JSHandle {
if ( if (
(result.type === 'node' || result.type === 'window') && (result.type === 'node' || result.type === 'window') &&
context._contextId context._contextId
) { ) {
throw new Error('ElementHandle not implemented'); // TODO: Implement ElementHandle
return new JSHandle(context, result);
} }
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"] "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"], "platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox"], "parameters": ["firefox"],
"expectations": ["FAIL"] "expectations": ["FAIL"]
@ -1763,24 +1763,12 @@
"parameters": ["webDriverBiDi"], "parameters": ["webDriverBiDi"],
"expectations": ["PASS"] "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", "testIdPattern": "[jshandle.spec] JSHandle Page.evaluateHandle should return the RemoteObject",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"], "parameters": ["webDriverBiDi"],
"expectations": ["FAIL"] "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", "testIdPattern": "[jshandle.spec] JSHandle JSHandle.jsonValue should not work with dates",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -1808,7 +1796,7 @@
{ {
"testIdPattern": "[jshandle.spec] JSHandle JSHandle.toString should work with different subtypes", "testIdPattern": "[jshandle.spec] JSHandle JSHandle.toString should work with different subtypes",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"], "parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL"] "expectations": ["FAIL"]
}, },
{ {
@ -1822,5 +1810,17 @@
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"], "parameters": ["webDriverBiDi"],
"expectations": ["SKIP", "FAIL"] "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()[1]!.jsonValue()).toEqual(5);
expect(await message.args()[2]!.jsonValue()).toEqual({foo: 'bar'}); 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 {page} = getTestState();
const messages: any[] = []; const messages: any[] = [];
@ -680,29 +712,13 @@ describe('Page', function () {
// A pair of time/timeEnd generates only one Console API call. // A pair of time/timeEnd generates only one Console API call.
console.time('calling console.time'); console.time('calling console.time');
console.timeEnd('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( expect(
messages.map(msg => { messages.map(msg => {
return msg.type(); return msg.type();
}) })
).toEqual(['timeEnd', 'trace', 'dir', 'warning', 'error', 'log']); ).toEqual(['timeEnd']);
expect(messages[0]!.text()).toContain('calling console.time'); 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 () => { it('should not fail for window object', async () => {
const {page} = getTestState(); const {page} = getTestState();