2019-11-18 18:18:28 -08:00
|
|
|
/**
|
|
|
|
* Copyright 2017 Google Inc. All rights reserved.
|
|
|
|
* Modifications 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.
|
|
|
|
*/
|
|
|
|
|
2021-05-19 01:30:20 +00:00
|
|
|
import * as channels from '../protocol/channels';
|
2020-04-01 14:42:47 -07:00
|
|
|
import { ConsoleMessage } from './console';
|
2019-11-27 16:02:31 -08:00
|
|
|
import * as dom from './dom';
|
2021-07-07 21:14:16 +02:00
|
|
|
import { helper } from './helper';
|
|
|
|
import { eventsHelper, RegisteredListener } from '../utils/eventsHelper';
|
2020-04-01 14:42:47 -07:00
|
|
|
import * as js from './javascript';
|
|
|
|
import * as network from './network';
|
2019-12-11 12:36:42 -08:00
|
|
|
import { Page } from './page';
|
2020-04-01 14:42:47 -07:00
|
|
|
import * as types from './types';
|
2020-05-18 14:28:06 -07:00
|
|
|
import { BrowserContext } from './browserContext';
|
2021-02-09 14:44:48 -08:00
|
|
|
import { Progress, ProgressController } from './progress';
|
2021-07-06 21:16:37 +02:00
|
|
|
import { assert, constructURLBasedOnBaseURL, makeWaitForNextTask } from '../utils/utils';
|
2020-08-24 06:51:51 -07:00
|
|
|
import { debugLogger } from '../utils/debugLogger';
|
2021-04-07 07:01:38 +08:00
|
|
|
import { CallMetadata, internalCallMetadata, SdkObject } from './instrumentation';
|
2021-02-10 12:36:26 -08:00
|
|
|
import { ElementStateWithoutStable } from './injected/injectedScript';
|
2019-11-18 18:18:28 -08:00
|
|
|
|
2019-12-12 21:11:52 -08:00
|
|
|
type ContextData = {
|
|
|
|
contextPromise: Promise<dom.FrameExecutionContext>;
|
|
|
|
contextResolveCallback: (c: dom.FrameExecutionContext) => void;
|
|
|
|
context: dom.FrameExecutionContext | null;
|
2021-01-25 14:49:26 -08:00
|
|
|
rerunnableTasks: Set<RerunnableTask>;
|
2019-11-26 15:37:25 -08:00
|
|
|
};
|
|
|
|
|
2020-07-06 15:58:27 -07:00
|
|
|
type DocumentInfo = {
|
|
|
|
// Unfortunately, we don't have documentId when we find out about
|
|
|
|
// a pending navigation from things like frameScheduledNavigaiton.
|
|
|
|
documentId: string | undefined,
|
|
|
|
request: network.Request | undefined,
|
|
|
|
};
|
|
|
|
|
2019-12-16 22:02:33 -08:00
|
|
|
export type GotoResult = {
|
|
|
|
newDocumentId?: string,
|
|
|
|
};
|
2019-11-26 16:19:43 -08:00
|
|
|
|
2020-01-27 16:51:52 -08:00
|
|
|
type ConsoleTagHandler = () => void;
|
2019-12-14 19:13:22 -08:00
|
|
|
|
2020-05-18 14:28:06 -07:00
|
|
|
export type FunctionWithSource = (source: { context: BrowserContext, page: Page, frame: Frame}, ...args: any) => any;
|
|
|
|
|
2020-07-15 18:48:19 -07:00
|
|
|
export type NavigationEvent = {
|
|
|
|
// New frame url after navigation.
|
|
|
|
url: string,
|
|
|
|
// New frame name after navigation.
|
|
|
|
name: string,
|
|
|
|
// Information about the new document for cross-document navigations.
|
|
|
|
// Undefined for same-document navigations.
|
|
|
|
newDocument?: DocumentInfo,
|
|
|
|
// Error for cross-document navigations if any. When error is present,
|
|
|
|
// the navigation did not commit.
|
2020-07-07 15:22:05 -07:00
|
|
|
error?: Error,
|
|
|
|
};
|
|
|
|
|
2019-12-16 15:56:11 -08:00
|
|
|
export class FrameManager {
|
|
|
|
private _page: Page;
|
|
|
|
private _frames = new Map<string, Frame>();
|
|
|
|
private _mainFrame: Frame;
|
2020-01-27 16:51:52 -08:00
|
|
|
readonly _consoleMessageTags = new Map<string, ConsoleTagHandler>();
|
2020-04-16 13:09:24 -07:00
|
|
|
readonly _signalBarriers = new Set<SignalBarrier>();
|
2020-10-26 22:20:43 -07:00
|
|
|
private _webSockets = new Map<string, network.WebSocket>();
|
2021-04-27 11:07:07 -07:00
|
|
|
readonly _responses: network.Response[] = [];
|
2019-12-16 15:56:11 -08:00
|
|
|
|
|
|
|
constructor(page: Page) {
|
|
|
|
this._page = page;
|
2020-01-13 13:33:25 -08:00
|
|
|
this._mainFrame = undefined as any as Frame;
|
2019-12-16 15:56:11 -08:00
|
|
|
}
|
|
|
|
|
2020-07-10 16:38:01 -07:00
|
|
|
dispose() {
|
|
|
|
for (const frame of this._frames.values())
|
|
|
|
frame._stopNetworkIdleTimer();
|
|
|
|
}
|
|
|
|
|
2019-12-16 15:56:11 -08:00
|
|
|
mainFrame(): Frame {
|
|
|
|
return this._mainFrame;
|
|
|
|
}
|
|
|
|
|
|
|
|
frames() {
|
|
|
|
const frames: Frame[] = [];
|
|
|
|
collect(this._mainFrame);
|
|
|
|
return frames;
|
|
|
|
|
|
|
|
function collect(frame: Frame) {
|
|
|
|
frames.push(frame);
|
|
|
|
for (const subframe of frame.childFrames())
|
|
|
|
collect(subframe);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
frame(frameId: string): Frame | null {
|
|
|
|
return this._frames.get(frameId) || null;
|
|
|
|
}
|
|
|
|
|
|
|
|
frameAttached(frameId: string, parentFrameId: string | null | undefined): Frame {
|
2020-01-13 13:33:25 -08:00
|
|
|
const parentFrame = parentFrameId ? this._frames.get(parentFrameId)! : null;
|
2019-12-16 15:56:11 -08:00
|
|
|
if (!parentFrame) {
|
|
|
|
if (this._mainFrame) {
|
|
|
|
// Update frame id to retain frame identity on cross-process navigation.
|
|
|
|
this._frames.delete(this._mainFrame._id);
|
|
|
|
this._mainFrame._id = frameId;
|
|
|
|
} else {
|
|
|
|
assert(!this._frames.has(frameId));
|
|
|
|
this._mainFrame = new Frame(this._page, frameId, parentFrame);
|
|
|
|
}
|
|
|
|
this._frames.set(frameId, this._mainFrame);
|
|
|
|
return this._mainFrame;
|
|
|
|
} else {
|
|
|
|
assert(!this._frames.has(frameId));
|
|
|
|
const frame = new Frame(this._page, frameId, parentFrame);
|
|
|
|
this._frames.set(frameId, frame);
|
2020-08-21 16:26:33 -07:00
|
|
|
this._page.emit(Page.Events.FrameAttached, frame);
|
2019-12-16 15:56:11 -08:00
|
|
|
return frame;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-03 11:23:24 -07:00
|
|
|
async waitForSignalsCreatedBy<T>(progress: Progress | null, noWaitAfter: boolean | undefined, action: () => Promise<T>, source?: 'input'): Promise<T> {
|
|
|
|
if (noWaitAfter)
|
2020-03-06 14:32:15 -08:00
|
|
|
return action();
|
2020-06-03 11:23:24 -07:00
|
|
|
const barrier = new SignalBarrier(progress);
|
2020-04-16 13:09:24 -07:00
|
|
|
this._signalBarriers.add(barrier);
|
2020-06-03 11:23:24 -07:00
|
|
|
if (progress)
|
2020-06-04 16:43:48 -07:00
|
|
|
progress.cleanupWhenAborted(() => this._signalBarriers.delete(barrier));
|
2020-06-03 11:23:24 -07:00
|
|
|
const result = await action();
|
2021-04-23 09:28:18 -07:00
|
|
|
if (source === 'input')
|
2020-06-03 11:23:24 -07:00
|
|
|
await this._page._delegate.inputActionEpilogue();
|
|
|
|
await barrier.waitFor();
|
|
|
|
this._signalBarriers.delete(barrier);
|
|
|
|
// Resolve in the next task, after all waitForNavigations.
|
2021-02-03 13:49:25 -08:00
|
|
|
await new Promise<void>(makeWaitForNextTask());
|
2020-06-03 11:23:24 -07:00
|
|
|
return result;
|
2020-03-04 19:15:01 -08:00
|
|
|
}
|
|
|
|
|
2020-03-05 14:47:04 -08:00
|
|
|
frameWillPotentiallyRequestNavigation() {
|
2020-04-16 13:09:24 -07:00
|
|
|
for (const barrier of this._signalBarriers)
|
2020-03-05 14:47:04 -08:00
|
|
|
barrier.retain();
|
2020-03-04 19:15:01 -08:00
|
|
|
}
|
|
|
|
|
2020-03-05 14:47:04 -08:00
|
|
|
frameDidPotentiallyRequestNavigation() {
|
2020-04-16 13:09:24 -07:00
|
|
|
for (const barrier of this._signalBarriers)
|
2020-03-05 14:47:04 -08:00
|
|
|
barrier.release();
|
|
|
|
}
|
|
|
|
|
2020-07-06 15:58:27 -07:00
|
|
|
frameRequestedNavigation(frameId: string, documentId?: string) {
|
2020-03-05 14:47:04 -08:00
|
|
|
const frame = this._frames.get(frameId);
|
|
|
|
if (!frame)
|
|
|
|
return;
|
2020-04-16 13:09:24 -07:00
|
|
|
for (const barrier of this._signalBarriers)
|
|
|
|
barrier.addFrameNavigation(frame);
|
2021-04-29 09:28:19 -07:00
|
|
|
if (frame.pendingDocument() && frame.pendingDocument()!.documentId === documentId) {
|
2020-07-06 15:58:27 -07:00
|
|
|
// Do not override request with undefined.
|
2020-04-09 19:03:06 -07:00
|
|
|
return;
|
2020-07-06 15:58:27 -07:00
|
|
|
}
|
2021-04-29 09:28:19 -07:00
|
|
|
frame.setPendingDocument({ documentId, request: undefined });
|
2020-03-04 19:15:01 -08:00
|
|
|
}
|
|
|
|
|
2019-12-16 15:56:11 -08:00
|
|
|
frameCommittedNewDocumentNavigation(frameId: string, url: string, name: string, documentId: string, initial: boolean) {
|
2020-01-13 13:33:25 -08:00
|
|
|
const frame = this._frames.get(frameId)!;
|
2020-04-06 15:09:43 -07:00
|
|
|
this.removeChildFramesRecursively(frame);
|
2020-10-26 22:20:43 -07:00
|
|
|
this.clearWebSockets(frame);
|
2019-12-16 15:56:11 -08:00
|
|
|
frame._url = url;
|
|
|
|
frame._name = name;
|
2020-08-11 08:59:00 -07:00
|
|
|
|
|
|
|
let keepPending: DocumentInfo | undefined;
|
2021-04-29 09:28:19 -07:00
|
|
|
const pendingDocument = frame.pendingDocument();
|
|
|
|
if (pendingDocument) {
|
|
|
|
if (pendingDocument.documentId === undefined) {
|
2020-08-11 08:59:00 -07:00
|
|
|
// Pending with unknown documentId - assume it is the one being committed.
|
2021-04-29 09:28:19 -07:00
|
|
|
pendingDocument.documentId = documentId;
|
2020-08-11 08:59:00 -07:00
|
|
|
}
|
2021-04-29 09:28:19 -07:00
|
|
|
if (pendingDocument.documentId === documentId) {
|
2020-08-11 08:59:00 -07:00
|
|
|
// Committing a pending document.
|
2021-04-29 09:28:19 -07:00
|
|
|
frame._currentDocument = pendingDocument;
|
2020-08-11 08:59:00 -07:00
|
|
|
} else {
|
|
|
|
// Sometimes, we already have a new pending when the old one commits.
|
|
|
|
// An example would be Chromium error page followed by a new navigation request,
|
|
|
|
// where the error page commit arrives after Network.requestWillBeSent for the
|
|
|
|
// new navigation.
|
|
|
|
// We commit, but keep the pending request since it's not done yet.
|
2021-04-29 09:28:19 -07:00
|
|
|
keepPending = pendingDocument;
|
2020-08-11 08:59:00 -07:00
|
|
|
frame._currentDocument = { documentId, request: undefined };
|
|
|
|
}
|
2021-04-29 09:28:19 -07:00
|
|
|
frame.setPendingDocument(undefined);
|
2020-08-11 08:59:00 -07:00
|
|
|
} else {
|
|
|
|
// No pending - just commit a new document.
|
2020-07-06 15:58:27 -07:00
|
|
|
frame._currentDocument = { documentId, request: undefined };
|
2020-08-11 08:59:00 -07:00
|
|
|
}
|
|
|
|
|
2020-07-06 17:33:56 -07:00
|
|
|
frame._onClearLifecycle();
|
2020-07-15 18:48:19 -07:00
|
|
|
const navigationEvent: NavigationEvent = { url, name, newDocument: frame._currentDocument };
|
2020-08-21 16:26:33 -07:00
|
|
|
frame.emit(Frame.Events.Navigation, navigationEvent);
|
2021-04-27 11:07:07 -07:00
|
|
|
this._responses.length = 0;
|
2020-07-07 15:22:05 -07:00
|
|
|
if (!initial) {
|
2020-08-17 14:12:31 -07:00
|
|
|
debugLogger.log('api', ` navigated to "${url}"`);
|
2021-01-13 14:25:42 -08:00
|
|
|
this._page.frameNavigatedToNewDocument(frame);
|
2020-07-07 15:22:05 -07:00
|
|
|
}
|
2020-08-11 08:59:00 -07:00
|
|
|
// Restore pending if any - see comments above about keepPending.
|
2021-04-29 09:28:19 -07:00
|
|
|
frame.setPendingDocument(keepPending);
|
2019-12-16 15:56:11 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
frameCommittedSameDocumentNavigation(frameId: string, url: string) {
|
|
|
|
const frame = this._frames.get(frameId);
|
|
|
|
if (!frame)
|
|
|
|
return;
|
|
|
|
frame._url = url;
|
2020-07-15 18:48:19 -07:00
|
|
|
const navigationEvent: NavigationEvent = { url, name: frame._name };
|
2020-08-21 16:26:33 -07:00
|
|
|
frame.emit(Frame.Events.Navigation, navigationEvent);
|
2020-08-17 14:12:31 -07:00
|
|
|
debugLogger.log('api', ` navigated to "${url}"`);
|
2019-12-16 15:56:11 -08:00
|
|
|
}
|
|
|
|
|
2020-07-06 15:58:27 -07:00
|
|
|
frameAbortedNavigation(frameId: string, errorText: string, documentId?: string) {
|
|
|
|
const frame = this._frames.get(frameId);
|
2021-04-29 09:28:19 -07:00
|
|
|
if (!frame || !frame.pendingDocument())
|
2020-07-06 15:58:27 -07:00
|
|
|
return;
|
2021-04-29 09:28:19 -07:00
|
|
|
if (documentId !== undefined && frame.pendingDocument()!.documentId !== documentId)
|
2020-07-06 15:58:27 -07:00
|
|
|
return;
|
2020-07-15 18:48:19 -07:00
|
|
|
const navigationEvent: NavigationEvent = {
|
|
|
|
url: frame._url,
|
|
|
|
name: frame._name,
|
2021-04-29 09:28:19 -07:00
|
|
|
newDocument: frame.pendingDocument(),
|
2020-07-15 18:48:19 -07:00
|
|
|
error: new Error(errorText),
|
|
|
|
};
|
2021-04-29 09:28:19 -07:00
|
|
|
frame.setPendingDocument(undefined);
|
2020-08-21 16:26:33 -07:00
|
|
|
frame.emit(Frame.Events.Navigation, navigationEvent);
|
2020-07-06 15:58:27 -07:00
|
|
|
}
|
|
|
|
|
2019-12-16 15:56:11 -08:00
|
|
|
frameDetached(frameId: string) {
|
|
|
|
const frame = this._frames.get(frameId);
|
|
|
|
if (frame)
|
|
|
|
this._removeFramesRecursively(frame);
|
|
|
|
}
|
|
|
|
|
|
|
|
frameStoppedLoading(frameId: string) {
|
2020-06-04 16:43:48 -07:00
|
|
|
this.frameLifecycleEvent(frameId, 'domcontentloaded');
|
|
|
|
this.frameLifecycleEvent(frameId, 'load');
|
2019-12-16 15:56:11 -08:00
|
|
|
}
|
|
|
|
|
2020-03-06 08:24:32 -08:00
|
|
|
frameLifecycleEvent(frameId: string, event: types.LifecycleEvent) {
|
2019-12-16 15:56:11 -08:00
|
|
|
const frame = this._frames.get(frameId);
|
2020-07-06 17:33:56 -07:00
|
|
|
if (frame)
|
|
|
|
frame._onLifecycleEvent(event);
|
2020-01-03 12:59:27 -08:00
|
|
|
}
|
|
|
|
|
2019-12-16 16:32:04 -08:00
|
|
|
requestStarted(request: network.Request) {
|
2020-07-06 15:58:27 -07:00
|
|
|
const frame = request.frame();
|
2020-01-03 12:59:27 -08:00
|
|
|
this._inflightRequestStarted(request);
|
2020-07-06 15:58:27 -07:00
|
|
|
if (request._documentId)
|
2021-04-29 09:28:19 -07:00
|
|
|
frame.setPendingDocument({ documentId: request._documentId, request });
|
2020-05-15 15:22:29 -07:00
|
|
|
if (request._isFavicon) {
|
|
|
|
const route = request._route();
|
|
|
|
if (route)
|
|
|
|
route.continue();
|
|
|
|
return;
|
|
|
|
}
|
2021-05-13 10:29:14 -07:00
|
|
|
this._page._browserContext.emit(BrowserContext.Events.Request, request);
|
2020-05-15 15:22:29 -07:00
|
|
|
this._page._requestStarted(request);
|
2019-12-16 16:32:04 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
requestReceivedResponse(response: network.Response) {
|
2021-04-27 11:07:07 -07:00
|
|
|
if (response.request()._isFavicon)
|
|
|
|
return;
|
|
|
|
this._responses.push(response);
|
2021-05-13 10:29:14 -07:00
|
|
|
this._page._browserContext.emit(BrowserContext.Events.Response, response);
|
2019-12-16 16:32:04 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
requestFinished(request: network.Request) {
|
2020-01-03 12:59:27 -08:00
|
|
|
this._inflightRequestFinished(request);
|
2021-05-13 10:29:14 -07:00
|
|
|
if (request._isFavicon)
|
|
|
|
return;
|
|
|
|
this._page._browserContext.emit(BrowserContext.Events.RequestFinished, request);
|
2019-12-16 16:32:04 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
requestFailed(request: network.Request, canceled: boolean) {
|
2020-07-06 15:58:27 -07:00
|
|
|
const frame = request.frame();
|
2020-01-03 12:59:27 -08:00
|
|
|
this._inflightRequestFinished(request);
|
2021-04-29 09:28:19 -07:00
|
|
|
if (frame.pendingDocument() && frame.pendingDocument()!.request === request) {
|
2020-07-06 15:58:27 -07:00
|
|
|
let errorText = request.failure()!.errorText;
|
|
|
|
if (canceled)
|
|
|
|
errorText += '; maybe frame was detached?';
|
2021-04-29 09:28:19 -07:00
|
|
|
this.frameAbortedNavigation(frame._id, errorText, frame.pendingDocument()!.documentId);
|
2019-12-16 16:32:04 -08:00
|
|
|
}
|
2021-05-13 10:29:14 -07:00
|
|
|
if (request._isFavicon)
|
|
|
|
return;
|
|
|
|
this._page._browserContext.emit(BrowserContext.Events.RequestFailed, request);
|
2019-12-16 16:32:04 -08:00
|
|
|
}
|
|
|
|
|
2020-04-06 15:09:43 -07:00
|
|
|
removeChildFramesRecursively(frame: Frame) {
|
2019-12-16 15:56:11 -08:00
|
|
|
for (const child of frame.childFrames())
|
|
|
|
this._removeFramesRecursively(child);
|
2020-04-06 15:09:43 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
private _removeFramesRecursively(frame: Frame) {
|
|
|
|
this.removeChildFramesRecursively(frame);
|
2019-12-16 15:56:11 -08:00
|
|
|
frame._onDetached();
|
|
|
|
this._frames.delete(frame._id);
|
2020-11-02 13:06:54 -08:00
|
|
|
if (!this._page.isClosed())
|
|
|
|
this._page.emit(Page.Events.FrameDetached, frame);
|
2019-12-16 15:56:11 -08:00
|
|
|
}
|
2019-12-16 16:32:04 -08:00
|
|
|
|
2020-01-03 12:59:27 -08:00
|
|
|
private _inflightRequestFinished(request: network.Request) {
|
|
|
|
const frame = request.frame();
|
2020-03-10 11:39:35 -07:00
|
|
|
if (request._isFavicon)
|
2020-01-13 13:33:25 -08:00
|
|
|
return;
|
2020-01-03 12:59:27 -08:00
|
|
|
if (!frame._inflightRequests.has(request))
|
|
|
|
return;
|
|
|
|
frame._inflightRequests.delete(request);
|
|
|
|
if (frame._inflightRequests.size === 0)
|
2020-04-20 16:52:26 -07:00
|
|
|
frame._startNetworkIdleTimer();
|
2019-12-16 16:32:04 -08:00
|
|
|
}
|
|
|
|
|
2020-01-03 12:59:27 -08:00
|
|
|
private _inflightRequestStarted(request: network.Request) {
|
|
|
|
const frame = request.frame();
|
2020-03-10 11:39:35 -07:00
|
|
|
if (request._isFavicon)
|
2020-01-13 13:33:25 -08:00
|
|
|
return;
|
2020-01-03 12:59:27 -08:00
|
|
|
frame._inflightRequests.add(request);
|
|
|
|
if (frame._inflightRequests.size === 1)
|
2020-04-20 16:52:26 -07:00
|
|
|
frame._stopNetworkIdleTimer();
|
2019-12-16 16:32:04 -08:00
|
|
|
}
|
2020-01-27 16:51:52 -08:00
|
|
|
|
|
|
|
interceptConsoleMessage(message: ConsoleMessage): boolean {
|
|
|
|
if (message.type() !== 'debug')
|
|
|
|
return false;
|
|
|
|
const tag = message.text();
|
|
|
|
const handler = this._consoleMessageTags.get(tag);
|
|
|
|
if (!handler)
|
|
|
|
return false;
|
|
|
|
this._consoleMessageTags.delete(tag);
|
|
|
|
handler();
|
|
|
|
return true;
|
|
|
|
}
|
2020-10-26 22:20:43 -07:00
|
|
|
|
|
|
|
clearWebSockets(frame: Frame) {
|
|
|
|
// TODO: attribute sockets to frames.
|
|
|
|
if (frame.parentFrame())
|
|
|
|
return;
|
|
|
|
this._webSockets.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
onWebSocketCreated(requestId: string, url: string) {
|
2021-02-09 09:00:00 -08:00
|
|
|
const ws = new network.WebSocket(this._page, url);
|
2020-10-26 22:20:43 -07:00
|
|
|
this._webSockets.set(requestId, ws);
|
|
|
|
}
|
|
|
|
|
|
|
|
onWebSocketRequest(requestId: string) {
|
|
|
|
const ws = this._webSockets.get(requestId);
|
|
|
|
if (ws)
|
|
|
|
this._page.emit(Page.Events.WebSocket, ws);
|
|
|
|
}
|
|
|
|
|
|
|
|
onWebSocketResponse(requestId: string, status: number, statusText: string) {
|
|
|
|
const ws = this._webSockets.get(requestId);
|
2020-11-02 14:09:58 -08:00
|
|
|
if (status < 400)
|
2020-10-26 22:20:43 -07:00
|
|
|
return;
|
|
|
|
if (ws)
|
|
|
|
ws.error(`${statusText}: ${status}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
onWebSocketFrameSent(requestId: string, opcode: number, data: string) {
|
|
|
|
const ws = this._webSockets.get(requestId);
|
|
|
|
if (ws)
|
|
|
|
ws.frameSent(opcode, data);
|
|
|
|
}
|
|
|
|
|
|
|
|
webSocketFrameReceived(requestId: string, opcode: number, data: string) {
|
|
|
|
const ws = this._webSockets.get(requestId);
|
|
|
|
if (ws)
|
|
|
|
ws.frameReceived(opcode, data);
|
|
|
|
}
|
|
|
|
|
|
|
|
webSocketClosed(requestId: string) {
|
|
|
|
const ws = this._webSockets.get(requestId);
|
|
|
|
if (ws)
|
|
|
|
ws.closed();
|
|
|
|
this._webSockets.delete(requestId);
|
|
|
|
}
|
|
|
|
|
|
|
|
webSocketError(requestId: string, errorMessage: string): void {
|
|
|
|
const ws = this._webSockets.get(requestId);
|
|
|
|
if (ws)
|
|
|
|
ws.error(errorMessage);
|
|
|
|
}
|
2019-12-16 15:56:11 -08:00
|
|
|
}
|
|
|
|
|
2021-02-09 09:00:00 -08:00
|
|
|
export class Frame extends SdkObject {
|
2020-08-21 16:26:33 -07:00
|
|
|
static Events = {
|
|
|
|
Navigation: 'navigation',
|
|
|
|
AddLifecycle: 'addlifecycle',
|
|
|
|
RemoveLifecycle: 'removelifecycle',
|
|
|
|
};
|
|
|
|
|
2019-12-13 10:52:33 -08:00
|
|
|
_id: string;
|
2020-07-06 17:33:56 -07:00
|
|
|
private _firedLifecycleEvents = new Set<types.LifecycleEvent>();
|
|
|
|
_subtreeLifecycleEvents = new Set<types.LifecycleEvent>();
|
2020-07-06 15:58:27 -07:00
|
|
|
_currentDocument: DocumentInfo;
|
2021-04-29 09:28:19 -07:00
|
|
|
private _pendingDocument: DocumentInfo | undefined;
|
2019-12-11 07:17:32 -08:00
|
|
|
readonly _page: Page;
|
2020-01-13 13:33:25 -08:00
|
|
|
private _parentFrame: Frame | null;
|
2019-12-16 15:56:11 -08:00
|
|
|
_url = '';
|
2019-11-18 18:18:28 -08:00
|
|
|
private _detached = false;
|
2020-06-24 17:03:28 -07:00
|
|
|
private _contextData = new Map<types.World, ContextData>();
|
2019-11-27 16:02:31 -08:00
|
|
|
private _childFrames = new Set<Frame>();
|
2020-01-13 13:33:25 -08:00
|
|
|
_name = '';
|
2020-01-03 12:59:27 -08:00
|
|
|
_inflightRequests = new Set<network.Request>();
|
2020-04-20 16:52:26 -07:00
|
|
|
private _networkIdleTimer: NodeJS.Timer | undefined;
|
2020-01-27 16:51:52 -08:00
|
|
|
private _setContentCounter = 0;
|
2020-03-18 20:05:35 -07:00
|
|
|
readonly _detachedPromise: Promise<void>;
|
2020-02-10 18:35:47 -08:00
|
|
|
private _detachedCallback = () => {};
|
2021-04-29 09:28:19 -07:00
|
|
|
private _nonStallingEvaluations = new Set<(error: Error) => void>();
|
2019-11-18 18:18:28 -08:00
|
|
|
|
2019-12-13 10:52:33 -08:00
|
|
|
constructor(page: Page, id: string, parentFrame: Frame | null) {
|
2021-04-20 23:03:56 -07:00
|
|
|
super(page, 'frame');
|
2021-02-09 09:00:00 -08:00
|
|
|
this.attribution.frame = this;
|
2019-12-13 10:52:33 -08:00
|
|
|
this._id = id;
|
2019-12-11 07:17:32 -08:00
|
|
|
this._page = page;
|
2019-11-18 18:18:28 -08:00
|
|
|
this._parentFrame = parentFrame;
|
2020-07-06 15:58:27 -07:00
|
|
|
this._currentDocument = { documentId: undefined, request: undefined };
|
2019-11-18 18:18:28 -08:00
|
|
|
|
2020-02-10 18:35:47 -08:00
|
|
|
this._detachedPromise = new Promise<void>(x => this._detachedCallback = x);
|
|
|
|
|
2019-12-12 21:11:52 -08:00
|
|
|
this._contextData.set('main', { contextPromise: new Promise(() => {}), contextResolveCallback: () => {}, context: null, rerunnableTasks: new Set() });
|
|
|
|
this._contextData.set('utility', { contextPromise: new Promise(() => {}), contextResolveCallback: () => {}, context: null, rerunnableTasks: new Set() });
|
2019-11-26 15:37:25 -08:00
|
|
|
this._setContext('main', null);
|
|
|
|
this._setContext('utility', null);
|
2019-11-18 18:18:28 -08:00
|
|
|
|
|
|
|
if (this._parentFrame)
|
|
|
|
this._parentFrame._childFrames.add(this);
|
|
|
|
}
|
|
|
|
|
2020-07-06 17:33:56 -07:00
|
|
|
_onLifecycleEvent(event: types.LifecycleEvent) {
|
|
|
|
if (this._firedLifecycleEvents.has(event))
|
|
|
|
return;
|
|
|
|
this._firedLifecycleEvents.add(event);
|
|
|
|
// Recalculate subtree lifecycle for the whole tree - it should not be that big.
|
|
|
|
this._page.mainFrame()._recalculateLifecycle();
|
|
|
|
}
|
|
|
|
|
|
|
|
_onClearLifecycle() {
|
|
|
|
this._firedLifecycleEvents.clear();
|
|
|
|
// Recalculate subtree lifecycle for the whole tree - it should not be that big.
|
|
|
|
this._page.mainFrame()._recalculateLifecycle();
|
|
|
|
// Keep the current navigation request if any.
|
|
|
|
this._inflightRequests = new Set(Array.from(this._inflightRequests).filter(request => request === this._currentDocument.request));
|
|
|
|
this._stopNetworkIdleTimer();
|
|
|
|
if (this._inflightRequests.size === 0)
|
|
|
|
this._startNetworkIdleTimer();
|
|
|
|
}
|
|
|
|
|
2021-04-29 09:28:19 -07:00
|
|
|
setPendingDocument(documentInfo: DocumentInfo | undefined) {
|
|
|
|
this._pendingDocument = documentInfo;
|
|
|
|
if (documentInfo)
|
|
|
|
this._invalidateNonStallingEvaluations();
|
|
|
|
}
|
|
|
|
|
|
|
|
pendingDocument(): DocumentInfo | undefined {
|
|
|
|
return this._pendingDocument;
|
|
|
|
}
|
|
|
|
|
|
|
|
private async _invalidateNonStallingEvaluations() {
|
|
|
|
if (!this._nonStallingEvaluations)
|
|
|
|
return;
|
|
|
|
const error = new Error('Navigation interrupted the evaluation');
|
|
|
|
for (const callback of this._nonStallingEvaluations)
|
|
|
|
callback(error);
|
|
|
|
}
|
|
|
|
|
|
|
|
async nonStallingRawEvaluateInExistingMainContext(expression: string): Promise<any> {
|
|
|
|
if (this._pendingDocument)
|
|
|
|
throw new Error('Frame is currently attempting a navigation');
|
|
|
|
const context = this._existingMainContext();
|
|
|
|
if (!context)
|
|
|
|
throw new Error('Frame does not yet have a main execution context');
|
|
|
|
|
|
|
|
let callback = () => {};
|
|
|
|
const frameInvalidated = new Promise<void>((f, r) => callback = r);
|
|
|
|
this._nonStallingEvaluations.add(callback);
|
|
|
|
try {
|
|
|
|
return await Promise.race([
|
|
|
|
context.rawEvaluateJSON(expression),
|
|
|
|
frameInvalidated
|
|
|
|
]);
|
|
|
|
} finally {
|
|
|
|
this._nonStallingEvaluations.delete(callback);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-07 10:27:34 -07:00
|
|
|
async nonStallingEvaluateInExistingContext(expression: string, isFunction: boolean|undefined, world: types.World): Promise<any> {
|
|
|
|
if (this._pendingDocument)
|
|
|
|
throw new Error('Frame is currently attempting a navigation');
|
|
|
|
const context = this._contextData.get(world)?.context;
|
|
|
|
if (!context)
|
|
|
|
throw new Error('Frame does not yet have the execution context');
|
|
|
|
|
|
|
|
let callback = () => {};
|
|
|
|
const frameInvalidated = new Promise<void>((f, r) => callback = r);
|
|
|
|
this._nonStallingEvaluations.add(callback);
|
|
|
|
try {
|
|
|
|
return await Promise.race([
|
2021-07-09 16:19:42 +02:00
|
|
|
context.evaluateExpression(expression, isFunction),
|
2021-06-07 10:27:34 -07:00
|
|
|
frameInvalidated
|
|
|
|
]);
|
|
|
|
} finally {
|
|
|
|
this._nonStallingEvaluations.delete(callback);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-06 17:33:56 -07:00
|
|
|
private _recalculateLifecycle() {
|
|
|
|
const events = new Set<types.LifecycleEvent>(this._firedLifecycleEvents);
|
|
|
|
for (const child of this._childFrames) {
|
|
|
|
child._recalculateLifecycle();
|
|
|
|
// We require a particular lifecycle event to be fired in the whole
|
|
|
|
// frame subtree, and then consider it done.
|
|
|
|
for (const event of events) {
|
|
|
|
if (!child._subtreeLifecycleEvents.has(event))
|
|
|
|
events.delete(event);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const mainFrame = this._page.mainFrame();
|
|
|
|
for (const event of events) {
|
|
|
|
// Checking whether we have already notified about this event.
|
|
|
|
if (!this._subtreeLifecycleEvents.has(event)) {
|
2020-08-21 16:26:33 -07:00
|
|
|
this.emit(Frame.Events.AddLifecycle, event);
|
2020-07-07 15:22:05 -07:00
|
|
|
if (this === mainFrame && this._url !== 'about:blank')
|
2020-08-17 14:12:31 -07:00
|
|
|
debugLogger.log('api', ` "${event}" event fired`);
|
2020-07-06 17:33:56 -07:00
|
|
|
if (this === mainFrame && event === 'load')
|
2020-08-21 16:26:33 -07:00
|
|
|
this._page.emit(Page.Events.Load);
|
2020-07-06 17:33:56 -07:00
|
|
|
if (this === mainFrame && event === 'domcontentloaded')
|
2020-08-21 16:26:33 -07:00
|
|
|
this._page.emit(Page.Events.DOMContentLoaded);
|
2020-07-06 17:33:56 -07:00
|
|
|
}
|
|
|
|
}
|
2020-07-13 16:03:24 -07:00
|
|
|
for (const event of this._subtreeLifecycleEvents) {
|
|
|
|
if (!events.has(event))
|
2020-08-21 16:26:33 -07:00
|
|
|
this.emit(Frame.Events.RemoveLifecycle, event);
|
2020-07-13 16:03:24 -07:00
|
|
|
}
|
2020-07-06 17:33:56 -07:00
|
|
|
this._subtreeLifecycleEvents = events;
|
|
|
|
}
|
|
|
|
|
2021-02-25 10:00:54 -08:00
|
|
|
async raceNavigationAction<T>(action: () => Promise<T>): Promise<T> {
|
|
|
|
return Promise.race([
|
|
|
|
this._page._disconnectedPromise.then(() => { throw new Error('Navigation failed because page was closed!'); }),
|
|
|
|
this._page._crashedPromise.then(() => { throw new Error('Navigation failed because page crashed!'); }),
|
|
|
|
this._detachedPromise.then(() => { throw new Error('Navigating frame was detached!'); }),
|
|
|
|
action(),
|
|
|
|
]);
|
2020-09-14 16:43:17 -07:00
|
|
|
}
|
|
|
|
|
2021-02-09 14:44:48 -08:00
|
|
|
async goto(metadata: CallMetadata, url: string, options: types.GotoOptions = {}): Promise<network.Response | null> {
|
2021-07-06 21:16:37 +02:00
|
|
|
const constructedNavigationURL = constructURLBasedOnBaseURL(this._page._browserContext._options.baseURL, url);
|
2021-02-25 10:00:54 -08:00
|
|
|
const controller = new ProgressController(metadata, this);
|
2021-07-06 21:16:37 +02:00
|
|
|
return controller.run(progress => this._goto(progress, constructedNavigationURL, options), this._page._timeoutSettings.navigationTimeout(options));
|
2021-02-25 10:00:54 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
private async _goto(progress: Progress, url: string, options: types.GotoOptions): Promise<network.Response | null> {
|
|
|
|
return this.raceNavigationAction(async () => {
|
2020-07-21 15:25:31 -07:00
|
|
|
const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
|
2020-08-17 14:12:31 -07:00
|
|
|
progress.log(`navigating to "${url}", waiting until "${waitUntil}"`);
|
2020-08-18 15:38:29 -07:00
|
|
|
const headers = this._page._state.extraHTTPHeaders || [];
|
2020-08-18 17:32:11 -07:00
|
|
|
const refererHeader = headers.find(h => h.name.toLowerCase() === 'referer');
|
2020-08-18 15:38:29 -07:00
|
|
|
let referer = refererHeader ? refererHeader.value : undefined;
|
2020-06-04 16:43:48 -07:00
|
|
|
if (options.referer !== undefined) {
|
|
|
|
if (referer !== undefined && referer !== options.referer)
|
|
|
|
throw new Error('"referer" is already specified as extra HTTP header');
|
|
|
|
referer = options.referer;
|
|
|
|
}
|
|
|
|
url = helper.completeUserURL(url);
|
|
|
|
|
2020-08-21 16:26:33 -07:00
|
|
|
const sameDocument = helper.waitForEvent(progress, this, Frame.Events.Navigation, (e: NavigationEvent) => !e.newDocument);
|
2020-07-07 15:22:05 -07:00
|
|
|
const navigateResult = await this._page._delegate.navigateFrame(this, url, referer);
|
|
|
|
|
|
|
|
let event: NavigationEvent;
|
2020-06-04 16:43:48 -07:00
|
|
|
if (navigateResult.newDocumentId) {
|
2020-07-07 15:22:05 -07:00
|
|
|
sameDocument.dispose();
|
2020-08-21 16:26:33 -07:00
|
|
|
event = await helper.waitForEvent(progress, this, Frame.Events.Navigation, (event: NavigationEvent) => {
|
2020-07-07 15:22:05 -07:00
|
|
|
// We are interested either in this specific document, or any other document that
|
|
|
|
// did commit and replaced the expected document.
|
2020-07-15 18:48:19 -07:00
|
|
|
return event.newDocument && (event.newDocument.documentId === navigateResult.newDocumentId || !event.error);
|
2020-07-07 15:22:05 -07:00
|
|
|
}).promise;
|
|
|
|
|
2020-07-15 18:48:19 -07:00
|
|
|
if (event.newDocument!.documentId !== navigateResult.newDocumentId) {
|
2020-07-07 15:22:05 -07:00
|
|
|
// This is just a sanity check. In practice, new navigation should
|
|
|
|
// cancel the previous one and report "request cancelled"-like error.
|
|
|
|
throw new Error('Navigation interrupted by another one');
|
|
|
|
}
|
|
|
|
if (event.error)
|
|
|
|
throw event.error;
|
2020-06-04 16:43:48 -07:00
|
|
|
} else {
|
2020-07-07 15:22:05 -07:00
|
|
|
event = await sameDocument.promise;
|
2020-06-04 16:43:48 -07:00
|
|
|
}
|
2020-07-07 15:22:05 -07:00
|
|
|
|
|
|
|
if (!this._subtreeLifecycleEvents.has(waitUntil))
|
2020-08-21 16:26:33 -07:00
|
|
|
await helper.waitForEvent(progress, this, Frame.Events.AddLifecycle, (e: types.LifecycleEvent) => e === waitUntil).promise;
|
2020-07-07 15:22:05 -07:00
|
|
|
|
2020-07-15 18:48:19 -07:00
|
|
|
const request = event.newDocument ? event.newDocument.request : undefined;
|
2020-08-18 19:13:40 -07:00
|
|
|
const response = request ? request._finalRequest().response() : null;
|
|
|
|
await this._page._doSlowMo();
|
|
|
|
return response;
|
2021-02-25 10:00:54 -08:00
|
|
|
});
|
2020-02-10 18:35:47 -08:00
|
|
|
}
|
|
|
|
|
2020-09-14 16:43:17 -07:00
|
|
|
async _waitForNavigation(progress: Progress, options: types.NavigateOptions): Promise<network.Response | null> {
|
|
|
|
const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
|
|
|
|
progress.log(`waiting for navigation until "${waitUntil}"`);
|
2020-07-07 15:22:05 -07:00
|
|
|
|
2020-09-14 16:43:17 -07:00
|
|
|
const navigationEvent: NavigationEvent = await helper.waitForEvent(progress, this, Frame.Events.Navigation, (event: NavigationEvent) => {
|
|
|
|
// Any failed navigation results in a rejection.
|
|
|
|
if (event.error)
|
2020-08-19 13:27:58 -07:00
|
|
|
return true;
|
2020-09-14 16:43:17 -07:00
|
|
|
progress.log(` navigated to "${this._url}"`);
|
|
|
|
return true;
|
|
|
|
}).promise;
|
|
|
|
if (navigationEvent.error)
|
|
|
|
throw navigationEvent.error;
|
2020-07-07 15:22:05 -07:00
|
|
|
|
2020-09-14 16:43:17 -07:00
|
|
|
if (!this._subtreeLifecycleEvents.has(waitUntil))
|
|
|
|
await helper.waitForEvent(progress, this, Frame.Events.AddLifecycle, (e: types.LifecycleEvent) => e === waitUntil).promise;
|
2019-11-18 18:18:28 -08:00
|
|
|
|
2020-09-14 16:43:17 -07:00
|
|
|
const request = navigationEvent.newDocument ? navigationEvent.newDocument.request : undefined;
|
|
|
|
return request ? request._finalRequest().response() : null;
|
2020-06-04 16:43:48 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
async _waitForLoadState(progress: Progress, state: types.LifecycleEvent): Promise<void> {
|
2020-07-21 15:25:31 -07:00
|
|
|
const waitUntil = verifyLifecycle('state', state);
|
2020-07-07 15:22:05 -07:00
|
|
|
if (!this._subtreeLifecycleEvents.has(waitUntil))
|
2020-08-21 16:26:33 -07:00
|
|
|
await helper.waitForEvent(progress, this, Frame.Events.AddLifecycle, (e: types.LifecycleEvent) => e === waitUntil).promise;
|
2019-12-20 15:32:30 -08:00
|
|
|
}
|
|
|
|
|
2020-02-05 17:20:23 -08:00
|
|
|
async frameElement(): Promise<dom.ElementHandle> {
|
|
|
|
return this._page._delegate.getFrameElement(this);
|
|
|
|
}
|
|
|
|
|
2020-06-24 17:03:28 -07:00
|
|
|
_context(world: types.World): Promise<dom.FrameExecutionContext> {
|
2019-11-26 15:37:25 -08:00
|
|
|
if (this._detached)
|
|
|
|
throw new Error(`Execution Context is not available in detached frame "${this.url()}" (are you trying to evaluate?)`);
|
2020-06-24 17:03:28 -07:00
|
|
|
return this._contextData.get(world)!.contextPromise;
|
2019-12-17 14:30:02 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
_mainContext(): Promise<dom.FrameExecutionContext> {
|
|
|
|
return this._context('main');
|
2019-11-26 15:37:25 -08:00
|
|
|
}
|
|
|
|
|
2021-04-29 09:28:19 -07:00
|
|
|
private _existingMainContext(): dom.FrameExecutionContext | null {
|
2021-03-01 12:20:04 -08:00
|
|
|
return this._contextData.get('main')?.context || null;
|
|
|
|
}
|
|
|
|
|
2019-12-12 21:11:52 -08:00
|
|
|
_utilityContext(): Promise<dom.FrameExecutionContext> {
|
2019-12-17 14:30:02 -08:00
|
|
|
return this._context('utility');
|
2019-11-28 12:50:52 -08:00
|
|
|
}
|
|
|
|
|
2021-07-09 16:19:42 +02:00
|
|
|
async evaluateExpressionHandleAndWaitForSignals(expression: string, isFunction: boolean | undefined, arg: any, world: types.World = 'main'): Promise<any> {
|
2020-09-16 16:07:49 -07:00
|
|
|
const context = await this._context(world);
|
2021-07-09 16:19:42 +02:00
|
|
|
const handle = await context.evaluateExpressionHandleAndWaitForSignals(expression, isFunction, arg);
|
|
|
|
if (world === 'main')
|
|
|
|
await this._page._doSlowMo();
|
|
|
|
return handle;
|
|
|
|
}
|
|
|
|
|
|
|
|
async evaluateExpression(expression: string, isFunction: boolean | undefined, arg: any, world: types.World = 'main'): Promise<any> {
|
|
|
|
const context = await this._context(world);
|
|
|
|
const value = await context.evaluateExpression(expression, isFunction, arg);
|
|
|
|
if (world === 'main')
|
|
|
|
await this._page._doSlowMo();
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
|
|
|
async evaluateExpressionAndWaitForSignals(expression: string, isFunction: boolean | undefined, arg: any, world: types.World = 'main'): Promise<any> {
|
|
|
|
const context = await this._context(world);
|
|
|
|
const value = await context.evaluateExpressionAndWaitForSignals(expression, isFunction, arg);
|
2020-09-16 16:07:49 -07:00
|
|
|
if (world === 'main')
|
|
|
|
await this._page._doSlowMo();
|
2020-08-18 19:13:40 -07:00
|
|
|
return value;
|
2020-06-25 08:30:56 -07:00
|
|
|
}
|
|
|
|
|
2021-07-26 15:46:51 -07:00
|
|
|
async querySelector(selector: string): Promise<dom.ElementHandle<Element> | null> {
|
2021-06-30 23:47:09 +05:30
|
|
|
debugLogger.log('api', ` finding element using the selector "${selector}"`);
|
2020-09-02 16:15:43 -07:00
|
|
|
return this._page.selectors._query(this, selector);
|
2019-12-18 14:28:16 -08:00
|
|
|
}
|
|
|
|
|
2021-02-09 14:44:48 -08:00
|
|
|
async waitForSelector(metadata: CallMetadata, selector: string, options: types.WaitForElementOptions = {}): Promise<dom.ElementHandle<Element> | null> {
|
|
|
|
const controller = new ProgressController(metadata, this);
|
2020-06-01 08:54:18 -07:00
|
|
|
if ((options as any).visibility)
|
2020-05-04 11:03:44 -07:00
|
|
|
throw new Error('options.visibility is not supported, did you mean options.state?');
|
2020-06-01 08:54:18 -07:00
|
|
|
if ((options as any).waitFor && (options as any).waitFor !== 'visible')
|
2020-05-04 11:03:44 -07:00
|
|
|
throw new Error('options.waitFor is not supported, did you mean options.state?');
|
2020-06-01 08:54:18 -07:00
|
|
|
const { state = 'visible' } = options;
|
2020-05-04 11:03:44 -07:00
|
|
|
if (!['attached', 'detached', 'visible', 'hidden'].includes(state))
|
2020-07-21 15:25:31 -07:00
|
|
|
throw new Error(`state: expected one of (attached|detached|visible|hidden)`);
|
2020-09-02 16:15:43 -07:00
|
|
|
const info = this._page.selectors._parseSelector(selector);
|
2020-06-26 16:32:42 -07:00
|
|
|
const task = dom.waitForSelectorTask(info, state);
|
2021-02-09 14:44:48 -08:00
|
|
|
return controller.run(async progress => {
|
2020-08-17 14:12:31 -07:00
|
|
|
progress.log(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`);
|
2021-06-02 20:17:24 -07:00
|
|
|
while (progress.isRunning()) {
|
|
|
|
const result = await this._scheduleRerunnableHandleTask(progress, info.world, task);
|
|
|
|
if (!result.asElement()) {
|
|
|
|
result.dispose();
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
if ((options as any).__testHookBeforeAdoptNode)
|
|
|
|
await (options as any).__testHookBeforeAdoptNode();
|
|
|
|
try {
|
|
|
|
const handle = result.asElement() as dom.ElementHandle<Element>;
|
|
|
|
const adopted = await handle._adoptTo(await this._mainContext());
|
|
|
|
return adopted;
|
|
|
|
} catch (e) {
|
|
|
|
// Navigated while trying to adopt the node.
|
|
|
|
if (!js.isContextDestroyedError(e) && !e.message.includes(dom.kUnableToAdoptErrorMessage))
|
|
|
|
throw e;
|
|
|
|
result.dispose();
|
|
|
|
}
|
2020-06-01 08:54:18 -07:00
|
|
|
}
|
2021-06-02 20:17:24 -07:00
|
|
|
return null;
|
2020-08-14 18:25:32 -07:00
|
|
|
}, this._page._timeoutSettings.timeout(options));
|
2019-11-18 18:18:28 -08:00
|
|
|
}
|
|
|
|
|
2021-02-09 14:44:48 -08:00
|
|
|
async dispatchEvent(metadata: CallMetadata, selector: string, type: string, eventInit?: Object, options: types.TimeoutOptions = {}): Promise<void> {
|
|
|
|
const controller = new ProgressController(metadata, this);
|
2020-09-02 16:15:43 -07:00
|
|
|
const info = this._page.selectors._parseSelector(selector);
|
2020-06-26 16:32:42 -07:00
|
|
|
const task = dom.dispatchEventTask(info, type, eventInit || {});
|
2021-02-09 14:44:48 -08:00
|
|
|
await controller.run(async progress => {
|
2020-08-17 14:12:31 -07:00
|
|
|
progress.log(`Dispatching "${type}" event on selector "${selector}"...`);
|
2020-06-26 16:32:42 -07:00
|
|
|
// Note: we always dispatch events in the main world.
|
|
|
|
await this._scheduleRerunnableTask(progress, 'main', task);
|
2020-08-14 18:25:32 -07:00
|
|
|
}, this._page._timeoutSettings.timeout(options));
|
2020-08-18 19:13:40 -07:00
|
|
|
await this._page._doSlowMo();
|
2020-04-23 14:58:37 -07:00
|
|
|
}
|
|
|
|
|
2021-03-18 03:03:21 +08:00
|
|
|
async evalOnSelectorAndWaitForSignals(selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
|
2021-07-26 15:46:51 -07:00
|
|
|
const handle = await this.querySelector(selector);
|
2020-03-25 14:08:46 -07:00
|
|
|
if (!handle)
|
2019-12-17 14:30:02 -08:00
|
|
|
throw new Error(`Error: failed to find element matching selector "${selector}"`);
|
2021-03-18 03:03:21 +08:00
|
|
|
const result = await handle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg);
|
2020-03-25 14:08:46 -07:00
|
|
|
handle.dispose();
|
2019-12-17 14:30:02 -08:00
|
|
|
return result;
|
2019-11-18 18:18:28 -08:00
|
|
|
}
|
|
|
|
|
2021-03-18 03:03:21 +08:00
|
|
|
async evalOnSelectorAllAndWaitForSignals(selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
|
2020-09-02 16:15:43 -07:00
|
|
|
const arrayHandle = await this._page.selectors._queryArray(this, selector);
|
2021-03-18 03:03:21 +08:00
|
|
|
const result = await arrayHandle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg);
|
2020-03-04 17:57:35 -08:00
|
|
|
arrayHandle.dispose();
|
2019-12-17 14:30:02 -08:00
|
|
|
return result;
|
2019-11-18 18:18:28 -08:00
|
|
|
}
|
|
|
|
|
2021-07-26 15:46:51 -07:00
|
|
|
async querySelectorAll(selector: string): Promise<dom.ElementHandle<Element>[]> {
|
2020-09-14 10:38:14 -07:00
|
|
|
return this._page.selectors._queryAll(this, selector, undefined, true /* adoptToMain */);
|
2019-11-18 18:18:28 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
async content(): Promise<string> {
|
2019-11-26 15:37:25 -08:00
|
|
|
const context = await this._utilityContext();
|
2021-03-18 01:47:07 +08:00
|
|
|
return context.evaluate(() => {
|
2019-11-26 08:57:53 -08:00
|
|
|
let retVal = '';
|
|
|
|
if (document.doctype)
|
|
|
|
retVal = new XMLSerializer().serializeToString(document.doctype);
|
|
|
|
if (document.documentElement)
|
|
|
|
retVal += document.documentElement.outerHTML;
|
|
|
|
return retVal;
|
|
|
|
});
|
2019-11-18 18:18:28 -08:00
|
|
|
}
|
|
|
|
|
2021-02-09 14:44:48 -08:00
|
|
|
async setContent(metadata: CallMetadata, html: string, options: types.NavigateOptions = {}): Promise<void> {
|
2021-02-25 10:00:54 -08:00
|
|
|
const controller = new ProgressController(metadata, this);
|
|
|
|
return controller.run(progress => this.raceNavigationAction(async () => {
|
2020-06-04 16:43:48 -07:00
|
|
|
const waitUntil = options.waitUntil === undefined ? 'load' : options.waitUntil;
|
2020-08-17 14:12:31 -07:00
|
|
|
progress.log(`setting frame content, waiting until "${waitUntil}"`);
|
2020-06-04 16:43:48 -07:00
|
|
|
const tag = `--playwright--set--content--${this._id}--${++this._setContentCounter}--`;
|
|
|
|
const context = await this._utilityContext();
|
|
|
|
const lifecyclePromise = new Promise((resolve, reject) => {
|
|
|
|
this._page._frameManager._consoleMessageTags.set(tag, () => {
|
|
|
|
// Clear lifecycle right after document.open() - see 'tag' below.
|
2020-07-06 17:33:56 -07:00
|
|
|
this._onClearLifecycle();
|
2020-06-04 16:43:48 -07:00
|
|
|
this._waitForLoadState(progress, waitUntil).then(resolve).catch(reject);
|
|
|
|
});
|
2020-02-10 18:35:47 -08:00
|
|
|
});
|
2021-03-18 01:47:07 +08:00
|
|
|
const contentPromise = context.evaluate(({ html, tag }) => {
|
2020-06-04 16:43:48 -07:00
|
|
|
window.stop();
|
|
|
|
document.open();
|
|
|
|
console.debug(tag); // eslint-disable-line no-console
|
|
|
|
document.write(html);
|
|
|
|
document.close();
|
|
|
|
}, { html, tag });
|
|
|
|
await Promise.all([contentPromise, lifecyclePromise]);
|
2020-08-18 19:13:40 -07:00
|
|
|
await this._page._doSlowMo();
|
2021-02-25 10:00:54 -08:00
|
|
|
}), this._page._timeoutSettings.navigationTimeout(options));
|
2019-11-18 18:18:28 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
name(): string {
|
|
|
|
return this._name || '';
|
|
|
|
}
|
|
|
|
|
|
|
|
url(): string {
|
|
|
|
return this._url;
|
|
|
|
}
|
|
|
|
|
2019-11-27 16:02:31 -08:00
|
|
|
parentFrame(): Frame | null {
|
2019-11-18 18:18:28 -08:00
|
|
|
return this._parentFrame;
|
|
|
|
}
|
|
|
|
|
2019-11-27 16:02:31 -08:00
|
|
|
childFrames(): Frame[] {
|
2019-11-18 18:18:28 -08:00
|
|
|
return Array.from(this._childFrames);
|
|
|
|
}
|
|
|
|
|
2020-12-14 15:27:02 -08:00
|
|
|
async addScriptTag(params: {
|
2020-08-18 18:46:56 -07:00
|
|
|
url?: string,
|
|
|
|
content?: string,
|
|
|
|
type?: string,
|
2019-11-27 16:02:31 -08:00
|
|
|
}): Promise<dom.ElementHandle> {
|
2019-11-26 08:57:53 -08:00
|
|
|
const {
|
|
|
|
url = null,
|
|
|
|
content = null,
|
|
|
|
type = ''
|
2020-12-14 15:27:02 -08:00
|
|
|
} = params;
|
2020-08-18 18:46:56 -07:00
|
|
|
if (!url && !content)
|
2019-12-11 13:51:03 -08:00
|
|
|
throw new Error('Provide an object with a `url`, `path` or `content` property');
|
|
|
|
|
|
|
|
const context = await this._mainContext();
|
|
|
|
return this._raceWithCSPError(async () => {
|
|
|
|
if (url !== null)
|
2021-03-18 01:47:07 +08:00
|
|
|
return (await context.evaluateHandle(addScriptUrl, { url, type })).asElement()!;
|
|
|
|
const result = (await context.evaluateHandle(addScriptContent, { content: content!, type })).asElement()!;
|
2020-04-17 08:51:54 -07:00
|
|
|
// Another round trip to the browser to ensure that we receive CSP error messages
|
|
|
|
// (if any) logged asynchronously in a separate task on the content main thread.
|
|
|
|
if (this._page._delegate.cspErrorsAsynchronousForInlineScipts)
|
2021-03-18 01:47:07 +08:00
|
|
|
await context.evaluate(() => true);
|
2020-04-17 08:51:54 -07:00
|
|
|
return result;
|
2019-12-11 13:51:03 -08:00
|
|
|
});
|
2019-11-26 08:57:53 -08:00
|
|
|
|
2020-12-14 15:27:02 -08:00
|
|
|
async function addScriptUrl(params: { url: string, type: string }): Promise<HTMLElement> {
|
2019-11-26 08:57:53 -08:00
|
|
|
const script = document.createElement('script');
|
2020-12-14 15:27:02 -08:00
|
|
|
script.src = params.url;
|
|
|
|
if (params.type)
|
|
|
|
script.type = params.type;
|
2019-11-26 08:57:53 -08:00
|
|
|
const promise = new Promise((res, rej) => {
|
|
|
|
script.onload = res;
|
2020-04-12 18:46:53 -07:00
|
|
|
script.onerror = e => rej(typeof e === 'string' ? new Error(e) : new Error(`Failed to load script at ${script.src}`));
|
2019-11-26 08:57:53 -08:00
|
|
|
});
|
|
|
|
document.head.appendChild(script);
|
|
|
|
await promise;
|
|
|
|
return script;
|
|
|
|
}
|
|
|
|
|
2020-12-14 15:27:02 -08:00
|
|
|
function addScriptContent(params: { content: string, type: string }): HTMLElement {
|
2019-11-26 08:57:53 -08:00
|
|
|
const script = document.createElement('script');
|
2020-12-14 15:27:02 -08:00
|
|
|
script.type = params.type || 'text/javascript';
|
|
|
|
script.text = params.content;
|
2019-11-26 08:57:53 -08:00
|
|
|
let error = null;
|
|
|
|
script.onerror = e => error = e;
|
|
|
|
document.head.appendChild(script);
|
|
|
|
if (error)
|
|
|
|
throw error;
|
|
|
|
return script;
|
|
|
|
}
|
2019-11-18 18:18:28 -08:00
|
|
|
}
|
|
|
|
|
2020-12-14 15:27:02 -08:00
|
|
|
async addStyleTag(params: { url?: string, content?: string }): Promise<dom.ElementHandle> {
|
2019-11-26 08:57:53 -08:00
|
|
|
const {
|
|
|
|
url = null,
|
|
|
|
content = null
|
2020-12-14 15:27:02 -08:00
|
|
|
} = params;
|
2020-08-18 18:46:56 -07:00
|
|
|
if (!url && !content)
|
2019-12-11 13:51:03 -08:00
|
|
|
throw new Error('Provide an object with a `url`, `path` or `content` property');
|
|
|
|
|
|
|
|
const context = await this._mainContext();
|
|
|
|
return this._raceWithCSPError(async () => {
|
|
|
|
if (url !== null)
|
2021-03-18 01:47:07 +08:00
|
|
|
return (await context.evaluateHandle(addStyleUrl, url)).asElement()!;
|
|
|
|
return (await context.evaluateHandle(addStyleContent, content!)).asElement()!;
|
2019-12-11 13:51:03 -08:00
|
|
|
});
|
2019-12-12 20:21:29 -08:00
|
|
|
|
2019-11-26 08:57:53 -08:00
|
|
|
async function addStyleUrl(url: string): Promise<HTMLElement> {
|
|
|
|
const link = document.createElement('link');
|
|
|
|
link.rel = 'stylesheet';
|
|
|
|
link.href = url;
|
|
|
|
const promise = new Promise((res, rej) => {
|
|
|
|
link.onload = res;
|
|
|
|
link.onerror = rej;
|
|
|
|
});
|
|
|
|
document.head.appendChild(link);
|
|
|
|
await promise;
|
|
|
|
return link;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function addStyleContent(content: string): Promise<HTMLElement> {
|
|
|
|
const style = document.createElement('style');
|
|
|
|
style.type = 'text/css';
|
|
|
|
style.appendChild(document.createTextNode(content));
|
|
|
|
const promise = new Promise((res, rej) => {
|
|
|
|
style.onload = res;
|
|
|
|
style.onerror = rej;
|
|
|
|
});
|
|
|
|
document.head.appendChild(style);
|
|
|
|
await promise;
|
|
|
|
return style;
|
|
|
|
}
|
2019-11-18 18:18:28 -08:00
|
|
|
}
|
|
|
|
|
2019-12-11 13:51:03 -08:00
|
|
|
private async _raceWithCSPError(func: () => Promise<dom.ElementHandle>): Promise<dom.ElementHandle> {
|
|
|
|
const listeners: RegisteredListener[] = [];
|
2020-01-13 13:33:25 -08:00
|
|
|
let result: dom.ElementHandle;
|
2019-12-11 13:51:03 -08:00
|
|
|
let error: Error | undefined;
|
|
|
|
let cspMessage: ConsoleMessage | undefined;
|
2021-02-03 13:49:25 -08:00
|
|
|
const actionPromise = new Promise<void>(async resolve => {
|
2019-12-11 13:51:03 -08:00
|
|
|
try {
|
|
|
|
result = await func();
|
|
|
|
} catch (e) {
|
|
|
|
error = e;
|
|
|
|
}
|
|
|
|
resolve();
|
|
|
|
});
|
2021-02-03 13:49:25 -08:00
|
|
|
const errorPromise = new Promise<void>(resolve => {
|
2021-07-07 21:14:16 +02:00
|
|
|
listeners.push(eventsHelper.addEventListener(this._page, Page.Events.Console, (message: ConsoleMessage) => {
|
2019-12-11 13:51:03 -08:00
|
|
|
if (message.type() === 'error' && message.text().includes('Content Security Policy')) {
|
|
|
|
cspMessage = message;
|
|
|
|
resolve();
|
|
|
|
}
|
|
|
|
}));
|
|
|
|
});
|
|
|
|
await Promise.race([actionPromise, errorPromise]);
|
2021-07-07 21:14:16 +02:00
|
|
|
eventsHelper.removeEventListeners(listeners);
|
2019-12-11 13:51:03 -08:00
|
|
|
if (cspMessage)
|
|
|
|
throw new Error(cspMessage.text());
|
|
|
|
if (error)
|
|
|
|
throw error;
|
2020-01-13 13:33:25 -08:00
|
|
|
return result!;
|
2019-12-11 13:51:03 -08:00
|
|
|
}
|
|
|
|
|
2020-09-10 21:42:09 -07:00
|
|
|
private async _retryWithProgressIfNotConnected<R>(
|
|
|
|
progress: Progress,
|
|
|
|
selector: string,
|
|
|
|
action: (handle: dom.ElementHandle<Element>) => Promise<R | 'error:notconnected'>): Promise<R> {
|
|
|
|
const info = this._page.selectors._parseSelector(selector);
|
|
|
|
while (progress.isRunning()) {
|
|
|
|
progress.log(`waiting for selector "${selector}"`);
|
|
|
|
const task = dom.waitForSelectorTask(info, 'attached');
|
|
|
|
const handle = await this._scheduleRerunnableHandleTask(progress, info.world, task);
|
|
|
|
const element = handle.asElement() as dom.ElementHandle<Element>;
|
|
|
|
progress.cleanupWhenAborted(() => {
|
|
|
|
// Do not await here to avoid being blocked, either by stalled
|
|
|
|
// page (e.g. alert) or unresolved navigation in Chromium.
|
|
|
|
element.dispose();
|
|
|
|
});
|
|
|
|
const result = await action(element);
|
|
|
|
element.dispose();
|
|
|
|
if (result === 'error:notconnected') {
|
|
|
|
progress.log('element was detached from the DOM, retrying');
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
return undefined as any;
|
|
|
|
}
|
|
|
|
|
2020-04-18 18:29:31 -07:00
|
|
|
private async _retryWithSelectorIfNotConnected<R>(
|
2021-02-09 14:44:48 -08:00
|
|
|
controller: ProgressController,
|
2020-04-18 18:29:31 -07:00
|
|
|
selector: string, options: types.TimeoutOptions,
|
2020-08-14 18:25:32 -07:00
|
|
|
action: (progress: Progress, handle: dom.ElementHandle<Element>) => Promise<R | 'error:notconnected'>): Promise<R> {
|
2021-02-09 14:44:48 -08:00
|
|
|
return controller.run(async progress => {
|
2020-09-10 21:42:09 -07:00
|
|
|
return this._retryWithProgressIfNotConnected(progress, selector, handle => action(progress, handle));
|
2020-08-14 18:25:32 -07:00
|
|
|
}, this._page._timeoutSettings.timeout(options));
|
2020-04-18 18:29:31 -07:00
|
|
|
}
|
|
|
|
|
2021-02-09 14:44:48 -08:00
|
|
|
async click(metadata: CallMetadata, selector: string, options: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) {
|
|
|
|
const controller = new ProgressController(metadata, this);
|
2020-09-17 09:32:54 -07:00
|
|
|
return controller.run(async progress => {
|
|
|
|
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._click(progress, options)));
|
|
|
|
}, this._page._timeoutSettings.timeout(options));
|
2019-11-18 18:18:28 -08:00
|
|
|
}
|
|
|
|
|
2021-02-09 14:44:48 -08:00
|
|
|
async dblclick(metadata: CallMetadata, selector: string, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
|
|
|
|
const controller = new ProgressController(metadata, this);
|
2020-09-17 09:32:54 -07:00
|
|
|
return controller.run(async progress => {
|
|
|
|
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._dblclick(progress, options)));
|
|
|
|
}, this._page._timeoutSettings.timeout(options));
|
2019-11-18 18:18:28 -08:00
|
|
|
}
|
|
|
|
|
2021-07-19 12:42:55 -05:00
|
|
|
async dragAndDrop(metadata: CallMetadata, source: string, target: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
|
|
|
|
const controller = new ProgressController(metadata, this);
|
|
|
|
await controller.run(async progress => {
|
|
|
|
await dom.assertDone(await this._retryWithProgressIfNotConnected(progress, source, async handle => {
|
|
|
|
return handle._retryPointerAction(progress, 'move and down', false, async point => {
|
|
|
|
await this._page.mouse.move(point.x, point.y);
|
|
|
|
await this._page.mouse.down();
|
|
|
|
}, {
|
|
|
|
...options,
|
|
|
|
timeout: progress.timeUntilDeadline(),
|
|
|
|
});
|
|
|
|
}));
|
|
|
|
await dom.assertDone(await this._retryWithProgressIfNotConnected(progress, target, async handle => {
|
|
|
|
return handle._retryPointerAction(progress, 'move and up', false, async point => {
|
|
|
|
await this._page.mouse.move(point.x, point.y);
|
|
|
|
await this._page.mouse.up();
|
|
|
|
}, {
|
|
|
|
...options,
|
|
|
|
timeout: progress.timeUntilDeadline(),
|
|
|
|
});
|
|
|
|
}));
|
|
|
|
}, this._page._timeoutSettings.timeout(options));
|
|
|
|
}
|
|
|
|
|
2021-02-09 14:44:48 -08:00
|
|
|
async tap(metadata: CallMetadata, selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) {
|
|
|
|
const controller = new ProgressController(metadata, this);
|
2020-10-19 10:07:33 -07:00
|
|
|
return controller.run(async progress => {
|
|
|
|
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._tap(progress, options)));
|
|
|
|
}, this._page._timeoutSettings.timeout(options));
|
|
|
|
}
|
|
|
|
|
2021-06-24 08:18:09 -07:00
|
|
|
async fill(metadata: CallMetadata, selector: string, value: string, options: types.NavigatingActionWaitOptions & { force?: boolean }) {
|
2021-02-09 14:44:48 -08:00
|
|
|
const controller = new ProgressController(metadata, this);
|
2020-09-17 09:32:54 -07:00
|
|
|
return controller.run(async progress => {
|
|
|
|
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._fill(progress, value, options)));
|
|
|
|
}, this._page._timeoutSettings.timeout(options));
|
2019-11-18 18:18:28 -08:00
|
|
|
}
|
|
|
|
|
2021-02-09 14:44:48 -08:00
|
|
|
async focus(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}) {
|
|
|
|
const controller = new ProgressController(metadata, this);
|
|
|
|
await this._retryWithSelectorIfNotConnected(controller, selector, options, (progress, handle) => handle._focus(progress));
|
2020-08-18 19:13:40 -07:00
|
|
|
await this._page._doSlowMo();
|
2019-11-18 18:18:28 -08:00
|
|
|
}
|
|
|
|
|
2021-02-09 14:44:48 -08:00
|
|
|
async textContent(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise<string | null> {
|
|
|
|
const controller = new ProgressController(metadata, this);
|
2020-09-02 16:15:43 -07:00
|
|
|
const info = this._page.selectors._parseSelector(selector);
|
2020-06-26 16:32:42 -07:00
|
|
|
const task = dom.textContentTask(info);
|
2021-02-09 14:44:48 -08:00
|
|
|
return controller.run(async progress => {
|
2020-08-17 14:12:31 -07:00
|
|
|
progress.log(` retrieving textContent from "${selector}"`);
|
2020-06-26 16:32:42 -07:00
|
|
|
return this._scheduleRerunnableTask(progress, info.world, task);
|
2020-08-14 18:25:32 -07:00
|
|
|
}, this._page._timeoutSettings.timeout(options));
|
2020-05-18 17:58:23 -07:00
|
|
|
}
|
|
|
|
|
2021-02-09 14:44:48 -08:00
|
|
|
async innerText(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise<string> {
|
|
|
|
const controller = new ProgressController(metadata, this);
|
2020-09-02 16:15:43 -07:00
|
|
|
const info = this._page.selectors._parseSelector(selector);
|
2020-07-01 16:10:53 -07:00
|
|
|
const task = dom.innerTextTask(info);
|
2021-02-09 14:44:48 -08:00
|
|
|
return controller.run(async progress => {
|
2020-08-17 14:12:31 -07:00
|
|
|
progress.log(` retrieving innerText from "${selector}"`);
|
2020-07-01 16:10:53 -07:00
|
|
|
const result = dom.throwFatalDOMError(await this._scheduleRerunnableTask(progress, info.world, task));
|
|
|
|
return result.innerText;
|
2020-08-14 18:25:32 -07:00
|
|
|
}, this._page._timeoutSettings.timeout(options));
|
2020-05-18 17:58:23 -07:00
|
|
|
}
|
|
|
|
|
2021-02-09 14:44:48 -08:00
|
|
|
async innerHTML(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise<string> {
|
|
|
|
const controller = new ProgressController(metadata, this);
|
2020-09-02 16:15:43 -07:00
|
|
|
const info = this._page.selectors._parseSelector(selector);
|
2020-07-01 16:10:53 -07:00
|
|
|
const task = dom.innerHTMLTask(info);
|
2021-02-09 14:44:48 -08:00
|
|
|
return controller.run(async progress => {
|
2020-08-17 14:12:31 -07:00
|
|
|
progress.log(` retrieving innerHTML from "${selector}"`);
|
2020-07-01 16:10:53 -07:00
|
|
|
return this._scheduleRerunnableTask(progress, info.world, task);
|
2020-08-14 18:25:32 -07:00
|
|
|
}, this._page._timeoutSettings.timeout(options));
|
2020-05-18 17:58:23 -07:00
|
|
|
}
|
|
|
|
|
2021-02-09 14:44:48 -08:00
|
|
|
async getAttribute(metadata: CallMetadata, selector: string, name: string, options: types.TimeoutOptions = {}): Promise<string | null> {
|
|
|
|
const controller = new ProgressController(metadata, this);
|
2020-09-02 16:15:43 -07:00
|
|
|
const info = this._page.selectors._parseSelector(selector);
|
2020-07-01 16:10:53 -07:00
|
|
|
const task = dom.getAttributeTask(info, name);
|
2021-02-09 14:44:48 -08:00
|
|
|
return controller.run(async progress => {
|
2020-08-17 14:12:31 -07:00
|
|
|
progress.log(` retrieving attribute "${name}" from "${selector}"`);
|
2020-07-01 16:10:53 -07:00
|
|
|
return this._scheduleRerunnableTask(progress, info.world, task);
|
2020-08-14 18:25:32 -07:00
|
|
|
}, this._page._timeoutSettings.timeout(options));
|
2020-05-18 17:58:23 -07:00
|
|
|
}
|
|
|
|
|
2021-06-23 22:19:20 -07:00
|
|
|
async inputValue(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise<string> {
|
|
|
|
const controller = new ProgressController(metadata, this);
|
|
|
|
const info = this._page.selectors._parseSelector(selector);
|
|
|
|
const task = dom.inputValueTask(info);
|
|
|
|
return controller.run(async progress => {
|
|
|
|
progress.log(` retrieving value from "${selector}"`);
|
|
|
|
return dom.throwFatalDOMError(await this._scheduleRerunnableTask(progress, info.world, task));
|
|
|
|
}, this._page._timeoutSettings.timeout(options));
|
|
|
|
}
|
|
|
|
|
2021-02-10 12:36:26 -08:00
|
|
|
private async _checkElementState(metadata: CallMetadata, selector: string, state: ElementStateWithoutStable, options: types.TimeoutOptions = {}): Promise<boolean> {
|
|
|
|
const controller = new ProgressController(metadata, this);
|
|
|
|
const info = this._page.selectors._parseSelector(selector);
|
|
|
|
const task = dom.elementStateTask(info, state);
|
|
|
|
const result = await controller.run(async progress => {
|
|
|
|
progress.log(` checking "${state}" state of "${selector}"`);
|
|
|
|
return this._scheduleRerunnableTask(progress, info.world, task);
|
|
|
|
}, this._page._timeoutSettings.timeout(options));
|
|
|
|
return dom.throwFatalDOMError(dom.throwRetargetableDOMError(result));
|
|
|
|
}
|
|
|
|
|
2021-07-14 01:46:00 -07:00
|
|
|
async isVisible(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise<boolean> {
|
2021-02-09 14:44:48 -08:00
|
|
|
const controller = new ProgressController(metadata, this);
|
|
|
|
return controller.run(async progress => {
|
2021-01-08 12:27:54 -08:00
|
|
|
progress.log(` checking visibility of "${selector}"`);
|
2021-07-26 15:46:51 -07:00
|
|
|
const element = await this.querySelector(selector);
|
2021-02-10 07:12:43 -08:00
|
|
|
return element ? await element.isVisible() : false;
|
2021-07-14 01:46:00 -07:00
|
|
|
}, this._page._timeoutSettings.timeout(options));
|
2021-01-08 12:27:54 -08:00
|
|
|
}
|
|
|
|
|
2021-07-14 01:46:00 -07:00
|
|
|
async isHidden(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise<boolean> {
|
|
|
|
return !(await this.isVisible(metadata, selector, options));
|
2021-01-08 12:27:54 -08:00
|
|
|
}
|
|
|
|
|
2021-02-09 14:44:48 -08:00
|
|
|
async isDisabled(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise<boolean> {
|
2021-02-10 12:36:26 -08:00
|
|
|
return this._checkElementState(metadata, selector, 'disabled', options);
|
2021-01-08 12:27:54 -08:00
|
|
|
}
|
|
|
|
|
2021-02-09 14:44:48 -08:00
|
|
|
async isEnabled(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise<boolean> {
|
2021-02-10 12:36:26 -08:00
|
|
|
return this._checkElementState(metadata, selector, 'enabled', options);
|
2021-01-08 12:27:54 -08:00
|
|
|
}
|
|
|
|
|
2021-02-09 14:44:48 -08:00
|
|
|
async isEditable(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise<boolean> {
|
2021-02-10 12:36:26 -08:00
|
|
|
return this._checkElementState(metadata, selector, 'editable', options);
|
2021-01-08 12:27:54 -08:00
|
|
|
}
|
|
|
|
|
2021-02-09 14:44:48 -08:00
|
|
|
async isChecked(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise<boolean> {
|
2021-02-10 12:36:26 -08:00
|
|
|
return this._checkElementState(metadata, selector, 'checked', options);
|
2021-01-08 17:36:17 -08:00
|
|
|
}
|
|
|
|
|
2021-02-09 14:44:48 -08:00
|
|
|
async hover(metadata: CallMetadata, selector: string, options: types.PointerActionOptions & types.PointerActionWaitOptions = {}) {
|
|
|
|
const controller = new ProgressController(metadata, this);
|
2020-09-17 09:32:54 -07:00
|
|
|
return controller.run(async progress => {
|
|
|
|
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._hover(progress, options)));
|
|
|
|
}, this._page._timeoutSettings.timeout(options));
|
2019-11-18 18:18:28 -08:00
|
|
|
}
|
|
|
|
|
2021-06-24 08:18:09 -07:00
|
|
|
async selectOption(metadata: CallMetadata, selector: string, elements: dom.ElementHandle[], values: types.SelectOption[], options: types.NavigatingActionWaitOptions & types.ForceOptions = {}): Promise<string[]> {
|
2021-02-09 14:44:48 -08:00
|
|
|
const controller = new ProgressController(metadata, this);
|
2020-09-17 09:32:54 -07:00
|
|
|
return controller.run(async progress => {
|
|
|
|
return await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._selectOption(progress, elements, values, options));
|
|
|
|
}, this._page._timeoutSettings.timeout(options));
|
2020-04-16 10:25:28 -07:00
|
|
|
}
|
|
|
|
|
2021-05-19 01:30:20 +00:00
|
|
|
async setInputFiles(metadata: CallMetadata, selector: string, files: channels.ElementHandleSetInputFilesParams['files'], options: types.NavigatingActionWaitOptions = {}): Promise<void> {
|
2021-02-09 14:44:48 -08:00
|
|
|
const controller = new ProgressController(metadata, this);
|
2020-09-17 09:32:54 -07:00
|
|
|
return controller.run(async progress => {
|
|
|
|
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._setInputFiles(progress, files, options)));
|
|
|
|
}, this._page._timeoutSettings.timeout(options));
|
2019-11-18 18:18:28 -08:00
|
|
|
}
|
|
|
|
|
2021-02-09 14:44:48 -08:00
|
|
|
async type(metadata: CallMetadata, selector: string, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) {
|
|
|
|
const controller = new ProgressController(metadata, this);
|
2020-09-17 09:32:54 -07:00
|
|
|
return controller.run(async progress => {
|
|
|
|
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._type(progress, text, options)));
|
|
|
|
}, this._page._timeoutSettings.timeout(options));
|
2019-11-18 18:18:28 -08:00
|
|
|
}
|
|
|
|
|
2021-02-09 14:44:48 -08:00
|
|
|
async press(metadata: CallMetadata, selector: string, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) {
|
|
|
|
const controller = new ProgressController(metadata, this);
|
2020-09-17 09:32:54 -07:00
|
|
|
return controller.run(async progress => {
|
|
|
|
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._press(progress, key, options)));
|
|
|
|
}, this._page._timeoutSettings.timeout(options));
|
2020-03-06 09:38:08 -08:00
|
|
|
}
|
|
|
|
|
2021-02-09 14:44:48 -08:00
|
|
|
async check(metadata: CallMetadata, selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
|
|
|
|
const controller = new ProgressController(metadata, this);
|
2020-09-17 09:32:54 -07:00
|
|
|
return controller.run(async progress => {
|
|
|
|
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._setChecked(progress, true, options)));
|
|
|
|
}, this._page._timeoutSettings.timeout(options));
|
2020-02-04 14:39:11 -08:00
|
|
|
}
|
|
|
|
|
2021-02-09 14:44:48 -08:00
|
|
|
async uncheck(metadata: CallMetadata, selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
|
|
|
|
const controller = new ProgressController(metadata, this);
|
2020-09-17 09:32:54 -07:00
|
|
|
return controller.run(async progress => {
|
|
|
|
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._setChecked(progress, false, options)));
|
|
|
|
}, this._page._timeoutSettings.timeout(options));
|
2020-02-04 14:39:11 -08:00
|
|
|
}
|
|
|
|
|
2021-04-29 14:53:53 -07:00
|
|
|
async _waitForFunctionExpression<R>(metadata: CallMetadata, expression: string, isFunction: boolean | undefined, arg: any, options: types.WaitForFunctionOptions, world: types.World = 'main'): Promise<js.SmartHandle<R>> {
|
2021-02-09 14:44:48 -08:00
|
|
|
const controller = new ProgressController(metadata, this);
|
2020-08-22 07:07:13 -07:00
|
|
|
if (typeof options.pollingInterval === 'number')
|
|
|
|
assert(options.pollingInterval > 0, 'Cannot poll with non-positive interval: ' + options.pollingInterval);
|
2021-02-04 08:45:59 -08:00
|
|
|
expression = js.normalizeEvaluationExpression(expression, isFunction);
|
|
|
|
const task: dom.SchedulableTask<R> = injectedScript => injectedScript.evaluateHandle((injectedScript, { expression, isFunction, polling, arg }) => {
|
|
|
|
const predicate = (arg: any): R => {
|
|
|
|
let result = self.eval(expression);
|
|
|
|
if (isFunction === true) {
|
|
|
|
result = result(arg);
|
|
|
|
} else if (isFunction === false) {
|
|
|
|
result = result;
|
|
|
|
} else {
|
|
|
|
// auto detect.
|
|
|
|
if (typeof result === 'function')
|
|
|
|
result = result(arg);
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
};
|
2020-08-22 07:07:13 -07:00
|
|
|
if (typeof polling !== 'number')
|
2021-02-04 08:45:59 -08:00
|
|
|
return injectedScript.pollRaf((progress, continuePolling) => predicate(arg) || continuePolling);
|
|
|
|
return injectedScript.pollInterval(polling, (progress, continuePolling) => predicate(arg) || continuePolling);
|
|
|
|
}, { expression, isFunction, polling: options.pollingInterval, arg });
|
2021-02-09 14:44:48 -08:00
|
|
|
return controller.run(
|
2021-04-29 14:53:53 -07:00
|
|
|
progress => this._scheduleRerunnableHandleTask(progress, world, task),
|
2020-08-14 18:25:32 -07:00
|
|
|
this._page._timeoutSettings.timeout(options));
|
2019-12-18 18:11:02 -08:00
|
|
|
}
|
|
|
|
|
2021-04-29 14:53:53 -07:00
|
|
|
async waitForFunctionValueInUtility<R>(progress: Progress, pageFunction: js.Func1<any, R>) {
|
2021-04-07 07:01:38 +08:00
|
|
|
const expression = `() => {
|
|
|
|
const result = (${pageFunction})();
|
|
|
|
if (!result)
|
|
|
|
return result;
|
|
|
|
return JSON.stringify(result);
|
|
|
|
}`;
|
2021-04-29 14:53:53 -07:00
|
|
|
const handle = await this._waitForFunctionExpression(internalCallMetadata(), expression, true, undefined, { timeout: progress.timeUntilDeadline() }, 'utility');
|
2021-04-07 07:01:38 +08:00
|
|
|
return JSON.parse(handle.rawValue()) as R;
|
|
|
|
}
|
|
|
|
|
2019-11-18 18:18:28 -08:00
|
|
|
async title(): Promise<string> {
|
2019-11-26 15:37:25 -08:00
|
|
|
const context = await this._utilityContext();
|
2021-03-18 01:47:07 +08:00
|
|
|
return context.evaluate(() => document.title);
|
2019-11-18 18:18:28 -08:00
|
|
|
}
|
|
|
|
|
2019-12-11 07:17:32 -08:00
|
|
|
_onDetached() {
|
2020-07-10 16:38:01 -07:00
|
|
|
this._stopNetworkIdleTimer();
|
2019-11-18 18:18:28 -08:00
|
|
|
this._detached = true;
|
2020-02-10 18:35:47 -08:00
|
|
|
this._detachedCallback();
|
2019-12-12 21:11:52 -08:00
|
|
|
for (const data of this._contextData.values()) {
|
|
|
|
for (const rerunnableTask of data.rerunnableTasks)
|
2019-12-03 10:51:41 -08:00
|
|
|
rerunnableTask.terminate(new Error('waitForFunction failed: frame got detached.'));
|
2019-11-26 15:37:25 -08:00
|
|
|
}
|
2019-11-18 18:18:28 -08:00
|
|
|
if (this._parentFrame)
|
|
|
|
this._parentFrame._childFrames.delete(this);
|
|
|
|
this._parentFrame = null;
|
|
|
|
}
|
2019-11-26 15:37:25 -08:00
|
|
|
|
2020-06-26 16:32:42 -07:00
|
|
|
private _scheduleRerunnableTask<T>(progress: Progress, world: types.World, task: dom.SchedulableTask<T>): Promise<T> {
|
|
|
|
const data = this._contextData.get(world)!;
|
|
|
|
const rerunnableTask = new RerunnableTask(data, progress, task, true /* returnByValue */);
|
|
|
|
if (this._detached)
|
|
|
|
rerunnableTask.terminate(new Error('waitForFunction failed: frame got detached.'));
|
|
|
|
if (data.context)
|
|
|
|
rerunnableTask.rerun(data.context);
|
|
|
|
return rerunnableTask.promise;
|
|
|
|
}
|
|
|
|
|
|
|
|
private _scheduleRerunnableHandleTask<T>(progress: Progress, world: types.World, task: dom.SchedulableTask<T>): Promise<js.SmartHandle<T>> {
|
2020-06-24 17:03:28 -07:00
|
|
|
const data = this._contextData.get(world)!;
|
2020-06-26 16:32:42 -07:00
|
|
|
const rerunnableTask = new RerunnableTask(data, progress, task, false /* returnByValue */);
|
2020-06-25 16:57:21 -07:00
|
|
|
if (this._detached)
|
|
|
|
rerunnableTask.terminate(new Error('waitForFunction failed: frame got detached.'));
|
2019-12-12 21:11:52 -08:00
|
|
|
if (data.context)
|
|
|
|
rerunnableTask.rerun(data.context);
|
2019-12-03 10:51:41 -08:00
|
|
|
return rerunnableTask.promise;
|
2019-11-26 15:37:25 -08:00
|
|
|
}
|
|
|
|
|
2020-06-24 17:03:28 -07:00
|
|
|
private _setContext(world: types.World, context: dom.FrameExecutionContext | null) {
|
|
|
|
const data = this._contextData.get(world)!;
|
2019-12-12 21:11:52 -08:00
|
|
|
data.context = context;
|
2019-11-26 15:37:25 -08:00
|
|
|
if (context) {
|
2019-12-12 21:11:52 -08:00
|
|
|
data.contextResolveCallback.call(null, context);
|
|
|
|
for (const rerunnableTask of data.rerunnableTasks)
|
|
|
|
rerunnableTask.rerun(context);
|
2019-11-26 15:37:25 -08:00
|
|
|
} else {
|
2019-12-12 21:11:52 -08:00
|
|
|
data.contextPromise = new Promise(fulfill => {
|
|
|
|
data.contextResolveCallback = fulfill;
|
2019-11-26 15:37:25 -08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-24 17:03:28 -07:00
|
|
|
_contextCreated(world: types.World, context: dom.FrameExecutionContext) {
|
|
|
|
const data = this._contextData.get(world)!;
|
2019-11-26 15:37:25 -08:00
|
|
|
// In case of multiple sessions to the same target, there's a race between
|
|
|
|
// connections so we might end up creating multiple isolated worlds.
|
|
|
|
// We can use either.
|
2019-12-12 21:11:52 -08:00
|
|
|
if (data.context)
|
2020-06-24 17:03:28 -07:00
|
|
|
this._setContext(world, null);
|
|
|
|
this._setContext(world, context);
|
2019-11-26 15:37:25 -08:00
|
|
|
}
|
|
|
|
|
2019-12-12 21:11:52 -08:00
|
|
|
_contextDestroyed(context: dom.FrameExecutionContext) {
|
2020-06-24 17:03:28 -07:00
|
|
|
for (const [world, data] of this._contextData) {
|
2019-12-12 21:11:52 -08:00
|
|
|
if (data.context === context)
|
2020-06-24 17:03:28 -07:00
|
|
|
this._setContext(world, null);
|
2019-11-26 15:37:25 -08:00
|
|
|
}
|
|
|
|
}
|
2020-04-20 16:52:26 -07:00
|
|
|
|
|
|
|
_startNetworkIdleTimer() {
|
|
|
|
assert(!this._networkIdleTimer);
|
2020-11-19 19:20:53 -08:00
|
|
|
// We should not start a timer and report networkidle in detached frames.
|
|
|
|
// This happens at least in Firefox for child frames, where we may get requestFinished
|
|
|
|
// after the frame was detached - probably a race in the Firefox itself.
|
|
|
|
if (this._firedLifecycleEvents.has('networkidle') || this._detached)
|
2020-04-20 16:52:26 -07:00
|
|
|
return;
|
2020-07-06 17:33:56 -07:00
|
|
|
this._networkIdleTimer = setTimeout(() => this._onLifecycleEvent('networkidle'), 500);
|
2020-04-20 16:52:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
_stopNetworkIdleTimer() {
|
|
|
|
if (this._networkIdleTimer)
|
|
|
|
clearTimeout(this._networkIdleTimer);
|
|
|
|
this._networkIdleTimer = undefined;
|
|
|
|
}
|
2020-09-07 15:50:25 -07:00
|
|
|
|
2021-05-18 16:40:24 +00:00
|
|
|
async extendInjectedScript(world: types.World, source: string, arg?: any): Promise<js.JSHandle> {
|
|
|
|
const context = await this._context(world);
|
|
|
|
const injectedScriptHandle = await context.injectedScript();
|
2020-09-07 15:50:25 -07:00
|
|
|
return injectedScriptHandle.evaluateHandle((injectedScript, {source, arg}) => {
|
|
|
|
return injectedScript.extend(source, arg);
|
|
|
|
}, { source, arg });
|
|
|
|
}
|
2019-11-18 18:18:28 -08:00
|
|
|
}
|
2019-12-03 10:51:41 -08:00
|
|
|
|
2020-06-26 16:32:42 -07:00
|
|
|
class RerunnableTask {
|
|
|
|
readonly promise: Promise<any>;
|
|
|
|
private _task: dom.SchedulableTask<any>;
|
|
|
|
private _resolve: (result: any) => void = () => {};
|
2020-01-13 13:33:25 -08:00
|
|
|
private _reject: (reason: Error) => void = () => {};
|
2020-06-01 08:54:18 -07:00
|
|
|
private _progress: Progress;
|
2020-06-26 16:32:42 -07:00
|
|
|
private _returnByValue: boolean;
|
2020-09-14 14:55:37 -07:00
|
|
|
private _contextData: ContextData;
|
2019-12-03 10:51:41 -08:00
|
|
|
|
2020-06-26 16:32:42 -07:00
|
|
|
constructor(data: ContextData, progress: Progress, task: dom.SchedulableTask<any>, returnByValue: boolean) {
|
2019-12-03 10:51:41 -08:00
|
|
|
this._task = task;
|
2020-06-01 08:54:18 -07:00
|
|
|
this._progress = progress;
|
2020-06-26 16:32:42 -07:00
|
|
|
this._returnByValue = returnByValue;
|
2020-09-14 14:55:37 -07:00
|
|
|
this._contextData = data;
|
|
|
|
this._contextData.rerunnableTasks.add(this);
|
2020-06-26 16:32:42 -07:00
|
|
|
this.promise = new Promise<any>((resolve, reject) => {
|
2020-06-01 08:54:18 -07:00
|
|
|
// The task is either resolved with a value, or rejected with a meaningful evaluation error.
|
2020-05-30 15:00:53 -07:00
|
|
|
this._resolve = resolve;
|
|
|
|
this._reject = reject;
|
2020-06-04 16:43:48 -07:00
|
|
|
});
|
2020-06-01 08:54:18 -07:00
|
|
|
}
|
2019-12-03 10:51:41 -08:00
|
|
|
|
2020-06-01 08:54:18 -07:00
|
|
|
terminate(error: Error) {
|
2020-06-01 15:48:23 -07:00
|
|
|
this._reject(error);
|
2019-12-03 10:51:41 -08:00
|
|
|
}
|
|
|
|
|
2019-12-12 21:11:52 -08:00
|
|
|
async rerun(context: dom.FrameExecutionContext) {
|
2019-12-03 10:51:41 -08:00
|
|
|
try {
|
2020-06-26 16:32:42 -07:00
|
|
|
const injectedScript = await context.injectedScript();
|
|
|
|
const pollHandler = new dom.InjectedScriptPollHandler(this._progress, await this._task(injectedScript));
|
|
|
|
const result = this._returnByValue ? await pollHandler.finish() : await pollHandler.finishHandle();
|
2020-09-14 14:55:37 -07:00
|
|
|
this._contextData.rerunnableTasks.delete(this);
|
2020-05-30 15:00:53 -07:00
|
|
|
this._resolve(result);
|
2019-12-03 10:51:41 -08:00
|
|
|
} catch (e) {
|
2020-05-30 15:00:53 -07:00
|
|
|
// We will try again in the new execution context.
|
2021-06-02 20:17:24 -07:00
|
|
|
if (js.isContextDestroyedError(e))
|
2020-05-30 15:00:53 -07:00
|
|
|
return;
|
2020-09-14 14:55:37 -07:00
|
|
|
this._contextData.rerunnableTasks.delete(this);
|
2020-05-30 15:00:53 -07:00
|
|
|
this._reject(e);
|
|
|
|
}
|
2019-12-03 10:51:41 -08:00
|
|
|
}
|
|
|
|
}
|
2019-12-11 07:17:32 -08:00
|
|
|
|
2020-06-05 10:06:59 -07:00
|
|
|
class SignalBarrier {
|
2020-06-03 11:23:24 -07:00
|
|
|
private _progress: Progress | null;
|
2020-03-05 14:47:04 -08:00
|
|
|
private _protectCount = 0;
|
|
|
|
private _promise: Promise<void>;
|
|
|
|
private _promiseCallback = () => {};
|
|
|
|
|
2020-06-03 11:23:24 -07:00
|
|
|
constructor(progress: Progress | null) {
|
|
|
|
this._progress = progress;
|
2020-03-05 14:47:04 -08:00
|
|
|
this._promise = new Promise(f => this._promiseCallback = f);
|
|
|
|
this.retain();
|
|
|
|
}
|
|
|
|
|
|
|
|
waitFor(): Promise<void> {
|
|
|
|
this.release();
|
|
|
|
return this._promise;
|
|
|
|
}
|
|
|
|
|
2020-04-16 13:09:24 -07:00
|
|
|
async addFrameNavigation(frame: Frame) {
|
2021-03-18 05:47:51 +08:00
|
|
|
// Auto-wait top-level navigations only.
|
|
|
|
if (frame.parentFrame())
|
|
|
|
return;
|
2020-03-05 14:47:04 -08:00
|
|
|
this.retain();
|
2020-08-21 16:26:33 -07:00
|
|
|
const waiter = helper.waitForEvent(null, frame, Frame.Events.Navigation, (e: NavigationEvent) => {
|
2020-07-07 15:22:05 -07:00
|
|
|
if (!e.error && this._progress)
|
2020-08-17 14:12:31 -07:00
|
|
|
this._progress.log(` navigated to "${frame._url}"`);
|
2020-07-07 15:22:05 -07:00
|
|
|
return true;
|
|
|
|
});
|
2020-06-04 16:43:48 -07:00
|
|
|
await Promise.race([
|
|
|
|
frame._page._disconnectedPromise,
|
2020-06-25 09:53:56 -07:00
|
|
|
frame._page._crashedPromise,
|
2020-06-04 16:43:48 -07:00
|
|
|
frame._detachedPromise,
|
2020-07-07 15:22:05 -07:00
|
|
|
waiter.promise,
|
2020-06-04 16:43:48 -07:00
|
|
|
]).catch(e => {});
|
2020-07-07 15:22:05 -07:00
|
|
|
waiter.dispose();
|
2020-03-05 14:47:04 -08:00
|
|
|
this.release();
|
|
|
|
}
|
|
|
|
|
|
|
|
retain() {
|
|
|
|
++this._protectCount;
|
|
|
|
}
|
|
|
|
|
|
|
|
release() {
|
|
|
|
--this._protectCount;
|
2020-05-13 17:20:33 -07:00
|
|
|
if (!this._protectCount)
|
2020-03-05 14:47:04 -08:00
|
|
|
this._promiseCallback();
|
|
|
|
}
|
|
|
|
}
|
2020-03-18 20:05:35 -07:00
|
|
|
|
2020-07-21 15:25:31 -07:00
|
|
|
function verifyLifecycle(name: string, waitUntil: types.LifecycleEvent): types.LifecycleEvent {
|
2020-07-07 15:22:05 -07:00
|
|
|
if (waitUntil as unknown === 'networkidle0')
|
|
|
|
waitUntil = 'networkidle';
|
|
|
|
if (!types.kLifecycleEvents.has(waitUntil))
|
2020-07-21 15:25:31 -07:00
|
|
|
throw new Error(`${name}: expected one of (load|domcontentloaded|networkidle)`);
|
2020-07-07 15:22:05 -07:00
|
|
|
return waitUntil;
|
|
|
|
}
|