"use strict";
/**
 * 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.RecorderSupplement = void 0;
const codeGenerator_1 = require("./recorder/codeGenerator");
const utils_1 = require("./recorder/utils");
const page_1 = require("../page");
const frames_1 = require("../frames");
const browserContext_1 = require("../browserContext");
const javascript_1 = require("./recorder/javascript");
const csharp_1 = require("./recorder/csharp");
const python_1 = require("./recorder/python");
const progress_1 = require("../progress");
const recorderSource = require("../../generated/recorderSource");
const consoleApiSource = require("../../generated/consoleApiSource");
const outputs_1 = require("./recorder/outputs");
const symbol = Symbol('RecorderSupplement');
class RecorderSupplement {
    constructor(context, app, params) {
        this._pageAliases = new Map();
        this._lastPopupOrdinal = 0;
        this._lastDialogOrdinal = 0;
        this._timers = new Set();
        this._resumeCallback = null;
        this._paused = false;
        this._context = context;
        this._app = app;
        this._recorderState = { mode: app === 'codegen' ? 'recording' : 'none' };
        let languageGenerator;
        switch (params.language) {
            case 'javascript':
                languageGenerator = new javascript_1.JavaScriptLanguageGenerator();
                break;
            case 'csharp':
                languageGenerator = new csharp_1.CSharpLanguageGenerator();
                break;
            case 'python':
            case 'python-async':
                languageGenerator = new python_1.PythonLanguageGenerator(params.language === 'python-async');
                break;
            default: throw new Error(`Invalid target: '${params.language}'`);
        }
        let highlighterType = params.language;
        if (highlighterType === 'python-async')
            highlighterType = 'python';
        const writable = {
            write: (text) => context.emit(browserContext_1.BrowserContext.Events.StdOut, text)
        };
        const outputs = [params.terminal ? new outputs_1.TerminalOutput(writable, highlighterType) : new outputs_1.FlushingTerminalOutput(writable)];
        if (params.outputFile)
            outputs.push(new outputs_1.FileOutput(params.outputFile));
        this._output = new outputs_1.OutputMultiplexer(outputs);
        this._output.setEnabled(app === 'codegen');
        context.on(browserContext_1.BrowserContext.Events.BeforeClose, () => this._output.flush());
        const generator = new codeGenerator_1.CodeGenerator(context._browser._options.name, app === 'codegen', params.launchOptions || {}, params.contextOptions || {}, this._output, languageGenerator, params.device, params.saveStorage);
        this._generator = generator;
    }
    static getOrCreate(context, app, params) {
        let recorderPromise = context[symbol];
        if (!recorderPromise) {
            const recorder = new RecorderSupplement(context, app, params);
            recorderPromise = recorder.install().then(() => recorder);
            context[symbol] = recorderPromise;
        }
        return recorderPromise;
    }
    async install() {
        this._context.on('page', page => this._onPage(page));
        for (const page of this._context.pages())
            this._onPage(page);
        this._context.once('close', () => {
            for (const timer of this._timers)
                clearTimeout(timer);
            this._timers.clear();
        });
        // Input actions that potentially lead to navigation are intercepted on the page and are
        // performed by the Playwright.
        await this._context.exposeBinding('playwrightRecorderPerformAction', false, (source, action) => this._performAction(source.frame, action));
        // Other non-essential actions are simply being recorded.
        await this._context.exposeBinding('playwrightRecorderRecordAction', false, (source, action) => this._recordAction(source.frame, action));
        // Commits last action so that no further signals are added to it.
        await this._context.exposeBinding('playwrightRecorderCommitAction', false, (source, action) => this._generator.commitLastAction());
        await this._context.exposeBinding('playwrightRecorderState', false, () => {
            return {
                state: this._recorderState,
                app: this._app,
                paused: this._paused
            };
        });
        await this._context.exposeBinding('playwrightRecorderSetState', false, (source, state) => {
            this._recorderState = state;
            this._output.setEnabled(state.mode === 'recording');
        });
        await this._context.exposeBinding('playwrightRecorderResume', false, () => {
            if (this._resumeCallback) {
                this._resumeCallback();
                this._resumeCallback = null;
            }
            this._paused = false;
        });
        await this._context.extendInjectedScript(recorderSource.source);
        await this._context.extendInjectedScript(consoleApiSource.source);
    }
    async pause() {
        this._paused = true;
        return new Promise(f => this._resumeCallback = f);
    }
    async _onPage(page) {
        // First page is called page, others are called popup1, popup2, etc.
        const frame = page.mainFrame();
        page.on('close', () => {
            this._pageAliases.delete(page);
            this._generator.addAction({
                pageAlias,
                frame: page.mainFrame(),
                committed: true,
                action: {
                    name: 'closePage',
                    signals: [],
                }
            });
        });
        frame.on(frames_1.Frame.Events.Navigation, () => this._onFrameNavigated(frame, page));
        page.on(page_1.Page.Events.Download, () => this._onDownload(page));
        page.on(page_1.Page.Events.Popup, popup => this._onPopup(page, popup));
        page.on(page_1.Page.Events.Dialog, () => this._onDialog(page));
        const suffix = this._pageAliases.size ? String(++this._lastPopupOrdinal) : '';
        const pageAlias = 'page' + suffix;
        this._pageAliases.set(page, pageAlias);
        const isPopup = !!await page.opener();
        // Could happen due to the await above.
        if (page.isClosed())
            return;
        if (!isPopup) {
            this._generator.addAction({
                pageAlias,
                frame: page.mainFrame(),
                committed: true,
                action: {
                    name: 'openPage',
                    url: page.mainFrame().url(),
                    signals: [],
                }
            });
        }
    }
    async _performAction(frame, action) {
        const page = frame._page;
        const controller = new progress_1.ProgressController();
        const actionInContext = {
            pageAlias: this._pageAliases.get(page),
            frame,
            action
        };
        this._generator.willPerformAction(actionInContext);
        if (action.name === 'click') {
            const { options } = utils_1.toClickOptions(action);
            await frame.click(controller, action.selector, options);
        }
        if (action.name === 'press') {
            const modifiers = utils_1.toModifiers(action.modifiers);
            const shortcut = [...modifiers, action.key].join('+');
            await frame.press(controller, action.selector, shortcut);
        }
        if (action.name === 'check')
            await frame.check(controller, action.selector);
        if (action.name === 'uncheck')
            await frame.uncheck(controller, action.selector);
        if (action.name === 'select')
            await frame.selectOption(controller, action.selector, [], action.options.map(value => ({ value })));
        const timer = setTimeout(() => {
            actionInContext.committed = true;
            this._timers.delete(timer);
        }, 5000);
        this._generator.didPerformAction(actionInContext);
        this._timers.add(timer);
    }
    async _recordAction(frame, action) {
        // We are lacking frame.page() in
        this._generator.addAction({
            pageAlias: this._pageAliases.get(frame._page),
            frame,
            action
        });
    }
    _onFrameNavigated(frame, page) {
        const pageAlias = this._pageAliases.get(page);
        this._generator.signal(pageAlias, frame, { name: 'navigation', url: frame.url() });
    }
    _onPopup(page, popup) {
        const pageAlias = this._pageAliases.get(page);
        const popupAlias = this._pageAliases.get(popup);
        this._generator.signal(pageAlias, page.mainFrame(), { name: 'popup', popupAlias });
    }
    _onDownload(page) {
        const pageAlias = this._pageAliases.get(page);
        this._generator.signal(pageAlias, page.mainFrame(), { name: 'download' });
    }
    _onDialog(page) {
        const pageAlias = this._pageAliases.get(page);
        this._generator.signal(pageAlias, page.mainFrame(), { name: 'dialog', dialogAlias: String(++this._lastDialogOrdinal) });
    }
}
exports.RecorderSupplement = RecorderSupplement;
//# sourceMappingURL=recorderSupplement.js.map