"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.NavigationAbortedError = exports.FrameManager = exports.Frame = void 0; var dom = _interopRequireWildcard(require("./dom")); var _helper = require("./helper"); var _eventsHelper = require("../utils/eventsHelper"); var js = _interopRequireWildcard(require("./javascript")); var network = _interopRequireWildcard(require("./network")); var _page = require("./page"); var types = _interopRequireWildcard(require("./types")); var _browserContext = require("./browserContext"); var _progress = require("./progress"); var _utils = require("../utils"); var _manualPromise = require("../utils/manualPromise"); var _debugLogger = require("../common/debugLogger"); var _instrumentation = require("./instrumentation"); var _protocolError = require("./protocolError"); var _selectorParser = require("../utils/isomorphic/selectorParser"); var _locatorGenerators = require("../utils/isomorphic/locatorGenerators"); var _frameSelectors = require("./frameSelectors"); var _errors = require("./errors"); var _fileUploadUtils = require("./fileUploadUtils"); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } /** * 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. */ class NavigationAbortedError extends Error { constructor(documentId, message) { super(message); this.documentId = void 0; this.documentId = documentId; } } exports.NavigationAbortedError = NavigationAbortedError; const kDummyFrameId = ''; class FrameManager { constructor(page) { this._page = void 0; this._frames = new Map(); this._mainFrame = void 0; this._consoleMessageTags = new Map(); this._signalBarriers = new Set(); this._webSockets = new Map(); this._openedDialogs = new Set(); this._closeAllOpeningDialogs = false; this._page = page; this._mainFrame = undefined; } createDummyMainFrameIfNeeded() { if (!this._mainFrame) this.frameAttached(kDummyFrameId, null); } dispose() { for (const frame of this._frames.values()) { frame._stopNetworkIdleTimer(); frame._invalidateNonStallingEvaluations('Target crashed'); } } mainFrame() { return this._mainFrame; } frames() { const frames = []; collect(this._mainFrame); return frames; function collect(frame) { frames.push(frame); for (const subframe of frame.childFrames()) collect(subframe); } } frame(frameId) { return this._frames.get(frameId) || null; } frameAttached(frameId, parentFrameId) { const parentFrame = parentFrameId ? this._frames.get(parentFrameId) : null; 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 { (0, _utils.assert)(!this._frames.has(frameId)); this._mainFrame = new Frame(this._page, frameId, parentFrame); } this._frames.set(frameId, this._mainFrame); return this._mainFrame; } else { (0, _utils.assert)(!this._frames.has(frameId)); const frame = new Frame(this._page, frameId, parentFrame); this._frames.set(frameId, frame); this._page.emit(_page.Page.Events.FrameAttached, frame); return frame; } } async waitForSignalsCreatedBy(progress, noWaitAfter, action, source) { if (noWaitAfter) return action(); const barrier = new SignalBarrier(progress); this._signalBarriers.add(barrier); if (progress) progress.cleanupWhenAborted(() => this._signalBarriers.delete(barrier)); const result = await action(); if (source === 'input') await this._page._delegate.inputActionEpilogue(); await barrier.waitFor(); this._signalBarriers.delete(barrier); // Resolve in the next task, after all waitForNavigations. await new Promise((0, _utils.makeWaitForNextTask)()); return result; } frameWillPotentiallyRequestNavigation() { for (const barrier of this._signalBarriers) barrier.retain(); } frameDidPotentiallyRequestNavigation() { for (const barrier of this._signalBarriers) barrier.release(); } frameRequestedNavigation(frameId, documentId) { const frame = this._frames.get(frameId); if (!frame) return; for (const barrier of this._signalBarriers) barrier.addFrameNavigation(frame); if (frame.pendingDocument() && frame.pendingDocument().documentId === documentId) { // Do not override request with undefined. return; } const request = documentId ? Array.from(frame._inflightRequests).find(request => request._documentId === documentId) : undefined; frame.setPendingDocument({ documentId, request }); } frameCommittedNewDocumentNavigation(frameId, url, name, documentId, initial) { const frame = this._frames.get(frameId); this.removeChildFramesRecursively(frame); this.clearWebSockets(frame); frame._url = url; frame._name = name; let keepPending; const pendingDocument = frame.pendingDocument(); if (pendingDocument) { if (pendingDocument.documentId === undefined) { // Pending with unknown documentId - assume it is the one being committed. pendingDocument.documentId = documentId; } if (pendingDocument.documentId === documentId) { // Committing a pending document. frame._currentDocument = pendingDocument; } 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. keepPending = pendingDocument; frame._currentDocument = { documentId, request: undefined }; } frame.setPendingDocument(undefined); } else { // No pending - just commit a new document. frame._currentDocument = { documentId, request: undefined }; } frame._onClearLifecycle(); const navigationEvent = { url, name, newDocument: frame._currentDocument, isPublic: true }; this._fireInternalFrameNavigation(frame, navigationEvent); if (!initial) { _debugLogger.debugLogger.log('api', ` navigated to "${url}"`); this._page.frameNavigatedToNewDocument(frame); } // Restore pending if any - see comments above about keepPending. frame.setPendingDocument(keepPending); } frameCommittedSameDocumentNavigation(frameId, url) { const frame = this._frames.get(frameId); if (!frame) return; frame._url = url; const navigationEvent = { url, name: frame._name, isPublic: true }; this._fireInternalFrameNavigation(frame, navigationEvent); _debugLogger.debugLogger.log('api', ` navigated to "${url}"`); } frameAbortedNavigation(frameId, errorText, documentId) { const frame = this._frames.get(frameId); if (!frame || !frame.pendingDocument()) return; if (documentId !== undefined && frame.pendingDocument().documentId !== documentId) return; const navigationEvent = { url: frame._url, name: frame._name, newDocument: frame.pendingDocument(), error: new NavigationAbortedError(documentId, errorText), isPublic: !(documentId && frame._redirectedNavigations.has(documentId)) }; frame.setPendingDocument(undefined); this._fireInternalFrameNavigation(frame, navigationEvent); } frameDetached(frameId) { const frame = this._frames.get(frameId); if (frame) { this._removeFramesRecursively(frame); this._page.mainFrame()._recalculateNetworkIdle(); } } frameLifecycleEvent(frameId, event) { const frame = this._frames.get(frameId); if (frame) frame._onLifecycleEvent(event); } requestStarted(request, route) { const frame = request.frame(); this._inflightRequestStarted(request); if (request._documentId) frame.setPendingDocument({ documentId: request._documentId, request }); if (request._isFavicon) { if (route) route.continue(request, { isFallback: true }).catch(() => {}); return; } this._page.emitOnContext(_browserContext.BrowserContext.Events.Request, request); if (route) { var _this$_page$_serverRe, _this$_page, _this$_page$_clientRe, _this$_page2, _this$_page$_browserC, _this$_page$_browserC2; const r = new network.Route(request, route); if ((_this$_page$_serverRe = (_this$_page = this._page)._serverRequestInterceptor) !== null && _this$_page$_serverRe !== void 0 && _this$_page$_serverRe.call(_this$_page, r, request)) return; if ((_this$_page$_clientRe = (_this$_page2 = this._page)._clientRequestInterceptor) !== null && _this$_page$_clientRe !== void 0 && _this$_page$_clientRe.call(_this$_page2, r, request)) return; if ((_this$_page$_browserC = (_this$_page$_browserC2 = this._page._browserContext)._requestInterceptor) !== null && _this$_page$_browserC !== void 0 && _this$_page$_browserC.call(_this$_page$_browserC2, r, request)) return; r.continue({ isFallback: true }).catch(() => {}); } } requestReceivedResponse(response) { if (response.request()._isFavicon) return; this._page.emitOnContext(_browserContext.BrowserContext.Events.Response, response); } reportRequestFinished(request, response) { this._inflightRequestFinished(request); if (request._isFavicon) return; this._page.emitOnContext(_browserContext.BrowserContext.Events.RequestFinished, { request, response }); } requestFailed(request, canceled) { const frame = request.frame(); this._inflightRequestFinished(request); if (frame.pendingDocument() && frame.pendingDocument().request === request) { let errorText = request.failure().errorText; if (canceled) errorText += '; maybe frame was detached?'; this.frameAbortedNavigation(frame._id, errorText, frame.pendingDocument().documentId); } if (request._isFavicon) return; this._page.emitOnContext(_browserContext.BrowserContext.Events.RequestFailed, request); } dialogDidOpen(dialog) { // Any ongoing evaluations will be stalled until the dialog is closed. for (const frame of this._frames.values()) frame._invalidateNonStallingEvaluations('JavaScript dialog interrupted evaluation'); if (this._closeAllOpeningDialogs) dialog.close().then(() => {});else this._openedDialogs.add(dialog); } dialogWillClose(dialog) { this._openedDialogs.delete(dialog); } async closeOpenDialogs() { await Promise.all([...this._openedDialogs].map(dialog => dialog.close())).catch(() => {}); this._openedDialogs.clear(); } setCloseAllOpeningDialogs(closeDialogs) { this._closeAllOpeningDialogs = closeDialogs; } removeChildFramesRecursively(frame) { for (const child of frame.childFrames()) this._removeFramesRecursively(child); } _removeFramesRecursively(frame) { this.removeChildFramesRecursively(frame); frame._onDetached(); this._frames.delete(frame._id); if (!this._page.isClosed()) this._page.emit(_page.Page.Events.FrameDetached, frame); } _inflightRequestFinished(request) { const frame = request.frame(); if (request._isFavicon) return; if (!frame._inflightRequests.has(request)) return; frame._inflightRequests.delete(request); if (frame._inflightRequests.size === 0) frame._startNetworkIdleTimer(); } _inflightRequestStarted(request) { const frame = request.frame(); if (request._isFavicon) return; frame._inflightRequests.add(request); if (frame._inflightRequests.size === 1) frame._stopNetworkIdleTimer(); } interceptConsoleMessage(message) { 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; } clearWebSockets(frame) { // TODO: attribute sockets to frames. if (frame.parentFrame()) return; this._webSockets.clear(); } onWebSocketCreated(requestId, url) { const ws = new network.WebSocket(this._page, url); this._webSockets.set(requestId, ws); } onWebSocketRequest(requestId) { const ws = this._webSockets.get(requestId); if (ws && ws.markAsNotified()) this._page.emit(_page.Page.Events.WebSocket, ws); } onWebSocketResponse(requestId, status, statusText) { const ws = this._webSockets.get(requestId); if (status < 400) return; if (ws) ws.error(`${statusText}: ${status}`); } onWebSocketFrameSent(requestId, opcode, data) { const ws = this._webSockets.get(requestId); if (ws) ws.frameSent(opcode, data); } webSocketFrameReceived(requestId, opcode, data) { const ws = this._webSockets.get(requestId); if (ws) ws.frameReceived(opcode, data); } webSocketClosed(requestId) { const ws = this._webSockets.get(requestId); if (ws) ws.closed(); this._webSockets.delete(requestId); } webSocketError(requestId, errorMessage) { const ws = this._webSockets.get(requestId); if (ws) ws.error(errorMessage); } _fireInternalFrameNavigation(frame, event) { frame.emit(Frame.Events.InternalNavigation, event); } } exports.FrameManager = FrameManager; class Frame extends _instrumentation.SdkObject { constructor(page, id, parentFrame) { super(page, 'frame'); this._id = void 0; this._firedLifecycleEvents = new Set(); this._firedNetworkIdleSelf = false; this._currentDocument = void 0; this._pendingDocument = void 0; this._page = void 0; this._parentFrame = void 0; this._url = ''; this._contextData = new Map(); this._childFrames = new Set(); this._name = ''; this._inflightRequests = new Set(); this._networkIdleTimer = void 0; this._setContentCounter = 0; this._detachedScope = new _utils.LongStandingScope(); this._raceAgainstEvaluationStallingEventsPromises = new Set(); this._redirectedNavigations = new Map(); // documentId -> data this.selectors = void 0; this.attribution.frame = this; this._id = id; this._page = page; this._parentFrame = parentFrame; this._currentDocument = { documentId: undefined, request: undefined }; this.selectors = new _frameSelectors.FrameSelectors(this); this._contextData.set('main', { contextPromise: new _manualPromise.ManualPromise(), context: null }); this._contextData.set('utility', { contextPromise: new _manualPromise.ManualPromise(), context: null }); this._setContext('main', null); this._setContext('utility', null); if (this._parentFrame) this._parentFrame._childFrames.add(this); this._firedLifecycleEvents.add('commit'); if (id !== kDummyFrameId) this._startNetworkIdleTimer(); } isDetached() { return this._detachedScope.isClosed(); } _onLifecycleEvent(event) { if (this._firedLifecycleEvents.has(event)) return; this._firedLifecycleEvents.add(event); this.emit(Frame.Events.AddLifecycle, event); if (this === this._page.mainFrame() && this._url !== 'about:blank') _debugLogger.debugLogger.log('api', ` "${event}" event fired`); this._page.mainFrame()._recalculateNetworkIdle(); } _onClearLifecycle() { for (const event of this._firedLifecycleEvents) this.emit(Frame.Events.RemoveLifecycle, event); this._firedLifecycleEvents.clear(); // 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(); this._page.mainFrame()._recalculateNetworkIdle(this); this._onLifecycleEvent('commit'); } setPendingDocument(documentInfo) { this._pendingDocument = documentInfo; if (documentInfo) this._invalidateNonStallingEvaluations('Navigation interrupted the evaluation'); } pendingDocument() { return this._pendingDocument; } _invalidateNonStallingEvaluations(message) { if (!this._raceAgainstEvaluationStallingEventsPromises.size) return; const error = new Error(message); for (const promise of this._raceAgainstEvaluationStallingEventsPromises) promise.reject(error); } async raceAgainstEvaluationStallingEvents(cb) { if (this._pendingDocument) throw new Error('Frame is currently attempting a navigation'); if (this._page._frameManager._openedDialogs.size) throw new Error('Open JavaScript dialog prevents evaluation'); const promise = new _manualPromise.ManualPromise(); this._raceAgainstEvaluationStallingEventsPromises.add(promise); try { return await Promise.race([cb(), promise]); } finally { this._raceAgainstEvaluationStallingEventsPromises.delete(promise); } } nonStallingRawEvaluateInExistingMainContext(expression) { return this.raceAgainstEvaluationStallingEvents(() => { const context = this._existingMainContext(); if (!context) throw new Error('Frame does not yet have a main execution context'); return context.rawEvaluateJSON(expression); }); } nonStallingEvaluateInExistingContext(expression, isFunction, world) { return this.raceAgainstEvaluationStallingEvents(() => { var _this$_contextData$ge; const context = (_this$_contextData$ge = this._contextData.get(world)) === null || _this$_contextData$ge === void 0 ? void 0 : _this$_contextData$ge.context; if (!context) throw new Error('Frame does not yet have the execution context'); return context.evaluateExpression(expression, { isFunction }); }); } _recalculateNetworkIdle(frameThatAllowsRemovingNetworkIdle) { let isNetworkIdle = this._firedNetworkIdleSelf; for (const child of this._childFrames) { child._recalculateNetworkIdle(frameThatAllowsRemovingNetworkIdle); // We require networkidle event to be fired in the whole frame subtree, and then consider it done. if (!child._firedLifecycleEvents.has('networkidle')) isNetworkIdle = false; } if (isNetworkIdle && !this._firedLifecycleEvents.has('networkidle')) { this._firedLifecycleEvents.add('networkidle'); this.emit(Frame.Events.AddLifecycle, 'networkidle'); if (this === this._page.mainFrame() && this._url !== 'about:blank') _debugLogger.debugLogger.log('api', ` "networkidle" event fired`); } if (frameThatAllowsRemovingNetworkIdle !== this && this._firedLifecycleEvents.has('networkidle') && !isNetworkIdle) { // Usually, networkidle is fired once and not removed after that. // However, when we clear them right before a new commit, this is allowed for a particular frame. this._firedLifecycleEvents.delete('networkidle'); this.emit(Frame.Events.RemoveLifecycle, 'networkidle'); } } async raceNavigationAction(progress, options, action) { return _utils.LongStandingScope.raceMultiple([this._detachedScope, this._page.openScope], action().catch(e => { if (e instanceof NavigationAbortedError && e.documentId) { const data = this._redirectedNavigations.get(e.documentId); if (data) { progress.log(`waiting for redirected navigation to "${data.url}"`); return data.gotoPromise; } } throw e; })); } redirectNavigation(url, documentId, referer) { const controller = new _progress.ProgressController((0, _instrumentation.serverSideCallMetadata)(), this); const data = { url, gotoPromise: controller.run(progress => this._gotoAction(progress, url, { referer }), 0) }; this._redirectedNavigations.set(documentId, data); data.gotoPromise.finally(() => this._redirectedNavigations.delete(documentId)); } async goto(metadata, url, options = {}) { const constructedNavigationURL = (0, _utils.constructURLBasedOnBaseURL)(this._page._browserContext._options.baseURL, url); const controller = new _progress.ProgressController(metadata, this); return controller.run(progress => this._goto(progress, constructedNavigationURL, options), this._page._timeoutSettings.navigationTimeout(options)); } async _goto(progress, url, options) { return this.raceNavigationAction(progress, options, async () => this._gotoAction(progress, url, options)); } async _gotoAction(progress, url, options) { const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil); progress.log(`navigating to "${url}", waiting until "${waitUntil}"`); const headers = this._page.extraHTTPHeaders() || []; const refererHeader = headers.find(h => h.name.toLowerCase() === 'referer'); let referer = refererHeader ? refererHeader.value : undefined; 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.helper.completeUserURL(url); const sameDocument = _helper.helper.waitForEvent(progress, this, Frame.Events.InternalNavigation, e => !e.newDocument); const navigateResult = await this._page._delegate.navigateFrame(this, url, referer); let event; if (navigateResult.newDocumentId) { sameDocument.dispose(); event = await _helper.helper.waitForEvent(progress, this, Frame.Events.InternalNavigation, event => { // We are interested either in this specific document, or any other document that // did commit and replaced the expected document. return event.newDocument && (event.newDocument.documentId === navigateResult.newDocumentId || !event.error); }).promise; if (event.newDocument.documentId !== navigateResult.newDocumentId) { // This is just a sanity check. In practice, new navigation should // cancel the previous one and report "request cancelled"-like error. throw new NavigationAbortedError(navigateResult.newDocumentId, `Navigation to "${url}" is interrupted by another navigation to "${event.url}"`); } if (event.error) throw event.error; } else { event = await sameDocument.promise; } if (!this._firedLifecycleEvents.has(waitUntil)) await _helper.helper.waitForEvent(progress, this, Frame.Events.AddLifecycle, e => e === waitUntil).promise; const request = event.newDocument ? event.newDocument.request : undefined; const response = request ? request._finalRequest().response() : null; return response; } async _waitForNavigation(progress, requiresNewDocument, options) { const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil); progress.log(`waiting for navigation until "${waitUntil}"`); const navigationEvent = await _helper.helper.waitForEvent(progress, this, Frame.Events.InternalNavigation, event => { // Any failed navigation results in a rejection. if (event.error) return true; if (requiresNewDocument && !event.newDocument) return false; progress.log(` navigated to "${this._url}"`); return true; }).promise; if (navigationEvent.error) throw navigationEvent.error; if (!this._firedLifecycleEvents.has(waitUntil)) await _helper.helper.waitForEvent(progress, this, Frame.Events.AddLifecycle, e => e === waitUntil).promise; const request = navigationEvent.newDocument ? navigationEvent.newDocument.request : undefined; return request ? request._finalRequest().response() : null; } async _waitForLoadState(progress, state) { const waitUntil = verifyLifecycle('state', state); if (!this._firedLifecycleEvents.has(waitUntil)) await _helper.helper.waitForEvent(progress, this, Frame.Events.AddLifecycle, e => e === waitUntil).promise; } async frameElement() { return this._page._delegate.getFrameElement(this); } _context(world) { return this._contextData.get(world).contextPromise.then(contextOrDestroyedReason => { if (contextOrDestroyedReason instanceof js.ExecutionContext) return contextOrDestroyedReason; throw new Error(contextOrDestroyedReason.destroyedReason); }); } _mainContext() { return this._context('main'); } _existingMainContext() { var _this$_contextData$ge2; return ((_this$_contextData$ge2 = this._contextData.get('main')) === null || _this$_contextData$ge2 === void 0 ? void 0 : _this$_contextData$ge2.context) || null; } _utilityContext() { return this._context('utility'); } async evaluateExpression(expression, options = {}, arg) { var _options$world; const context = await this._context((_options$world = options.world) !== null && _options$world !== void 0 ? _options$world : 'main'); const value = await context.evaluateExpression(expression, options, arg); return value; } async evaluateExpressionHandle(expression, options = {}, arg) { var _options$world2; const context = await this._context((_options$world2 = options.world) !== null && _options$world2 !== void 0 ? _options$world2 : 'main'); const value = await context.evaluateExpressionHandle(expression, options, arg); return value; } async querySelector(selector, options) { _debugLogger.debugLogger.log('api', ` finding element using the selector "${selector}"`); return this.selectors.query(selector, options); } async waitForSelector(metadata, selector, options, scope) { const controller = new _progress.ProgressController(metadata, this); if (options.visibility) throw new Error('options.visibility is not supported, did you mean options.state?'); if (options.waitFor && options.waitFor !== 'visible') throw new Error('options.waitFor is not supported, did you mean options.state?'); const { state = 'visible' } = options; if (!['attached', 'detached', 'visible', 'hidden'].includes(state)) throw new Error(`state: expected one of (attached|detached|visible|hidden)`); return controller.run(async progress => { progress.log(`waiting for ${this._asLocator(selector)}${state === 'attached' ? '' : ' to be ' + state}`); const promise = this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async continuePolling => { const resolved = await this.selectors.resolveInjectedForSelector(selector, options, scope); progress.throwIfAborted(); if (!resolved) { if (state === 'hidden' || state === 'detached') return null; return continuePolling; } const result = await resolved.injected.evaluateHandle((injected, { info, root }) => { const elements = injected.querySelectorAll(info.parsed, root || document); const element = elements[0]; const visible = element ? injected.isVisible(element) : false; let log = ''; if (elements.length > 1) { if (info.strict) throw injected.strictModeViolationError(info.parsed, elements); log = ` locator resolved to ${elements.length} elements. Proceeding with the first one: ${injected.previewNode(elements[0])}`; } else if (element) { log = ` locator resolved to ${visible ? 'visible' : 'hidden'} ${injected.previewNode(element)}`; } return { log, element, visible, attached: !!element }; }, { info: resolved.info, root: resolved.frame === this ? scope : undefined }); const { log, visible, attached } = await result.evaluate(r => ({ log: r.log, visible: r.visible, attached: r.attached })); if (log) progress.log(log); const success = { attached, detached: !attached, visible, hidden: !visible }[state]; if (!success) { result.dispose(); return continuePolling; } if (options.omitReturnValue) { result.dispose(); return null; } const element = state === 'attached' || state === 'visible' ? await result.evaluateHandle(r => r.element) : null; result.dispose(); if (!element) return null; if (options.__testHookBeforeAdoptNode) await options.__testHookBeforeAdoptNode(); try { return await element._adoptTo(await resolved.frame._mainContext()); } catch (e) { return continuePolling; } }); return scope ? scope._context._raceAgainstContextDestroyed(promise) : promise; }, this._page._timeoutSettings.timeout(options)); } async dispatchEvent(metadata, selector, type, eventInit = {}, options = {}, scope) { await this._callOnElementOnceMatches(metadata, selector, (injectedScript, element, data) => { injectedScript.dispatchEvent(element, data.type, data.eventInit); }, { type, eventInit }, { mainWorld: true, ...options }, scope); } async evalOnSelector(selector, strict, expression, isFunction, arg, scope) { const handle = await this.selectors.query(selector, { strict }, scope); if (!handle) throw new Error(`Failed to find element matching selector "${selector}"`); const result = await handle.evaluateExpression(expression, { isFunction }, arg); handle.dispose(); return result; } async evalOnSelectorAll(selector, expression, isFunction, arg, scope) { const arrayHandle = await this.selectors.queryArrayInMainWorld(selector, scope); const result = await arrayHandle.evaluateExpression(expression, { isFunction }, arg); arrayHandle.dispose(); return result; } async maskSelectors(selectors, color) { const context = await this._utilityContext(); const injectedScript = await context.injectedScript(); await injectedScript.evaluate((injected, { parsed, color }) => { injected.maskSelectors(parsed, color); }, { parsed: selectors, color: color }); } async querySelectorAll(selector) { return this.selectors.queryAll(selector); } async queryCount(selector) { return await this.selectors.queryCount(selector); } async content() { try { const context = await this._utilityContext(); return await context.evaluate(() => { let retVal = ''; if (document.doctype) retVal = new XMLSerializer().serializeToString(document.doctype); if (document.documentElement) retVal += document.documentElement.outerHTML; return retVal; }); } catch (e) { if (js.isJavaScriptErrorInEvaluate(e) || (0, _protocolError.isSessionClosedError)(e)) throw e; throw new Error(`Unable to retrieve content because the page is navigating and changing the content.`); } } async setContent(metadata, html, options = {}) { const controller = new _progress.ProgressController(metadata, this); return controller.run(async progress => { await this.raceNavigationAction(progress, options, async () => { const waitUntil = options.waitUntil === undefined ? 'load' : options.waitUntil; progress.log(`setting frame content, waiting until "${waitUntil}"`); 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. this._onClearLifecycle(); this._waitForLoadState(progress, waitUntil).then(resolve).catch(reject); }); }); const contentPromise = context.evaluate(({ html, tag }) => { document.open(); console.debug(tag); // eslint-disable-line no-console document.write(html); document.close(); }, { html, tag }); await Promise.all([contentPromise, lifecyclePromise]); return null; }); }, this._page._timeoutSettings.navigationTimeout(options)); } name() { return this._name || ''; } url() { return this._url; } parentFrame() { return this._parentFrame; } childFrames() { return Array.from(this._childFrames); } async addScriptTag(params) { const { url = null, content = null, type = '' } = params; if (!url && !content) 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) return (await context.evaluateHandle(addScriptUrl, { url, type })).asElement(); const result = (await context.evaluateHandle(addScriptContent, { content: content, type })).asElement(); // 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) await context.evaluate(() => true); return result; }); async function addScriptUrl(params) { const script = document.createElement('script'); script.src = params.url; if (params.type) script.type = params.type; const promise = new Promise((res, rej) => { script.onload = res; script.onerror = e => rej(typeof e === 'string' ? new Error(e) : new Error(`Failed to load script at ${script.src}`)); }); document.head.appendChild(script); await promise; return script; } function addScriptContent(params) { const script = document.createElement('script'); script.type = params.type || 'text/javascript'; script.text = params.content; let error = null; script.onerror = e => error = e; document.head.appendChild(script); if (error) throw error; return script; } } async addStyleTag(params) { const { url = null, content = null } = params; if (!url && !content) 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) return (await context.evaluateHandle(addStyleUrl, url)).asElement(); return (await context.evaluateHandle(addStyleContent, content)).asElement(); }); async function addStyleUrl(url) { 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) { 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; } } async _raceWithCSPError(func) { const listeners = []; let result; let error; let cspMessage; const actionPromise = func().then(r => result = r).catch(e => error = e); const errorPromise = new Promise(resolve => { listeners.push(_eventsHelper.eventsHelper.addEventListener(this._page._browserContext, _browserContext.BrowserContext.Events.Console, message => { if (message.page() !== this._page || message.type() !== 'error') return; if (message.text().includes('Content-Security-Policy') || message.text().includes('Content Security Policy')) { cspMessage = message; resolve(); } })); }); await Promise.race([actionPromise, errorPromise]); _eventsHelper.eventsHelper.removeEventListeners(listeners); if (cspMessage) throw new Error(cspMessage.text()); if (error) throw error; return result; } async retryWithProgressAndTimeouts(progress, timeouts, action) { const continuePolling = Symbol('continuePolling'); timeouts = [0, ...timeouts]; let timeoutIndex = 0; while (progress.isRunning()) { const timeout = timeouts[Math.min(timeoutIndex++, timeouts.length - 1)]; if (timeout) { // Make sure we react immediately upon page close or frame detach. // We need this to show expected/received values in time. const actionPromise = new Promise(f => setTimeout(f, timeout)); await _utils.LongStandingScope.raceMultiple([this._page.openScope, this._detachedScope], actionPromise); } progress.throwIfAborted(); try { const result = await action(continuePolling); if (result === continuePolling) continue; return result; } catch (e) { if (this._isErrorThatCannotBeRetried(e)) throw e; continue; } } progress.throwIfAborted(); return undefined; } _isErrorThatCannotBeRetried(e) { // Always fail on JavaScript errors or when the main connection is closed. if (js.isJavaScriptErrorInEvaluate(e) || (0, _protocolError.isSessionClosedError)(e)) return true; // Certain errors opt-out of the retries, throw. if (dom.isNonRecoverableDOMError(e) || (0, _selectorParser.isInvalidSelectorError)(e)) return true; // If the call is made on the detached frame - throw. if (this.isDetached()) return true; // Retry upon all other errors. return false; } async _retryWithProgressIfNotConnected(progress, selector, strict, action) { progress.log(`waiting for ${this._asLocator(selector)}`); return this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async continuePolling => { const resolved = await this.selectors.resolveInjectedForSelector(selector, { strict }); progress.throwIfAborted(); if (!resolved) return continuePolling; const result = await resolved.injected.evaluateHandle((injected, { info }) => { const elements = injected.querySelectorAll(info.parsed, document); const element = elements[0]; let log = ''; if (elements.length > 1) { if (info.strict) throw injected.strictModeViolationError(info.parsed, elements); log = ` locator resolved to ${elements.length} elements. Proceeding with the first one: ${injected.previewNode(elements[0])}`; } else if (element) { log = ` locator resolved to ${injected.previewNode(element)}`; } return { log, success: !!element, element }; }, { info: resolved.info }); const { log, success } = await result.evaluate(r => ({ log: r.log, success: r.success })); if (log) progress.log(log); if (!success) { result.dispose(); return continuePolling; } const element = await result.evaluateHandle(r => r.element); result.dispose(); try { const result = await action(element); if (result === 'error:notconnected') { progress.log('element was detached from the DOM, retrying'); return continuePolling; } return result; } finally { element === null || element === void 0 ? void 0 : element.dispose(); } }); } async rafrafTimeoutScreenshotElementWithProgress(progress, selector, timeout, options) { return await this._retryWithProgressIfNotConnected(progress, selector, true /* strict */, async handle => { await handle._frame.rafrafTimeout(timeout); return await this._page._screenshotter.screenshotElement(progress, handle, options); }); } async click(metadata, selector, options) { const controller = new _progress.ProgressController(metadata, this); return controller.run(async progress => { return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options.strict, handle => handle._click(progress, options))); }, this._page._timeoutSettings.timeout(options)); } async dblclick(metadata, selector, options = {}) { const controller = new _progress.ProgressController(metadata, this); return controller.run(async progress => { return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options.strict, handle => handle._dblclick(progress, options))); }, this._page._timeoutSettings.timeout(options)); } async dragAndDrop(metadata, source, target, options = {}) { const controller = new _progress.ProgressController(metadata, this); await controller.run(async progress => { dom.assertDone(await this._retryWithProgressIfNotConnected(progress, source, options.strict, 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, position: options.sourcePosition, timeout: progress.timeUntilDeadline() }); })); dom.assertDone(await this._retryWithProgressIfNotConnected(progress, target, options.strict, 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, position: options.targetPosition, timeout: progress.timeUntilDeadline() }); })); }, this._page._timeoutSettings.timeout(options)); } async tap(metadata, selector, options) { if (!this._page._browserContext._options.hasTouch) throw new Error('The page does not support tap. Use hasTouch context option to enable touch support.'); const controller = new _progress.ProgressController(metadata, this); return controller.run(async progress => { return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options.strict, handle => handle._tap(progress, options))); }, this._page._timeoutSettings.timeout(options)); } async fill(metadata, selector, value, options) { const controller = new _progress.ProgressController(metadata, this); return controller.run(async progress => { return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options.strict, handle => handle._fill(progress, value, options))); }, this._page._timeoutSettings.timeout(options)); } async focus(metadata, selector, options = {}) { const controller = new _progress.ProgressController(metadata, this); await controller.run(async progress => { dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options.strict, handle => handle._focus(progress))); }, this._page._timeoutSettings.timeout(options)); } async blur(metadata, selector, options = {}) { const controller = new _progress.ProgressController(metadata, this); await controller.run(async progress => { dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options.strict, handle => handle._blur(progress))); }, this._page._timeoutSettings.timeout(options)); } async textContent(metadata, selector, options = {}, scope) { return this._callOnElementOnceMatches(metadata, selector, (injected, element) => element.textContent, undefined, options, scope); } async innerText(metadata, selector, options = {}, scope) { return this._callOnElementOnceMatches(metadata, selector, (injectedScript, element) => { if (element.namespaceURI !== 'http://www.w3.org/1999/xhtml') throw injectedScript.createStacklessError('Node is not an HTMLElement'); return element.innerText; }, undefined, options, scope); } async innerHTML(metadata, selector, options = {}, scope) { return this._callOnElementOnceMatches(metadata, selector, (injected, element) => element.innerHTML, undefined, options, scope); } async getAttribute(metadata, selector, name, options = {}, scope) { return this._callOnElementOnceMatches(metadata, selector, (injected, element, data) => element.getAttribute(data.name), { name }, options, scope); } async inputValue(metadata, selector, options = {}, scope) { return this._callOnElementOnceMatches(metadata, selector, (injectedScript, node) => { const element = injectedScript.retarget(node, 'follow-label'); if (!element || element.nodeName !== 'INPUT' && element.nodeName !== 'TEXTAREA' && element.nodeName !== 'SELECT') throw injectedScript.createStacklessError('Node is not an ,