"use strict";
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
Object.defineProperty(exports, "__esModule", { value: true });
exports.DEFAULT_DEVTOOLS_SETTINGS = exports.DevToolsPage = void 0;
exports.setupDevToolsPage = setupDevToolsPage;
const node_fs_1 = require("node:fs");
const node_path_1 = require("node:path");
const events_js_1 = require("../../conductor/events.js");
const test_config_js_1 = require("../../conductor/test_config.js");
const page_wrapper_js_1 = require("./page-wrapper.js");
const envLatePromises = process.env['LATE_PROMISES'] !== undefined ?
    ['true', ''].includes(process.env['LATE_PROMISES'].toLowerCase()) ? 10 : Number(process.env['LATE_PROMISES']) :
    0;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const globalThis = global;
let heapSnapshotCounter = 0;
class DevToolsPage extends page_wrapper_js_1.PageWrapper {
    screenshotLog = {};
    screenshotAssertionsEnabled = false;
    #currentHighlightedElement;
    #cdpSession;
    async delayPromisesIfRequired() {
        if (envLatePromises === 0) {
            return;
        }
        /* eslint-disable-next-line no-console */
        console.log(`Delaying promises by ${envLatePromises}ms`);
        await this.evaluate(delay => {
            global.Promise = class extends Promise {
                constructor(executor) {
                    super((resolve, reject) => {
                        executor(value => setTimeout(() => resolve(value), delay), reason => setTimeout(() => reject(reason), delay));
                    });
                }
            };
        }, envLatePromises);
    }
    async throttleCPUIfRequired() {
        if (test_config_js_1.TestConfig.cpuThrottle === 1) {
            return;
        }
        /* eslint-disable-next-line no-console */
        console.log(`Throttling CPU: ${test_config_js_1.TestConfig.cpuThrottle}x slowdown`);
        const client = await this.#getCDPSession();
        await client.send('Emulation.setCPUThrottlingRate', {
            rate: test_config_js_1.TestConfig.cpuThrottle,
        });
    }
    async reload(options) {
        await super.reload(options);
        await this.ensureReadyForTesting();
        if (this.screenshotAssertionsEnabled) {
            await setupTestFonts(this);
        }
    }
    /**
     * Use the Runtime.setQueryParamForTesting to mock the parameter before
     * DevTools is loaded.
     *
     * Important information for the implementation:
     * Trying to change the url and then reload or navigate to the new one will
     * hit this check in the back end:
     * https://crsrc.org/c/chrome/browser/devtools/devtools_ui_bindings.cc;l=406?q=devtools_ui_b&ss=chromium
     *
     * @param panel Mocks DevTools URL search params to make it open a specific panel on load.
     * @param canDock Mocks DevTools URL search params to make it think whether it can dock or not.
     * This does not control whether or not the panel can actually dock or not.
     * @param persistReloads If this is true running {@link DevToolsPage.reload} will reloading with
     * the provided options
     */
    async reloadWithParams({ panel, canDock }, persistReloads = false) {
        if (!panel && !canDock) {
            await this.reload();
            return;
        }
        // evaluateOnNewDocument is ran before all other JS is loaded
        // ES Modules are only resolved once and always resolved asynchronously
        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#other_differences_between_modules_and_classic_scripts
        // This means this will always resolve before DevTools tries to read the values
        const token = await this.page.evaluateOnNewDocument(async (panelName, canDockDevTools) => {
            // @ts-expect-error Evaluated in DevTools context
            const Root = await import('./core/root/root.js');
            if (panelName) {
                Root.Runtime.Runtime.setQueryParamForTesting('panel', panelName);
            }
            if (canDockDevTools) {
                Root.Runtime.Runtime.setQueryParamForTesting('can_dock', `${canDockDevTools}`);
            }
        }, panel, canDock);
        await this.reload();
        if (!persistReloads) {
            await this.page.removeScriptToEvaluateOnNewDocument(token.identifier);
        }
        if (panel) {
            await this.waitFor(`.panel.${panel}`);
        }
    }
    async ensureReadyForTesting() {
        const devToolsVeLogging = { enabled: true, testing: true };
        await this.evaluateOnNewDocument(`globalThis.hostConfigForTesting = ${JSON.stringify({ devToolsVeLogging })};`);
        await this.waitForFunction(async () => {
            try {
                const result = await this.page.evaluate(async function () {
                    // @ts-expect-error Executed in DevTools realm
                    const Main = await import('./entrypoints/main/main.js');
                    return Main.MainImpl.MainImpl.instanceForTest !== null;
                });
                return result;
            }
            catch (err) {
                // We might be navigating, so we retry execution context destroyed
                // errors.
                if (err.message.startsWith('Execution context was destroyed')) {
                    return false;
                }
                throw err;
            }
        });
        await this.evaluate(async () => {
            // @ts-expect-error Executed in DevTools realm
            const Main = await import('./entrypoints/main/main.js');
            await Main.MainImpl.MainImpl.instanceForTest.readyForTest();
        });
    }
    async useSoftMenu() {
        await this.evaluate(() => {
            // @ts-expect-error different context
            DevToolsAPI.setUseSoftMenu(true);
        });
    }
    async $(selector, root, handler = 'pierce') {
        const element = await super.$(selector, root, handler);
        await this.#maybeHighlight(element);
        return element;
    }
    async #maybeHighlight(element) {
        if (!test_config_js_1.TestConfig.debug) {
            return;
        }
        if (!element) {
            return;
        }
        if (this.#currentHighlightedElement) {
            await this.#currentHighlightedElement.reset();
        }
        this.#currentHighlightedElement = new HighlightedElement(element);
        await this.#currentHighlightedElement.highlight();
    }
    /**
     * @deprecated This method is not able to recover from unstable DOM. Use hover(selector) instead.
     */
    async hoverElement(element) {
        // Retries here just in case the element gets connected to DOM / becomes visible.
        await this.waitForFunction(async () => {
            try {
                await element.hover();
                await this.drainTaskQueue();
                return true;
            }
            catch {
                return false;
            }
        });
    }
    debuggerStatement() {
        return this.page.evaluate(() => {
            // eslint-disable-next-line no-debugger
            debugger;
        });
    }
    async clickMoreTabsButton(root) {
        await this.click('.tabbed-pane-header-tabs-drop-down-container', { root });
    }
    async closePanelTab(panelTabSelector) {
        // Get close button from tab element
        const selector = `${panelTabSelector} > .tabbed-pane-close-button`;
        await this.click(selector);
        await this.waitForNone(selector);
    }
    async closeAllCloseableTabs() {
        // get all closeable tools by looking for the available x buttons on tabs
        const selector = '.tabbed-pane-close-button';
        const allCloseButtons = await this.$$(selector);
        // Get all panel ids
        const panelTabIds = await Promise.all(allCloseButtons.map(button => {
            return button.evaluate(button => button.parentElement ? button.parentElement.id : '');
        }));
        // Close each tab
        for (const tabId of panelTabIds) {
            const selector = `#${tabId}`;
            await this.closePanelTab(selector);
        }
    }
    // Noisy! Do not leave this in your test but it may be helpful
    // when debugging.
    async enableCDPLogging() {
        await this.page.evaluate(() => {
            globalThis.ProtocolClient.test.dumpProtocol = console.log; // eslint-disable-line no-console
        });
    }
    async enableCDPTracking() {
        await this.page.evaluate(() => {
            globalThis.__messageMapForTest = new Map();
            globalThis.ProtocolClient.test.onMessageSent = (message) => {
                globalThis.__messageMapForTest.set(message.id, message.method);
            };
            globalThis.ProtocolClient.test.onMessageReceived = (message) => {
                if (message.id) {
                    globalThis.__messageMapForTest.delete(message.id);
                }
            };
        });
    }
    async logOutstandingCDP() {
        await this.page.evaluate(() => {
            for (const entry of globalThis.__messageMapForTest) {
                console.error(entry);
            }
        });
    }
    installEventListener(eventType) {
        return this.page.evaluate(eventType => {
            window.__pendingEvents = window.__pendingEvents || new Map();
            window.addEventListener(eventType, (e) => {
                let events = window.__pendingEvents.get(eventType);
                if (!events) {
                    events = [];
                    window.__pendingEvents.set(eventType, events);
                }
                events.push(e);
            });
        }, eventType);
    }
    getPendingEvents(eventType) {
        return this.page.evaluate(eventType => {
            if (!('__pendingEvents' in window)) {
                return;
            }
            const pendingEvents = window.__pendingEvents.get(eventType);
            window.__pendingEvents.set(eventType, []);
            return pendingEvents;
        }, eventType);
    }
    async renderCoordinatorQueueEmpty() {
        await this.page.evaluate(() => {
            return new Promise(resolve => {
                const pendingFrames = globalThis.__getRenderCoordinatorPendingFrames();
                if (pendingFrames < 1) {
                    resolve();
                    return;
                }
                globalThis.addEventListener('renderqueueempty', resolve, { once: true });
            });
        });
    }
    async summonSearchBox() {
        await this.pressKey('f', { control: true });
    }
    async readClipboard() {
        await this.page.browserContext().overridePermissions(this.page.url(), ['clipboard-read', 'clipboard-write']);
        const clipboard = await this.page.evaluate(async () => await navigator.clipboard.readText());
        await this.page.browserContext().clearPermissionOverrides();
        return clipboard;
    }
    async setupOverridesFSMocks() {
        await this.evaluateOnNewDocument(`
      Object.defineProperty(window, 'InspectorFrontendHost', {
        configurable: true,
        enumerable: true,
        get() {
            return this._InspectorFrontendHost;
        },
        set(value) {
            this._InspectorFrontendHost = value;
            this._InspectorFrontendHost.fileSystem = null;
            this._InspectorFrontendHost.addFileSystem = (type) => {
              const onFileSystem = (fs) => {
                this._InspectorFrontendHost.fileSystem = fs;
                const fileSystem = {
                  fileSystemName: 'sandboxedRequestedFileSystem',
                  fileSystemPath: '/overrides',
                  rootURL: 'filesystem:devtools://devtools/isolated/',
                  type: 'overrides',
                };
                this._InspectorFrontendHost.events.dispatchEventToListeners('fileSystemAdded', {fileSystem});
              };
              window.webkitRequestFileSystem(window.TEMPORARY, 1024 * 1024, onFileSystem);
            };
            this._InspectorFrontendHost.removeFileSystem = (fileSystemPath) => {
              const removalCallback = (entries) => {
                entries.forEach(entry => {
                  if (entry.isDirectory) {
                    entry.removeRecursively(() => {});
                  } else if (entry.isFile) {
                    entry.remove(() => {});
                  }
                });
              };

              if (this._InspectorFrontendHost.fileSystem) {
                this._InspectorFrontendHost.fileSystem.root.createReader().readEntries(removalCallback);
              }

              this._InspectorFrontendHost.fileSystem = null;
              this._InspectorFrontendHost.events.dispatchEventToListeners('fileSystemRemoved', '/overrides');
            }
            this._InspectorFrontendHost.isolatedFileSystem = (_fileSystemId, _registeredName) => {
              return this._InspectorFrontendHost.fileSystem;
            };
        }
      });
    `);
        await this.reload();
    }
    async setupMockHostConfigAndReload(hostConfig, aidaOverride) {
        const syncInformation = {
            accountEmail: 'some-email',
            isSyncActive: true,
            arePreferencesSynced: false,
        };
        const { identifier } = await this.evaluateOnNewDocument(`
      Object.defineProperty(window, 'InspectorFrontendHost', {
        configurable: true,
        enumerable: true,
        get() {
            return this._InspectorFrontendHost;
        },
        set(value) {
            value.getHostConfig = (cb) => {
              cb({
                ...globalThis.hostConfigForTesting ?? {},
                ...JSON.parse('${JSON.stringify(hostConfig)}'),
              });
            }

            value.getSyncInformation = (cb) => {
              cb(JSON.parse('${JSON.stringify(syncInformation)}'));
            };
            ${aidaOverride}
            this._InspectorFrontendHost = value;
        }
      });
    `);
        await this.reload({
            waitUntil: 'networkidle0',
        });
        return identifier;
    }
    async #getCDPSession() {
        if (!this.#cdpSession) {
            this.#cdpSession = await this.page.createCDPSession();
        }
        return this.#cdpSession;
    }
    async disableAnimations() {
        const session = await this.#getCDPSession();
        await session.send('Animation.enable');
        await session.send('Animation.setPlaybackRate', { playbackRate: 30_000 });
    }
    async enableAnimations() {
        const session = await this.#getCDPSession();
        await session.send('Animation.setPlaybackRate', { playbackRate: 1 });
    }
    // Debugging utility to be used around flaky code and hopefully reveal visual glitches.
    // Use it with the rdb wrapper to inspect the collected screenshots after a test failure.
    async captureScreenshot(name) {
        const index = Object.keys(this.screenshotLog).length + 1;
        const fullName = index + ' ' + (name ?? 'screenshot');
        this.screenshotLog[fullName] = await this.screenshot();
    }
    /**
     * A helper that takes heap snapshots of the DevTools page that can be used to
     * detect memory leaks. The snapshots are stored in out/<OutDir>.
     *
     * Usage:
     *
     * ```
     *   ... prepare test state ...
     *   await devToolsPsage.captureHeapSnapshot();
     *   ... perform actions that trigger the leak ...
     *   await devToolsPsage.captureHeapSnapshot();
     * ```
     *
     * Now `out/<OutDir>/heap-snapshot-0.heapsnapshot` and
     * `out/<OutDir>/heap-snapshot-1.heapsnapshot` are two snapshots that can be
     * compared in the Memory panel of DevTools.
     */
    async captureHeapSnapshot(snapshotName = 'heap-snapshot') {
        const session = await this.page.createCDPSession();
        await session.send('HeapProfiler.enable');
        await session.send('HeapProfiler.collectGarbage');
        const snapshotId = heapSnapshotCounter++;
        const fileName = `${snapshotName}-${snapshotId}.heapsnapshot`;
        const filePath = (0, node_path_1.join)(process.cwd(), fileName);
        const stream = (0, node_fs_1.createWriteStream)(filePath);
        session.on('HeapProfiler.addHeapSnapshotChunk', ({ chunk }) => {
            stream.write(chunk);
        });
        await session.send('HeapProfiler.takeHeapSnapshot', { reportProgress: false });
        await new Promise(resolve => {
            stream.end(() => resolve());
        });
        await session.detach();
        console.warn(`Heap snapshot saved to: ${filePath}`);
    }
}
exports.DevToolsPage = DevToolsPage;
exports.DEFAULT_DEVTOOLS_SETTINGS = {
    enabledDevToolsExperiments: [],
    disabledDevToolsExperiments: [],
    devToolsSettings: {
        veLogsTestMode: true,
    },
    dockingMode: 'right',
    enableScreenshotAssertion: false,
};
/**
 * @internal
 */
