mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-14 21:53:35 +03:00
feat(rpc): introduce Waiter for various waitFor implementations (#2935)
Use it for waitForEvent and waitForLoadState.
This commit is contained in:
parent
b2d820a185
commit
65d45c18c3
@ -57,7 +57,8 @@ type NavigationEvent = {
|
|||||||
documentInfo?: DocumentInfo, // Undefined for same-document navigations.
|
documentInfo?: DocumentInfo, // Undefined for same-document navigations.
|
||||||
error?: Error,
|
error?: Error,
|
||||||
};
|
};
|
||||||
export const kLifecycleEvent = Symbol('lifecycle');
|
export const kAddLifecycleEvent = Symbol('addLifecycle');
|
||||||
|
export const kRemoveLifecycleEvent = Symbol('removeLifecycle');
|
||||||
|
|
||||||
export class FrameManager {
|
export class FrameManager {
|
||||||
private _page: Page;
|
private _page: Page;
|
||||||
@ -380,7 +381,7 @@ export class Frame {
|
|||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
// Checking whether we have already notified about this event.
|
// Checking whether we have already notified about this event.
|
||||||
if (!this._subtreeLifecycleEvents.has(event)) {
|
if (!this._subtreeLifecycleEvents.has(event)) {
|
||||||
this._eventEmitter.emit(kLifecycleEvent, event);
|
this._eventEmitter.emit(kAddLifecycleEvent, event);
|
||||||
if (this === mainFrame && this._url !== 'about:blank')
|
if (this === mainFrame && this._url !== 'about:blank')
|
||||||
this._page._logger.info(` "${event}" event fired`);
|
this._page._logger.info(` "${event}" event fired`);
|
||||||
if (this === mainFrame && event === 'load')
|
if (this === mainFrame && event === 'load')
|
||||||
@ -389,6 +390,10 @@ export class Frame {
|
|||||||
this._page.emit(Events.Page.DOMContentLoaded);
|
this._page.emit(Events.Page.DOMContentLoaded);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (const event of this._subtreeLifecycleEvents) {
|
||||||
|
if (!events.has(event))
|
||||||
|
this._eventEmitter.emit(kRemoveLifecycleEvent, event);
|
||||||
|
}
|
||||||
this._subtreeLifecycleEvents = events;
|
this._subtreeLifecycleEvents = events;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -429,7 +434,7 @@ export class Frame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!this._subtreeLifecycleEvents.has(waitUntil))
|
if (!this._subtreeLifecycleEvents.has(waitUntil))
|
||||||
await helper.waitForEvent(progress, this._eventEmitter, kLifecycleEvent, (e: types.LifecycleEvent) => e === waitUntil).promise;
|
await helper.waitForEvent(progress, this._eventEmitter, kAddLifecycleEvent, (e: types.LifecycleEvent) => e === waitUntil).promise;
|
||||||
|
|
||||||
const request = event.documentInfo ? event.documentInfo.request : undefined;
|
const request = event.documentInfo ? event.documentInfo.request : undefined;
|
||||||
return request ? request._finalRequest().response() : null;
|
return request ? request._finalRequest().response() : null;
|
||||||
@ -453,7 +458,7 @@ export class Frame {
|
|||||||
throw navigationEvent.error;
|
throw navigationEvent.error;
|
||||||
|
|
||||||
if (!this._subtreeLifecycleEvents.has(waitUntil))
|
if (!this._subtreeLifecycleEvents.has(waitUntil))
|
||||||
await helper.waitForEvent(progress, this._eventEmitter, kLifecycleEvent, (e: types.LifecycleEvent) => e === waitUntil).promise;
|
await helper.waitForEvent(progress, this._eventEmitter, kAddLifecycleEvent, (e: types.LifecycleEvent) => e === waitUntil).promise;
|
||||||
|
|
||||||
const request = navigationEvent.documentInfo ? navigationEvent.documentInfo.request : undefined;
|
const request = navigationEvent.documentInfo ? navigationEvent.documentInfo.request : undefined;
|
||||||
return request ? request._finalRequest().response() : null;
|
return request ? request._finalRequest().response() : null;
|
||||||
@ -467,7 +472,7 @@ export class Frame {
|
|||||||
async _waitForLoadState(progress: Progress, state: types.LifecycleEvent): Promise<void> {
|
async _waitForLoadState(progress: Progress, state: types.LifecycleEvent): Promise<void> {
|
||||||
const waitUntil = verifyLifecycle(state);
|
const waitUntil = verifyLifecycle(state);
|
||||||
if (!this._subtreeLifecycleEvents.has(waitUntil))
|
if (!this._subtreeLifecycleEvents.has(waitUntil))
|
||||||
await helper.waitForEvent(progress, this._eventEmitter, kLifecycleEvent, (e: types.LifecycleEvent) => e === waitUntil).promise;
|
await helper.waitForEvent(progress, this._eventEmitter, kAddLifecycleEvent, (e: types.LifecycleEvent) => e === waitUntil).promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
async frameElement(): Promise<dom.ElementHandle> {
|
async frameElement(): Promise<dom.ElementHandle> {
|
||||||
|
@ -92,7 +92,6 @@ export interface BrowserContextChannel extends Channel {
|
|||||||
setHTTPCredentials(params: { httpCredentials: types.Credentials | null }): Promise<void>;
|
setHTTPCredentials(params: { httpCredentials: types.Credentials | null }): Promise<void>;
|
||||||
setNetworkInterceptionEnabled(params: { enabled: boolean }): Promise<void>;
|
setNetworkInterceptionEnabled(params: { enabled: boolean }): Promise<void>;
|
||||||
setOffline(params: { offline: boolean }): Promise<void>;
|
setOffline(params: { offline: boolean }): Promise<void>;
|
||||||
waitForEvent(params: { event: string }): Promise<any>;
|
|
||||||
|
|
||||||
on(event: 'crBackgroundPage', callback: (params: PageChannel) => void): this;
|
on(event: 'crBackgroundPage', callback: (params: PageChannel) => void): this;
|
||||||
on(event: 'crServiceWorker', callback: (params: WorkerChannel) => void): this;
|
on(event: 'crServiceWorker', callback: (params: WorkerChannel) => void): this;
|
||||||
@ -169,6 +168,8 @@ export type PageInitializer = {
|
|||||||
export type PageAttribution = { isPage?: boolean };
|
export type PageAttribution = { isPage?: boolean };
|
||||||
|
|
||||||
export interface FrameChannel extends Channel {
|
export interface FrameChannel extends Channel {
|
||||||
|
on(event: 'loadstate', callback: (params: { add?: types.LifecycleEvent, remove?: types.LifecycleEvent }) => void): this;
|
||||||
|
|
||||||
evalOnSelector(params: { selector: string; expression: string, isFunction: boolean, arg: any} & PageAttribution): Promise<any>;
|
evalOnSelector(params: { selector: string; expression: string, isFunction: boolean, arg: any} & PageAttribution): Promise<any>;
|
||||||
evalOnSelectorAll(params: { selector: string; expression: string, isFunction: boolean, arg: any} & PageAttribution): Promise<any>;
|
evalOnSelectorAll(params: { selector: string; expression: string, isFunction: boolean, arg: any} & PageAttribution): Promise<any>;
|
||||||
addScriptTag(params: { url?: string | undefined, path?: string | undefined, content?: string | undefined, type?: string | undefined} & PageAttribution): Promise<ElementHandleChannel>;
|
addScriptTag(params: { url?: string | undefined, path?: string | undefined, content?: string | undefined, type?: string | undefined} & PageAttribution): Promise<ElementHandleChannel>;
|
||||||
@ -199,14 +200,14 @@ export interface FrameChannel extends Channel {
|
|||||||
type(params: { selector: string, text: string, delay?: number, noWaitAfter?: boolean } & types.TimeoutOptions & PageAttribution): Promise<void>;
|
type(params: { selector: string, text: string, delay?: number, noWaitAfter?: boolean } & types.TimeoutOptions & PageAttribution): Promise<void>;
|
||||||
uncheck(params: { selector: string, force?: boolean, noWaitAfter?: boolean } & types.TimeoutOptions & PageAttribution): Promise<void>;
|
uncheck(params: { selector: string, force?: boolean, noWaitAfter?: boolean } & types.TimeoutOptions & PageAttribution): Promise<void>;
|
||||||
waitForFunction(params: { expression: string, isFunction: boolean, arg: any } & types.WaitForFunctionOptions & PageAttribution): Promise<JSHandleChannel>;
|
waitForFunction(params: { expression: string, isFunction: boolean, arg: any } & types.WaitForFunctionOptions & PageAttribution): Promise<JSHandleChannel>;
|
||||||
waitForLoadState(params: { state: types.LifecycleEvent } & types.TimeoutOptions & PageAttribution): Promise<void>;
|
|
||||||
waitForNavigation(params: types.WaitForNavigationOptions & PageAttribution): Promise<ResponseChannel | null>;
|
waitForNavigation(params: types.WaitForNavigationOptions & PageAttribution): Promise<ResponseChannel | null>;
|
||||||
waitForSelector(params: { selector: string } & types.WaitForElementOptions & PageAttribution): Promise<ElementHandleChannel | null>;
|
waitForSelector(params: { selector: string } & types.WaitForElementOptions & PageAttribution): Promise<ElementHandleChannel | null>;
|
||||||
}
|
}
|
||||||
export type FrameInitializer = {
|
export type FrameInitializer = {
|
||||||
url: string,
|
url: string,
|
||||||
name: string,
|
name: string,
|
||||||
parentFrame: FrameChannel | null
|
parentFrame: FrameChannel | null,
|
||||||
|
loadStates: types.LifecycleEvent[],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as frames from './frame';
|
import * as frames from './frame';
|
||||||
import { Page, BindingCall, waitForEvent } from './page';
|
import { Page, BindingCall } from './page';
|
||||||
import * as types from '../../types';
|
import * as types from '../../types';
|
||||||
import * as network from './network';
|
import * as network from './network';
|
||||||
import { BrowserContextChannel, BrowserContextInitializer } from '../channels';
|
import { BrowserContextChannel, BrowserContextInitializer } from '../channels';
|
||||||
@ -26,6 +26,8 @@ import { Browser } from './browser';
|
|||||||
import { Events } from '../../events';
|
import { Events } from '../../events';
|
||||||
import { TimeoutSettings } from '../../timeoutSettings';
|
import { TimeoutSettings } from '../../timeoutSettings';
|
||||||
import { BrowserType } from './browserType';
|
import { BrowserType } from './browserType';
|
||||||
|
import { Waiter } from './waiter';
|
||||||
|
import { TimeoutError } from '../../errors';
|
||||||
|
|
||||||
export class BrowserContext extends ChannelOwner<BrowserContextChannel, BrowserContextInitializer> {
|
export class BrowserContext extends ChannelOwner<BrowserContextChannel, BrowserContextInitializer> {
|
||||||
_pages = new Set<Page>();
|
_pages = new Set<Page>();
|
||||||
@ -33,7 +35,6 @@ export class BrowserContext extends ChannelOwner<BrowserContextChannel, BrowserC
|
|||||||
readonly _browser: Browser | undefined;
|
readonly _browser: Browser | undefined;
|
||||||
readonly _browserType: BrowserType;
|
readonly _browserType: BrowserType;
|
||||||
readonly _bindings = new Map<string, frames.FunctionWithSource>();
|
readonly _bindings = new Map<string, frames.FunctionWithSource>();
|
||||||
private _pendingWaitForEvents = new Map<(error: Error) => void, string>();
|
|
||||||
_timeoutSettings = new TimeoutSettings();
|
_timeoutSettings = new TimeoutSettings();
|
||||||
_ownerPage: Page | undefined;
|
_ownerPage: Page | undefined;
|
||||||
private _isClosedOrClosing = false;
|
private _isClosedOrClosing = false;
|
||||||
@ -87,6 +88,7 @@ export class BrowserContext extends ChannelOwner<BrowserContextChannel, BrowserC
|
|||||||
}
|
}
|
||||||
|
|
||||||
setDefaultNavigationTimeout(timeout: number) {
|
setDefaultNavigationTimeout(timeout: number) {
|
||||||
|
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
|
||||||
this._channel.setDefaultNavigationTimeoutNoReply({ timeout });
|
this._channel.setDefaultNavigationTimeoutNoReply({ timeout });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,14 +179,15 @@ export class BrowserContext extends ChannelOwner<BrowserContextChannel, BrowserC
|
|||||||
await this._channel.setNetworkInterceptionEnabled({ enabled: false });
|
await this._channel.setNetworkInterceptionEnabled({ enabled: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitForEvent(event: string, optionsOrPredicate?: Function | (types.TimeoutOptions & { predicate?: Function })): Promise<any> {
|
async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise<any> {
|
||||||
const hasTimeout = optionsOrPredicate && !(optionsOrPredicate instanceof Function);
|
const timeout = this._timeoutSettings.timeout(optionsOrPredicate instanceof Function ? {} : optionsOrPredicate);
|
||||||
let reject: () => void;
|
const predicate = optionsOrPredicate instanceof Function ? optionsOrPredicate : optionsOrPredicate.predicate;
|
||||||
const result = await Promise.race([
|
const waiter = new Waiter();
|
||||||
waitForEvent(this, event, optionsOrPredicate, this._timeoutSettings.timeout(hasTimeout ? optionsOrPredicate as any : {})),
|
waiter.rejectOnTimeout(timeout, new TimeoutError(`Timeout while waiting for event "${event}"`));
|
||||||
new Promise((f, r) => { reject = r; this._pendingWaitForEvents.set(reject, event); })
|
if (event !== Events.BrowserContext.Close)
|
||||||
]);
|
waiter.rejectOnEvent(this, Events.BrowserContext.Close, new Error('Context closed'));
|
||||||
this._pendingWaitForEvents.delete(reject!);
|
const result = await waiter.waitForEvent(this, event, predicate as any);
|
||||||
|
waiter.dispose();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,13 +195,6 @@ export class BrowserContext extends ChannelOwner<BrowserContextChannel, BrowserC
|
|||||||
this._isClosedOrClosing = true;
|
this._isClosedOrClosing = true;
|
||||||
if (this._browser)
|
if (this._browser)
|
||||||
this._browser._contexts.delete(this);
|
this._browser._contexts.delete(this);
|
||||||
|
|
||||||
for (const [listener, event] of this._pendingWaitForEvents) {
|
|
||||||
if (event === Events.BrowserContext.Close)
|
|
||||||
continue;
|
|
||||||
listener(new Error('Context closed'));
|
|
||||||
}
|
|
||||||
this._pendingWaitForEvents.clear();
|
|
||||||
this.emit(Events.BrowserContext.Close);
|
this.emit(Events.BrowserContext.Close);
|
||||||
this._dispose();
|
this._dispose();
|
||||||
}
|
}
|
||||||
|
@ -23,8 +23,11 @@ import { ChannelOwner } from './channelOwner';
|
|||||||
import { ElementHandle, convertSelectOptionValues, convertInputFiles } from './elementHandle';
|
import { ElementHandle, convertSelectOptionValues, convertInputFiles } from './elementHandle';
|
||||||
import { JSHandle, Func1, FuncOn, SmartHandle, serializeArgument, parseResult } from './jsHandle';
|
import { JSHandle, Func1, FuncOn, SmartHandle, serializeArgument, parseResult } from './jsHandle';
|
||||||
import * as network from './network';
|
import * as network from './network';
|
||||||
import { Response } from './network';
|
|
||||||
import { Page } from './page';
|
import { Page } from './page';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import { Waiter } from './waiter';
|
||||||
|
import { Events } from '../../events';
|
||||||
|
import { TimeoutError } from '../../errors';
|
||||||
|
|
||||||
export type GotoOptions = types.NavigateOptions & {
|
export type GotoOptions = types.NavigateOptions & {
|
||||||
referer?: string,
|
referer?: string,
|
||||||
@ -33,6 +36,8 @@ export type GotoOptions = types.NavigateOptions & {
|
|||||||
export type FunctionWithSource = (source: { context: BrowserContext, page: Page, frame: Frame }, ...args: any) => any;
|
export type FunctionWithSource = (source: { context: BrowserContext, page: Page, frame: Frame }, ...args: any) => any;
|
||||||
|
|
||||||
export class Frame extends ChannelOwner<FrameChannel, FrameInitializer> {
|
export class Frame extends ChannelOwner<FrameChannel, FrameInitializer> {
|
||||||
|
_eventEmitter: EventEmitter;
|
||||||
|
_loadStates: Set<types.LifecycleEvent>;
|
||||||
_parentFrame: Frame | null = null;
|
_parentFrame: Frame | null = null;
|
||||||
_url = '';
|
_url = '';
|
||||||
_name = '';
|
_name = '';
|
||||||
@ -50,23 +55,45 @@ export class Frame extends ChannelOwner<FrameChannel, FrameInitializer> {
|
|||||||
|
|
||||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: FrameInitializer) {
|
constructor(parent: ChannelOwner, type: string, guid: string, initializer: FrameInitializer) {
|
||||||
super(parent, type, guid, initializer);
|
super(parent, type, guid, initializer);
|
||||||
|
this._eventEmitter = new EventEmitter();
|
||||||
|
this._eventEmitter.setMaxListeners(0);
|
||||||
this._parentFrame = Frame.fromNullable(initializer.parentFrame);
|
this._parentFrame = Frame.fromNullable(initializer.parentFrame);
|
||||||
if (this._parentFrame)
|
if (this._parentFrame)
|
||||||
this._parentFrame._childFrames.add(this);
|
this._parentFrame._childFrames.add(this);
|
||||||
this._name = initializer.name;
|
this._name = initializer.name;
|
||||||
this._url = initializer.url;
|
this._url = initializer.url;
|
||||||
|
this._loadStates = new Set(initializer.loadStates);
|
||||||
|
this._channel.on('loadstate', event => {
|
||||||
|
if (event.add) {
|
||||||
|
this._loadStates.add(event.add);
|
||||||
|
this._eventEmitter.emit('loadstate', event.add);
|
||||||
|
}
|
||||||
|
if (event.remove)
|
||||||
|
this._loadStates.delete(event.remove);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async goto(url: string, options: GotoOptions = {}): Promise<network.Response | null> {
|
async goto(url: string, options: GotoOptions = {}): Promise<network.Response | null> {
|
||||||
return Response.fromNullable(await this._channel.goto({ url, ...options, isPage: this._page!._isPageCall }));
|
return network.Response.fromNullable(await this._channel.goto({ url, ...options, isPage: this._page!._isPageCall }));
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitForNavigation(options: types.WaitForNavigationOptions = {}): Promise<network.Response | null> {
|
async waitForNavigation(options: types.WaitForNavigationOptions = {}): Promise<network.Response | null> {
|
||||||
return Response.fromNullable(await this._channel.waitForNavigation({ ...options, isPage: this._page!._isPageCall }));
|
return network.Response.fromNullable(await this._channel.waitForNavigation({ ...options, isPage: this._page!._isPageCall }));
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitForLoadState(state: types.LifecycleEvent = 'load', options: types.TimeoutOptions = {}): Promise<void> {
|
async waitForLoadState(state: types.LifecycleEvent = 'load', options: types.TimeoutOptions = {}): Promise<void> {
|
||||||
await this._channel.waitForLoadState({ state, ...options, isPage: this._page!._isPageCall });
|
state = verifyLoadState(state);
|
||||||
|
if (this._loadStates.has(state))
|
||||||
|
return;
|
||||||
|
const timeout = this._page!._timeoutSettings.navigationTimeout(options);
|
||||||
|
const apiName = this._page!._isPageCall ? 'page.waitForLoadState' : 'frame.waitForLoadState';
|
||||||
|
const waiter = new Waiter();
|
||||||
|
waiter.rejectOnEvent(this._page!, Events.Page.Close, new Error('Navigation failed because page was closed!'));
|
||||||
|
waiter.rejectOnEvent(this._page!, Events.Page.Crash, new Error('Navigation failed because page crashed!'));
|
||||||
|
waiter.rejectOnEvent<Frame>(this._page!, Events.Page.FrameDetached, new Error('Navigating frame was detached!'), frame => frame === this);
|
||||||
|
waiter.rejectOnTimeout(timeout, new TimeoutError(`Timeout ${timeout}ms exceeded during ${apiName}.`));
|
||||||
|
await waiter.waitForEvent<types.LifecycleEvent>(this._eventEmitter, 'loadstate', s => s === state);
|
||||||
|
waiter.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
async frameElement(): Promise<ElementHandle> {
|
async frameElement(): Promise<ElementHandle> {
|
||||||
@ -228,3 +255,11 @@ export class Frame extends ChannelOwner<FrameChannel, FrameInitializer> {
|
|||||||
return await this._channel.title();
|
return await this._channel.title();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function verifyLoadState(waitUntil: types.LifecycleEvent): types.LifecycleEvent {
|
||||||
|
if (waitUntil as unknown === 'networkidle0')
|
||||||
|
waitUntil = 'networkidle';
|
||||||
|
if (!types.kLifecycleEvents.has(waitUntil))
|
||||||
|
throw new Error(`Unsupported waitUntil option ${String(waitUntil)}`);
|
||||||
|
return waitUntil;
|
||||||
|
}
|
||||||
|
@ -15,7 +15,6 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from 'events';
|
|
||||||
import { TimeoutError } from '../../errors';
|
import { TimeoutError } from '../../errors';
|
||||||
import { Events } from '../../events';
|
import { Events } from '../../events';
|
||||||
import { assert, assertMaxArguments, helper, Listener } from '../../helper';
|
import { assert, assertMaxArguments, helper, Listener } from '../../helper';
|
||||||
@ -38,6 +37,7 @@ import { Request, Response, Route, RouteHandler } from './network';
|
|||||||
import { FileChooser } from './fileChooser';
|
import { FileChooser } from './fileChooser';
|
||||||
import { Buffer } from 'buffer';
|
import { Buffer } from 'buffer';
|
||||||
import { Coverage } from './coverage';
|
import { Coverage } from './coverage';
|
||||||
|
import { Waiter } from './waiter';
|
||||||
|
|
||||||
export class Page extends ChannelOwner<PageChannel, PageInitializer> {
|
export class Page extends ChannelOwner<PageChannel, PageInitializer> {
|
||||||
private _browserContext: BrowserContext;
|
private _browserContext: BrowserContext;
|
||||||
@ -57,8 +57,7 @@ export class Page extends ChannelOwner<PageChannel, PageInitializer> {
|
|||||||
pdf?: (options?: types.PDFOptions) => Promise<Buffer>;
|
pdf?: (options?: types.PDFOptions) => Promise<Buffer>;
|
||||||
|
|
||||||
readonly _bindings = new Map<string, FunctionWithSource>();
|
readonly _bindings = new Map<string, FunctionWithSource>();
|
||||||
private _pendingWaitForEvents = new Map<(error: Error) => void, string>();
|
readonly _timeoutSettings: TimeoutSettings;
|
||||||
private _timeoutSettings: TimeoutSettings;
|
|
||||||
_isPageCall = false;
|
_isPageCall = false;
|
||||||
|
|
||||||
static from(page: PageChannel): Page {
|
static from(page: PageChannel): Page {
|
||||||
@ -166,26 +165,13 @@ export class Page extends ChannelOwner<PageChannel, PageInitializer> {
|
|||||||
private _onClose() {
|
private _onClose() {
|
||||||
this._closed = true;
|
this._closed = true;
|
||||||
this._browserContext._pages.delete(this);
|
this._browserContext._pages.delete(this);
|
||||||
this._rejectPendingOperations(false);
|
|
||||||
this.emit(Events.Page.Close);
|
this.emit(Events.Page.Close);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onCrash() {
|
private _onCrash() {
|
||||||
this._rejectPendingOperations(true);
|
|
||||||
this.emit(Events.Page.Crash);
|
this.emit(Events.Page.Crash);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _rejectPendingOperations(isCrash: boolean) {
|
|
||||||
for (const [listener, event] of this._pendingWaitForEvents) {
|
|
||||||
if (event === Events.Page.Close && !isCrash)
|
|
||||||
continue;
|
|
||||||
if (event === Events.Page.Crash && isCrash)
|
|
||||||
continue;
|
|
||||||
listener(new Error(isCrash ? 'Page crashed' : 'Page closed'));
|
|
||||||
}
|
|
||||||
this._pendingWaitForEvents.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
context(): BrowserContext {
|
context(): BrowserContext {
|
||||||
return this._browserContext;
|
return this._browserContext;
|
||||||
}
|
}
|
||||||
@ -214,6 +200,7 @@ export class Page extends ChannelOwner<PageChannel, PageInitializer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setDefaultNavigationTimeout(timeout: number) {
|
setDefaultNavigationTimeout(timeout: number) {
|
||||||
|
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
|
||||||
this._channel.setDefaultNavigationTimeoutNoReply({ timeout });
|
this._channel.setDefaultNavigationTimeoutNoReply({ timeout });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -340,12 +327,16 @@ export class Page extends ChannelOwner<PageChannel, PageInitializer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise<any> {
|
async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise<any> {
|
||||||
let reject: () => void;
|
const timeout = this._timeoutSettings.timeout(optionsOrPredicate instanceof Function ? {} : optionsOrPredicate);
|
||||||
const result = await Promise.race([
|
const predicate = optionsOrPredicate instanceof Function ? optionsOrPredicate : optionsOrPredicate.predicate;
|
||||||
waitForEvent(this, event, optionsOrPredicate, this._timeoutSettings.timeout(optionsOrPredicate instanceof Function ? {} : optionsOrPredicate)),
|
const waiter = new Waiter();
|
||||||
new Promise((f, r) => { reject = r; this._pendingWaitForEvents.set(reject, event); })
|
waiter.rejectOnTimeout(timeout, new TimeoutError(`Timeout while waiting for event "${event}"`));
|
||||||
]);
|
if (event !== Events.Page.Crash)
|
||||||
this._pendingWaitForEvents.delete(reject!);
|
waiter.rejectOnEvent(this, Events.Page.Crash, new Error('Page crashed'));
|
||||||
|
if (event !== Events.Page.Close)
|
||||||
|
waiter.rejectOnEvent(this, Events.Page.Close, new Error('Page closed'));
|
||||||
|
const result = await waiter.waitForEvent(this, event, predicate as any);
|
||||||
|
waiter.dispose();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -543,29 +534,3 @@ export class BindingCall extends ChannelOwner<BindingCallChannel, BindingCallIni
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function waitForEvent(emitter: EventEmitter, event: string, optionsOrPredicate: types.WaitForEventOptions = {}, defaultTimeout: number): Promise<any> {
|
|
||||||
let predicate: Function | undefined;
|
|
||||||
let timeout = defaultTimeout;
|
|
||||||
if (typeof optionsOrPredicate === 'function') {
|
|
||||||
predicate = optionsOrPredicate;
|
|
||||||
} else if (optionsOrPredicate.predicate) {
|
|
||||||
if (optionsOrPredicate.timeout !== undefined)
|
|
||||||
timeout = optionsOrPredicate.timeout;
|
|
||||||
predicate = optionsOrPredicate.predicate;
|
|
||||||
}
|
|
||||||
let callback: (a: any) => void;
|
|
||||||
const result = new Promise(f => callback = f);
|
|
||||||
const listener = helper.addEventListener(emitter, event, param => {
|
|
||||||
if (predicate && !predicate(param))
|
|
||||||
return;
|
|
||||||
callback(param);
|
|
||||||
helper.removeEventListeners([listener]);
|
|
||||||
});
|
|
||||||
if (timeout === 0)
|
|
||||||
return result;
|
|
||||||
return Promise.race([
|
|
||||||
result,
|
|
||||||
new Promise((f, r) => setTimeout(() => r(new TimeoutError('Timeout while waiting for event')), timeout))
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
91
src/rpc/client/waiter.ts
Normal file
91
src/rpc/client/waiter.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
|
export class Waiter {
|
||||||
|
private _dispose: (() => void)[] = [];
|
||||||
|
private _failures: Promise<any>[] = [];
|
||||||
|
|
||||||
|
async waitForEvent<T = void>(emitter: EventEmitter, event: string, predicate?: (arg: T) => boolean): Promise<T> {
|
||||||
|
const { promise, dispose } = waitForEvent(emitter, event, predicate);
|
||||||
|
return this._wait(promise, dispose);
|
||||||
|
}
|
||||||
|
|
||||||
|
rejectOnEvent<T = void>(emitter: EventEmitter, event: string, error: Error, predicate?: (arg: T) => boolean) {
|
||||||
|
const { promise, dispose } = waitForEvent(emitter, event, predicate);
|
||||||
|
this._rejectOn(promise.then(() => { throw error; }), dispose);
|
||||||
|
}
|
||||||
|
|
||||||
|
rejectOnTimeout(timeout: number, error: Error) {
|
||||||
|
if (!timeout)
|
||||||
|
return;
|
||||||
|
const { promise, dispose } = waitForTimeout(timeout);
|
||||||
|
this._rejectOn(promise.then(() => { throw error; }), dispose);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
for (const dispose of this._dispose)
|
||||||
|
dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _wait<T>(promise: Promise<T>, dispose?: () => void): Promise<T> {
|
||||||
|
try {
|
||||||
|
const result = await Promise.race([promise, ...this._failures]);
|
||||||
|
if (dispose)
|
||||||
|
dispose();
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
if (dispose)
|
||||||
|
dispose();
|
||||||
|
this.dispose();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _rejectOn(promise: Promise<any>, dispose?: () => void) {
|
||||||
|
this._failures.push(promise);
|
||||||
|
if (dispose)
|
||||||
|
this._dispose.push(dispose);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitForEvent<T = void>(emitter: EventEmitter, event: string, predicate?: (arg: T) => boolean): { promise: Promise<T>, dispose: () => void } {
|
||||||
|
let listener: (eventArg: any) => void;
|
||||||
|
const promise = new Promise<T>((resolve, reject) => {
|
||||||
|
listener = (eventArg: any) => {
|
||||||
|
try {
|
||||||
|
if (predicate && !predicate(eventArg))
|
||||||
|
return;
|
||||||
|
emitter.removeListener(event, listener);
|
||||||
|
resolve(eventArg);
|
||||||
|
} catch (e) {
|
||||||
|
emitter.removeListener(event, listener);
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
emitter.addListener(event, listener);
|
||||||
|
});
|
||||||
|
const dispose = () => emitter.removeListener(event, listener);
|
||||||
|
return { promise, dispose };
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitForTimeout(timeout: number): { promise: Promise<void>, dispose: () => void } {
|
||||||
|
let timeoutId: number;
|
||||||
|
const promise = new Promise<void>(resolve => timeoutId = setTimeout(resolve, timeout));
|
||||||
|
const dispose = () => clearTimeout(timeoutId);
|
||||||
|
return { promise, dispose };
|
||||||
|
}
|
@ -17,11 +17,10 @@
|
|||||||
import * as types from '../../types';
|
import * as types from '../../types';
|
||||||
import { BrowserContextBase, BrowserContext } from '../../browserContext';
|
import { BrowserContextBase, BrowserContext } from '../../browserContext';
|
||||||
import { Events } from '../../events';
|
import { Events } from '../../events';
|
||||||
import { Dispatcher, DispatcherScope, lookupNullableDispatcher, lookupDispatcher } from './dispatcher';
|
import { Dispatcher, DispatcherScope, lookupDispatcher } from './dispatcher';
|
||||||
import { PageDispatcher, BindingCallDispatcher, WorkerDispatcher } from './pageDispatcher';
|
import { PageDispatcher, BindingCallDispatcher, WorkerDispatcher } from './pageDispatcher';
|
||||||
import { PageChannel, BrowserContextChannel, BrowserContextInitializer, CDPSessionChannel } from '../channels';
|
import { PageChannel, BrowserContextChannel, BrowserContextInitializer, CDPSessionChannel } from '../channels';
|
||||||
import { RouteDispatcher, RequestDispatcher } from './networkDispatchers';
|
import { RouteDispatcher, RequestDispatcher } from './networkDispatchers';
|
||||||
import { Page } from '../../page';
|
|
||||||
import { CRBrowserContext } from '../../chromium/crBrowser';
|
import { CRBrowserContext } from '../../chromium/crBrowser';
|
||||||
import { CDPSessionDispatcher } from './cdpSessionDispatcher';
|
import { CDPSessionDispatcher } from './cdpSessionDispatcher';
|
||||||
import { Events as ChromiumEvents } from '../../chromium/events';
|
import { Events as ChromiumEvents } from '../../chromium/events';
|
||||||
@ -121,13 +120,6 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, Browser
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitForEvent(params: { event: string }): Promise<any> {
|
|
||||||
const result = await this._context.waitForEvent(params.event);
|
|
||||||
if (result instanceof Page)
|
|
||||||
return lookupNullableDispatcher<PageDispatcher>(result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
await this._context.close();
|
await this._context.close();
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,6 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements Eleme
|
|||||||
constructor(scope: DispatcherScope, elementHandle: ElementHandle) {
|
constructor(scope: DispatcherScope, elementHandle: ElementHandle) {
|
||||||
super(scope, elementHandle);
|
super(scope, elementHandle);
|
||||||
this._elementHandle = elementHandle;
|
this._elementHandle = elementHandle;
|
||||||
this._elementHandle = elementHandle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async ownerFrame(): Promise<FrameChannel | null> {
|
async ownerFrame(): Promise<FrameChannel | null> {
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Frame } from '../../frames';
|
import { Frame, kAddLifecycleEvent, kRemoveLifecycleEvent } from '../../frames';
|
||||||
import * as types from '../../types';
|
import * as types from '../../types';
|
||||||
import { ElementHandleChannel, FrameChannel, FrameInitializer, JSHandleChannel, ResponseChannel, PageAttribution } from '../channels';
|
import { ElementHandleChannel, FrameChannel, FrameInitializer, JSHandleChannel, ResponseChannel, PageAttribution } from '../channels';
|
||||||
import { Dispatcher, DispatcherScope, lookupNullableDispatcher, existingDispatcher } from './dispatcher';
|
import { Dispatcher, DispatcherScope, lookupNullableDispatcher, existingDispatcher } from './dispatcher';
|
||||||
@ -34,9 +34,16 @@ export class FrameDispatcher extends Dispatcher<Frame, FrameInitializer> impleme
|
|||||||
super(scope, frame, 'frame', {
|
super(scope, frame, 'frame', {
|
||||||
url: frame.url(),
|
url: frame.url(),
|
||||||
name: frame.name(),
|
name: frame.name(),
|
||||||
parentFrame: lookupNullableDispatcher<FrameDispatcher>(frame.parentFrame())
|
parentFrame: lookupNullableDispatcher<FrameDispatcher>(frame.parentFrame()),
|
||||||
|
loadStates: Array.from(frame._subtreeLifecycleEvents),
|
||||||
});
|
});
|
||||||
this._frame = frame;
|
this._frame = frame;
|
||||||
|
frame._eventEmitter.on(kAddLifecycleEvent, (event: types.LifecycleEvent) => {
|
||||||
|
this._dispatchEvent('loadstate', { add: event });
|
||||||
|
});
|
||||||
|
frame._eventEmitter.on(kRemoveLifecycleEvent, (event: types.LifecycleEvent) => {
|
||||||
|
this._dispatchEvent('loadstate', { remove: event });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async goto(params: { url: string } & types.GotoOptions & PageAttribution): Promise<ResponseChannel | null> {
|
async goto(params: { url: string } & types.GotoOptions & PageAttribution): Promise<ResponseChannel | null> {
|
||||||
@ -44,11 +51,6 @@ export class FrameDispatcher extends Dispatcher<Frame, FrameInitializer> impleme
|
|||||||
return lookupNullableDispatcher<ResponseDispatcher>(await target.goto(params.url, params));
|
return lookupNullableDispatcher<ResponseDispatcher>(await target.goto(params.url, params));
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitForLoadState(params: { state?: 'load' | 'domcontentloaded' | 'networkidle' } & types.TimeoutOptions & PageAttribution): Promise<void> {
|
|
||||||
const target = params.isPage ? this._frame._page : this._frame;
|
|
||||||
await target.waitForLoadState(params.state, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async waitForNavigation(params: types.WaitForNavigationOptions & PageAttribution): Promise<ResponseChannel | null> {
|
async waitForNavigation(params: types.WaitForNavigationOptions & PageAttribution): Promise<ResponseChannel | null> {
|
||||||
const target = params.isPage ? this._frame._page : this._frame;
|
const target = params.isPage ? this._frame._page : this._frame;
|
||||||
return lookupNullableDispatcher<ResponseDispatcher>(await target.waitForNavigation(params));
|
return lookupNullableDispatcher<ResponseDispatcher>(await target.waitForNavigation(params));
|
||||||
|
@ -125,7 +125,8 @@ class TraceTestEnvironment {
|
|||||||
this._session = null;
|
this._session = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async beforeEach() {
|
async beforeEach(state, testRun) {
|
||||||
|
const t = testRun.test();
|
||||||
const inspector = require('inspector');
|
const inspector = require('inspector');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const util = require('util');
|
const util = require('util');
|
||||||
|
Loading…
Reference in New Issue
Block a user