mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(api): wait for popups and downloads when performing actions (#1744)
This commit is contained in:
parent
67cd5698a7
commit
f5942295d4
@ -134,6 +134,10 @@ export class CRBrowser extends BrowserBase {
|
|||||||
const opener = targetInfo.openerId ? this._crPages.get(targetInfo.openerId) || null : null;
|
const opener = targetInfo.openerId ? this._crPages.get(targetInfo.openerId) || null : null;
|
||||||
const crPage = new CRPage(session, targetInfo.targetId, context, opener);
|
const crPage = new CRPage(session, targetInfo.targetId, context, opener);
|
||||||
this._crPages.set(targetInfo.targetId, crPage);
|
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(() => {
|
crPage.pageOrError().then(() => {
|
||||||
this._firstPageCallback();
|
this._firstPageCallback();
|
||||||
context.emit(CommonEvents.BrowserContext.Page, crPage._page);
|
context.emit(CommonEvents.BrowserContext.Page, crPage._page);
|
||||||
|
@ -315,7 +315,8 @@ class FrameSession {
|
|||||||
private _eventListeners: RegisteredListener[] = [];
|
private _eventListeners: RegisteredListener[] = [];
|
||||||
readonly _targetId: string;
|
readonly _targetId: string;
|
||||||
private _firstNonInitialNavigationCommittedPromise: Promise<void>;
|
private _firstNonInitialNavigationCommittedPromise: Promise<void>;
|
||||||
private _firstNonInitialNavigationCommittedCallback = () => {};
|
private _firstNonInitialNavigationCommittedFulfill = () => {};
|
||||||
|
private _firstNonInitialNavigationCommittedReject = (e: Error) => {};
|
||||||
|
|
||||||
constructor(crPage: CRPage, client: CRSession, targetId: string) {
|
constructor(crPage: CRPage, client: CRSession, targetId: string) {
|
||||||
this._client = client;
|
this._client = client;
|
||||||
@ -323,7 +324,13 @@ class FrameSession {
|
|||||||
this._page = crPage._page;
|
this._page = crPage._page;
|
||||||
this._targetId = targetId;
|
this._targetId = targetId;
|
||||||
this._networkManager = new CRNetworkManager(client, this._page);
|
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 {
|
private _isMainFrame(): boolean {
|
||||||
@ -386,7 +393,7 @@ class FrameSession {
|
|||||||
this._eventListeners.push(helper.addEventListener(this._client, 'Page.lifecycleEvent', event => this._onLifecycleEvent(event)));
|
this._eventListeners.push(helper.addEventListener(this._client, 'Page.lifecycleEvent', event => this._onLifecycleEvent(event)));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this._firstNonInitialNavigationCommittedCallback();
|
this._firstNonInitialNavigationCommittedFulfill();
|
||||||
this._eventListeners.push(helper.addEventListener(this._client, 'Page.lifecycleEvent', event => this._onLifecycleEvent(event)));
|
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) {
|
_onFrameNavigated(framePayload: Protocol.Page.Frame, initial: boolean) {
|
||||||
this._page._frameManager.frameCommittedNewDocumentNavigation(framePayload.id, framePayload.url, framePayload.name || '', framePayload.loaderId, initial);
|
this._page._frameManager.frameCommittedNewDocumentNavigation(framePayload.id, framePayload.url, framePayload.name || '', framePayload.loaderId, initial);
|
||||||
if (!initial)
|
if (!initial)
|
||||||
this._firstNonInitialNavigationCommittedCallback();
|
this._firstNonInitialNavigationCommittedFulfill();
|
||||||
}
|
}
|
||||||
|
|
||||||
_onFrameRequestedNavigation(payload: Protocol.Page.frameRequestedNavigationPayload) {
|
_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> {
|
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);
|
return this._delegate.evaluate(this, returnByValue, pageFunction, ...args);
|
||||||
}, Number.MAX_SAFE_INTEGER, waitForNavigations ? undefined : { waitUntil: 'nowait' });
|
}, 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)
|
if (!force)
|
||||||
await this._waitForHitTargetAt(point, deadline);
|
await this._waitForHitTargetAt(point, deadline);
|
||||||
|
|
||||||
await this._page._frameManager.waitForNavigationsCreatedBy(async () => {
|
await this._page._frameManager.waitForSignalsCreatedBy(async () => {
|
||||||
let restoreModifiers: input.Modifier[] | undefined;
|
let restoreModifiers: input.Modifier[] | undefined;
|
||||||
if (options && options.modifiers)
|
if (options && options.modifiers)
|
||||||
restoreModifiers = await this._page.keyboard._ensureModifiers(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)
|
if (option.index !== undefined)
|
||||||
assert(helper.isNumber(option.index), 'Indices must be numbers. Found index "' + option.index + '" of type "' + (typeof option.index) + '"');
|
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);
|
return this._evaluateInUtility(({ injected, node }, selectOptions) => injected.selectOptions(node, selectOptions), selectOptions);
|
||||||
}, deadline, options);
|
}, 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> {
|
async fill(value: string, options?: types.NavigatingActionWaitOptions): Promise<void> {
|
||||||
assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"');
|
assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"');
|
||||||
const deadline = this._page._timeoutSettings.computeDeadline(options);
|
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);
|
const errorOrNeedsInput = await this._evaluateInUtility(({ injected, node }, value) => injected.fill(node, value), value);
|
||||||
if (typeof errorOrNeedsInput === 'string')
|
if (typeof errorOrNeedsInput === 'string')
|
||||||
throw new Error(errorOrNeedsInput);
|
throw new Error(errorOrNeedsInput);
|
||||||
@ -323,7 +323,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||||||
filePayloads.push(item);
|
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);
|
await this._page._delegate.setInputFiles(this as any as ElementHandle<HTMLInputElement>, filePayloads);
|
||||||
}, deadline, options);
|
}, 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) {
|
async type(text: string, options?: { delay?: number } & types.NavigatingActionWaitOptions) {
|
||||||
const deadline = this._page._timeoutSettings.computeDeadline(options);
|
const deadline = this._page._timeoutSettings.computeDeadline(options);
|
||||||
await this._page._frameManager.waitForNavigationsCreatedBy(async () => {
|
await this._page._frameManager.waitForSignalsCreatedBy(async () => {
|
||||||
await this.focus();
|
await this.focus();
|
||||||
await this._page.keyboard.type(text, options);
|
await this._page.keyboard.type(text, options);
|
||||||
}, deadline, options, true);
|
}, 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) {
|
async press(key: string, options?: { delay?: number } & types.NavigatingActionWaitOptions) {
|
||||||
const deadline = this._page._timeoutSettings.computeDeadline(options);
|
const deadline = this._page._timeoutSettings.computeDeadline(options);
|
||||||
await this._page._frameManager.waitForNavigationsCreatedBy(async () => {
|
await this._page._frameManager.waitForSignalsCreatedBy(async () => {
|
||||||
await this.focus();
|
await this.focus();
|
||||||
await this._page.keyboard.press(key, options);
|
await this._page.keyboard.press(key, options);
|
||||||
}, deadline, options, true);
|
}, deadline, options, true);
|
||||||
|
@ -39,6 +39,8 @@ export class Download {
|
|||||||
this._url = url;
|
this._url = url;
|
||||||
this._finishedCallback = () => {};
|
this._finishedCallback = () => {};
|
||||||
this._finishedPromise = new Promise(f => this._finishedCallback = f);
|
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);
|
this._page.emit(Events.Page.Download, this);
|
||||||
page._browserContext._downloads.add(this);
|
page._browserContext._downloads.add(this);
|
||||||
this._acceptDownloads = !!this._page._browserContext._options.acceptDownloads;
|
this._acceptDownloads = !!this._page._browserContext._options.acceptDownloads;
|
||||||
|
@ -129,6 +129,10 @@ export class FFBrowser extends BrowserBase {
|
|||||||
const ffPage = new FFPage(session, context, opener);
|
const ffPage = new FFPage(session, context, opener);
|
||||||
this._ffPages.set(targetId, ffPage);
|
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 () => {
|
ffPage.pageOrError().then(async () => {
|
||||||
this._firstPageCallback();
|
this._firstPageCallback();
|
||||||
const page = ffPage._page;
|
const page = ffPage._page;
|
||||||
@ -200,7 +204,7 @@ export class FFBrowserContext extends BrowserContextBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pages(): Page[] {
|
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> {
|
async newPage(): Promise<Page> {
|
||||||
|
@ -43,7 +43,7 @@ export class FFPage implements PageDelegate {
|
|||||||
readonly _browserContext: FFBrowserContext;
|
readonly _browserContext: FFBrowserContext;
|
||||||
private _pagePromise: Promise<Page | Error>;
|
private _pagePromise: Promise<Page | Error>;
|
||||||
private _pageCallback: (pageOrError: Page | Error) => void = () => {};
|
private _pageCallback: (pageOrError: Page | Error) => void = () => {};
|
||||||
private _initialized = false;
|
_initializedPage: Page | null = null;
|
||||||
private readonly _opener: FFPage | null;
|
private readonly _opener: FFPage | null;
|
||||||
private readonly _contextIdToContext: Map<string, dom.FrameExecutionContext>;
|
private readonly _contextIdToContext: Map<string, dom.FrameExecutionContext>;
|
||||||
private _eventListeners: RegisteredListener[];
|
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.executionContextCreated', this._onExecutionContextCreated.bind(this)),
|
||||||
helper.addEventListener(this._session, 'Runtime.executionContextDestroyed', this._onExecutionContextDestroyed.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.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, 'Page.uncaughtError', this._onUncaughtError.bind(this)),
|
||||||
helper.addEventListener(this._session, 'Runtime.console', this._onConsole.bind(this)),
|
helper.addEventListener(this._session, 'Runtime.console', this._onConsole.bind(this)),
|
||||||
helper.addEventListener(this._session, 'Page.dialogOpened', this._onDialogOpened.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());
|
session.once(FFSessionEvents.Disconnected, () => this._page._didDisconnect());
|
||||||
this._session.once('Page.ready', () => {
|
this._session.once('Page.ready', () => {
|
||||||
this._pageCallback(this._page);
|
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.
|
// 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.
|
// 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);
|
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> {
|
async pageOrError(): Promise<Page | Error> {
|
||||||
return this._pagePromise;
|
return this._pagePromise;
|
||||||
}
|
}
|
||||||
@ -136,12 +133,21 @@ export class FFPage implements PageDelegate {
|
|||||||
this._page._frameManager.frameDidPotentiallyRequestNavigation();
|
this._page._frameManager.frameDidPotentiallyRequestNavigation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onWillOpenNewWindowAsynchronously() {
|
||||||
|
for (const barrier of this._page._frameManager._signalBarriers)
|
||||||
|
barrier.expectPopup();
|
||||||
|
}
|
||||||
|
|
||||||
_onNavigationStarted(params: Protocol.Page.navigationStartedPayload) {
|
_onNavigationStarted(params: Protocol.Page.navigationStartedPayload) {
|
||||||
this._page._frameManager.frameRequestedNavigation(params.frameId, params.navigationId);
|
this._page._frameManager.frameRequestedNavigation(params.frameId, params.navigationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onNavigationAborted(params: Protocol.Page.navigationAbortedPayload) {
|
_onNavigationAborted(params: Protocol.Page.navigationAbortedPayload) {
|
||||||
const frame = this._page._frameManager.frame(params.frameId)!;
|
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)
|
for (const task of frame._frameTasks)
|
||||||
task.onNewDocument(params.navigationId, new Error(params.errorText));
|
task.onNewDocument(params.navigationId, new Error(params.errorText));
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ export class FrameManager {
|
|||||||
private _frames = new Map<string, Frame>();
|
private _frames = new Map<string, Frame>();
|
||||||
private _mainFrame: Frame;
|
private _mainFrame: Frame;
|
||||||
readonly _consoleMessageTags = new Map<string, ConsoleTagHandler>();
|
readonly _consoleMessageTags = new Map<string, ConsoleTagHandler>();
|
||||||
private _pendingNavigationBarriers = new Set<PendingNavigationBarrier>();
|
readonly _signalBarriers = new Set<SignalBarrier>();
|
||||||
|
|
||||||
constructor(page: Page) {
|
constructor(page: Page) {
|
||||||
this._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')
|
if (options.waitUntil === 'nowait')
|
||||||
return action();
|
return action();
|
||||||
const barrier = new PendingNavigationBarrier({ waitUntil: 'domcontentloaded', ...options }, deadline);
|
const barrier = new SignalBarrier({ waitUntil: 'domcontentloaded', ...options }, deadline);
|
||||||
this._pendingNavigationBarriers.add(barrier);
|
this._signalBarriers.add(barrier);
|
||||||
try {
|
try {
|
||||||
const result = await action();
|
const result = await action();
|
||||||
if (input)
|
if (input)
|
||||||
@ -114,17 +114,17 @@ export class FrameManager {
|
|||||||
await new Promise(helper.makeWaitForNextTask());
|
await new Promise(helper.makeWaitForNextTask());
|
||||||
return result;
|
return result;
|
||||||
} finally {
|
} finally {
|
||||||
this._pendingNavigationBarriers.delete(barrier);
|
this._signalBarriers.delete(barrier);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
frameWillPotentiallyRequestNavigation() {
|
frameWillPotentiallyRequestNavigation() {
|
||||||
for (const barrier of this._pendingNavigationBarriers)
|
for (const barrier of this._signalBarriers)
|
||||||
barrier.retain();
|
barrier.retain();
|
||||||
}
|
}
|
||||||
|
|
||||||
frameDidPotentiallyRequestNavigation() {
|
frameDidPotentiallyRequestNavigation() {
|
||||||
for (const barrier of this._pendingNavigationBarriers)
|
for (const barrier of this._signalBarriers)
|
||||||
barrier.release();
|
barrier.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,8 +132,8 @@ export class FrameManager {
|
|||||||
const frame = this._frames.get(frameId);
|
const frame = this._frames.get(frameId);
|
||||||
if (!frame)
|
if (!frame)
|
||||||
return;
|
return;
|
||||||
for (const barrier of this._pendingNavigationBarriers)
|
for (const barrier of this._signalBarriers)
|
||||||
barrier.addFrame(frame);
|
barrier.addFrameNavigation(frame);
|
||||||
frame._pendingDocumentId = documentId;
|
frame._pendingDocumentId = documentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -939,10 +939,12 @@ function selectorToString(selector: string, waitFor: 'attached' | 'detached' | '
|
|||||||
return `${label}${selector}`;
|
return `${label}${selector}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
class PendingNavigationBarrier {
|
export class SignalBarrier {
|
||||||
private _frameIds = new Map<string, number>();
|
private _frameIds = new Map<string, number>();
|
||||||
private _options: types.NavigatingActionWaitOptions;
|
private _options: types.NavigatingActionWaitOptions;
|
||||||
private _protectCount = 0;
|
private _protectCount = 0;
|
||||||
|
private _expectedPopups = 0;
|
||||||
|
private _expectedDownloads = 0;
|
||||||
private _promise: Promise<void>;
|
private _promise: Promise<void>;
|
||||||
private _promiseCallback = () => {};
|
private _promiseCallback = () => {};
|
||||||
private _deadline: number;
|
private _deadline: number;
|
||||||
@ -959,7 +961,7 @@ class PendingNavigationBarrier {
|
|||||||
return this._promise;
|
return this._promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addFrame(frame: Frame) {
|
async addFrameNavigation(frame: Frame) {
|
||||||
this.retain();
|
this.retain();
|
||||||
const timeout = helper.timeUntilDeadline(this._deadline);
|
const timeout = helper.timeUntilDeadline(this._deadline);
|
||||||
const options = { ...this._options, timeout } as types.NavigateOptions;
|
const options = { ...this._options, timeout } as types.NavigateOptions;
|
||||||
@ -967,6 +969,33 @@ class PendingNavigationBarrier {
|
|||||||
this.release();
|
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() {
|
retain() {
|
||||||
++this._protectCount;
|
++this._protectCount;
|
||||||
}
|
}
|
||||||
@ -977,7 +1006,7 @@ class PendingNavigationBarrier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _maybeResolve() {
|
private async _maybeResolve() {
|
||||||
if (!this._protectCount && !this._frameIds.size)
|
if (!this._protectCount && !this._expectedPopups && !this._expectedDownloads && !this._frameIds.size)
|
||||||
this._promiseCallback();
|
this._promiseCallback();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -128,6 +128,10 @@ export class WKBrowser extends BrowserBase {
|
|||||||
const wkPage = new WKPage(context, pageProxySession, opener || null);
|
const wkPage = new WKPage(context, pageProxySession, opener || null);
|
||||||
this._wkPages.set(pageProxyId, wkPage);
|
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 () => {
|
wkPage.pageOrError().then(async () => {
|
||||||
this._firstPageCallback();
|
this._firstPageCallback();
|
||||||
const page = wkPage._page;
|
const page = wkPage._page;
|
||||||
@ -216,7 +220,7 @@ export class WKBrowserContext extends BrowserContextBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pages(): Page[] {
|
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> {
|
async newPage(): Promise<Page> {
|
||||||
|
@ -58,9 +58,10 @@ export class WKPage implements PageDelegate {
|
|||||||
private _eventListeners: RegisteredListener[];
|
private _eventListeners: RegisteredListener[];
|
||||||
private readonly _evaluateOnNewDocumentSources: string[] = [];
|
private readonly _evaluateOnNewDocumentSources: string[] = [];
|
||||||
readonly _browserContext: WKBrowserContext;
|
readonly _browserContext: WKBrowserContext;
|
||||||
private _initialized = false;
|
_initializedPage: Page | null = null;
|
||||||
private _firstNonInitialNavigationCommittedPromise: Promise<void>;
|
private _firstNonInitialNavigationCommittedPromise: Promise<void>;
|
||||||
private _firstNonInitialNavigationCommittedCallback = () => {};
|
private _firstNonInitialNavigationCommittedFulfill = () => {};
|
||||||
|
private _firstNonInitialNavigationCommittedReject = (e: Error) => {};
|
||||||
|
|
||||||
constructor(browserContext: WKBrowserContext, pageProxySession: WKSession, opener: WKPage | null) {
|
constructor(browserContext: WKBrowserContext, pageProxySession: WKSession, opener: WKPage | null) {
|
||||||
this._pageProxySession = pageProxySession;
|
this._pageProxySession = pageProxySession;
|
||||||
@ -80,11 +81,10 @@ export class WKPage implements PageDelegate {
|
|||||||
helper.addEventListener(this._pageProxySession, 'Target.didCommitProvisionalTarget', this._onDidCommitProvisionalTarget.bind(this)),
|
helper.addEventListener(this._pageProxySession, 'Target.didCommitProvisionalTarget', this._onDidCommitProvisionalTarget.bind(this)),
|
||||||
];
|
];
|
||||||
this._pagePromise = new Promise(f => this._pagePromiseCallback = f);
|
this._pagePromise = new Promise(f => this._pagePromiseCallback = f);
|
||||||
this._firstNonInitialNavigationCommittedPromise = new Promise(f => this._firstNonInitialNavigationCommittedCallback = f);
|
this._firstNonInitialNavigationCommittedPromise = new Promise((f, r) => {
|
||||||
}
|
this._firstNonInitialNavigationCommittedFulfill = f;
|
||||||
|
this._firstNonInitialNavigationCommittedReject = r;
|
||||||
_initializedPage(): Page | null {
|
});
|
||||||
return this._initialized ? this._page : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _initializePageProxySession() {
|
private async _initializePageProxySession() {
|
||||||
@ -217,6 +217,7 @@ export class WKPage implements PageDelegate {
|
|||||||
this._provisionalPage = null;
|
this._provisionalPage = null;
|
||||||
}
|
}
|
||||||
this._page._didDisconnect();
|
this._page._didDisconnect();
|
||||||
|
this._firstNonInitialNavigationCommittedReject(new Error('Page closed'));
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatchMessageToSession(message: any) {
|
dispatchMessageToSession(message: any) {
|
||||||
@ -224,7 +225,11 @@ export class WKPage implements PageDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleProvisionalLoadFailed(event: Protocol.Playwright.provisionalLoadFailedPayload) {
|
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;
|
return;
|
||||||
let errorText = event.error;
|
let errorText = event.error;
|
||||||
if (errorText.includes('cancelled'))
|
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);
|
assert(targetInfo.type === 'page', 'Only page targets are expected in WebKit, received: ' + targetInfo.type);
|
||||||
|
|
||||||
if (!this._initialized) {
|
if (!this._initializedPage) {
|
||||||
assert(!targetInfo.isProvisional);
|
assert(!targetInfo.isProvisional);
|
||||||
let pageOrError: Page | Error;
|
let pageOrError: Page | Error;
|
||||||
try {
|
try {
|
||||||
@ -263,12 +268,19 @@ export class WKPage implements PageDelegate {
|
|||||||
if (targetInfo.isPaused)
|
if (targetInfo.isPaused)
|
||||||
this._pageProxySession.send('Target.resume', { targetId: targetInfo.targetId }).catch(debugError);
|
this._pageProxySession.send('Target.resume', { targetId: targetInfo.targetId }).catch(debugError);
|
||||||
if ((pageOrError instanceof Page) && this._page.mainFrame().url() === '') {
|
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,
|
try {
|
||||||
// even if that url is about:blank. This is especially important for popups, where we need the
|
// Initial empty page has an empty url. We should wait until the first real url has been loaded,
|
||||||
// actual url before interacting with it.
|
// even if that url is about:blank. This is especially important for popups, where we need the
|
||||||
await this._firstNonInitialNavigationCommittedPromise;
|
// 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);
|
this._pagePromiseCallback(pageOrError);
|
||||||
} else {
|
} else {
|
||||||
assert(targetInfo.isProvisional);
|
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.frameStoppedLoading', event => this._onFrameStoppedLoading(event.frameId)),
|
||||||
helper.addEventListener(this._session, 'Page.loadEventFired', event => this._onLifecycleEvent(event.frameId, 'load')),
|
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.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, 'Runtime.executionContextCreated', event => this._onExecutionContextCreated(event.context)),
|
||||||
helper.addEventListener(this._session, 'Console.messageAdded', event => this._onConsoleMessage(event)),
|
helper.addEventListener(this._session, 'Console.messageAdded', event => this._onConsoleMessage(event)),
|
||||||
helper.addEventListener(this._pageProxySession, 'Dialog.javascriptDialogOpening', event => this._onDialog(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);
|
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) {
|
private _handleFrameTree(frameTree: Protocol.Page.FrameResourceTree) {
|
||||||
this._onFrameAttached(frameTree.frame.id, frameTree.frame.parentId || null);
|
this._onFrameAttached(frameTree.frame.id, frameTree.frame.parentId || null);
|
||||||
this._onFrameNavigated(frameTree.frame, true);
|
this._onFrameNavigated(frameTree.frame, true);
|
||||||
@ -368,7 +394,7 @@ export class WKPage implements PageDelegate {
|
|||||||
this._workers.clear();
|
this._workers.clear();
|
||||||
this._page._frameManager.frameCommittedNewDocumentNavigation(framePayload.id, framePayload.url, framePayload.name || '', framePayload.loaderId, initial);
|
this._page._frameManager.frameCommittedNewDocumentNavigation(framePayload.id, framePayload.url, framePayload.name || '', framePayload.loaderId, initial);
|
||||||
if (!initial)
|
if (!initial)
|
||||||
this._firstNonInitialNavigationCommittedCallback();
|
this._firstNonInitialNavigationCommittedFulfill();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onFrameNavigatedWithinDocument(frameId: string, url: string) {
|
private _onFrameNavigatedWithinDocument(frameId: string, url: string) {
|
||||||
|
@ -34,6 +34,40 @@ describe('Auto waiting', () => {
|
|||||||
]);
|
]);
|
||||||
expect(messages.join('|')).toBe('route|domcontentloaded|click');
|
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}) => {
|
it('should await cross-process navigation when clicking anchor', async({page, server}) => {
|
||||||
const messages = [];
|
const messages = [];
|
||||||
server.setRoute('/empty.html', async (req, res) => {
|
server.setRoute('/empty.html', async (req, res) => {
|
||||||
@ -130,6 +164,15 @@ describe('Auto waiting', () => {
|
|||||||
]);
|
]);
|
||||||
expect(messages.join('|')).toBe('route|domcontentloaded|evaluate');
|
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}) => {
|
it('should await navigating specified target', async({page, server}) => {
|
||||||
const messages = [];
|
const messages = [];
|
||||||
server.setRoute('/empty.html', async (req, res) => {
|
server.setRoute('/empty.html', async (req, res) => {
|
||||||
|
@ -630,7 +630,6 @@ describe('Events.BrowserContext.Page', function() {
|
|||||||
context.waitForEvent('page'),
|
context.waitForEvent('page'),
|
||||||
page.goto(server.PREFIX + '/popup/window-open.html')
|
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(popup.url()).toBe(server.PREFIX + '/popup/popup.html');
|
||||||
expect(await popup.opener()).toBe(page);
|
expect(await popup.opener()).toBe(page);
|
||||||
expect(await page.opener()).toBe(null);
|
expect(await page.opener()).toBe(null);
|
||||||
|
@ -217,6 +217,20 @@ describe('Page.Events.Popup', function() {
|
|||||||
expect(popup).toBeTruthy();
|
expect(popup).toBeTruthy();
|
||||||
await context.close();
|
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}) => {
|
it('should be able to capture alert', async({browser}) => {
|
||||||
const context = await browser.newContext();
|
const context = await browser.newContext();
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user