/**
 * 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.
 */
import { isInvalidSelectorError, stringifySelector } from '@isomorphic/selectorParser';
import { ManualPromise } from '@isomorphic/manualPromise';
import { parseEvaluationResultValue } from '@isomorphic/utilityScriptSerializers';
import { getComparator } from '@utils/comparators';
import { debugLogger } from '@utils/debugLogger';
import { LongStandingScope } from '@isomorphic/manualPromise';
import { assert } from '@isomorphic/assert';
import { renderTitleForCall } from '@isomorphic/protocolFormatter';
import { trimStringWithEllipsis } from '@isomorphic/stringUtils';
import { asLocator } from '@isomorphic/locatorGenerators';
import { BrowserContext } from './browserContext';
import { DisposableObject } from './disposable';
import { ConsoleMessage } from './console';
import { TargetClosedError, TimeoutError } from './errors';
import { FileChooser } from './fileChooser';
import * as frames from './frames';
import { helper } from './helper';
import * as input from './input';
import { SdkObject } from './instrumentation';
import * as js from './javascript';
import { Screenshotter, validateScreenshotOptions } from './screenshotter';
import { compressCallLog } from './callLog';
import * as rawBindingsControllerSource from '../generated/bindingsControllerSource';
import { Overlay } from './overlay';
import { NonRecoverableDOMError } from './dom';
import { Screencast } from './screencast';
const PageEvent = {
    Close: 'close',
    Crash: 'crash',
    Download: 'download',
    EmulatedSizeChanged: 'emulatedsizechanged',
    FileChooser: 'filechooser',
    FrameAttached: 'frameattached',
    FrameDetached: 'framedetached',
    InternalFrameNavigatedToNewDocument: 'internalframenavigatedtonewdocument',
    LocatorHandlerTriggered: 'locatorhandlertriggered',
    WebSocket: 'websocket',
    Worker: 'worker',
};
const navigationMarkSymbol = Symbol('navigationMark');
export class Page extends SdkObject {
    static Events = PageEvent;
    _closedState = 'open';
    closedPromise = new ManualPromise();
    _initialized;
    _initializedPromise = new ManualPromise();
    _consoleMessages = [];
    _pageErrors = [];
    _crashed = false;
    openScope = new LongStandingScope();
    browserContext;
    keyboard;
    mouse;
    touchscreen;
    delegate;
    _emulatedSize;
    _extraHTTPHeaders;
    _emulatedMedia = {};
    _fileChooserInterceptedBy = new Set();
    _pageBindings = new Map();
    initScripts = [];
    screenshotter;
    frameManager;
    _workers = new Map();
    pdf;
    coverage;
    requestInterceptors = [];
    video;
    _opener;
    isStorageStatePage;
    _locatorHandlers = new Map();
    _lastLocatorHandlerUid = 0;
    _locatorHandlerRunningCounter = 0;
    _networkRequests = [];
    overlay;
    screencast;
    _closeReason;
    constructor(delegate, browserContext) {
        super(browserContext, 'page');
        this.attribution.page = this;
        this.delegate = delegate;
        this.browserContext = browserContext;
        this.keyboard = new input.Keyboard(delegate.rawKeyboard, this);
        this.mouse = new input.Mouse(delegate.rawMouse, this);
        this.touchscreen = new input.Touchscreen(delegate.rawTouchscreen, this);
        this.screenshotter = new Screenshotter(this);
        this.frameManager = new frames.FrameManager(this);
        this.overlay = new Overlay(this);
        this.screencast = new Screencast(this);
        if (delegate.pdf)
            this.pdf = delegate.pdf.bind(delegate);
        this.coverage = delegate.coverage ? delegate.coverage() : null;
        this.isStorageStatePage = browserContext.isCreatingStorageStatePage();
    }
    async reportAsNew(opener, error) {
        if (opener) {
            const openerPageOrError = await opener.waitForInitializedOrError();
            if (openerPageOrError instanceof Page && !openerPageOrError.isClosed())
                this._opener = openerPageOrError;
        }
        this._markInitialized(error);
    }
    _markInitialized(error = undefined) {
        if (error) {
            // Initialization error could have happened because of
            // context/browser closure. Just ignore the page.
            if (this.browserContext.isClosingOrClosed())
                return;
            this.frameManager.createDummyMainFrameIfNeeded();
        }
        this._initialized = error || this;
        this.emitOnContext(BrowserContext.Events.Page, this);
        for (const pageError of this._pageErrors)
            this.emitOnContext(BrowserContext.Events.PageError, pageError, this);
        for (const message of this._consoleMessages)
            this.emitOnContext(BrowserContext.Events.Console, message);
        // It may happen that page initialization finishes after Close event has already been sent,
        // in that case we fire another Close event to ensure that each reported Page will have
        // corresponding Close event after it is reported on the context.
        if (this.isClosed())
            this.emit(Page.Events.Close);
        else
            this.instrumentation.onPageOpen(this);
        // Note: it is important to resolve _initializedPromise at the end,
        // so that anyone who awaits waitForInitializedOrError got a ready and reported page.
        this._initializedPromise.resolve(this._initialized);
    }
    initializedOrUndefined() {
        return this._initialized ? this : undefined;
    }
    waitForInitializedOrError() {
        return this._initializedPromise;
    }
    emitOnContext(event, ...args) {
        if (this.isStorageStatePage)
            return;
        this.browserContext.emit(event, ...args);
    }
    async resetForReuse(progress) {
        // Re-navigate once init scripts are gone.
        await this.mainFrame().gotoImpl(progress, 'about:blank', {});
        this._emulatedSize = undefined;
        this._emulatedMedia = {};
        this._extraHTTPHeaders = undefined;
        await progress.race(Promise.all([
            this.delegate.updateEmulatedViewportSize(),
            this.delegate.updateEmulateMedia(),
            this.delegate.updateExtraHTTPHeaders(),
        ]));
        await this.delegate.resetForReuse(progress);
    }
    _didClose() {
        this.frameManager.dispose();
        this.screencast.dispose();
        this.overlay.dispose();
        assert(this._closedState !== 'closed', 'Page closed twice');
        this._closedState = 'closed';
        this.emit(Page.Events.Close);
        this.browserContext.emit(BrowserContext.Events.PageClosed, this);
        this.closedPromise.resolve();
        this.instrumentation.onPageClose(this);
        this.openScope.close(new TargetClosedError(this.closeReason()));
    }
    _didCrash() {
        this.frameManager.dispose();
        this.screencast.dispose();
        this.overlay.dispose();
        this.emit(Page.Events.Crash);
        this._crashed = true;
        this.instrumentation.onPageClose(this);
        this.openScope.close(new Error('Page crashed'));
    }
    async _onFileChooserOpened(handle) {
        let multiple;
        try {
            multiple = await handle.evaluate(element => !!element.multiple);
        }
        catch (e) {
            // Frame/context may be gone during async processing. Do not throw.
            return;
        }
        if (!this.listenerCount(Page.Events.FileChooser)) {
            handle.dispose();
            return;
        }
        const fileChooser = new FileChooser(handle, multiple);
        this.emit(Page.Events.FileChooser, fileChooser);
    }
    opener() {
        return this._opener;
    }
    mainFrame() {
        return this.frameManager.mainFrame();
    }
    frames() {
        return this.frameManager.frames();
    }
    async exposeBinding(progress, name, playwrightBinding) {
        if (this._pageBindings.has(name))
            throw new Error(`Function "${name}" has been already registered`);
        if (this.browserContext._pageBindings.has(name))
            throw new Error(`Function "${name}" has been already registered in the browser context`);
        await progress.race(this.browserContext.exposePlaywrightBindingIfNeeded());
        const binding = new PageBinding(this, name, playwrightBinding);
        this._pageBindings.set(name, binding);
        try {
            await progress.race(this.delegate.addInitScript(binding.initScript));
            await progress.race(this.safeNonStallingEvaluateInAllFrames(binding.initScript.source, 'main'));
            return binding;
        }
        catch (error) {
            this._pageBindings.delete(name);
            throw error;
        }
    }
    async removeExposedBinding(binding) {
        if (this._pageBindings.get(binding.name) !== binding)
            return;
        this._pageBindings.delete(binding.name);
        await this.delegate.removeInitScripts([binding.initScript]);
        const cleanup = `{ ${binding.cleanupScript} };`;
        await this.safeNonStallingEvaluateInAllFrames(cleanup, 'main');
    }
    async setExtraHTTPHeaders(progress, headers) {
        const oldHeaders = this._extraHTTPHeaders;
        try {
            this._extraHTTPHeaders = headers;
            await progress.race(this.delegate.updateExtraHTTPHeaders());
        }
        catch (error) {
            this._extraHTTPHeaders = oldHeaders;
            // Note: no await, headers will be updated in the background as soon as possible.
            this.delegate.updateExtraHTTPHeaders().catch(() => { });
            throw error;
        }
    }
    extraHTTPHeaders() {
        return this._extraHTTPHeaders;
    }
    addNetworkRequest(request) {
        this._networkRequests.push(request);
        ensureArrayLimit(this._networkRequests, 100);
    }
    networkRequests() {
        return this._networkRequests;
    }
    async onBindingCalled(payload, context) {
        if (this._closedState === 'closed')
            return;
        await PageBinding.dispatch(this, payload, context);
    }
    addConsoleMessage(worker, type, args, location, text, timestamp) {
        const message = new ConsoleMessage(this, worker, type, text, args, location, timestamp);
        const intercepted = this.frameManager.interceptConsoleMessage(message);
        if (intercepted) {
            args.forEach(arg => arg.dispose());
            return;
        }
        this._consoleMessages.push(message);
        ensureArrayLimit(this._consoleMessages, 200); // Avoid unbounded memory growth.
        // Console messages may come before the page is ready. In this case,
        // we'll dispatch them to the client later, either on the live Page,
        // or on the "errored" Page.
        if (this._initialized)
            this.emitOnContext(BrowserContext.Events.Console, message);
    }
    clearConsoleMessages() {
        this._consoleMessages.length = 0;
    }
    consoleMessages(filter) {
        if (filter === 'all')
            return this._consoleMessages;
        const marked = this._consoleMessages.findLastIndex(m => m[navigationMarkSymbol]);
        return marked === -1 ? this._consoleMessages : this._consoleMessages.slice(marked + 1);
    }
    addPageError(error, location) {
        const pageError = { error, location };
        this._pageErrors.push(pageError);
        ensureArrayLimit(this._pageErrors, 200); // Avoid unbounded memory growth.
        // Page errors may come before the page is ready. In this case,
        // we'll dispatch them to the client later, either on the live Page,
        // or on the "errored" Page.
        if (this._initialized)
            this.emitOnContext(BrowserContext.Events.PageError, pageError, this);
    }
    clearPageErrors() {
        this._pageErrors.length = 0;
    }
    pageErrors(filter) {
        if (filter === 'all')
            return this._pageErrors.map(e => e.error);
        const marked = this._pageErrors.findLastIndex(e => e[navigationMarkSymbol]);
        return (marked === -1 ? this._pageErrors : this._pageErrors.slice(marked + 1)).map(e => e.error);
    }
    async reload(progress, options) {
        return this.mainFrame().raceNavigationAction(progress, async () => {
            // Note: waitForNavigation may fail before we get response to reload(),
            // so we should await it immediately.
            const [response] = await Promise.all([
                // Reload must be a new document, and should not be confused with a stray pushState.
                this.mainFrame().waitForNavigation(progress, true /* requiresNewDocument */, options),
                progress.race(this.delegate.reload()),
            ]);
            return response;
        });
    }
    async goBack(progress, options) {
        return this.mainFrame().raceNavigationAction(progress, async () => {
            // Note: waitForNavigation may fail before we get response to goBack,
            // so we should catch it immediately.
            let error;
            const waitPromise = this.mainFrame().waitForNavigation(progress, false /* requiresNewDocument */, options).catch(e => {
                error = e;
                return null;
            });
            const result = await progress.race(this.delegate.goBack());
            if (!result) {
                waitPromise.catch(() => { }); // Avoid an unhandled rejection.
                return null;
            }
            const response = await waitPromise;
            if (error)
                throw error;
            return response;
        });
    }
    async goForward(progress, options) {
        return this.mainFrame().raceNavigationAction(progress, async () => {
            // Note: waitForNavigation may fail before we get response to goForward,
            // so we should catch it immediately.
            let error;
            const waitPromise = this.mainFrame().waitForNavigation(progress, false /* requiresNewDocument */, options).catch(e => {
                error = e;
                return null;
            });
            const result = await progress.race(this.delegate.goForward());
            if (!result) {
                waitPromise.catch(() => { }); // Avoid an unhandled rejection.
                return null;
            }
            const response = await waitPromise;
            if (error)
                throw error;
            return response;
        });
    }
    requestGC(progress) {
        return progress.race(this.delegate.requestGC());
    }
    registerLocatorHandler(selector, noWaitAfter) {
        const uid = ++this._lastLocatorHandlerUid;
        this._locatorHandlers.set(uid, { selector, noWaitAfter });
        return uid;
    }
    resolveLocatorHandler(uid, remove) {
        const handler = this._locatorHandlers.get(uid);
        if (remove)
            this._locatorHandlers.delete(uid);
        if (handler) {
            handler.resolved?.resolve();
            handler.resolved = undefined;
        }
    }
    unregisterLocatorHandler(uid) {
        this._locatorHandlers.delete(uid);
    }
    async performActionPreChecks(progress) {
        await this._performWaitForNavigationCheck(progress);
        await this._performLocatorHandlersCheckpoint(progress);
        // Wait once again, just in case a locator handler caused a navigation.
        await this._performWaitForNavigationCheck(progress);
    }
    async _performWaitForNavigationCheck(progress) {
        if (process.env.PLAYWRIGHT_SKIP_NAVIGATION_CHECK)
            return;
        const mainFrame = this.frameManager.mainFrame();
        if (!mainFrame || !mainFrame.pendingDocument())
            return;
        const url = mainFrame.pendingDocument()?.request?.url();
        const toUrl = url ? `" ${trimStringWithEllipsis(url, 200)}"` : '';
        progress.log(`  waiting for${toUrl} navigation to finish...`);
        await helper.waitForEvent(progress, mainFrame, frames.Frame.Events.InternalNavigation, (e) => {
            if (!e.isPublic)
                return false;
            if (!e.error)
                progress.log(`  navigated to "${trimStringWithEllipsis(mainFrame.url(), 200)}"`);
            return true;
        }).promise;
    }
    async _performLocatorHandlersCheckpoint(progress) {
        // Do not run locator handlers from inside locator handler callbacks to avoid deadlocks.
        if (this._locatorHandlerRunningCounter)
            return;
        for (const [uid, handler] of this._locatorHandlers) {
            if (!handler.resolved) {
                if (await this.mainFrame().isVisibleInternal(progress, handler.selector, { strict: true })) {
                    handler.resolved = new ManualPromise();
                    this.emit(Page.Events.LocatorHandlerTriggered, uid);
                }
            }
            if (handler.resolved) {
                ++this._locatorHandlerRunningCounter;
                progress.log(`  found ${asLocator(this.browserContext._browser.sdkLanguage(), handler.selector)}, intercepting action to run the handler`);
                const promise = handler.resolved.then(async () => {
                    if (!handler.noWaitAfter) {
                        progress.log(`  locator handler has finished, waiting for ${asLocator(this.browserContext._browser.sdkLanguage(), handler.selector)} to be hidden`);
                        await this.mainFrame().waitForSelector(progress, handler.selector, false, { state: 'hidden' });
                    }
                    else {
                        progress.log(`  locator handler has finished`);
                    }
                });
                try {
                    progress.setAllowConcurrentOrNestedRaces(true);
                    await progress.race(this.openScope.race(promise));
                }
                finally {
                    progress.setAllowConcurrentOrNestedRaces(false);
                    --this._locatorHandlerRunningCounter;
                }
                progress.log(`  interception handler has finished, continuing`);
            }
        }
    }
    async emulateMedia(progress, options) {
        const oldEmulatedMedia = { ...this._emulatedMedia };
        if (options.media !== undefined)
            this._emulatedMedia.media = options.media;
        if (options.colorScheme !== undefined)
            this._emulatedMedia.colorScheme = options.colorScheme;
        if (options.reducedMotion !== undefined)
            this._emulatedMedia.reducedMotion = options.reducedMotion;
        if (options.forcedColors !== undefined)
            this._emulatedMedia.forcedColors = options.forcedColors;
        if (options.contrast !== undefined)
            this._emulatedMedia.contrast = options.contrast;
        try {
            await progress.race(this.delegate.updateEmulateMedia());
        }
        catch (error) {
            this._emulatedMedia = oldEmulatedMedia;
            // Note: no await, emulated media will be updated in the background as soon as possible.
            this.delegate.updateEmulateMedia().catch(() => { });
            throw error;
        }
    }
    emulatedMedia() {
        const contextOptions = this.browserContext._options;
        return {
            media: this._emulatedMedia.media || 'no-override',
            colorScheme: this._emulatedMedia.colorScheme !== undefined ? this._emulatedMedia.colorScheme : contextOptions.colorScheme ?? 'light',
            reducedMotion: this._emulatedMedia.reducedMotion !== undefined ? this._emulatedMedia.reducedMotion : contextOptions.reducedMotion ?? 'no-preference',
            forcedColors: this._emulatedMedia.forcedColors !== undefined ? this._emulatedMedia.forcedColors : contextOptions.forcedColors ?? 'none',
            contrast: this._emulatedMedia.contrast !== undefined ? this._emulatedMedia.contrast : contextOptions.contrast ?? 'no-preference',
        };
    }
    async setViewportSize(progress, viewportSize) {
        const oldEmulatedSize = this._emulatedSize;
        try {
            this._setEmulatedSize({ viewport: { ...viewportSize }, screen: { ...viewportSize } });
            await progress.race(this.delegate.updateEmulatedViewportSize());
        }
        catch (error) {
            this._emulatedSize = oldEmulatedSize;
            // Note: no await, emulated size will be updated in the background as soon as possible.
            this.delegate.updateEmulatedViewportSize().catch(() => { });
            throw error;
        }
    }
    setEmulatedSizeFromWindowOpen(emulatedSize) {
        this._setEmulatedSize(emulatedSize);
    }
    _setEmulatedSize(emulatedSize) {
        this._emulatedSize = emulatedSize;
        this.emit(Page.Events.EmulatedSizeChanged);
    }
    emulatedSize() {
        if (this._emulatedSize)
            return this._emulatedSize;
        const contextOptions = this.browserContext._options;
        return contextOptions.viewport ? { viewport: contextOptions.viewport, screen: contextOptions.screen || contextOptions.viewport } : undefined;
    }
    async bringToFront(progress) {
        await progress.race(this.delegate.bringToFront());
    }
    async addInitScript(progress, source) {
        return await progress.race(this._addInitScript(source));
    }
    async _addInitScript(source) {
        const initScript = new InitScript(this, source);
        this.initScripts.push(initScript);
        try {
            await this.delegate.addInitScript(initScript);
        }
        catch (error) {
            // Note: no await, script will be removed in the background as soon as possible.
            initScript.dispose().catch(() => { });
            throw error;
        }
        return initScript;
    }
    async removeInitScript(initScript) {
        this.initScripts = this.initScripts.filter(script => initScript !== script);
        await this.delegate.removeInitScripts([initScript]);
    }
    needsRequestInterception() {
        return this.requestInterceptors.length > 0 || this.browserContext.requestInterceptors.length > 0;
    }
    async addRequestInterceptor(progress, handler, prepend) {
        // Note: progress is intentionally ignored, because this operation is not cancellable and should not block in the browser anyway.
        if (prepend)
            this.requestInterceptors.unshift(handler);
        else
            this.requestInterceptors.push(handler);
        await progress.race(this.delegate.updateRequestInterception());
    }
    async removeRequestInterceptor(handler) {
        const index = this.requestInterceptors.indexOf(handler);
        if (index === -1)
            return;
        this.requestInterceptors.splice(index, 1);
        await this.browserContext.notifyRoutesInFlightAboutRemovedHandler(handler);
        await this.delegate.updateRequestInterception();
    }
    async expectScreenshot(progress, options) {
        const locator = options.locator;
        const rafrafScreenshot = locator ? async (progress, timeout) => {
            return await locator.frame.rafrafTimeoutScreenshotElementWithProgress(progress, locator.selector, timeout, options || {});
        } : async (progress, timeout) => {
            await this.performActionPreChecks(progress);
            await this.mainFrame().rafrafTimeout(progress, timeout);
            return await this.screenshotter.screenshotPage(progress, options || {});
        };
        const comparator = getComparator('image/png');
        if (!options.expected && options.isNot)
            return { errorMessage: '"not" matcher requires expected result' };
        try {
            const format = validateScreenshotOptions(options || {});
            if (format !== 'png')
                throw new Error('Only PNG screenshots are supported');
        }
        catch (error) {
            return { errorMessage: error.message };
        }
        let intermediateResult;
        const areEqualScreenshots = (actual, expected, previous) => {
            const comparatorResult = actual && expected ? comparator(actual, expected, options) : undefined;
            if (comparatorResult !== undefined && !!comparatorResult === !!options.isNot)
                return true;
            if (comparatorResult)
                intermediateResult = { errorMessage: comparatorResult.errorMessage, diff: comparatorResult.diff, actual, previous };
            return false;
        };
        try {
            let actual;
            let previous;
            const pollIntervals = [0, 100, 250, 500];
            progress.log(`${renderTitleForCall(progress.metadata)}${options.timeout ? ` with timeout ${options.timeout}ms` : ''}`);
            if (options.expected)
                progress.log(`  verifying given screenshot expectation`);
            else
                progress.log(`  generating new stable screenshot expectation`);
            let isFirstIteration = true;
            while (true) {
                if (this.isClosed())
                    throw new Error('The page has closed');
                const screenshotTimeout = pollIntervals.shift() ?? 1000;
                if (screenshotTimeout)
                    progress.log(`waiting ${screenshotTimeout}ms before taking screenshot`);
                previous = actual;
                actual = await rafrafScreenshot(progress, screenshotTimeout).catch(e => {
                    if (this.mainFrame().isNonRetriableError(e))
                        throw e;
                    progress.log(`failed to take screenshot - ` + e.message);
                    return undefined;
                });
                if (!actual)
                    continue;
                // Compare against expectation for the first iteration.
                const expectation = options.expected && isFirstIteration ? options.expected : previous;
                if (areEqualScreenshots(actual, expectation, previous))
                    break;
                if (intermediateResult)
                    progress.log(intermediateResult.errorMessage);
                isFirstIteration = false;
            }
            if (!isFirstIteration)
                progress.log(`captured a stable screenshot`);
            if (!options.expected)
                return { actual };
            if (isFirstIteration) {
                progress.log(`screenshot matched expectation`);
                return {};
            }
            if (areEqualScreenshots(actual, options.expected, undefined)) {
                progress.log(`screenshot matched expectation`);
                return {};
            }
            throw new Error(intermediateResult.errorMessage);
        }
        catch (e) {
            // Q: Why not throw upon isNonRetriableError(e) as in other places?
            // A: We want user to receive a friendly diff between actual and expected/previous.
            if (js.isJavaScriptErrorInEvaluate(e) || isInvalidSelectorError(e))
                throw e;
            let errorMessage = e.message;
            if (e instanceof TimeoutError && intermediateResult?.previous)
                errorMessage = `Failed to take two consecutive stable screenshots.`;
            return {
                log: compressCallLog(e.message ? [...progress.metadata.log, e.message] : progress.metadata.log),
                ...intermediateResult,
                errorMessage,
                timedOut: (e instanceof TimeoutError),
            };
        }
    }
    async screenshot(progress, options) {
        return await this.screenshotter.screenshotPage(progress, options);
    }
    async close(progress, options = {}) {
        await progress.race(this._close(options));
    }
    async _close(options = {}) {
        if (this._closedState === 'closed')
            return;
        if (options.reason)
            this._closeReason = options.reason;
        const runBeforeUnload = !!options.runBeforeUnload;
        if (!runBeforeUnload)
            await this.screencast.handlePageOrContextClose();
        if (this._closedState !== 'closing') {
            // If runBeforeUnload is true, we don't know if we will close, so don't modify the state
            if (!runBeforeUnload)
                this._closedState = 'closing';
            // This might throw if the browser context containing the page closes
            // while we are trying to close the page.
            await this.delegate.closePage(runBeforeUnload).catch(e => debugLogger.log('error', e));
        }
        if (!runBeforeUnload)
            await this.closedPromise;
    }
    isClosed() {
        return this._closedState === 'closed';
    }
    hasCrashed() {
        return this._crashed;
    }
    isClosedOrClosingOrCrashed() {
        return this._closedState !== 'open' || this._crashed;
    }
    addWorker(workerId, worker) {
        this._workers.set(workerId, worker);
        this.emit(Page.Events.Worker, worker);
    }
    removeWorker(workerId) {
        const worker = this._workers.get(workerId);
        if (!worker)
            return;
        worker.didClose();
        this._workers.delete(workerId);
    }
    clearWorkers() {
        for (const [workerId, worker] of this._workers) {
            worker.didClose();
            this._workers.delete(workerId);
        }
    }
    async setFileChooserInterceptedBy(progress, enabled, by) {
        await progress.race(this._setFileChooserInterceptedBy(enabled, by));
    }
    async _setFileChooserInterceptedBy(enabled, by) {
        const wasIntercepted = this.fileChooserIntercepted();
        if (enabled)
            this._fileChooserInterceptedBy.add(by);
        else
            this._fileChooserInterceptedBy.delete(by);
        if (wasIntercepted !== this.fileChooserIntercepted())
            await this.delegate.updateFileChooserInterception();
    }
    fileChooserIntercepted() {
        return this._fileChooserInterceptedBy.size > 0;
    }
    frameNavigatedToNewDocument(frame) {
        this.emit(Page.Events.InternalFrameNavigatedToNewDocument, frame);
        this.browserContext.emit(BrowserContext.Events.InternalFrameNavigatedToNewDocument, frame, this);
        const origin = frame.origin();
        if (origin)
            this.browserContext.addVisitedOrigin(origin);
        if (frame === this.mainFrame()) {
            if (this._consoleMessages.length > 0)
                this._consoleMessages[this._consoleMessages.length - 1][navigationMarkSymbol] = true;
            if (this._pageErrors.length > 0)
                this._pageErrors[this._pageErrors.length - 1][navigationMarkSymbol] = true;
        }
    }
    allInitScripts() {
        const bindings = [...this.browserContext._pageBindings.values(), ...this._pageBindings.values()].map(binding => binding.initScript);
        if (this.browserContext.bindingsInitScript)
            bindings.unshift(this.browserContext.bindingsInitScript);
        return [...bindings, ...this.browserContext.initScripts, ...this.initScripts];
    }
    getBinding(name) {
        return this._pageBindings.get(name) || this.browserContext._pageBindings.get(name);
    }
    async safeNonStallingEvaluateInAllFrames(expression, world, options = {}) {
        await Promise.all(this.frames().map(async (frame) => {
            try {
                await frame.nonStallingEvaluateInExistingContext(expression, world);
            }
            catch (e) {
                if (options.throwOnJSErrors && js.isJavaScriptErrorInEvaluate(e))
                    throw e;
            }
        }));
    }
    async hideHighlight() {
        await Promise.all(this.frames().map(frame => frame.hideHighlight().catch(() => { })));
    }
    async setDockTile(image) {
        await this.delegate.setDockTile(image);
    }
}
export const WorkerEvent = {
    Console: 'console',
    Close: 'close',
};
export class Worker extends SdkObject {
    static Events = WorkerEvent;
    url;
    _onDisconnect;
    _executionContextPromise = new ManualPromise();
    _workerScriptLoaded = false;
    existingExecutionContext = null;
    openScope = new LongStandingScope();
    _closeReason;
    constructor(parent, url, onDisconnect) {
        super(parent, 'worker');
        this.attribution.worker = this;
        this.url = url;
        this._onDisconnect = onDisconnect;
    }
    createExecutionContext(delegate) {
        this.existingExecutionContext = new js.ExecutionContext(this, delegate, 'worker');
        if (this._workerScriptLoaded)
            this._executionContextPromise.resolve(this.existingExecutionContext);
        return this.existingExecutionContext;
    }
    destroyExecutionContext(errorMessage) {
        if (this.existingExecutionContext)
            this.existingExecutionContext.contextDestroyed(errorMessage);
        this.existingExecutionContext = null;
        this._workerScriptLoaded = false;
        this._executionContextPromise = new ManualPromise();
    }
    workerScriptLoaded() {
        this._workerScriptLoaded = true;
        if (this.existingExecutionContext)
            this._executionContextPromise.resolve(this.existingExecutionContext);
    }
    didClose() {
        if (this.existingExecutionContext)
            this.existingExecutionContext.contextDestroyed('Worker was closed');
        this.emit(Worker.Events.Close, this);
        this.openScope.close(new Error('Worker closed'));
    }
    async evaluateExpression(progress, expression, isFunction, arg) {
        return progress.race(js.evaluateExpression(await this._executionContextPromise, expression, { returnByValue: true, isFunction }, arg));
    }
    async evaluateExpressionHandle(progress, expression, isFunction, arg) {
        return progress.race(js.evaluateExpression(await this._executionContextPromise, expression, { returnByValue: false, isFunction }, arg));
    }
    async disconnect(progress, options = {}) {
        if (!this._onDisconnect)
            throw new Error('Cannot disconnect from this worker');
        this._closeReason = options.reason;
        await progress.race(this._onDisconnect());
    }
}
export class PageBinding extends DisposableObject {
    static kController = '__playwright__binding__controller__';
    static kBindingName = '__playwright__binding__';
    static createInitScript(browserContext) {
        return new InitScript(browserContext, `
      (() => {
        const module = {};
        ${rawBindingsControllerSource.source}
        const property = '${PageBinding.kController}';
        if (!globalThis[property])
          globalThis[property] = new (module.exports.BindingsController())(globalThis, '${PageBinding.kBindingName}');
      })();
    `);
    }
    name;
    playwrightFunction;
    initScript;
    cleanupScript;
    forClient;
    constructor(parent, name, playwrightFunction) {
        super(parent);
        this.name = name;
        this.playwrightFunction = playwrightFunction;
        this.initScript = new InitScript(parent, `globalThis['${PageBinding.kController}'].addBinding(${JSON.stringify(name)})`);
        this.cleanupScript = `globalThis['${PageBinding.kController}'].removeBinding(${JSON.stringify(name)})`;
    }
    static async dispatch(page, payload, context) {
        const { name, seq, serializedArgs } = JSON.parse(payload);
        try {
            assert(context.world);
            const binding = page.getBinding(name);
            if (!binding)
                throw new Error(`Function "${name}" is not exposed`);
            if (!Array.isArray(serializedArgs))
                throw new Error(`serializedArgs is not an array. This can happen when Array.prototype.toJSON is defined incorrectly`);
            const args = serializedArgs.map(a => parseEvaluationResultValue(a));
            const result = await binding.playwrightFunction({ frame: context.frame, page, context: page.browserContext }, ...args);
            context.evaluateExpressionHandle(`arg => globalThis['${PageBinding.kController}'].deliverBindingResult(arg)`, { isFunction: true }, { name, seq, result }).catch(e => debugLogger.log('error', e));
        }
        catch (error) {
            context.evaluateExpressionHandle(`arg => globalThis['${PageBinding.kController}'].deliverBindingResult(arg)`, { isFunction: true }, { name, seq, error }).catch(e => debugLogger.log('error', e));
        }
    }
    async dispose() {
        await this.parent.removeExposedBinding(this);
    }
}
export class InitScript extends DisposableObject {
    source;
    constructor(owner, source) {
        super(owner);
        this.source = `(() => {
      ${source}
    })();`;
    }
    async dispose() {
        await this.parent.removeInitScript(this);
    }
}
export async function ariaSnapshotForFrame(progress, frame, options = {}) {
    // Only await the topmost navigations, inner frames will be empty when racing.
    const snapshot = await frame.retryWithProgressAndTimeouts(progress, [1000, 2000, 4000, 8000], async (progress, continuePolling) => {
        try {
            const context = await progress.race(frame.utilityContext());
            const injectedScript = await progress.race(context.injectedScript());
            const snapshotOrRetry = await progress.race(injectedScript.evaluate((injected, options) => {
                if (options.info) {
                    const element = injected.querySelector(options.info.parsed, injected.document, options.info.strict);
                    if (!element)
                        return false;
                    return injected.incrementalAriaSnapshot(element, options);
                }
                const node = injected.document.body;
                if (!node)
                    return true;
                return injected.incrementalAriaSnapshot(node, options);
            }, {
                mode: options.mode ?? 'default',
                refPrefix: frame.seq ? 'f' + frame.seq : '',
                track: options.track,
                doNotRenderActive: options.doNotRenderActive,
                info: options.info,
                depth: options.depth,
                boxes: options.boxes,
            }));
            if (snapshotOrRetry === true)
                return continuePolling;
            if (snapshotOrRetry === false)
                throw new NonRecoverableDOMError(`Selector "${stringifySelector(options.info.parsed)}" does not match any element`);
            return snapshotOrRetry;
        }
        catch (e) {
            if (frame.isNonRetriableError(e))
                throw e;
            return continuePolling;
        }
    });
    // Only fetch child snapshots for iframes that were actually rendered (not filtered by depth).
    const renderedIframeRefs = snapshot.iframeRefs.filter(ref => ref in snapshot.iframeDepths);
    progress.setAllowConcurrentOrNestedRaces(true);
    const childSnapshotPromises = renderedIframeRefs.map(ref => {
        const iframeDepth = snapshot.iframeDepths[ref];
        const childDepth = options.depth ? options.depth - iframeDepth - 1 : undefined;
        return ariaSnapshotFrameRef(progress, frame, ref, { ...options, depth: childDepth });
    });
    const childSnapshots = await Promise.all(childSnapshotPromises);
    progress.setAllowConcurrentOrNestedRaces(false);
    const full = [];
    let incremental;
    if (snapshot.incremental !== undefined) {
        incremental = snapshot.incremental.split('\n');
        for (let i = 0; i < renderedIframeRefs.length; i++) {
            const childSnapshot = childSnapshots[i];
            if (childSnapshot.incremental)
                incremental.push(...childSnapshot.incremental);
            else if (childSnapshot.full.length)
                incremental.push('- <changed> iframe [ref=' + renderedIframeRefs[i] + ']:', ...childSnapshot.full.map(l => '  ' + l));
        }
    }
    for (const line of snapshot.full.split('\n')) {
        const match = line.match(/^(\s*)- iframe (?:\[active\] )?\[ref=([^\]]*)\]/);
        if (!match) {
            full.push(line);
            continue;
        }
        const leadingSpace = match[1];
        const ref = match[2];
        const childSnapshot = childSnapshots[renderedIframeRefs.indexOf(ref)] ?? { full: [] };
        full.push(childSnapshot.full.length ? line + ':' : line);
        full.push(...childSnapshot.full.map(l => leadingSpace + '  ' + l));
    }
    return { full, incremental };
}
async function ariaSnapshotFrameRef(progress, parentFrame, frameRef, options) {
    const frameSelector = `aria-ref=${frameRef} >> internal:control=enter-frame`;
    const frameBodySelector = `${frameSelector} >> body`;
    const child = await progress.race(parentFrame.selectors.resolveFrameForSelector(frameBodySelector, { strict: true }));
    if (!child)
        return { full: [] };
    try {
        return await ariaSnapshotForFrame(progress, child.frame, { ...options, info: undefined });
    }
    catch {
        return { full: [] };
    }
}
function ensureArrayLimit(array, limit) {
    if (array.length > limit)
        return array.splice(0, limit / 10);
    return [];
}
//# sourceMappingURL=page.js.map