async function setDevToolsSettings(devToolsPata, settings) {
    if (!Object.keys(settings).length) {
        return;
    }
    const rawValues = Object.entries(settings).map(value => {
        switch (typeof value[1]) {
            case 'boolean':
                return [value[0], value[1].toString()];
            case 'string':
            case 'number':
            case 'bigint':
                return [value[0], `'${value[1]}'`];
            default:
                return [value[0], JSON.stringify(value[1])];
        }
    });
    const expression = `(async () => {
      const Common = await import('./core/common/common.js');
      var setting;
      ${rawValues
        .map(([settingName, value]) => `setting = Common.Settings.Settings.instance().createSetting('${settingName}');
      setting.set(${value})`)
        .join('\n      ')}
    })()`;
    return await devToolsPata.evaluate(expression);
}
/**
 * @internal
 */
async function setDevToolsExperiments(devToolsPage, experiments) {
    if (!experiments.length) {
        return;
    }
    return await devToolsPage.evaluate(async (experiments) => {
        // @ts-expect-error evaluate in DevTools page
        const Root = await import('./core/root/root.js');
        for (const experiment of experiments) {
            Root.Runtime.experiments.setEnabled(experiment, true);
        }
    }, experiments);
}
/**
 * @internal
 */
async function setDisabledDevToolsExperiments(devToolsPage, experiments) {
    if (!experiments.length) {
        return;
    }
    return await devToolsPage.evaluate(async (experiments) => {
        // @ts-expect-error evaluate in DevTools page
        const Root = await import('./core/root/root.js');
        for (const experiment of experiments) {
            Root.Runtime.experiments.setEnabled(experiment, false);
        }
    }, experiments);
}
/**
 * @internal
 */
