"use strict";
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.InstrumentedTestFunction = void 0;
exports.screenshotError = screenshotError;
const async_scope_js_1 = require("../../conductor/async-scope.js");
const events_js_1 = require("../../conductor/events.js");
const screenshot_error_js_1 = require("../../conductor/screenshot-error.js");
const test_config_js_1 = require("../../conductor/test_config.js");
const state_provider_js_1 = require("./state-provider.js");
async function takeScreenshots(state) {
    try {
        const { devToolsPage, inspectedPage } = state;
        const inspectedPageScreenshot = await inspectedPage.screenshot();
        const devToolsPageScreenshot = await devToolsPage.screenshot();
        return { inspectedPage: inspectedPageScreenshot, devToolsPage: devToolsPageScreenshot };
    }
    catch (err) {
        console.error('Error taking a screenshot', err);
        return {};
    }
}
/**
 * Produces the final test error and cleans up the test state. This
 * function should not be allowed to throw.
 */
async function finalizeTestError(state, error) {
    if (!state) {
        console.error('Missing browsing state. Skipping screenshot taking for the error:', error);
        return error;
    }
    if (error instanceof screenshot_error_js_1.ScreenshotError) {
        return error;
    }
    return await screenshotError(state.state, error);
}
async function screenshotError(state, error) {
    if (!state.browser.connected) {
        console.error('Browser was disconnected, skipping screenshots');
        return error;
    }
    console.error('Taking screenshots for the error:', error);
    if (!test_config_js_1.TestConfig.debug) {
        try {
            const screenshotTimeout = 5_000;
            let timer;
            const { inspectedPage, devToolsPage } = await Promise.race([
                takeScreenshots(state).then(result => {
                    clearTimeout(timer);
                    return result;
                }),
                new Promise(resolve => {
                    timer = setTimeout(resolve, screenshotTimeout);
                }).then(() => {
                    console.error(`Could not take screenshots within ${screenshotTimeout}ms.`);
                    return { inspectedPage: undefined, devToolsPage: undefined };
                }),
            ]);
            return screenshot_error_js_1.ScreenshotError.fromBase64Images(error, inspectedPage, devToolsPage, state.devToolsPage.screenshotLog);
        }
        catch (e) {
            console.error('Unexpected error saving screenshots', e);
            return e;
        }
    }
    return error;
}
class InstrumentedTestFunction {
    /**
     * We track the initial timeouts for each context if we reset it back
     * Mocha check timing of the full executed function and fails
     * the test.
     * https://github.com/mochajs/mocha/blob/main/lib/runnable.js#L307
     */
    static timeoutByContext = new WeakMap();
    /**
     * We track the initial timeouts for each functions because mocha
     * does not reset test timeout for retries.
     */
    static timeoutByTestFunction = new WeakMap();
    #abortController = new AbortController();
    state;
    fn;
    label;
    suite;
    actualTimeout = 0;
    originalContextTimeout = 0;
    constructor(fn, label, suite) {
        this.fn = fn;
        this.label = label;
        this.suite = suite;
    }
    async #executeTest(context) {
        this.#abortController = new AbortController();
        async_scope_js_1.AsyncScope.abortSignal = this.#abortController.signal;
        if (this.state) {
            // eslint-disable-next-line no-debugger
            debugger; // If you're paused here while debugging, stepping into the next line will step into your test.
        }
        const start = performance.now();
        const testResult = await (this.state === undefined ?
            this.fn.call(context) :
            this.fn.call(undefined, this.state.state));
        if (context.test) {
            context.test.realDuration = Math.ceil(performance.now() - start);
        }
        return testResult;
    }
    async #clearState() {
        // State can be cleaned up after testPromise is finished,
        // including all error and timeout handling that still might rely
        // on the browserContext and pages.
        try {
            if (this.state?.state.browser.connected) {
                await this.state?.browsingContext.close();
            }
        }
        catch (e) {
            console.error('Unexpected error during cleanup', e);
        }
    }
    #buildErrorFromTimedoutScopeStacks(context) {
        const stacks = [];
        const scopes = async_scope_js_1.AsyncScope.scopes;
        for (const scope of scopes.values()) {
            const { descriptions, stack } = scope;
            if (stack) {
                const stepDescription = descriptions.length > 0 ? `${descriptions.join(' > ')}:\n` : '';
                stacks.push(`${stepDescription}${stack.join('\n')}\n`);
            }
        }
        const err = new Error(`A test function (${this.label}) for "${context.test?.title}" timed out (${this.actualTimeout} ms)`);
        if (stacks.length > 0) {
            const msg = `Pending async operations during timeout:\n${stacks.join('\n\n')}`;
            err.cause = new Error(msg);
        }
        return err;
    }
    #setupTimeout(context) {
        this.originalContextTimeout = _a.timeoutByContext.get(context) ?? context.timeout();
        this.actualTimeout = _a.timeoutByTestFunction.get(this.fn) ?? this.originalContextTimeout;
        _a.timeoutByContext.set(context, this.originalContextTimeout);
        _a.timeoutByTestFunction.set(this.fn, this.actualTimeout);
        // Disable mocha test timeout.
        // This way we rely only on our timeouts
        context.timeout(0);
    }
    async #executeWithTimeout(context) {
        // This needs to be the first thing we do
        // Else we may hit Mocha's timeouts
        this.#setupTimeout(context);
        // Get the state before starting the test timeouts
        this.state = this.suite ? await state_provider_js_1.StateProvider.instance.getState(this.suite) : undefined;
        let cleanupTimeoutPromise = undefined;
        let timeoutPromise = undefined;
        const executionPromise = this.#executeTest(context);
        if (this.actualTimeout !== 0) {
            timeoutPromise = new Promise((_, reject) => {
                const timeout = setTimeout(async () => {
                    reject(this.#buildErrorFromTimedoutScopeStacks(context));
                }, this.actualTimeout);
                cleanupTimeoutPromise = () => {
                    clearTimeout(timeout);
                    // Don't keep the Promise as pending
                    reject();
                };
            });
        }
        const racePromise = timeoutPromise ? Promise.race([executionPromise, timeoutPromise]) : executionPromise;
        return await racePromise
            .then(() => {
            this.#abortController.abort();
            async_scope_js_1.AsyncScope.abortSignal = undefined;
        }, async (err) => {
            this.#abortController.abort();
            async_scope_js_1.AsyncScope.abortSignal = undefined;
            throw await finalizeTestError(this.state, err);
        })
            .finally(async () => {
            cleanupTimeoutPromise?.();
            await this.#clearState();
            // Under some situations we report error when
            // we disconnect CDP sessions,
            // because of this we want to keep this last
            // else it will report the error for the next test
            (0, events_js_1.dumpCollectedErrors)();
        });
    }
    static instrument(fn, label, suite) {
        const test = new _a(fn, label, suite);
        return async function () {
            return await test.#executeWithTimeout(this);
        };
    }
}
exports.InstrumentedTestFunction = InstrumentedTestFunction;
_a = InstrumentedTestFunction;
//# sourceMappingURL=mocha-interface-helpers.js.map