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 * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {HTTPResponse} from '../../api/HTTPResponse.js';
import {WaitForOptions} from '../../api/Page.js'; import {WaitForOptions} from '../../api/Page.js';
import {assert} from '../../util/assert.js'; import {assert} from '../../util/assert.js';
import {stringifyFunction} from '../../util/Function.js'; import {stringifyFunction} from '../../util/Function.js';
@ -24,7 +25,7 @@ import {EventEmitter} from '../EventEmitter.js';
import {PuppeteerLifeCycleEvent} from '../LifecycleWatcher.js'; import {PuppeteerLifeCycleEvent} from '../LifecycleWatcher.js';
import {TimeoutSettings} from '../TimeoutSettings.js'; import {TimeoutSettings} from '../TimeoutSettings.js';
import {EvaluateFunc, HandleFor} from '../types.js'; import {EvaluateFunc, HandleFor} from '../types.js';
import {isString} from '../util.js'; import {isString, waitWithTimeout} from '../util.js';
import {Connection} from './Connection.js'; import {Connection} from './Connection.js';
import {ElementHandle} from './ElementHandle.js'; import {ElementHandle} from './ElementHandle.js';
@ -34,7 +35,7 @@ import {BidiSerializer} from './Serializer.js';
/** /**
* @internal * @internal
*/ */
const puppeteerToReadinessState = new Map< const lifeCycleToReadinessState = new Map<
PuppeteerLifeCycleEvent, PuppeteerLifeCycleEvent,
Bidi.BrowsingContext.ReadinessState Bidi.BrowsingContext.ReadinessState
>([ >([
@ -42,6 +43,14 @@ const puppeteerToReadinessState = new Map<
['domcontentloaded', 'interactive'], ['domcontentloaded', 'interactive'],
]); ]);
/**
* @internal
*/
const lifeCycleToSubscribedEvent = new Map<PuppeteerLifeCycleEvent, string>([
['load', 'browsingContext.load'],
['domcontentloaded', 'browsingContext.domContentLoaded'],
]);
/** /**
* @internal * @internal
*/ */
@ -150,69 +159,102 @@ export class Context extends EventEmitter {
referer?: string | undefined; referer?: string | undefined;
referrerPolicy?: string | undefined; referrerPolicy?: string | undefined;
} = {} } = {}
): Promise<null> { ): Promise<HTTPResponse | null> {
const {waitUntil = 'load'} = options; const {
waitUntil = 'load',
timeout = this._timeoutSettings.navigationTimeout(),
} = options;
const readinessState = lifeCycleToReadinessState.get(
getWaitUntilSingle(waitUntil)
) as Bidi.BrowsingContext.ReadinessState;
try { try {
const response = await Promise.race([ const response = await waitWithTimeout(
this.connection.send('browsingContext.navigate', { this.connection.send('browsingContext.navigate', {
url: url, url: url,
context: this.id, context: this.id,
wait: getWaitUntil(waitUntil), wait: readinessState,
}), }),
new Promise((_, reject) => { 'Navigation',
const timeout = timeout
options.timeout ?? this._timeoutSettings.navigationTimeout(); );
if (!timeout) { this.#url = response.result.url;
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;
return null; return null;
} catch (error) { } catch (error) {
if (error instanceof ProtocolError) { if (error instanceof ProtocolError) {
error.message += ` at ${url}`; error.message += ` at ${url}`;
} else if (error instanceof TimeoutError) {
error.message = 'Navigation timeout of ' + timeout + ' ms exceeded';
} }
throw error; 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 { url(): string {
return this.#url; 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, WaitForOptions,
} from '../../api/Page.js'; } from '../../api/Page.js';
import {ConsoleMessage, ConsoleMessageLocation} from '../ConsoleMessage.js'; import {ConsoleMessage, ConsoleMessageLocation} from '../ConsoleMessage.js';
import {Handler} from '../EventEmitter.js';
import {EvaluateFunc, HandleFor} from '../types.js'; import {EvaluateFunc, HandleFor} from '../types.js';
import {Context, getBidiHandle} from './Context.js'; import {Context, getBidiHandle} from './Context.js';
@ -33,25 +34,26 @@ import {BidiSerializer} from './Serializer.js';
*/ */
export class Page extends PageBase { export class Page extends PageBase {
#context: Context; #context: Context;
#subscribedEvents = [ #subscribedEvents = new Map<string, Handler<any>>([
'log.entryAdded', ['log.entryAdded', this.#onLogEntryAdded.bind(this)],
'browsingContext.load', ['browsingContext.load', this.#onLoad.bind(this)],
] as Bidi.Session.SubscribeParameters['events']; ['browsingContext.domContentLoaded', this.#onDOMLoad.bind(this)],
]) as Map<Bidi.Session.SubscribeParametersEvent, Handler>;
#boundOnLogEntryAdded = this.#onLogEntryAdded.bind(this);
#boundOnLoaded = this.#onLoad.bind(this);
constructor(context: Context) { constructor(context: Context) {
super(); super();
this.#context = context; this.#context = context;
this.#context.connection.send('session.subscribe', { this.#context.connection.send('session.subscribe', {
events: this.#subscribedEvents, events: [
...this.#subscribedEvents.keys(),
] as Bidi.Session.SubscribeParameters['events'],
contexts: [this.#context.id], contexts: [this.#context.id],
}); });
this.#context.on('log.entryAdded', this.#boundOnLogEntryAdded); for (const [event, subscriber] of this.#subscribedEvents) {
this.#context.on('browsingContext.load', this.#boundOnLoaded); this.#context.on(event, subscriber);
}
} }
#onLogEntryAdded(event: Bidi.Log.LogEntry): void { #onLogEntryAdded(event: Bidi.Log.LogEntry): void {
@ -95,9 +97,13 @@ export class Page extends PageBase {
this.emit(PageEmittedEvents.Load); this.emit(PageEmittedEvents.Load);
} }
#onDOMLoad(_event: Bidi.BrowsingContext.NavigationInfo): void {
this.emit(PageEmittedEvents.DOMContentLoaded);
}
override async close(): Promise<void> { override async close(): Promise<void> {
await this.#context.connection.send('session.unsubscribe', { await this.#context.connection.send('session.unsubscribe', {
events: this.#subscribedEvents, events: [...this.#subscribedEvents.keys()],
contexts: [this.#context.id], contexts: [this.#context.id],
}); });
@ -105,8 +111,9 @@ export class Page extends PageBase {
context: this.#context.id, context: this.#context.id,
}); });
this.#context.off('log.entryAdded', this.#boundOnLogEntryAdded); for (const [event, subscriber] of this.#subscribedEvents) {
this.#context.off('browsingContext.load', this.#boundOnLogEntryAdded); this.#context.off(event, subscriber);
}
} }
override async evaluateHandle< override async evaluateHandle<
@ -150,6 +157,26 @@ export class Page extends PageBase {
override setDefaultTimeout(timeout: number): void { override setDefaultTimeout(timeout: number): void {
this.#context._timeoutSettings.setDefaultTimeout(timeout); 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( function isConsoleLogEntry(

View File

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

View File

@ -1802,8 +1802,8 @@
{ {
"testIdPattern": "[jshandle.spec] JSHandle JSHandle.asElement should return ElementHandle for TextNodes", "testIdPattern": "[jshandle.spec] JSHandle JSHandle.asElement should return ElementHandle for TextNodes",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"], "parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL"] "expectations": ["FAIL", "TIMEOUT"]
}, },
{ {
"testIdPattern": "[jshandle.spec] JSHandle JSHandle.toString should work for complicated objects", "testIdPattern": "[jshandle.spec] JSHandle JSHandle.toString should work for complicated objects",
@ -1962,88 +1962,28 @@
"expectations": ["FAIL"] "expectations": ["FAIL"]
}, },
{ {
"testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work", "testIdPattern": "[navigation.spec] navigation Page.waitForNavigation *",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"], "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"], "platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"], "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"], "platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"], "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"], "platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"], "parameters": ["webDriverBiDi"],
"expectations": ["FAIL"] "expectations": ["SKIP"]
},
{
"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"]
}, },
{ {
"testIdPattern": "[navigation.spec] navigation Page.reload should work", "testIdPattern": "[navigation.spec] navigation Page.reload should work",
@ -2068,5 +2008,47 @@
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "headless", "webDriverBiDi"], "parameters": ["firefox", "headless", "webDriverBiDi"],
"expectations": ["FAIL"] "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> => { ): Promise<any> => {
return new Promise(fulfill => { return new Promise(fulfill => {
emitter.on(eventName, function listener(event: any) { emitter.once(eventName, (event: any) => {
if (!predicate(event)) { if (!predicate(event)) {
return; return;
} }
emitter.off(eventName, listener);
fulfill(event); fulfill(event);
}); });
}); });