feat(api): wait for popups and downloads when performing actions (#1744)
This commit is contained in:
Родитель
67cd5698a7
Коммит
f5942295d4
|
@ -134,6 +134,10 @@ export class CRBrowser extends BrowserBase {
|
|||
const opener = targetInfo.openerId ? this._crPages.get(targetInfo.openerId) || null : null;
|
||||
const crPage = new CRPage(session, targetInfo.targetId, context, opener);
|
||||
this._crPages.set(targetInfo.targetId, crPage);
|
||||
if (opener && opener._initializedPage) {
|
||||
for (const signalBarrier of opener._initializedPage._frameManager._signalBarriers)
|
||||
signalBarrier.addPopup(crPage.pageOrError());
|
||||
}
|
||||
crPage.pageOrError().then(() => {
|
||||
this._firstPageCallback();
|
||||
context.emit(CommonEvents.BrowserContext.Page, crPage._page);
|
||||
|
|
|
@ -315,7 +315,8 @@ class FrameSession {
|
|||
private _eventListeners: RegisteredListener[] = [];
|
||||
readonly _targetId: string;
|
||||
private _firstNonInitialNavigationCommittedPromise: Promise<void>;
|
||||
private _firstNonInitialNavigationCommittedCallback = () => {};
|
||||
private _firstNonInitialNavigationCommittedFulfill = () => {};
|
||||
private _firstNonInitialNavigationCommittedReject = (e: Error) => {};
|
||||
|
||||
constructor(crPage: CRPage, client: CRSession, targetId: string) {
|
||||
this._client = client;
|
||||
|
@ -323,7 +324,13 @@ class FrameSession {
|
|||
this._page = crPage._page;
|
||||
this._targetId = targetId;
|
||||
this._networkManager = new CRNetworkManager(client, this._page);
|
||||
this._firstNonInitialNavigationCommittedPromise = new Promise(f => this._firstNonInitialNavigationCommittedCallback = f);
|
||||
this._firstNonInitialNavigationCommittedPromise = new Promise((f, r) => {
|
||||
this._firstNonInitialNavigationCommittedFulfill = f;
|
||||
this._firstNonInitialNavigationCommittedReject = r;
|
||||
});
|
||||
client.once(CRSessionEvents.Disconnected, () => {
|
||||
this._firstNonInitialNavigationCommittedReject(new Error('Page closed'));
|
||||
});
|
||||
}
|
||||
|
||||
private _isMainFrame(): boolean {
|
||||
|
@ -386,7 +393,7 @@ class FrameSession {
|
|||
this._eventListeners.push(helper.addEventListener(this._client, 'Page.lifecycleEvent', event => this._onLifecycleEvent(event)));
|
||||
});
|
||||
} else {
|
||||
this._firstNonInitialNavigationCommittedCallback();
|
||||
this._firstNonInitialNavigationCommittedFulfill();
|
||||
this._eventListeners.push(helper.addEventListener(this._client, 'Page.lifecycleEvent', event => this._onLifecycleEvent(event)));
|
||||
}
|
||||
}),
|
||||
|
@ -479,7 +486,7 @@ class FrameSession {
|
|||
_onFrameNavigated(framePayload: Protocol.Page.Frame, initial: boolean) {
|
||||
this._page._frameManager.frameCommittedNewDocumentNavigation(framePayload.id, framePayload.url, framePayload.name || '', framePayload.loaderId, initial);
|
||||
if (!initial)
|
||||
this._firstNonInitialNavigationCommittedCallback();
|
||||
this._firstNonInitialNavigationCommittedFulfill();
|
||||
}
|
||||
|
||||
_onFrameRequestedNavigation(payload: Protocol.Page.frameRequestedNavigationPayload) {
|
||||
|
|
14
src/dom.ts
14
src/dom.ts
|
@ -55,7 +55,7 @@ export class FrameExecutionContext extends js.ExecutionContext {
|
|||
}
|
||||
|
||||
async _doEvaluateInternal(returnByValue: boolean, waitForNavigations: boolean, pageFunction: string | Function, ...args: any[]): Promise<any> {
|
||||
return await this.frame._page._frameManager.waitForNavigationsCreatedBy(async () => {
|
||||
return await this.frame._page._frameManager.waitForSignalsCreatedBy(async () => {
|
||||
return this._delegate.evaluate(this, returnByValue, pageFunction, ...args);
|
||||
}, Number.MAX_SAFE_INTEGER, waitForNavigations ? undefined : { waitUntil: 'nowait' });
|
||||
}
|
||||
|
@ -227,7 +227,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
if (!force)
|
||||
await this._waitForHitTargetAt(point, deadline);
|
||||
|
||||
await this._page._frameManager.waitForNavigationsCreatedBy(async () => {
|
||||
await this._page._frameManager.waitForSignalsCreatedBy(async () => {
|
||||
let restoreModifiers: input.Modifier[] | undefined;
|
||||
if (options && options.modifiers)
|
||||
restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers);
|
||||
|
@ -269,7 +269,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
if (option.index !== undefined)
|
||||
assert(helper.isNumber(option.index), 'Indices must be numbers. Found index "' + option.index + '" of type "' + (typeof option.index) + '"');
|
||||
}
|
||||
return await this._page._frameManager.waitForNavigationsCreatedBy<string[]>(async () => {
|
||||
return await this._page._frameManager.waitForSignalsCreatedBy<string[]>(async () => {
|
||||
return this._evaluateInUtility(({ injected, node }, selectOptions) => injected.selectOptions(node, selectOptions), selectOptions);
|
||||
}, deadline, options);
|
||||
}
|
||||
|
@ -277,7 +277,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
async fill(value: string, options?: types.NavigatingActionWaitOptions): Promise<void> {
|
||||
assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"');
|
||||
const deadline = this._page._timeoutSettings.computeDeadline(options);
|
||||
await this._page._frameManager.waitForNavigationsCreatedBy(async () => {
|
||||
await this._page._frameManager.waitForSignalsCreatedBy(async () => {
|
||||
const errorOrNeedsInput = await this._evaluateInUtility(({ injected, node }, value) => injected.fill(node, value), value);
|
||||
if (typeof errorOrNeedsInput === 'string')
|
||||
throw new Error(errorOrNeedsInput);
|
||||
|
@ -323,7 +323,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
filePayloads.push(item);
|
||||
}
|
||||
}
|
||||
await this._page._frameManager.waitForNavigationsCreatedBy(async () => {
|
||||
await this._page._frameManager.waitForSignalsCreatedBy(async () => {
|
||||
await this._page._delegate.setInputFiles(this as any as ElementHandle<HTMLInputElement>, filePayloads);
|
||||
}, deadline, options);
|
||||
}
|
||||
|
@ -341,7 +341,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
|
||||
async type(text: string, options?: { delay?: number } & types.NavigatingActionWaitOptions) {
|
||||
const deadline = this._page._timeoutSettings.computeDeadline(options);
|
||||
await this._page._frameManager.waitForNavigationsCreatedBy(async () => {
|
||||
await this._page._frameManager.waitForSignalsCreatedBy(async () => {
|
||||
await this.focus();
|
||||
await this._page.keyboard.type(text, options);
|
||||
}, deadline, options, true);
|
||||
|
@ -349,7 +349,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
|
||||
async press(key: string, options?: { delay?: number } & types.NavigatingActionWaitOptions) {
|
||||
const deadline = this._page._timeoutSettings.computeDeadline(options);
|
||||
await this._page._frameManager.waitForNavigationsCreatedBy(async () => {
|
||||
await this._page._frameManager.waitForSignalsCreatedBy(async () => {
|
||||
await this.focus();
|
||||
await this._page.keyboard.press(key, options);
|
||||
}, deadline, options, true);
|
||||
|
|
|
@ -39,6 +39,8 @@ export class Download {
|
|||
this._url = url;
|
||||
this._finishedCallback = () => {};
|
||||
this._finishedPromise = new Promise(f => this._finishedCallback = f);
|
||||
for (const barrier of this._page._frameManager._signalBarriers)
|
||||
barrier.addDownload();
|
||||
this._page.emit(Events.Page.Download, this);
|
||||
page._browserContext._downloads.add(this);
|
||||
this._acceptDownloads = !!this._page._browserContext._options.acceptDownloads;
|
||||
|
|
|
@ -129,6 +129,10 @@ export class FFBrowser extends BrowserBase {
|
|||
const ffPage = new FFPage(session, context, opener);
|
||||
this._ffPages.set(targetId, ffPage);
|
||||
|
||||
if (opener && opener._initializedPage) {
|
||||
for (const signalBarrier of opener._initializedPage._frameManager._signalBarriers)
|
||||
signalBarrier.addPopup(ffPage.pageOrError());
|
||||
}
|
||||
ffPage.pageOrError().then(async () => {
|
||||
this._firstPageCallback();
|
||||
const page = ffPage._page;
|
||||
|
@ -200,7 +204,7 @@ export class FFBrowserContext extends BrowserContextBase {
|
|||
}
|
||||
|
||||
pages(): Page[] {
|
||||
return this._ffPages().map(ffPage => ffPage._initializedPage()).filter(pageOrNull => !!pageOrNull) as Page[];
|
||||
return this._ffPages().map(ffPage => ffPage._initializedPage).filter(pageOrNull => !!pageOrNull) as Page[];
|
||||
}
|
||||
|
||||
async newPage(): Promise<Page> {
|
||||
|
|
|
@ -43,7 +43,7 @@ export class FFPage implements PageDelegate {
|
|||
readonly _browserContext: FFBrowserContext;
|
||||
private _pagePromise: Promise<Page | Error>;
|
||||
private _pageCallback: (pageOrError: Page | Error) => void = () => {};
|
||||
private _initialized = false;
|
||||
_initializedPage: Page | null = null;
|
||||
private readonly _opener: FFPage | null;
|
||||
private readonly _contextIdToContext: Map<string, dom.FrameExecutionContext>;
|
||||
private _eventListeners: RegisteredListener[];
|
||||
|
@ -70,6 +70,7 @@ export class FFPage implements PageDelegate {
|
|||
helper.addEventListener(this._session, 'Runtime.executionContextCreated', this._onExecutionContextCreated.bind(this)),
|
||||
helper.addEventListener(this._session, 'Runtime.executionContextDestroyed', this._onExecutionContextDestroyed.bind(this)),
|
||||
helper.addEventListener(this._session, 'Page.linkClicked', event => this._onLinkClicked(event.phase)),
|
||||
helper.addEventListener(this._session, 'Page.willOpenNewWindowAsynchronously', this._onWillOpenNewWindowAsynchronously.bind(this)),
|
||||
helper.addEventListener(this._session, 'Page.uncaughtError', this._onUncaughtError.bind(this)),
|
||||
helper.addEventListener(this._session, 'Runtime.console', this._onConsole.bind(this)),
|
||||
helper.addEventListener(this._session, 'Page.dialogOpened', this._onDialogOpened.bind(this)),
|
||||
|
@ -84,17 +85,13 @@ export class FFPage implements PageDelegate {
|
|||
session.once(FFSessionEvents.Disconnected, () => this._page._didDisconnect());
|
||||
this._session.once('Page.ready', () => {
|
||||
this._pageCallback(this._page);
|
||||
this._initialized = true;
|
||||
this._initializedPage = this._page;
|
||||
});
|
||||
// Ideally, we somehow ensure that utility world is created before Page.ready arrives, but currently it is racy.
|
||||
// Therefore, we can end up with an initialized page without utility world, although very unlikely.
|
||||
this._session.send('Page.addScriptToEvaluateOnNewDocument', { script: '', worldName: UTILITY_WORLD_NAME }).catch(this._pageCallback);
|
||||
}
|
||||
|
||||
_initializedPage(): Page | null {
|
||||
return this._initialized ? this._page : null;
|
||||
}
|
||||
|
||||
async pageOrError(): Promise<Page | Error> {
|
||||
return this._pagePromise;
|
||||
}
|
||||
|
@ -136,12 +133,21 @@ export class FFPage implements PageDelegate {
|
|||
this._page._frameManager.frameDidPotentiallyRequestNavigation();
|
||||
}
|
||||
|
||||
_onWillOpenNewWindowAsynchronously() {
|
||||
for (const barrier of this._page._frameManager._signalBarriers)
|
||||
barrier.expectPopup();
|
||||
}
|
||||
|
||||
_onNavigationStarted(params: Protocol.Page.navigationStartedPayload) {
|
||||
this._page._frameManager.frameRequestedNavigation(params.frameId, params.navigationId);
|
||||
}
|
||||
|
||||
_onNavigationAborted(params: Protocol.Page.navigationAbortedPayload) {
|
||||
const frame = this._page._frameManager.frame(params.frameId)!;
|
||||
if (params.errorText === 'Will download to file') {
|
||||
for (const barrier of this._page._frameManager._signalBarriers)
|
||||
barrier.expectDownload();
|
||||
}
|
||||
for (const task of frame._frameTasks)
|
||||
task.onNewDocument(params.navigationId, new Error(params.errorText));
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ export class FrameManager {
|
|||
private _frames = new Map<string, Frame>();
|
||||
private _mainFrame: Frame;
|
||||
readonly _consoleMessageTags = new Map<string, ConsoleTagHandler>();
|
||||
private _pendingNavigationBarriers = new Set<PendingNavigationBarrier>();
|
||||
readonly _signalBarriers = new Set<SignalBarrier>();
|
||||
|
||||
constructor(page: Page) {
|
||||
this._page = page;
|
||||
|
@ -100,11 +100,11 @@ export class FrameManager {
|
|||
}
|
||||
}
|
||||
|
||||
async waitForNavigationsCreatedBy<T>(action: () => Promise<T>, deadline: number, options: types.NavigatingActionWaitOptions = {}, input?: boolean): Promise<T> {
|
||||
async waitForSignalsCreatedBy<T>(action: () => Promise<T>, deadline: number, options: types.NavigatingActionWaitOptions = {}, input?: boolean): Promise<T> {
|
||||
if (options.waitUntil === 'nowait')
|
||||
return action();
|
||||
const barrier = new PendingNavigationBarrier({ waitUntil: 'domcontentloaded', ...options }, deadline);
|
||||
this._pendingNavigationBarriers.add(barrier);
|
||||
const barrier = new SignalBarrier({ waitUntil: 'domcontentloaded', ...options }, deadline);
|
||||
this._signalBarriers.add(barrier);
|
||||
try {
|
||||
const result = await action();
|
||||
if (input)
|
||||
|
@ -114,17 +114,17 @@ export class FrameManager {
|
|||
await new Promise(helper.makeWaitForNextTask());
|
||||
return result;
|
||||
} finally {
|
||||
this._pendingNavigationBarriers.delete(barrier);
|
||||
this._signalBarriers.delete(barrier);
|
||||
}
|
||||
}
|
||||
|
||||
frameWillPotentiallyRequestNavigation() {
|
||||
for (const barrier of this._pendingNavigationBarriers)
|
||||
for (const barrier of this._signalBarriers)
|
||||
barrier.retain();
|
||||
}
|
||||
|
||||
frameDidPotentiallyRequestNavigation() {
|
||||
for (const barrier of this._pendingNavigationBarriers)
|
||||
for (const barrier of this._signalBarriers)
|
||||
barrier.release();
|
||||
}
|
||||
|
||||
|
@ -132,8 +132,8 @@ export class FrameManager {
|
|||
const frame = this._frames.get(frameId);
|
||||
if (!frame)
|
||||
return;
|
||||
for (const barrier of this._pendingNavigationBarriers)
|
||||
barrier.addFrame(frame);
|
||||
for (const barrier of this._signalBarriers)
|
||||
barrier.addFrameNavigation(frame);
|
||||
frame._pendingDocumentId = documentId;
|
||||
}
|
||||
|
||||
|
@ -939,10 +939,12 @@ function selectorToString(selector: string, waitFor: 'attached' | 'detached' | '
|
|||
return `${label}${selector}`;
|
||||
}
|
||||
|
||||
class PendingNavigationBarrier {
|
||||
export class SignalBarrier {
|
||||
private _frameIds = new Map<string, number>();
|
||||
private _options: types.NavigatingActionWaitOptions;
|
||||
private _protectCount = 0;
|
||||
private _expectedPopups = 0;
|
||||
private _expectedDownloads = 0;
|
||||
private _promise: Promise<void>;
|
||||
private _promiseCallback = () => {};
|
||||
private _deadline: number;
|
||||
|
@ -959,7 +961,7 @@ class PendingNavigationBarrier {
|
|||
return this._promise;
|
||||
}
|
||||
|
||||
async addFrame(frame: Frame) {
|
||||
async addFrameNavigation(frame: Frame) {
|
||||
this.retain();
|
||||
const timeout = helper.timeUntilDeadline(this._deadline);
|
||||
const options = { ...this._options, timeout } as types.NavigateOptions;
|
||||
|
@ -967,6 +969,33 @@ class PendingNavigationBarrier {
|
|||
this.release();
|
||||
}
|
||||
|
||||
async expectPopup() {
|
||||
++this._expectedPopups;
|
||||
}
|
||||
|
||||
async unexpectPopup() {
|
||||
--this._expectedPopups;
|
||||
this._maybeResolve();
|
||||
}
|
||||
|
||||
async addPopup(pageOrError: Promise<Page | Error>) {
|
||||
if (this._expectedPopups)
|
||||
--this._expectedPopups;
|
||||
this.retain();
|
||||
await pageOrError;
|
||||
this.release();
|
||||
}
|
||||
|
||||
async expectDownload() {
|
||||
++this._expectedDownloads;
|
||||
}
|
||||
|
||||
async addDownload() {
|
||||
if (this._expectedDownloads)
|
||||
--this._expectedDownloads;
|
||||
this._maybeResolve();
|
||||
}
|
||||
|
||||
retain() {
|
||||
++this._protectCount;
|
||||
}
|
||||
|
@ -977,7 +1006,7 @@ class PendingNavigationBarrier {
|
|||
}
|
||||
|
||||
private async _maybeResolve() {
|
||||
if (!this._protectCount && !this._frameIds.size)
|
||||
if (!this._protectCount && !this._expectedPopups && !this._expectedDownloads && !this._frameIds.size)
|
||||
this._promiseCallback();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -128,6 +128,10 @@ export class WKBrowser extends BrowserBase {
|
|||
const wkPage = new WKPage(context, pageProxySession, opener || null);
|
||||
this._wkPages.set(pageProxyId, wkPage);
|
||||
|
||||
if (opener && opener._initializedPage) {
|
||||
for (const signalBarrier of opener._initializedPage._frameManager._signalBarriers)
|
||||
signalBarrier.addPopup(wkPage.pageOrError());
|
||||
}
|
||||
wkPage.pageOrError().then(async () => {
|
||||
this._firstPageCallback();
|
||||
const page = wkPage._page;
|
||||
|
@ -216,7 +220,7 @@ export class WKBrowserContext extends BrowserContextBase {
|
|||
}
|
||||
|
||||
pages(): Page[] {
|
||||
return this._wkPages().map(wkPage => wkPage._initializedPage()).filter(pageOrNull => !!pageOrNull) as Page[];
|
||||
return this._wkPages().map(wkPage => wkPage._initializedPage).filter(pageOrNull => !!pageOrNull) as Page[];
|
||||
}
|
||||
|
||||
async newPage(): Promise<Page> {
|
||||
|
|
|
@ -58,9 +58,10 @@ export class WKPage implements PageDelegate {
|
|||
private _eventListeners: RegisteredListener[];
|
||||
private readonly _evaluateOnNewDocumentSources: string[] = [];
|
||||
readonly _browserContext: WKBrowserContext;
|
||||
private _initialized = false;
|
||||
_initializedPage: Page | null = null;
|
||||
private _firstNonInitialNavigationCommittedPromise: Promise<void>;
|
||||
private _firstNonInitialNavigationCommittedCallback = () => {};
|
||||
private _firstNonInitialNavigationCommittedFulfill = () => {};
|
||||
private _firstNonInitialNavigationCommittedReject = (e: Error) => {};
|
||||
|
||||
constructor(browserContext: WKBrowserContext, pageProxySession: WKSession, opener: WKPage | null) {
|
||||
this._pageProxySession = pageProxySession;
|
||||
|
@ -80,11 +81,10 @@ export class WKPage implements PageDelegate {
|
|||
helper.addEventListener(this._pageProxySession, 'Target.didCommitProvisionalTarget', this._onDidCommitProvisionalTarget.bind(this)),
|
||||
];
|
||||
this._pagePromise = new Promise(f => this._pagePromiseCallback = f);
|
||||
this._firstNonInitialNavigationCommittedPromise = new Promise(f => this._firstNonInitialNavigationCommittedCallback = f);
|
||||
}
|
||||
|
||||
_initializedPage(): Page | null {
|
||||
return this._initialized ? this._page : null;
|
||||
this._firstNonInitialNavigationCommittedPromise = new Promise((f, r) => {
|
||||
this._firstNonInitialNavigationCommittedFulfill = f;
|
||||
this._firstNonInitialNavigationCommittedReject = r;
|
||||
});
|
||||
}
|
||||
|
||||
private async _initializePageProxySession() {
|
||||
|
@ -217,6 +217,7 @@ export class WKPage implements PageDelegate {
|
|||
this._provisionalPage = null;
|
||||
}
|
||||
this._page._didDisconnect();
|
||||
this._firstNonInitialNavigationCommittedReject(new Error('Page closed'));
|
||||
}
|
||||
|
||||
dispatchMessageToSession(message: any) {
|
||||
|
@ -224,7 +225,11 @@ export class WKPage implements PageDelegate {
|
|||
}
|
||||
|
||||
handleProvisionalLoadFailed(event: Protocol.Playwright.provisionalLoadFailedPayload) {
|
||||
if (!this._initialized || !this._provisionalPage)
|
||||
if (!this._initializedPage) {
|
||||
this._firstNonInitialNavigationCommittedReject(new Error('Initial load failed'));
|
||||
return;
|
||||
}
|
||||
if (!this._provisionalPage)
|
||||
return;
|
||||
let errorText = event.error;
|
||||
if (errorText.includes('cancelled'))
|
||||
|
@ -247,7 +252,7 @@ export class WKPage implements PageDelegate {
|
|||
});
|
||||
assert(targetInfo.type === 'page', 'Only page targets are expected in WebKit, received: ' + targetInfo.type);
|
||||
|
||||
if (!this._initialized) {
|
||||
if (!this._initializedPage) {
|
||||
assert(!targetInfo.isProvisional);
|
||||
let pageOrError: Page | Error;
|
||||
try {
|
||||
|
@ -263,12 +268,19 @@ export class WKPage implements PageDelegate {
|
|||
if (targetInfo.isPaused)
|
||||
this._pageProxySession.send('Target.resume', { targetId: targetInfo.targetId }).catch(debugError);
|
||||
if ((pageOrError instanceof Page) && this._page.mainFrame().url() === '') {
|
||||
// Initial empty page has an empty url. We should wait until the first real url has been loaded,
|
||||
// even if that url is about:blank. This is especially important for popups, where we need the
|
||||
// actual url before interacting with it.
|
||||
await this._firstNonInitialNavigationCommittedPromise;
|
||||
try {
|
||||
// Initial empty page has an empty url. We should wait until the first real url has been loaded,
|
||||
// even if that url is about:blank. This is especially important for popups, where we need the
|
||||
// actual url before interacting with it.
|
||||
await this._firstNonInitialNavigationCommittedPromise;
|
||||
} catch (e) {
|
||||
pageOrError = e;
|
||||
}
|
||||
} else {
|
||||
// Avoid rejection on disconnect.
|
||||
this._firstNonInitialNavigationCommittedPromise.catch(() => {});
|
||||
}
|
||||
this._initialized = true;
|
||||
this._initializedPage = pageOrError instanceof Page ? pageOrError : null;
|
||||
this._pagePromiseCallback(pageOrError);
|
||||
} else {
|
||||
assert(targetInfo.isProvisional);
|
||||
|
@ -302,6 +314,8 @@ export class WKPage implements PageDelegate {
|
|||
helper.addEventListener(this._session, 'Page.frameStoppedLoading', event => this._onFrameStoppedLoading(event.frameId)),
|
||||
helper.addEventListener(this._session, 'Page.loadEventFired', event => this._onLifecycleEvent(event.frameId, 'load')),
|
||||
helper.addEventListener(this._session, 'Page.domContentEventFired', event => this._onLifecycleEvent(event.frameId, 'domcontentloaded')),
|
||||
helper.addEventListener(this._session, 'Page.willRequestOpenWindow', event => this._onWillRequestOpenWindow()),
|
||||
helper.addEventListener(this._session, 'Page.didRequestOpenWindow', event => this._onDidRequestOpenWindow(event)),
|
||||
helper.addEventListener(this._session, 'Runtime.executionContextCreated', event => this._onExecutionContextCreated(event.context)),
|
||||
helper.addEventListener(this._session, 'Console.messageAdded', event => this._onConsoleMessage(event)),
|
||||
helper.addEventListener(this._pageProxySession, 'Dialog.javascriptDialogOpening', event => this._onDialog(event)),
|
||||
|
@ -344,6 +358,18 @@ export class WKPage implements PageDelegate {
|
|||
this._page._frameManager.frameLifecycleEvent(frameId, event);
|
||||
}
|
||||
|
||||
private _onWillRequestOpenWindow() {
|
||||
for (const barrier of this._page._frameManager._signalBarriers)
|
||||
barrier.expectPopup();
|
||||
}
|
||||
|
||||
private _onDidRequestOpenWindow(event: Protocol.Page.didRequestOpenWindowPayload) {
|
||||
if (!event.opened) {
|
||||
for (const barrier of this._page._frameManager._signalBarriers)
|
||||
barrier.unexpectPopup();
|
||||
}
|
||||
}
|
||||
|
||||
private _handleFrameTree(frameTree: Protocol.Page.FrameResourceTree) {
|
||||
this._onFrameAttached(frameTree.frame.id, frameTree.frame.parentId || null);
|
||||
this._onFrameNavigated(frameTree.frame, true);
|
||||
|
@ -368,7 +394,7 @@ export class WKPage implements PageDelegate {
|
|||
this._workers.clear();
|
||||
this._page._frameManager.frameCommittedNewDocumentNavigation(framePayload.id, framePayload.url, framePayload.name || '', framePayload.loaderId, initial);
|
||||
if (!initial)
|
||||
this._firstNonInitialNavigationCommittedCallback();
|
||||
this._firstNonInitialNavigationCommittedFulfill();
|
||||
}
|
||||
|
||||
private _onFrameNavigatedWithinDocument(frameId: string, url: string) {
|
||||
|
|
|
@ -34,6 +34,40 @@ describe('Auto waiting', () => {
|
|||
]);
|
||||
expect(messages.join('|')).toBe('route|domcontentloaded|click');
|
||||
});
|
||||
it('should await popup when clicking anchor', async function({page, server}) {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.setContent('<a target=_blank rel=opener href="/empty.html">link</a>');
|
||||
const messages = [];
|
||||
await Promise.all([
|
||||
page.waitForEvent('popup').then(() => messages.push('popup')),
|
||||
page.click('a').then(() => messages.push('click')),
|
||||
]);
|
||||
expect(messages.join('|')).toBe('popup|click');
|
||||
});
|
||||
it('should await popup when clicking anchor with noopener', async function({page, server}) {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.setContent('<a target=_blank rel=noopener href="/empty.html">link</a>');
|
||||
const messages = [];
|
||||
await Promise.all([
|
||||
page.waitForEvent('popup').then(() => messages.push('popup')),
|
||||
page.click('a').then(() => messages.push('click')),
|
||||
]);
|
||||
expect(messages.join('|')).toBe('popup|click');
|
||||
});
|
||||
it('should await download when clicking anchor', async function({page, server}) {
|
||||
server.setRoute('/download', (req, res) => {
|
||||
res.setHeader('Content-Type', 'application/octet-stream');
|
||||
res.setHeader('Content-Disposition', 'attachment');
|
||||
res.end(`Hello world`);
|
||||
});
|
||||
await page.setContent(`<a download=true href="${server.PREFIX}/download">download</a>`);
|
||||
const messages = [];
|
||||
await Promise.all([
|
||||
page.waitForEvent('download').then(() => messages.push('download')),
|
||||
page.click('a').then(() => messages.push('click')),
|
||||
]);
|
||||
expect(messages.join('|')).toBe('download|click');
|
||||
});
|
||||
it('should await cross-process navigation when clicking anchor', async({page, server}) => {
|
||||
const messages = [];
|
||||
server.setRoute('/empty.html', async (req, res) => {
|
||||
|
@ -130,6 +164,15 @@ describe('Auto waiting', () => {
|
|||
]);
|
||||
expect(messages.join('|')).toBe('route|domcontentloaded|evaluate');
|
||||
});
|
||||
it('should await new popup when evaluating', async function({page, server}) {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const messages = [];
|
||||
await Promise.all([
|
||||
page.waitForEvent('popup').then(() => messages.push('popup')),
|
||||
page.evaluate(() => window._popup = window.open(window.location.href)).then(() => messages.push('evaluate')),
|
||||
]);
|
||||
expect(messages.join('|')).toBe('popup|evaluate');
|
||||
});
|
||||
it('should await navigating specified target', async({page, server}) => {
|
||||
const messages = [];
|
||||
server.setRoute('/empty.html', async (req, res) => {
|
||||
|
|
|
@ -630,7 +630,6 @@ describe('Events.BrowserContext.Page', function() {
|
|||
context.waitForEvent('page'),
|
||||
page.goto(server.PREFIX + '/popup/window-open.html')
|
||||
]);
|
||||
// The url is still about:blank in FF when 'page' event is fired.
|
||||
expect(popup.url()).toBe(server.PREFIX + '/popup/popup.html');
|
||||
expect(await popup.opener()).toBe(page);
|
||||
expect(await page.opener()).toBe(null);
|
||||
|
|
|
@ -217,6 +217,20 @@ describe('Page.Events.Popup', function() {
|
|||
expect(popup).toBeTruthy();
|
||||
await context.close();
|
||||
});
|
||||
it('should emit for immediately closed popups', async({browser, server}) => {
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const [popup] = await Promise.all([
|
||||
page.waitForEvent('popup'),
|
||||
page.evaluate(() => {
|
||||
const win = window.open(window.location.href);
|
||||
win.close();
|
||||
}),
|
||||
]);
|
||||
expect(popup).toBeTruthy();
|
||||
await context.close();
|
||||
});
|
||||
it('should be able to capture alert', async({browser}) => {
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
|
|
Загрузка…
Ссылка в новой задаче