diff --git a/packages/puppeteer-core/src/common/bidi/Context.ts b/packages/puppeteer-core/src/common/bidi/Context.ts index fb29886a..5620db2e 100644 --- a/packages/puppeteer-core/src/common/bidi/Context.ts +++ b/packages/puppeteer-core/src/common/bidi/Context.ts @@ -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([ + ['load', 'browsingContext.load'], + ['domcontentloaded', 'browsingContext.domContentLoaded'], +]); + /** * @internal */ @@ -150,69 +159,102 @@ export class Context extends EventEmitter { referer?: string | undefined; referrerPolicy?: string | undefined; } = {} - ): Promise { - const {waitUntil = 'load'} = options; + ): Promise { + 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 { + 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(resolve => { + this.once(waitUntilCommand, () => { + resolve(); + }); + }), + waitUntilCommand, + timeout + ), + ]); + } +} + +/** + * @internal + */ +function getWaitUntilSingle( + event: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[] +): Extract { + 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; } /** diff --git a/packages/puppeteer-core/src/common/bidi/Page.ts b/packages/puppeteer-core/src/common/bidi/Page.ts index 975b836b..b5177da8 100644 --- a/packages/puppeteer-core/src/common/bidi/Page.ts +++ b/packages/puppeteer-core/src/common/bidi/Page.ts @@ -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>([ + ['log.entryAdded', this.#onLogEntryAdded.bind(this)], + ['browsingContext.load', this.#onLoad.bind(this)], + ['browsingContext.domContentLoaded', this.#onDOMLoad.bind(this)], + ]) as Map; 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 { 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 { + await this.#context.setContent(html, options); + } + + override async content(): Promise { + 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( diff --git a/packages/puppeteer-core/src/common/util.ts b/packages/puppeteer-core/src/common/util.ts index d3f9c196..a13c803f 100644 --- a/packages/puppeteer-core/src/common/util.ts +++ b/packages/puppeteer-core/src/common/util.ts @@ -339,7 +339,7 @@ export async function waitWithTimeout( const timeoutError = new TimeoutError( `waiting for ${taskName} failed: timeout ${timeout}ms exceeded` ); - const timeoutPromise = new Promise((_res, rej) => { + const timeoutPromise = new Promise((_, rej) => { return (reject = rej); }); let timeoutTimer = null; diff --git a/test/TestExpectations.json b/test/TestExpectations.json index 75382d65..db98e588 100644 --- a/test/TestExpectations.json +++ b/test/TestExpectations.json @@ -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"] } ] diff --git a/test/src/utils.ts b/test/src/utils.ts index 939cda85..5a71b2d4 100644 --- a/test/src/utils.ts +++ b/test/src/utils.ts @@ -140,11 +140,10 @@ export const waitEvent = ( } ): Promise => { 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); }); });