"use strict";
/**
 * Copyright: (c) Myia SAS 2026.
 * This file and its contents are licensed under the AGPLv3 License.
 * Please see the LICENSE file at the root of this repository
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.reset = exports.expect = exports.test = void 0;
// Composition root for the heal-playwright-tracer auto-fixture.
//
// Each feature owns its own setup/cleanup; this file is the shopping
// list. Reading top-to-bottom tells you everything the tracer does to
// a test and in what order.
//
// Built-in features wired here (always-on, OSS):
//   - runtime (side-effect import installs globalThis.__heal_enter/__heal_ok/__heal_throw)
//   - step-tracking       → new PlaywrightStepTrackingAdapter(hooks).patch(test)
//   - test-context        → testContextAdapter.capture(testInfo)
//   - locator-screenshots → startLocatorScreenshotCapture(page, outDir)
//   - test-stdout-capture → new StdoutCaptureSession()
//   - NDJSON exporter         → default trace output to `heal-data/heal-traces.ndjson`
//
// User-extensible features (wired via `configureTracer(...)` from the
// host `playwright.config.ts`):
//   - exporters       → each factory returns a `HealTraceExporter` composed
//                   into a tee alongside the default NDJSON exporter
//   - lifecycles  → each entry exposes `setup(ctx)` + `teardown()`;
//                   setups run in declaration order at test start,
//                   teardowns in reverse order in `finally` (LIFO)
//   - onTestTeardown(fn) → runtime registration, drained before
//                   lifecycle teardowns so SDKs still see any globals
//                   a lifecycle installed
//
// Output shape (per (test, attempt)):
//   <cwd>/heal-traces/<executionId>/<playwrightTestId>/<attempt>/heal-traces.ndjson
// The fixture writes the ndjson + per-statement screenshots there;
// the reporter additionally copies Playwright artefacts (trace.zip,
// videos, failure screenshots, user attachments) into the same dir
// on `onTestEnd`. See
// `../../domain/trace-event-recorder/model/statement-trace-schema.ts`
// for the record contract.
//
// Any backend integration (HTTP shipping, APM bindings,
// telemetry-session setup, …) lives in user code and plugs in via
// `configureTracer`. The fixture knows nothing about any specific
// backend.
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const test_1 = require("@playwright/test");
// Side-effect: installs `globalThis.__heal_enter/__heal_ok/__heal_throw`.
const trace_event_recorder_runtime_1 = require("../trace-event-recorder-runtime");
Object.defineProperty(exports, "reset", { enumerable: true, get: function () { return trace_event_recorder_runtime_1.reset; } });
const playwright_locator_screenshot_adapter_1 = require("../../infrastructure/playwright-locator-screenshot-adapter");
const projectors_1 = require("../../domain/trace-event-recorder/service/projectors");
const ndjson_exporter_adapter_1 = require("../../infrastructure/ndjson-exporter-adapter");
const service_1 = require("../../domain/trace-event-recorder/service");
const playwright_step_tracking_adapter_1 = require("../../infrastructure/playwright-step-tracking-adapter");
const playwright_test_context_adapter_1 = require("../../infrastructure/playwright-test-context-adapter");
const playwright_locator_screenshot_adapter_2 = require("../../infrastructure/playwright-locator-screenshot-adapter");
const stdout_capture_adapter_1 = require("../../infrastructure/stdout-capture-adapter");
const playwright_console_capture_adapter_1 = require("../../infrastructure/playwright-console-capture-adapter");
const playwright_network_capture_adapter_1 = require("../../infrastructure/playwright-network-capture-adapter");
const heal_traces_layout_1 = require("../../infrastructure/heal-traces-layout");
const artifact_summary_printer_1 = require("../../infrastructure/artifact-summary-printer");
const heal_reporter_1 = require("../../infrastructure/heal-reporter");
const heal_config_1 = require("../heal-config");
const with_timeout_1 = require("../../util/with-timeout");
const logger_1 = require("../../util/logger");
const wire_all_pages_1 = require("./wire-all-pages");
const global_names_1 = require("../../domain/trace-event-recorder/model/global-names");
// Defaults for the optional `timeouts` block in `HealTracerConfig`.
// `screenshotMs` caps best-effort decoration calls in the screenshot
// pipeline; `lifecycleMs` caps user setup/teardown and the final
// projector close. On `lifecycleMs` timeouts the fixture logs to
// stderr and continues the rest of teardown — a hung user exporter
// must not block the next test from starting.
const DEFAULT_SCREENSHOT_TIMEOUT_MS = 10000;
const DEFAULT_LIFECYCLE_TIMEOUT_MS = 30000;
// Wrap `expect` so any assertion made against a Locator gets a
// highlight screenshot stamped onto the active statement, the same
// way locator actions do. Non-locator assertions fall through.
const expect = (0, playwright_locator_screenshot_adapter_1.wrapExpect)(test_1.expect);
exports.expect = expect;
function buildHealTraceExporter(ndjsonPath, ctx) {
    const legs = [new ndjson_exporter_adapter_1.NdjsonExporter(ndjsonPath)];
    const { exporters = [] } = (0, heal_config_1.getTracerConfig)();
    for (const factory of exporters) {
        try {
            legs.push(factory(ctx));
        }
        catch (err) {
            logger_1.log.error('exporter factory failed', err);
        }
    }
    return legs.length === 1 ? legs[0] : new service_1.CompositeHealTraceExporter(legs);
}
// Composition-root singletons — one per process.
const testContextAdapter = new playwright_test_context_adapter_1.PlaywrightTestContextAdapter({ setContext: trace_event_recorder_runtime_1.setContext }, { resolveExecutionId: heal_traces_layout_1.resolveExecutionId });
const stepTrackingAdapter = new playwright_step_tracking_adapter_1.PlaywrightStepTrackingAdapter({ pushStep: trace_event_recorder_runtime_1.pushStep, popStep: trace_event_recorder_runtime_1.popStep });
// The Babel plugin and the reporter are a pair — the in-worker fixture
// produces NDJSON; the reporter (main process) propagates the
// executionId, finalizes crashed traces, and copies Playwright
// artefacts. Running the plugin without the reporter silently produces
// half-broken output, so we hard-fail the first test in each worker.
// Reporter sets HEAL_TRACER_REPORTER=1 in onBegin; Playwright
// propagates env to workers.
let reporterCheckPassed = false;
function assertReporterRegistered() {
    if (reporterCheckPassed)
        return;
    if (process.env.HEAL_TRACER_REPORTER !== '1') {
        throw new Error('@heal-dev/heal-playwright-tracer: Babel plugin is wired but the ' +
            'reporter is not registered. Add to playwright.config.ts:\n' +
            "  reporter: [['@heal-dev/heal-playwright-tracer/reporter']]\n" +
            'See https://github.com/heal-dev/heal-playwright-tracer#register-the-reporter');
    }
    reporterCheckPassed = true;
}
exports.test = test_1.test.extend({
    _traceAuto: [
        async ({ page, browser, request }, use, testInfo) => {
            assertReporterRegistered();
            const captured = testContextAdapter.capture(testInfo);
            const layout = new heal_traces_layout_1.HealTracesLayout(process.cwd(), captured.executionId);
            const testDir = layout.testDir(captured.testId, captured.attempt);
            const ndjsonPath = layout.ndjsonPath(captured.testId, captured.attempt);
            const screenshotsDir = path.dirname(layout.screenshotPath(captured.testId, captured.attempt, 'x.png'));
            fs.mkdirSync(testDir, { recursive: true });
            fs.mkdirSync(screenshotsDir, { recursive: true });
            // Write a registry entry for the optional `HealTracerReporter`
            // so it can find this test's NDJSON from the main process.
            // Filesystem-based (not annotation/attachment) because
            // Playwright's IPC does not flush fixture-pushed annotations
            // on abrupt worker exit (OOM / SIGKILL / `process.exit()`) —
            // exactly the crash cases the reporter rescues. A file
            // written with writeSync before the test body runs survives
            // any subsequent abrupt exit. Cleaned up in the `finally`
            // block below on graceful teardown, so only crashed tests
            // leave an orphan for the reporter to pick up.
            const registryPath = (0, heal_reporter_1.healPendingRegistryPath)(testInfo.project.outputDir, captured.testId, captured.attempt);
            const registryEntry = {
                ndjsonPath,
                rootDir: testDir,
                executionId: captured.executionId,
                playwrightOutputDir: testInfo.outputDir,
            };
            fs.mkdirSync(path.dirname(registryPath), { recursive: true });
            fs.writeFileSync(registryPath, JSON.stringify(registryEntry), 'utf8');
            const tracerCtx = {
                testInfo,
                healDataDir: testDir,
                transport: {
                    testId: captured.testId,
                    attempt: captured.attempt,
                    executionId: captured.executionId,
                    rootDir: testDir,
                    healTracesFilePath: ndjsonPath,
                },
            };
            // Network and console sidecar streams are on by default. A
            // user can still opt out per-stream via
            // `configureTracer({ network: { enabled: false }, ... })` —
            // the field stays settable for tests and exceptional setups,
            // but the public docs treat the streams as always-on.
            const tracerConfig = (0, heal_config_1.getTracerConfig)();
            const networkEnabled = tracerConfig.network?.enabled !== false;
            const consoleEnabled = tracerConfig.console?.enabled !== false;
            const networkPath = networkEnabled
                ? layout.networkNdjsonPath(captured.testId, captured.attempt)
                : undefined;
            const consolePath = consoleEnabled
                ? layout.consoleNdjsonPath(captured.testId, captured.attempt)
                : undefined;
            // Fresh output pipeline per test: build a HealTraceExporter (default
            // NDJSON leg + any user-configured exporters), wrap it in a
            // projector, install on the recorder, then reset() — which
            // clears projector state and emits the test-header record via
            // the buildMetaEvent call inside the recorder.
            const output = buildHealTraceExporter(ndjsonPath, tracerCtx);
            const projector = new projectors_1.StatementProjector(output);
            (0, trace_event_recorder_runtime_1.setExporter)(projector);
            (0, trace_event_recorder_runtime_1.reset)();
            // Defensive: clear any teardown hooks that leaked from a
            // previous test that crashed before drain ran.
            (0, heal_config_1.resetTeardownHooks)();
            const { timeouts = {} } = tracerConfig;
            const screenshotTimeoutMs = timeouts.screenshotMs ?? DEFAULT_SCREENSHOT_TIMEOUT_MS;
            const lifecycleTimeoutMs = timeouts.lifecycleMs ?? DEFAULT_LIFECYCLE_TIMEOUT_MS;
            const stopScreenshots = (0, playwright_locator_screenshot_adapter_2.startLocatorScreenshotCapture)(page, screenshotsDir, trace_event_recorder_runtime_1.setCurrentStatementScreenshot, screenshotTimeoutMs);
            const stdoutSession = new stdout_capture_adapter_1.StdoutCaptureSession();
            // Sidecar capture: instantiate sessions for whichever streams
            // the user opted into, wire the test's initial context first
            // (so the very first request issued by the test body is
            // captured even if it races a popup) and then patch
            // `browser.newContext` / `browser.newPage` so any context the
            // test creates later is also wired. The patches are reverted
            // by `restoreWiring()` in the finally block — `browser` is
            // worker-scoped, so a stale patch would leak into the next
            // test in the same worker.
            const consoleSession = consoleEnabled && consolePath
                ? new playwright_console_capture_adapter_1.ConsoleCaptureSession(consolePath, {
                    clock: trace_event_recorder_runtime_1.clock,
                    startedAt: (0, trace_event_recorder_runtime_1.getStartedAt)(),
                    getCurrentStatementSeq: trace_event_recorder_runtime_1.getCurrentStatementSeq,
                    getCurrentStepPath: trace_event_recorder_runtime_1.getCurrentStepPath,
                }, tracerConfig.console ?? {})
                : undefined;
            const networkSession = networkEnabled && networkPath
                ? new playwright_network_capture_adapter_1.NetworkCaptureSession(networkPath, {
                    clock: trace_event_recorder_runtime_1.clock,
                    startedAt: (0, trace_event_recorder_runtime_1.getStartedAt)(),
                    getCurrentStatementSeq: trace_event_recorder_runtime_1.getCurrentStatementSeq,
                    getCurrentStepPath: trace_event_recorder_runtime_1.getCurrentStepPath,
                }, tracerConfig.network ?? {})
                : undefined;
            const wireableSessions = [];
            if (consoleSession)
                wireableSessions.push(consoleSession);
            if (networkSession)
                wireableSessions.push(networkSession);
            let restoreWiring;
            if (wireableSessions.length > 0) {
                // Wire the test's primary BrowserContext + APIRequestContext
                // immediately, then patch the Browser for any new contexts.
                for (const s of wireableSessions)
                    s.attachToContext(page.context());
                if (networkSession) {
                    networkSession.attachToApiRequestContext(request);
                }
                restoreWiring = (0, wire_all_pages_1.wireAllPages)(wireableSessions, {
                    browser,
                    apiRequest: test_1.request,
                });
            }
            // Install per-statement pre-processor chain on `globalThis`. The
            // Babel plugin emits `await globalThis.__heal_preprocess?.(meta)`
            // inside every async-context leaf statement; with no
            // pre-processors registered the slot stays `undefined` and the
            // optional call is a no-op. With one or more registered, the
            // installed function loops over them in declaration order and
            // awaits each. Errors propagate out of the chain (and into the
            // statement's own try/catch) — a pre-processor that wants to
            // be defensive about its own failures has to handle them.
            //
            // Installed BEFORE lifecycles so a lifecycle's `setup` can
            // exercise pre-processed code paths if it triggers traced
            // statements (rare but cleanly defined). Uninstalled AFTER
            // lifecycle teardowns for the symmetric reason.
            const preProcessors = tracerConfig.preProcessors ?? [];
            const preprocessCtx = {
                ...tracerCtx,
                browserContext: page.context(),
            };
            const g = globalThis;
            const previousPreprocess = g[global_names_1.HEAL_PREPROCESS];
            if (preProcessors.length > 0) {
                g[global_names_1.HEAL_PREPROCESS] = async (meta) => {
                    for (const pp of preProcessors) {
                        await pp({ meta, ctx: preprocessCtx });
                    }
                };
            }
            // Instantiate user-configured lifecycles for this test. Each
            // factory runs fresh per test so any closure state the factory
            // declares is isolated between tests. Setup failures are
            // isolated: a lifecycle whose `setup` throws is NOT pushed onto
            // `activeLifecycles`, so its `teardown` will not run. Later
            // lifecycles still get a chance to set up.
            const activeLifecycles = [];
            const { lifecycles = [] } = (0, heal_config_1.getTracerConfig)();
            for (const factory of lifecycles) {
                try {
                    const lc = factory();
                    await (0, with_timeout_1.withTimeout)(Promise.resolve(lc.setup(tracerCtx)), lifecycleTimeoutMs, 'lifecycle.setup');
                    activeLifecycles.push(lc);
                }
                catch (err) {
                    logger_1.log.error('lifecycle setup failed', err);
                }
            }
            try {
                await use();
            }
            finally {
                // Record each video-recording page's identity (synthetic
                // role label + URL) here, before any teardown hook navigates
                // or closes a page, so `pageUrl` reflects where the test body
                // left the page. The reporter pairs this list positionally
                // with `result.attachments` video entries — we cannot match
                // by path because Playwright renames the video file between
                // this teardown and the reporter's `onTestEnd`. Best-effort:
                // any failure just leaves the video attachments un-enriched.
                // Skipped entirely when `recordVideo` is off (`page.video()`
                // is null), so it costs nothing then.
                try {
                    const videoPages = [];
                    let pageIndex = 0;
                    for (const p of page.context().pages()) {
                        if (!p.video())
                            continue;
                        const name = p === page ? 'main' : `page-${(pageIndex += 1)}`;
                        videoPages.push({ name, url: p.url() });
                    }
                    if (videoPages.length > 0) {
                        fs.writeFileSync(registryPath, JSON.stringify({ ...registryEntry, videoPages }), 'utf8');
                    }
                }
                catch (err) {
                    logger_1.log.error('video-page capture failed', err);
                }
                // Run any teardown hooks registered during the test. Runs
                // BEFORE lifecycle teardowns so hooks that log via an SDK
                // still see the per-test globals a lifecycle installed, and
                // BEFORE stopping stdout capture so SDK teardown output lands
                // in the ndjson's `test-result.stdout/stderr`.
                try {
                    await (0, with_timeout_1.withTimeout)((0, heal_config_1.drainTeardownHooks)(), lifecycleTimeoutMs, 'onTestTeardown hooks');
                }
                catch (err) {
                    logger_1.log.error('teardown hooks did not finish', err);
                }
                // Teardown in reverse order (LIFO): the last lifecycle to set
                // up is the first to tear down, matching the mental model of
                // nested `using` blocks.
                for (let i = activeLifecycles.length - 1; i >= 0; i--) {
                    try {
                        await (0, with_timeout_1.withTimeout)(Promise.resolve(activeLifecycles[i].teardown()), lifecycleTimeoutMs, 'lifecycle.teardown');
                    }
                    catch (err) {
                        logger_1.log.error('lifecycle teardown failed', err);
                    }
                }
                // Uninstall the per-statement pre-processor chain. Restores
                // whatever was on the slot before this test installed (almost
                // always `undefined`) so back-to-back tests in the same worker
                // don't see the prior test's `browserContext` leak.
                if (preProcessors.length > 0) {
                    g[global_names_1.HEAL_PREPROCESS] = previousPreprocess;
                }
                const capturedStdout = stdoutSession.stop();
                stopScreenshots();
                // Stop sidecar capture BEFORE finalizing the projector. Order:
                //   1. restore the Browser patches so any post-teardown code
                //      paths (e.g. Playwright internal cleanup) see a clean
                //      newContext/newPage;
                //   2. close the console session (no async work to drain);
                //   3. drain + close the network session, passing the test's
                //      final status so 'on-error' bodies are flushed only
                //      when relevant. Network last so any in-flight response
                //      that fires during teardown still lands in the file.
                if (restoreWiring) {
                    try {
                        restoreWiring();
                    }
                    catch (err) {
                        logger_1.log.error('wireAllPages.restore failed', err);
                    }
                }
                if (consoleSession) {
                    try {
                        await (0, with_timeout_1.withTimeout)(consoleSession.close(), lifecycleTimeoutMs, 'console.close');
                    }
                    catch (err) {
                        logger_1.log.error('console.close did not finish', err);
                    }
                }
                if (networkSession) {
                    const failed = (testInfo.status ?? 'passed') !== 'passed';
                    try {
                        await (0, with_timeout_1.withTimeout)(networkSession.stop(failed), lifecycleTimeoutMs, 'network.stop');
                    }
                    catch (err) {
                        logger_1.log.error('network.stop did not finish', err);
                    }
                }
                // Emit the final test-result record and close the output
                // exporter chain (flushes NDJSON fd, awaits in-flight
                // user-exporter I/O). The projector has its own `finalized`
                // guard that silently swallows any stray events fired after
                // this point, so we do NOT swap the recorder's exporter back
                // to a no-op — it stays pointed at the (now-finalized)
                // projector until the next test installs a fresh one.
                //
                // Playwright-native artifacts (trace.zip, videos) are NOT
                // shipped from here — their attachments are populated by
                // Playwright in a later phase. Users who need them can
                // register a Playwright reporter in their `playwright.config`.
                // Pass testInfo.errors[0] so any root statement whose __enter
                // fired but whose __ok/__throw never did (Playwright aborted
                // the hanging action on timeout before the catch block could
                // run) gets flushed as `threw` with the test-level error.
                const pendingError = testInfo.errors[0] ?? testInfo.error;
                try {
                    await (0, with_timeout_1.withTimeout)(projector.finalize({
                        status: testInfo.status ?? 'passed',
                        duration: testInfo.duration,
                        stdout: capturedStdout.stdout.length ? capturedStdout.stdout : undefined,
                        stderr: capturedStdout.stderr.length ? capturedStdout.stderr : undefined,
                    }, pendingError), lifecycleTimeoutMs, 'projector.finalize');
                }
                catch (err) {
                    // The trace may be truncated (final test-result record
                    // and exporter close didn't complete in time) but the
                    // test itself has already finished — keep going so
                    // downstream registry cleanup still runs.
                    logger_1.log.error('projector.finalize did not finish', err);
                }
                new artifact_summary_printer_1.ArtifactSummaryPrinter(testDir).print({
                    title: testInfo.title,
                    status: testInfo.status ?? 'passed',
                });
                // Registry cleanup is owned by `HealTracerReporter` — it
                // runs on clean teardowns too because Playwright populates
                // `result.attachments` only after this fixture's afterEach
                // returns, so the reporter is the first hook with a final
                // attachment list. It appends a `test-attachments` record
                // and then deletes the registry entry.
            }
        },
        { auto: true },
    ],
});
// Thread test.step titles onto the runtime step stack. Must run AFTER
// base.extend because Playwright's extend creates a fresh `.step` on
// the new test object that doesn't inherit from base — patching `base`
// first would never reach the extended test.
stepTrackingAdapter.patch(exports.test);
