"use strict";
/**
 * Copyright 2019 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.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.FFPage = void 0;
const dialog = require("../dialog");
const dom = require("../dom");
const helper_1 = require("../helper");
const utils_1 = require("../../utils/utils");
const page_1 = require("../page");
const screenshotter_1 = require("../screenshotter");
const ffAccessibility_1 = require("./ffAccessibility");
const ffConnection_1 = require("./ffConnection");
const ffExecutionContext_1 = require("./ffExecutionContext");
const ffInput_1 = require("./ffInput");
const ffNetworkManager_1 = require("./ffNetworkManager");
const stackTrace_1 = require("../../utils/stackTrace");
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
class FFPage {
    constructor(session, browserContext, opener) {
        this.cspErrorsAsynchronousForInlineScipts = true;
        this._pageCallback = () => { };
        this._initializedPage = null;
        this._workers = new Map();
        this._session = session;
        this._opener = opener;
        this.rawKeyboard = new ffInput_1.RawKeyboardImpl(session);
        this.rawMouse = new ffInput_1.RawMouseImpl(session);
        this.rawTouchscreen = new ffInput_1.RawTouchscreenImpl(session);
        this._contextIdToContext = new Map();
        this._browserContext = browserContext;
        this._page = new page_1.Page(this, browserContext);
        this._networkManager = new ffNetworkManager_1.FFNetworkManager(session, this._page);
        this._page.on(page_1.Page.Events.FrameDetached, frame => this._removeContextsForFrame(frame));
        // TODO: remove Page.willOpenNewWindowAsynchronously from the protocol.
        this._eventListeners = [
            helper_1.helper.addEventListener(this._session, 'Page.eventFired', this._onEventFired.bind(this)),
            helper_1.helper.addEventListener(this._session, 'Page.frameAttached', this._onFrameAttached.bind(this)),
            helper_1.helper.addEventListener(this._session, 'Page.frameDetached', this._onFrameDetached.bind(this)),
            helper_1.helper.addEventListener(this._session, 'Page.navigationAborted', this._onNavigationAborted.bind(this)),
            helper_1.helper.addEventListener(this._session, 'Page.navigationCommitted', this._onNavigationCommitted.bind(this)),
            helper_1.helper.addEventListener(this._session, 'Page.navigationStarted', this._onNavigationStarted.bind(this)),
            helper_1.helper.addEventListener(this._session, 'Page.sameDocumentNavigation', this._onSameDocumentNavigation.bind(this)),
            helper_1.helper.addEventListener(this._session, 'Runtime.executionContextCreated', this._onExecutionContextCreated.bind(this)),
            helper_1.helper.addEventListener(this._session, 'Runtime.executionContextDestroyed', this._onExecutionContextDestroyed.bind(this)),
            helper_1.helper.addEventListener(this._session, 'Page.linkClicked', event => this._onLinkClicked(event.phase)),
            helper_1.helper.addEventListener(this._session, 'Page.uncaughtError', this._onUncaughtError.bind(this)),
            helper_1.helper.addEventListener(this._session, 'Runtime.console', this._onConsole.bind(this)),
            helper_1.helper.addEventListener(this._session, 'Page.dialogOpened', this._onDialogOpened.bind(this)),
            helper_1.helper.addEventListener(this._session, 'Page.bindingCalled', this._onBindingCalled.bind(this)),
            helper_1.helper.addEventListener(this._session, 'Page.fileChooserOpened', this._onFileChooserOpened.bind(this)),
            helper_1.helper.addEventListener(this._session, 'Page.workerCreated', this._onWorkerCreated.bind(this)),
            helper_1.helper.addEventListener(this._session, 'Page.workerDestroyed', this._onWorkerDestroyed.bind(this)),
            helper_1.helper.addEventListener(this._session, 'Page.dispatchMessageFromWorker', this._onDispatchMessageFromWorker.bind(this)),
            helper_1.helper.addEventListener(this._session, 'Page.crashed', this._onCrashed.bind(this)),
            helper_1.helper.addEventListener(this._session, 'Page.screencastStarted', this._onScreencastStarted.bind(this)),
            helper_1.helper.addEventListener(this._session, 'Page.webSocketCreated', this._onWebSocketCreated.bind(this)),
            helper_1.helper.addEventListener(this._session, 'Page.webSocketClosed', this._onWebSocketClosed.bind(this)),
            helper_1.helper.addEventListener(this._session, 'Page.webSocketFrameReceived', this._onWebSocketFrameReceived.bind(this)),
            helper_1.helper.addEventListener(this._session, 'Page.webSocketFrameSent', this._onWebSocketFrameSent.bind(this)),
        ];
        this._pagePromise = new Promise(f => this._pageCallback = f);
        session.once(ffConnection_1.FFSessionEvents.Disconnected, () => this._page._didDisconnect());
        this._session.once('Page.ready', () => {
            this._pageCallback(this._page);
            this._initializedPage = this._page;
        });
        // Ideally, we somehow ensure that utility world is created before Page.ready arrives, but currently it is racy.
        // Therefore, we can end up with an initialized page without utility world, although very unlikely.
        this._session.send('Page.addScriptToEvaluateOnNewDocument', { script: '', worldName: UTILITY_WORLD_NAME }).catch(this._pageCallback);
    }
    async pageOrError() {
        return this._pagePromise;
    }
    openerDelegate() {
        return this._opener;
    }
    _onWebSocketCreated(event) {
        this._page._frameManager.onWebSocketCreated(webSocketId(event.frameId, event.wsid), event.requestURL);
        this._page._frameManager.onWebSocketRequest(webSocketId(event.frameId, event.wsid));
    }
    _onWebSocketClosed(event) {
        if (event.error)
            this._page._frameManager.webSocketError(webSocketId(event.frameId, event.wsid), event.error);
        this._page._frameManager.webSocketClosed(webSocketId(event.frameId, event.wsid));
    }
    _onWebSocketFrameReceived(event) {
        this._page._frameManager.webSocketFrameReceived(webSocketId(event.frameId, event.wsid), event.opcode, event.data);
    }
    _onWebSocketFrameSent(event) {
        this._page._frameManager.onWebSocketFrameSent(webSocketId(event.frameId, event.wsid), event.opcode, event.data);
    }
    _onExecutionContextCreated(payload) {
        const { executionContextId, auxData } = payload;
        const frame = this._page._frameManager.frame(auxData ? auxData.frameId : null);
        if (!frame)
            return;
        const delegate = new ffExecutionContext_1.FFExecutionContext(this._session, executionContextId);
        const context = new dom.FrameExecutionContext(delegate, frame);
        if (auxData.name === UTILITY_WORLD_NAME)
            frame._contextCreated('utility', context);
        else if (!auxData.name)
            frame._contextCreated('main', context);
        this._contextIdToContext.set(executionContextId, context);
    }
    _onExecutionContextDestroyed(payload) {
        const { executionContextId } = payload;
        const context = this._contextIdToContext.get(executionContextId);
        if (!context)
            return;
        this._contextIdToContext.delete(executionContextId);
        context.frame._contextDestroyed(context);
    }
    _removeContextsForFrame(frame) {
        for (const [contextId, context] of this._contextIdToContext) {
            if (context.frame === frame)
                this._contextIdToContext.delete(contextId);
        }
    }
    _onLinkClicked(phase) {
        if (phase === 'before')
            this._page._frameManager.frameWillPotentiallyRequestNavigation();
        else
            this._page._frameManager.frameDidPotentiallyRequestNavigation();
    }
    _onNavigationStarted(params) {
        this._page._frameManager.frameRequestedNavigation(params.frameId, params.navigationId);
    }
    _onNavigationAborted(params) {
        this._page._frameManager.frameAbortedNavigation(params.frameId, params.errorText, params.navigationId);
    }
    _onNavigationCommitted(params) {
        for (const [workerId, worker] of this._workers) {
            if (worker.frameId === params.frameId)
                this._onWorkerDestroyed({ workerId });
        }
        this._page._frameManager.frameCommittedNewDocumentNavigation(params.frameId, params.url, params.name || '', params.navigationId || '', false);
    }
    _onSameDocumentNavigation(params) {
        this._page._frameManager.frameCommittedSameDocumentNavigation(params.frameId, params.url);
    }
    _onFrameAttached(params) {
        this._page._frameManager.frameAttached(params.frameId, params.parentFrameId);
    }
    _onFrameDetached(params) {
        this._page._frameManager.frameDetached(params.frameId);
    }
    _onEventFired(payload) {
        const { frameId, name } = payload;
        if (name === 'load')
            this._page._frameManager.frameLifecycleEvent(frameId, 'load');
        if (name === 'DOMContentLoaded')
            this._page._frameManager.frameLifecycleEvent(frameId, 'domcontentloaded');
    }
    _onUncaughtError(params) {
        const message = params.message.startsWith('Error: ') ? params.message.substring(7) : params.message;
        const error = new Error(message);
        error.stack = params.stack;
        this._page.emit(page_1.Page.Events.PageError, error);
    }
    _onConsole(payload) {
        const { type, args, executionContextId, location } = payload;
        const context = this._contextIdToContext.get(executionContextId);
        this._page._addConsoleMessage(type, args.map(arg => context.createHandle(arg)), location);
    }
    _onDialogOpened(params) {
        this._page.emit(page_1.Page.Events.Dialog, new dialog.Dialog(params.type, params.message, async (accept, promptText) => {
            await this._session.sendMayFail('Page.handleDialog', { dialogId: params.dialogId, accept, promptText });
        }, params.defaultValue));
    }
    async _onBindingCalled(event) {
        const context = this._contextIdToContext.get(event.executionContextId);
        const pageOrError = await this.pageOrError();
        if (!(pageOrError instanceof Error))
            this._page._onBindingCalled(event.payload, context);
    }
    async _onFileChooserOpened(payload) {
        const { executionContextId, element } = payload;
        const context = this._contextIdToContext.get(executionContextId);
        const handle = context.createHandle(element).asElement();
        this._page._onFileChooserOpened(handle);
    }
    async _onWorkerCreated(event) {
        const workerId = event.workerId;
        const worker = new page_1.Worker(event.url);
        const workerSession = new ffConnection_1.FFSession(this._session._connection, 'worker', workerId, (message) => {
            this._session.send('Page.sendMessageToWorker', {
                frameId: event.frameId,
                workerId: workerId,
                message: JSON.stringify(message)
            }).catch(e => {
                workerSession.dispatchMessage({ id: message.id, method: '', params: {}, error: { message: e.message, data: undefined } });
            });
        });
        this._workers.set(workerId, { session: workerSession, frameId: event.frameId });
        this._page._addWorker(workerId, worker);
        workerSession.once('Runtime.executionContextCreated', event => {
            worker._createExecutionContext(new ffExecutionContext_1.FFExecutionContext(workerSession, event.executionContextId));
        });
        workerSession.on('Runtime.console', event => {
            const { type, args, location } = event;
            const context = worker._existingExecutionContext;
            this._page._addConsoleMessage(type, args.map(arg => context.createHandle(arg)), location);
        });
        // Note: we receive worker exceptions directly from the page.
    }
    async _onWorkerDestroyed(event) {
        const workerId = event.workerId;
        const worker = this._workers.get(workerId);
        if (!worker)
            return;
        worker.session.dispose();
        this._workers.delete(workerId);
        this._page._removeWorker(workerId);
    }
    async _onDispatchMessageFromWorker(event) {
        const worker = this._workers.get(event.workerId);
        if (!worker)
            return;
        worker.session.dispatchMessage(JSON.parse(event.message));
    }
    async _onCrashed(event) {
        this._session.markAsCrashed();
        this._page._didCrash();
    }
    _onScreencastStarted(event) {
        this._browserContext._browser._videoStarted(this._browserContext, event.screencastId, event.file, this.pageOrError());
    }
    async exposeBinding(binding) {
        await this._session.send('Page.addBinding', { name: binding.name, script: binding.source });
    }
    didClose() {
        this._session.dispose();
        helper_1.helper.removeEventListeners(this._eventListeners);
        this._networkManager.dispose();
        this._page._didClose();
    }
    async navigateFrame(frame, url, referer) {
        const response = await this._session.send('Page.navigate', { url, referer, frameId: frame._id });
        return { newDocumentId: response.navigationId || undefined };
    }
    async updateExtraHTTPHeaders() {
        await this._session.send('Network.setExtraHTTPHeaders', { headers: this._page._state.extraHTTPHeaders || [] });
    }
    async setViewportSize(viewportSize) {
        utils_1.assert(this._page._state.viewportSize === viewportSize);
        await this._session.send('Page.setViewportSize', {
            viewportSize: {
                width: viewportSize.width,
                height: viewportSize.height,
            },
        });
    }
    async bringToFront() {
        await this._session.send('Page.bringToFront', {});
    }
    async updateEmulateMedia() {
        const colorScheme = this._page._state.colorScheme || this._browserContext._options.colorScheme || 'light';
        await this._session.send('Page.setEmulatedMedia', {
            // Empty string means reset.
            type: this._page._state.mediaType === null ? '' : this._page._state.mediaType,
            colorScheme
        });
    }
    async updateRequestInterception() {
        await this._networkManager.setRequestInterception(this._page._needsRequestInterception());
    }
    async setFileChooserIntercepted(enabled) {
        await this._session.send('Page.setInterceptFileChooserDialog', { enabled }).catch(e => { }); // target can be closed.
    }
    async opener() {
        if (!this._opener)
            return null;
        const result = await this._opener.pageOrError();
        if (result instanceof page_1.Page && !result.isClosed())
            return result;
        return null;
    }
    async reload() {
        await this._session.send('Page.reload', { frameId: this._page.mainFrame()._id });
    }
    async goBack() {
        const { success } = await this._session.send('Page.goBack', { frameId: this._page.mainFrame()._id });
        return success;
    }
    async goForward() {
        const { success } = await this._session.send('Page.goForward', { frameId: this._page.mainFrame()._id });
        return success;
    }
    async evaluateOnNewDocument(source) {
        await this._session.send('Page.addScriptToEvaluateOnNewDocument', { script: source });
    }
    async closePage(runBeforeUnload) {
        await this._session.send('Page.close', { runBeforeUnload });
    }
    canScreenshotOutsideViewport() {
        return true;
    }
    async setBackgroundColor(color) {
        if (color)
            throw new Error('Not implemented');
    }
    async startScreencast(options) {
        this._session.send('Page.startVideoRecording', {
            file: options.outputFile,
            width: options.width,
            height: options.height,
        });
    }
    async stopScreencast() {
        await this._session.send('Page.stopVideoRecording');
    }
    async takeScreenshot(format, documentRect, viewportRect, quality) {
        if (!documentRect) {
            const context = await this._page.mainFrame()._utilityContext();
            const scrollOffset = await context.evaluateInternal(() => ({ x: window.scrollX, y: window.scrollY }));
            documentRect = {
                x: viewportRect.x + scrollOffset.x,
                y: viewportRect.y + scrollOffset.y,
                width: viewportRect.width,
                height: viewportRect.height,
            };
        }
        // TODO: remove fullPage option from Page.screenshot.
        // TODO: remove Page.getBoundingBox method.
        const { data } = await this._session.send('Page.screenshot', {
            mimeType: ('image/' + format),
            clip: documentRect,
        }).catch(e => {
            if (e instanceof Error && e.message.includes('document.documentElement is null'))
                stackTrace_1.rewriteErrorMessage(e, screenshotter_1.kScreenshotDuringNavigationError);
            throw e;
        });
        return Buffer.from(data, 'base64');
    }
    async resetViewport() {
        utils_1.assert(false, 'Should not be called');
    }
    async getContentFrame(handle) {
        const { contentFrameId } = await this._session.send('Page.describeNode', {
            frameId: handle._context.frame._id,
            objectId: handle._objectId,
        });
        if (!contentFrameId)
            return null;
        return this._page._frameManager.frame(contentFrameId);
    }
    async getOwnerFrame(handle) {
        const { ownerFrameId } = await this._session.send('Page.describeNode', {
            frameId: handle._context.frame._id,
            objectId: handle._objectId
        });
        return ownerFrameId || null;
    }
    isElementHandle(remoteObject) {
        return remoteObject.subtype === 'node';
    }
    async getBoundingBox(handle) {
        const quads = await this.getContentQuads(handle);
        if (!quads || !quads.length)
            return null;
        let minX = Infinity;
        let maxX = -Infinity;
        let minY = Infinity;
        let maxY = -Infinity;
        for (const quad of quads) {
            for (const point of quad) {
                minX = Math.min(minX, point.x);
                maxX = Math.max(maxX, point.x);
                minY = Math.min(minY, point.y);
                maxY = Math.max(maxY, point.y);
            }
        }
        return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
    }
    async scrollRectIntoViewIfNeeded(handle, rect) {
        return await this._session.send('Page.scrollIntoViewIfNeeded', {
            frameId: handle._context.frame._id,
            objectId: handle._objectId,
            rect,
        }).then(() => 'done').catch(e => {
            if (e instanceof Error && e.message.includes('Node is detached from document'))
                return 'error:notconnected';
            if (e instanceof Error && e.message.includes('Node does not have a layout object'))
                return 'error:notvisible';
            throw e;
        });
    }
    rafCountForStablePosition() {
        return 1;
    }
    async getContentQuads(handle) {
        const result = await this._session.sendMayFail('Page.getContentQuads', {
            frameId: handle._context.frame._id,
            objectId: handle._objectId,
        });
        if (!result)
            return null;
        return result.quads.map(quad => [quad.p1, quad.p2, quad.p3, quad.p4]);
    }
    async setInputFiles(handle, files) {
        await handle._evaluateInUtility(([injected, node, files]) => injected.setInputFiles(node, files), files);
    }
    async adoptElementHandle(handle, to) {
        const result = await this._session.send('Page.adoptNode', {
            frameId: handle._context.frame._id,
            objectId: handle._objectId,
            executionContextId: to._delegate._executionContextId
        });
        if (!result.remoteObject)
            throw new Error('Unable to adopt element handle from a different document');
        return to.createHandle(result.remoteObject);
    }
    async getAccessibilityTree(needle) {
        return ffAccessibility_1.getAccessibilityTree(this._session, needle);
    }
    async inputActionEpilogue() {
    }
    async getFrameElement(frame) {
        const parent = frame.parentFrame();
        if (!parent)
            throw new Error('Frame has been detached.');
        const handles = await this._page.selectors._queryAll(parent, 'iframe', undefined);
        const items = await Promise.all(handles.map(async (handle) => {
            const frame = await handle.contentFrame().catch(e => null);
            return { handle, frame };
        }));
        const result = items.find(item => item.frame === frame);
        items.map(item => item === result ? Promise.resolve() : item.handle.dispose());
        if (!result)
            throw new Error('Frame has been detached.');
        return result.handle;
    }
}
exports.FFPage = FFPage;
function webSocketId(frameId, wsid) {
    return `${frameId}---${wsid}`;
}
//# sourceMappingURL=ffPage.js.map