chore: add BiDi support for SetContent (#9878)

This commit is contained in:
Nikolay Vitkov 2023-03-20 14:00:13 +01:00 committed by GitHub
parent 9ccde6ebf5
commit cb079378bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 183 additions and 133 deletions

View File

@ -16,6 +16,7 @@
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {HTTPResponse} from '../../api/HTTPResponse.js';
import {WaitForOptions} from '../../api/Page.js';
import {assert} from '../../util/assert.js';
import {stringifyFunction} from '../../util/Function.js';
@ -24,7 +25,7 @@ import {EventEmitter} from '../EventEmitter.js';
import {PuppeteerLifeCycleEvent} from '../LifecycleWatcher.js';
import {TimeoutSettings} from '../TimeoutSettings.js';
import {EvaluateFunc, HandleFor} from '../types.js';
import {isString} from '../util.js';
import {isString, waitWithTimeout} from '../util.js';
import {Connection} from './Connection.js';
import {ElementHandle} from './ElementHandle.js';
@ -34,7 +35,7 @@ import {BidiSerializer} from './Serializer.js';
/**
* @internal
*/
const puppeteerToReadinessState = new Map<
const lifeCycleToReadinessState = new Map<
PuppeteerLifeCycleEvent,
Bidi.BrowsingContext.ReadinessState
>([
@ -42,6 +43,14 @@ const puppeteerToReadinessState = new Map<
['domcontentloaded', 'interactive'],
]);
/**
* @internal
*/
const lifeCycleToSubscribedEvent = new Map<PuppeteerLifeCycleEvent, string>([
['load', 'browsingContext.load'],
['domcontentloaded', 'browsingContext.domContentLoaded'],
]);
/**
* @internal
*/
@ -150,69 +159,102 @@ export class Context extends EventEmitter {
referer?: string | undefined;
referrerPolicy?: string | undefined;
} = {}
): Promise<null> {
const {waitUntil = 'load'} = options;
): Promise<HTTPResponse | null> {
const {
waitUntil = 'load',
timeout = this._timeoutSettings.navigationTimeout(),
} = options;
const readinessState = lifeCycleToReadinessState.get(
getWaitUntilSingle(waitUntil)
) as Bidi.BrowsingContext.ReadinessState;
try {
const response = await Promise.race([
const response = await waitWithTimeout(
this.connection.send('browsingContext.navigate', {
url: url,
context: this.id,
wait: getWaitUntil(waitUntil),
wait: readinessState,
}),
new Promise((_, reject) => {
const timeout =
options.timeout ?? this._timeoutSettings.navigationTimeout();
if (!timeout) {
return;
}
const error = new TimeoutError(
'Navigation timeout of ' + timeout + ' ms exceeded'
);
return setTimeout(() => {
return reject(error);
}, timeout);
}),
]);
this.#url = (response as Bidi.BrowsingContext.NavigateResult).result.url;
'Navigation',
timeout
);
this.#url = response.result.url;
return null;
} catch (error) {
if (error instanceof ProtocolError) {
error.message += ` at ${url}`;
} else if (error instanceof TimeoutError) {
error.message = 'Navigation timeout of ' + timeout + ' ms exceeded';
}
throw error;
}
function getWaitUntil(
event: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]
): Bidi.BrowsingContext.ReadinessState {
if (Array.isArray(event) && event.length > 1) {
throw new Error('BiDi support only single `waitUntil` argument');
}
const waitUntilSingle = Array.isArray(event)
? (event.find(lifecycle => {
return lifecycle === 'domcontentloaded' || lifecycle === 'load';
}) as PuppeteerLifeCycleEvent)
: event;
if (
waitUntilSingle === 'networkidle0' ||
waitUntilSingle === 'networkidle2'
) {
throw new Error(`BiDi does not support 'waitUntil' ${waitUntilSingle}`);
}
assert(waitUntilSingle, `Invalid waitUntil option ${waitUntilSingle}`);
return puppeteerToReadinessState.get(
waitUntilSingle
) as Bidi.BrowsingContext.ReadinessState;
}
}
url(): string {
return this.#url;
}
async setContent(
html: string,
options: WaitForOptions | undefined = {}
): Promise<void> {
const {
waitUntil = 'load',
timeout = this._timeoutSettings.navigationTimeout(),
} = options;
const waitUntilCommand = lifeCycleToSubscribedEvent.get(
getWaitUntilSingle(waitUntil)
) as string;
await Promise.all([
// We rely upon the fact that document.open() will reset frame lifecycle with "init"
// lifecycle event. @see https://crrev.com/608658
this.evaluate(html => {
document.open();
document.write(html);
document.close();
}, html),
waitWithTimeout(
new Promise<void>(resolve => {
this.once(waitUntilCommand, () => {
resolve();
});
}),
waitUntilCommand,
timeout
),
]);
}
}
/**
* @internal
*/
function getWaitUntilSingle(
event: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]
): Extract<PuppeteerLifeCycleEvent, 'load' | 'domcontentloaded'> {
if (Array.isArray(event) && event.length > 1) {
throw new Error('BiDi support only single `waitUntil` argument');
}
const waitUntilSingle = Array.isArray(event)
? (event.find(lifecycle => {
return lifecycle === 'domcontentloaded' || lifecycle === 'load';
}) as PuppeteerLifeCycleEvent)
: event;
if (
waitUntilSingle === 'networkidle0' ||
waitUntilSingle === 'networkidle2'
) {
throw new Error(`BiDi does not support 'waitUntil' ${waitUntilSingle}`);
}
assert(waitUntilSingle, `Invalid waitUntil option ${waitUntilSingle}`);
return waitUntilSingle;
}
/**

View File

@ -23,6 +23,7 @@ import {
WaitForOptions,
} from '../../api/Page.js';
import {ConsoleMessage, ConsoleMessageLocation} from '../ConsoleMessage.js';
import {Handler} from '../EventEmitter.js';
import {EvaluateFunc, HandleFor} from '../types.js';
import {Context, getBidiHandle} from './Context.js';
@ -33,25 +34,26 @@ import {BidiSerializer} from './Serializer.js';
*/
export class Page extends PageBase {
#context: Context;
#subscribedEvents = [
'log.entryAdded',
'browsingContext.load',
] as Bidi.Session.SubscribeParameters['events'];
#boundOnLogEntryAdded = this.#onLogEntryAdded.bind(this);
#boundOnLoaded = this.#onLoad.bind(this);
#subscribedEvents = new Map<string, Handler<any>>([
['log.entryAdded', this.#onLogEntryAdded.bind(this)],
['browsingContext.load', this.#onLoad.bind(this)],
['browsingContext.domContentLoaded', this.#onDOMLoad.bind(this)],
]) as Map<Bidi.Session.SubscribeParametersEvent, Handler>;
constructor(context: Context) {
super();
this.#context = context;
this.#context.connection.send('session.subscribe', {
events: this.#subscribedEvents,
events: [
...this.#subscribedEvents.keys(),
] as Bidi.Session.SubscribeParameters['events'],
contexts: [this.#context.id],
});
this.#context.on('log.entryAdded', this.#boundOnLogEntryAdded);
this.#context.on('browsingContext.load', this.#boundOnLoaded);
for (const [event, subscriber] of this.#subscribedEvents) {
this.#context.on(event, subscriber);
}
}
#onLogEntryAdded(event: Bidi.Log.LogEntry): void {
@ -95,9 +97,13 @@ export class Page extends PageBase {
this.emit(PageEmittedEvents.Load);
}
#onDOMLoad(_event: Bidi.BrowsingContext.NavigationInfo): void {
this.emit(PageEmittedEvents.DOMContentLoaded);
}
override async close(): Promise<void> {
await this.#context.connection.send('session.unsubscribe', {
events: this.#subscribedEvents,
events: [...this.#subscribedEvents.keys()],
contexts: [this.#context.id],
});
@ -105,8 +111,9 @@ export class Page extends PageBase {
context: this.#context.id,
});
this.#context.off('log.entryAdded', this.#boundOnLogEntryAdded);
this.#context.off('browsingContext.load', this.#boundOnLogEntryAdded);
for (const [event, subscriber] of this.#subscribedEvents) {
this.#context.off(event, subscriber);
}
}
override async evaluateHandle<
@ -150,6 +157,26 @@ export class Page extends PageBase {
override setDefaultTimeout(timeout: number): void {
this.#context._timeoutSettings.setDefaultTimeout(timeout);
}
override async setContent(
html: string,
options: WaitForOptions = {}
): Promise<void> {
await this.#context.setContent(html, options);
}
override async content(): Promise<string> {
return await this.evaluate(() => {
let retVal = '';
if (document.doctype) {
retVal = new XMLSerializer().serializeToString(document.doctype);
}
if (document.documentElement) {
retVal += document.documentElement.outerHTML;
}
return retVal;
});
}
}
function isConsoleLogEntry(

View File

@ -339,7 +339,7 @@ export async function waitWithTimeout<T>(
const timeoutError = new TimeoutError(
`waiting for ${taskName} failed: timeout ${timeout}ms exceeded`
);
const timeoutPromise = new Promise<T>((_res, rej) => {
const timeoutPromise = new Promise<never>((_, rej) => {
return (reject = rej);
});
let timeoutTimer = null;

View File

@ -1802,8 +1802,8 @@
{
"testIdPattern": "[jshandle.spec] JSHandle JSHandle.asElement should return ElementHandle for TextNodes",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL", "TIMEOUT"]
},
{
"testIdPattern": "[jshandle.spec] JSHandle JSHandle.toString should work for complicated objects",
@ -1962,88 +1962,28 @@
"expectations": ["FAIL"]
},
{
"testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work",
"testIdPattern": "[navigation.spec] navigation Page.waitForNavigation *",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
"expectations": ["SKIP"]
},
{
"testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work with both domcontentloaded and load",
"testIdPattern": "[navigation.spec] navigation Page.goBack *",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
"expectations": ["SKIP"]
},
{
"testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work with clicking on anchor links",
"testIdPattern": "[navigation.spec] navigation Frame.goto *",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
"expectations": ["SKIP"]
},
{
"testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work with history.pushState()",
"testIdPattern": "[navigation.spec] navigation Frame.waitForNavigation *",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work with history.replaceState()",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work with DOM history.back()/history.forward()",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work when subframe issues window.stop()",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL", "TIMEOUT"]
},
{
"testIdPattern": "[navigation.spec] navigation Page.goBack should work",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[navigation.spec] navigation Page.goBack should work with HistoryAPI",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[navigation.spec] navigation Frame.goto should navigate subframes",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[navigation.spec] navigation Frame.goto should reject when frame detaches",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[navigation.spec] navigation Frame.goto should return matching responses",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[navigation.spec] navigation Frame.waitForNavigation should work",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[navigation.spec] navigation Frame.waitForNavigation should fail when frame detaches",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
"expectations": ["SKIP"]
},
{
"testIdPattern": "[navigation.spec] navigation Page.reload should work",
@ -2068,5 +2008,47 @@
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "headless", "webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[page.spec] Page Page.url should work",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[page.spec] Page Page.Events.DOMContentLoaded *",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[page.spec] Page Page.setContent *",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[page.spec] Page Page.setContent should work with tricky content",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[page.spec] Page Page.setContent should work with accents",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[page.spec] Page Page.setContent should work with emojis",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[page.spec] Page Page.setContent should work with newline",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["FAIL"]
}
]

View File

@ -140,11 +140,10 @@ export const waitEvent = (
}
): Promise<any> => {
return new Promise(fulfill => {
emitter.on(eventName, function listener(event: any) {
emitter.once(eventName, (event: any) => {
if (!predicate(event)) {
return;
}
emitter.off(eventName, listener);
fulfill(event);
});
});