async function setDockingSide(devToolsPage, side) {
    await devToolsPage.evaluate(`
    (async function() {
      const UI = await import('./ui/legacy/legacy.js');
      UI.DockController.DockController.instance().setDockSide('${side}');
    })();
  `);
}
async function setupTestFonts(devToolsPage) {
    await devToolsPage.evaluate(async () => {
        document.documentElement.classList.add('platform-screenshot-test');
        await new Promise((resolve, reject) => {
            const timer = window.setTimeout(() => reject(new Error('Failing loading the fonts from the network')), 5000);
            const link = document.createElement('link');
            link.rel = 'stylesheet';
            link.href = 'https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100..900;1,100..900&display=swap';
            link.onload = ev => {
                clearTimeout(timer);
                resolve(ev);
            };
            link.onerror = ev => {
                clearTimeout(timer);
                reject(ev);
            };
            document.head.appendChild(link);
        });
        const loadFontsPromise = Promise
            .all([
            document.fonts.load('400 16px "Roboto"'), // Normal
            document.fonts.load('500 16px "Roboto"'), // Medium
            document.fonts.load('700 16px "Roboto"'), // Bold
            document.fonts.load('italic 400 16px "Roboto"'),
            document.fonts.load('italic 500 16px "Roboto"'),
            document.fonts.load('italic 700 16px "Roboto"'),
        ])
            .then(async () => {
            return await document.fonts.ready;
        });
        await Promise.race([
            loadFontsPromise,
            new Promise((_res, rej) => setTimeout(() => rej(new Error('Failing loading the fonts from the network')), 5000)),
        ]);
    });
}
async function setupDevToolsPage(inspectedPage, settings) {
    const frontend = await inspectedPage.page.openDevTools();
    if (settings.enableScreenshotAssertion) {
        // DevTools has a strict Content Security Policy. We need to bypass it to load
        // the external Roboto fonts for visually consistent screenshot assertions.
        // We need to set it before the 'reload' as afterwards the strict CSP of the
        // page is final.
        await frontend.setBypassCSP(true);
    }
    (0, events_js_1.installPageErrorHandlers)(frontend);
    const devToolsPage = new DevToolsPage(frontend);
    await devToolsPage.ensureReadyForTesting();
    await Promise.all([
        devToolsPage.disableAnimations(),
        setDevToolsSettings(devToolsPage, settings.devToolsSettings),
        setDevToolsExperiments(devToolsPage, settings.enabledDevToolsExperiments),
        setDisabledDevToolsExperiments(devToolsPage, settings.disabledDevToolsExperiments),
    ]);
    await devToolsPage.reloadWithParams({ panel: settings.panel }, true);
    await Promise.all([
        devToolsPage.throttleCPUIfRequired(),
        devToolsPage.delayPromisesIfRequired(),
        devToolsPage.useSoftMenu(),
    ]);
    await setDockingSide(devToolsPage, settings.dockingMode);
    if (settings.enableScreenshotAssertion) {
        devToolsPage.screenshotAssertionsEnabled = true;
        await setupTestFonts(devToolsPage);
    }
    return devToolsPage;
}
class HighlightedElement {
    element;
    constructor(element) {
        this.element = element;
    }
    async reset() {
        await this.element.evaluate(el => {
            if (el instanceof HTMLElement) {
                el.style.outline = '';
            }
        });
    }
    async highlight() {
        await this.element.evaluate(el => {
            if (el instanceof HTMLElement) {
                el.style.outline = '2px solid red';
            }
        });
    }
}
//# sourceMappingURL=frontend-helper.js.map