/**
 * 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.
 */
import fs from 'fs';
import os from 'os';
import path from 'path';
import { Transform } from 'stream';
import colors from 'colors/safe';
import mime from 'mime';
import open from 'open';
import * as yazl from 'yazl';
import { MultiMap } from '@isomorphic/multimap';
import { calculateSha1 } from '@utils/crypto';
import { copyFileAndMakeWritable, removeFolders, sanitizeForFilePath, toPosixPath } from '@utils/fileUtils';
import { getPackageManagerExecCommand, isCodingAgent } from '@utils/env';
import { HttpServer, serveFolder } from '@utils/httpServer';
import { gracefullyProcessExitDoNotHang } from '@utils/processLauncher';
import { extractZip } from '@utils/third_party/extractZip';
import { formatError, formatResultFailure, internalScreen } from './base';
import * as babel from '../transform/babelBundle';
import { resolveReporterOutputPath, stripAnsiEscapes } from '../util';
const htmlReportOptions = ['always', 'never', 'on-failure'];
const isHtmlReportOption = (type) => {
    return htmlReportOptions.includes(type);
};
class HtmlReporter {
    config;
    suite;
    _options;
    _outputFolder;
    _attachmentsBaseURL;
    _open;
    _port;
    _host;
    _buildResult;
    _topLevelErrors = [];
    _reportConfigs = new Map();
    _machines = [];
    constructor(options) {
        this._options = options;
    }
    version() {
        return 'v2';
    }
    printsToStdio() {
        return false;
    }
    onConfigure(config) {
        this.config = config;
    }
    onBegin(suite) {
        const { outputFolder, open, attachmentsBaseURL, host, port } = this._resolveOptions();
        this._outputFolder = outputFolder;
        this._open = open;
        this._host = host;
        this._port = port;
        this._attachmentsBaseURL = attachmentsBaseURL;
        const reportedWarnings = new Set();
        for (const project of this.config.projects) {
            if (this._isSubdirectory(outputFolder, project.outputDir) || this._isSubdirectory(project.outputDir, outputFolder)) {
                const key = outputFolder + '|' + project.outputDir;
                if (reportedWarnings.has(key))
                    continue;
                reportedWarnings.add(key);
                writeLine(colors.red(`Configuration Error: HTML reporter output folder clashes with the tests output folder:`));
                writeLine(`
    html reporter folder: ${colors.bold(outputFolder)}
    test results folder: ${colors.bold(project.outputDir)}`);
                writeLine('');
                writeLine(`HTML reporter will clear its output directory prior to being generated, which will lead to the artifact loss.
`);
            }
        }
        this.suite = suite;
    }
    _resolveOptions() {
        const outputFolder = reportFolderFromEnv() ?? resolveReporterOutputPath('playwright-report', this._options.configDir, this._options.outputFolder);
        return {
            outputFolder,
            open: getHtmlReportOptionProcessEnv() || this._options.open || 'on-failure',
            attachmentsBaseURL: process.env.PLAYWRIGHT_HTML_ATTACHMENTS_BASE_URL || this._options.attachmentsBaseURL || 'data/',
            host: process.env.PLAYWRIGHT_HTML_HOST || this._options.host,
            port: process.env.PLAYWRIGHT_HTML_PORT ? +process.env.PLAYWRIGHT_HTML_PORT : this._options.port,
        };
    }
    _isSubdirectory(parentDir, dir) {
        const relativePath = path.relative(parentDir, dir);
        return !!relativePath && !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
    }
    onError(error) {
        this._topLevelErrors.push(error);
    }
    onReportConfigure(params) {
        this._reportConfigs.set(params.reportPath, params.config);
    }
    onReportEnd(params) {
        const config = this._reportConfigs.get(params.reportPath);
        if (config)
            this._machines.push({ config, result: params.result, reportPath: params.reportPath });
    }
    async onEnd(result) {
        const projectSuites = this.suite.suites;
        await removeFolders([this._outputFolder]);
        const noSnippets = parseBooleanEnvVar('PLAYWRIGHT_HTML_NO_SNIPPETS') ?? this._options.noSnippets;
        const noCopyPrompt = parseBooleanEnvVar('PLAYWRIGHT_HTML_NO_COPY_PROMPT') ?? this._options.noCopyPrompt;
        const doNotInlineAssets = parseBooleanEnvVar('PLAYWRIGHT_HTML_DO_NOT_INLINE_ASSETS') ?? this._options.doNotInlineAssets ?? false;
        const builder = new HtmlBuilder(yazl, this.config, this._outputFolder, this._attachmentsBaseURL, doNotInlineAssets, {
            title: process.env.PLAYWRIGHT_HTML_TITLE || this._options.title,
            noSnippets,
            noCopyPrompt,
        });
        this._buildResult = await builder.build(this.config.metadata, projectSuites, result, this._topLevelErrors, this._machines);
    }
    async onExit() {
        if (process.env.CI || !this._buildResult)
            return;
        const { ok, singleTestId } = this._buildResult;
        const shouldOpen = !isCodingAgent() && !!process.stdin.isTTY && (this._open === 'always' || (!ok && this._open === 'on-failure'));
        if (shouldOpen) {
            await showHTMLReport(this._outputFolder, this._host, this._port, singleTestId);
        }
        else if (this._options._mode === 'test' && !!process.stdin.isTTY) {
            const packageManagerCommand = getPackageManagerExecCommand();
            const relativeReportPath = this._outputFolder === standaloneDefaultFolder() ? '' : ' ' + path.relative(process.cwd(), this._outputFolder);
            const hostArg = this._host ? ` --host ${this._host}` : '';
            const portArg = this._port ? ` --port ${this._port}` : '';
            writeLine('');
            writeLine('To open last HTML report run:');
            writeLine(colors.cyan(`
  ${packageManagerCommand} playwright show-report${relativeReportPath}${hostArg}${portArg}
`));
        }
    }
}
function reportFolderFromEnv() {
    // Note: PLAYWRIGHT_HTML_REPORT is for backwards compatibility.
    const envValue = process.env.PLAYWRIGHT_HTML_OUTPUT_DIR || process.env.PLAYWRIGHT_HTML_REPORT;
    return envValue ? path.resolve(envValue) : undefined;
}
function getHtmlReportOptionProcessEnv() {
    // Note: PW_TEST_HTML_REPORT_OPEN is for backwards compatibility.
    const htmlOpenEnv = process.env.PLAYWRIGHT_HTML_OPEN || process.env.PW_TEST_HTML_REPORT_OPEN;
    if (!htmlOpenEnv)
        return undefined;
    if (!isHtmlReportOption(htmlOpenEnv)) {
        writeLine(colors.red(`Configuration Error: HTML reporter Invalid value for PLAYWRIGHT_HTML_OPEN: ${htmlOpenEnv}. Valid values are: ${htmlReportOptions.join(', ')}`));
        return undefined;
    }
    return htmlOpenEnv;
}
function parseBooleanEnvVar(name) {
    const value = process.env[name];
    if (value === 'false' || value === '0')
        return false;
    if (value)
        return true;
    return undefined;
}
function standaloneDefaultFolder() {
    return reportFolderFromEnv() ?? resolveReporterOutputPath('playwright-report', process.cwd(), undefined);
}
async function resolveReportFolder(reportPath) {
    const stat = await fs.promises.stat(reportPath).catch(() => null);
    if (!stat)
        throw new Error(`No report found at "${reportPath}"`);
    if (stat.isDirectory())
        return reportPath;
    if (stat.isFile() && reportPath.toLowerCase().endsWith('.zip'))
        return await extractReportZip(reportPath);
    throw new Error(`No report found at "${reportPath}"`);
}
async function extractReportZip(zipPath) {
    const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'playwright-show-report-'));
    const cleanup = () => {
        try {
            fs.rmSync(tempDir, { recursive: true, force: true });
        }
        catch {
        }
    };
    // Default Node behavior on SIGINT/SIGTERM is to terminate, which fires 'exit'.
    process.on('exit', cleanup);
    try {
        await extractZip(zipPath, { dir: tempDir });
    }
    catch (e) {
        cleanup();
        throw new Error(`Failed to extract report from "${zipPath}": ${e.message}`);
    }
    const hasIndex = await fs.promises.access(path.join(tempDir, 'index.html')).then(() => true, () => false);
    if (!hasIndex) {
        cleanup();
        throw new Error(`No "index.html" found at the top level of "${zipPath}"`);
    }
    return tempDir;
}
export async function showHTMLReport(reportFolder, host = 'localhost', port, testId) {
    const requestedPath = reportFolder ?? standaloneDefaultFolder();
    let folder;
    try {
        folder = await resolveReportFolder(requestedPath);
    }
    catch (e) {
        writeLine(colors.red(e.message));
        gracefullyProcessExitDoNotHang(1);
        return;
    }
    // HMR: watch builds serve the html-reporter through an embedded Vite dev
    // server so edits to packages/html-reporter/src/* reload live. Release
    // builds always take the static branch (the dev-server arm is DCE'd).
    // Set PW_HMR_STATIC=1 during watch to exercise the bundled output.
    const server = (__PW_HMR__ && process.env.PW_HMR_STATIC !== '1')
        ? await serveHtmlReportWithHMR(folder)
        : serveFolder(folder);
    await server.start({ port, host, preferredPort: port ? undefined : 9323 });
    let url = server.urlPrefix('human-readable');
    writeLine('');
    writeLine(colors.cyan(`  Serving HTML report at ${url}. Press Ctrl+C to quit.`));
    if (testId)
        url += `#?testId=${testId}`;
    url = url.replace('0.0.0.0', 'localhost');
    if (!isCodingAgent())
        await open(url, { wait: true }).catch(() => { });
    await new Promise(() => { });
}
// HMR begin: dev-mode branch — mounts a Vite dev server for the html-reporter
// source at `/` while still serving the generated attachments (and the bundled
// trace-viewer copy under /trace/) from the output folder. The report's data
// payload lives in a <template id="playwrightReportBase64"> tag at the tail of
// the generated index.html; we extract it and splice it into Vite's
// transformed HTML so the client still finds it at runtime.
async function serveHtmlReportWithHMR(folder) {
    const server = new HttpServer();
    const reporterRoot = path.resolve(__dirname, '..', '..', '..', 'html-reporter');
    const devServer = await server.createViteDevServer({ root: reporterRoot });
    const generatedIndex = await fs.promises.readFile(path.join(folder, 'index.html'), 'utf-8');
    const templateMatch = /<template id="playwrightReportBase64">[\s\S]*?<\/template>/.exec(generatedIndex);
    const reportTemplate = templateMatch ? templateMatch[0] : '';
    const sourceIndex = await fs.promises.readFile(path.join(reporterRoot, 'index.html'), 'utf-8');
    server.routePrefix('/', (request, response) => {
        const url = new URL('http://localhost' + request.url);
        if (url.pathname === '/' || url.pathname === '/index.html') {
            devServer.transformIndexHtml(url.pathname, sourceIndex).then(html => {
                const injected = html.replace(/<\/body>/i, `${reportTemplate}</body>`);
                response.statusCode = 200;
                response.setHeader('Content-Type', 'text/html; charset=utf-8');
                response.end(injected);
            }, () => {
                response.statusCode = 500;
                response.end();
            });
            return true;
        }
        // Serve attachments and the bundled trace-viewer copy from the generated
        // output folder first, falling through to Vite for source modules.
        const absolutePath = path.join(folder, ...url.pathname.split('/'));
        if (absolutePath.startsWith(folder) && server.serveFile(request, response, absolutePath))
            return true;
        devServer.middlewares(request, response, HttpServer.notFoundFallback(response));
        return true;
    });
    return server;
}
class HtmlBuilder {
    _config;
    _reportFolder;
    _stepsInFile = new MultiMap();
    _dataZipFile;
    _hasTraces = false;
    _attachmentsBaseURL;
    _options;
    _doNotInlineAssets;
    constructor(yazl, config, outputDir, attachmentsBaseURL, doNotInlineAssets, options) {
        this._dataZipFile = new yazl.ZipFile();
        this._config = config;
        this._reportFolder = outputDir;
        this._options = options;
        this._doNotInlineAssets = doNotInlineAssets;
        fs.mkdirSync(this._reportFolder, { recursive: true });
        this._attachmentsBaseURL = attachmentsBaseURL;
    }
    async build(metadata, projectSuites, result, topLevelErrors, machines) {
        const data = new Map();
        for (const projectSuite of projectSuites) {
            const projectName = projectSuite.project().name;
            for (const fileSuite of projectSuite.suites) {
                const fileName = this._relativeLocation(fileSuite.location).file;
                this._createEntryForSuite(data, projectName, fileSuite, fileName, true);
            }
        }
        if (!this._options.noSnippets)
            createSnippets(this._stepsInFile);
        let ok = true;
        for (const [fileId, { testFile, testFileSummary }] of data) {
            const stats = testFileSummary.stats;
            for (const test of testFileSummary.tests) {
                if (test.outcome === 'expected')
                    ++stats.expected;
                if (test.outcome === 'skipped')
                    ++stats.skipped;
                if (test.outcome === 'unexpected')
                    ++stats.unexpected;
                if (test.outcome === 'flaky')
                    ++stats.flaky;
                ++stats.total;
            }
            stats.ok = stats.unexpected + stats.flaky === 0;
            if (!stats.ok)
                ok = false;
            const testCaseSummaryComparator = (t1, t2) => {
                const w1 = (t1.outcome === 'unexpected' ? 1000 : 0) + (t1.outcome === 'flaky' ? 1 : 0);
                const w2 = (t2.outcome === 'unexpected' ? 1000 : 0) + (t2.outcome === 'flaky' ? 1 : 0);
                return w2 - w1;
            };
            testFileSummary.tests.sort(testCaseSummaryComparator);
            this._addDataFile(fileId + '.json', testFile);
        }
        const htmlReport = {
            metadata,
            startTime: result.startTime.getTime(),
            duration: result.duration,
            files: [...data.values()].map(e => e.testFileSummary),
            projectNames: projectSuites.map(r => r.project().name),
            stats: { ...[...data.values()].reduce((a, e) => addStats(a, e.testFileSummary.stats), emptyStats()) },
            errors: topLevelErrors.map(error => formatError(internalScreen, error).message),
            options: this._options,
            machines: machines.map(machine => ({
                duration: machine.result.duration,
                startTime: machine.result.startTime.getTime(),
                tag: machine.config.tags,
                shardIndex: machine.config.shard?.current,
            })),
        };
        htmlReport.files.sort((f1, f2) => {
            const w1 = f1.stats.unexpected * 1000 + f1.stats.flaky;
            const w2 = f2.stats.unexpected * 1000 + f2.stats.flaky;
            return w2 - w1;
        });
        this._addDataFile('report.json', htmlReport);
        let singleTestId;
        if (htmlReport.stats.total === 1) {
            const testFile = data.values().next().value.testFile;
            singleTestId = testFile.tests[0].testId;
        }
        const reportIndexFile = await this._writeStaticAssets();
        // Copy trace viewer.
        if (this._hasTraces) {
            const traceViewerFolder = path.join(require.resolve('playwright-core'), '..', 'lib', 'vite', 'traceViewer');
            const traceViewerTargetFolder = path.join(this._reportFolder, 'trace');
            const traceViewerAssetsTargetFolder = path.join(traceViewerTargetFolder, 'assets');
            fs.mkdirSync(traceViewerAssetsTargetFolder, { recursive: true });
            for (const file of fs.readdirSync(traceViewerFolder)) {
                if (file.endsWith('.map') || file.includes('watch') || file.includes('assets'))
                    continue;
                await copyFileAndMakeWritable(path.join(traceViewerFolder, file), path.join(traceViewerTargetFolder, file));
            }
            for (const file of fs.readdirSync(path.join(traceViewerFolder, 'assets'))) {
                if (file.endsWith('.map') || file.includes('xtermModule'))
                    continue;
                await copyFileAndMakeWritable(path.join(traceViewerFolder, 'assets', file), path.join(traceViewerAssetsTargetFolder, file));
            }
        }
        await this._writeReportData(reportIndexFile);
        return { ok, singleTestId };
    }
    async _writeStaticAssets() {
        const appFolder = path.join(require.resolve('playwright-core'), '..', 'lib', 'vite', 'htmlReport');
        const reportIndexFile = path.join(this._reportFolder, 'index.html');
        if (this._doNotInlineAssets) {
            const html = await fs.promises.readFile(path.join(appFolder, 'index.html'), 'utf-8');
            await Promise.all([
                fs.promises.writeFile(reportIndexFile, html),
                fs.promises.copyFile(path.join(appFolder, 'report.js'), path.join(this._reportFolder, 'report.js')),
                fs.promises.copyFile(path.join(appFolder, 'report.css'), path.join(this._reportFolder, 'report.css')),
            ]);
        }
        else {
            let html = await fs.promises.readFile(path.join(appFolder, 'index.html'), 'utf-8');
            const [js, css] = await Promise.all([
                fs.promises.readFile(path.join(appFolder, 'report.js'), 'utf-8'),
                fs.promises.readFile(path.join(appFolder, 'report.css'), 'utf-8'),
            ]);
            html = html.replace(/<script type="module"[^>]*><\/script>/, () => `<script type="module">${js}</script>`);
            html = html.replace(/<link rel="stylesheet"[^>]*>/, () => `<style type='text/css'>${css}</style>`);
            await fs.promises.writeFile(reportIndexFile, html);
        }
        return reportIndexFile;
    }
    async _writeReportData(filePath) {
        fs.appendFileSync(filePath, '<template id="playwrightReportBase64">data:application/zip;base64,');
        await new Promise(f => {
            this._dataZipFile.end(undefined, () => {
                this._dataZipFile.outputStream
                    .pipe(new Base64Encoder())
                    .pipe(fs.createWriteStream(filePath, { flags: 'a' })).on('close', f);
            });
        });
        fs.appendFileSync(filePath, '</template>');
    }
    _addDataFile(fileName, data) {
        this._dataZipFile.addBuffer(Buffer.from(JSON.stringify(data)), fileName);
    }
    _createEntryForSuite(data, projectName, suite, fileName, deep) {
        const fileId = calculateSha1(fileName).slice(0, 20);
        let fileEntry = data.get(fileId);
        if (!fileEntry) {
            fileEntry = {
                testFile: { fileId, fileName, tests: [] },
                testFileSummary: { fileId, fileName, tests: [], stats: emptyStats() },
            };
            data.set(fileId, fileEntry);
        }
        const { testFile, testFileSummary } = fileEntry;
        const testEntries = [];
        this._processSuite(suite, projectName, [], deep, testEntries);
        for (const test of testEntries) {
            testFile.tests.push(test.testCase);
            testFileSummary.tests.push(test.testCaseSummary);
        }
    }
    _processSuite(suite, projectName, path, deep, outTests) {
        const newPath = [...path, suite.title];
        suite.entries().forEach(e => {
            if (e.type === 'test')
                outTests.push(this._createTestEntry(e, projectName, newPath));
            else if (deep)
                this._processSuite(e, projectName, newPath, deep, outTests);
        });
    }
    _createTestEntry(test, projectName, path) {
        const duration = test.results.reduce((a, r) => a + r.duration, 0);
        const location = this._relativeLocation(test.location);
        path = path.slice(1).filter(path => path.length > 0);
        const results = test.results.map(r => this._createTestResult(test, r));
        return {
            testCase: {
                testId: test.id,
                title: test.title,
                projectName,
                location,
                duration,
                annotations: this._serializeAnnotations(test.annotations),
                tags: [...new Set(test.tags)],
                outcome: test.outcome(),
                path,
                results,
                repeatEachIndex: test.repeatEachIndex || undefined, // Do not include zero.
                ok: test.outcome() === 'expected' || test.outcome() === 'flaky',
            },
            testCaseSummary: {
                testId: test.id,
                title: test.title,
                projectName,
                location,
                duration,
                annotations: this._serializeAnnotations(test.annotations),
                tags: [...new Set(test.tags)],
                outcome: test.outcome(),
                path,
                ok: test.outcome() === 'expected' || test.outcome() === 'flaky',
                repeatEachIndex: test.repeatEachIndex || undefined, // Do not include zero.
                results: results.map(result => {
                    return {
                        attachments: result.attachments.map(a => ({ name: a.name, contentType: a.contentType, path: a.path })),
                        startTime: result.startTime,
                        workerIndex: result.workerIndex,
                    };
                }),
            },
        };
    }
    _serializeAttachments(attachments) {
        let lastAttachment;
        return attachments.map(a => {
            if (a.name === 'trace')
                this._hasTraces = true;
            if ((a.name === 'stdout' || a.name === 'stderr') && a.contentType === 'text/plain') {
                if (lastAttachment &&
                    lastAttachment.name === a.name &&
                    lastAttachment.contentType === a.contentType) {
                    lastAttachment.body += stripAnsiEscapes(a.body);
                    return null;
                }
                a.body = stripAnsiEscapes(a.body);
                lastAttachment = a;
                return a;
            }
            if (a.path) {
                let fileName = a.path;
                try {
                    const buffer = fs.readFileSync(a.path);
                    const sha1 = calculateSha1(buffer) + path.extname(a.path);
                    fileName = this._attachmentsBaseURL + sha1;
                    fs.mkdirSync(path.join(this._reportFolder, 'data'), { recursive: true });
                    fs.writeFileSync(path.join(this._reportFolder, 'data', sha1), buffer);
                }
                catch (e) {
                }
                return {
                    name: a.name,
                    contentType: a.contentType,
                    path: fileName,
                    body: a.body,
                };
            }
            if (a.body instanceof Buffer) {
                if (isTextContentType(a.contentType)) {
                    // Content type is like this: "text/html; charset=UTF-8"
                    const charset = a.contentType.match(/charset=(.*)/)?.[1];
                    try {
                        const body = a.body.toString(charset || 'utf-8');
                        return {
                            name: a.name,
                            contentType: a.contentType,
                            body,
                        };
                    }
                    catch (e) {
                        // Invalid encoding, fall through and save to file.
                    }
                }
                fs.mkdirSync(path.join(this._reportFolder, 'data'), { recursive: true });
                const extension = sanitizeForFilePath(path.extname(a.name).replace(/^\./, '')) || mime.getExtension(a.contentType) || 'dat';
                const sha1 = calculateSha1(a.body) + '.' + extension;
                fs.writeFileSync(path.join(this._reportFolder, 'data', sha1), a.body);
                return {
                    name: a.name,
                    contentType: a.contentType,
                    path: this._attachmentsBaseURL + sha1,
                };
            }
            // string
            return {
                name: a.name,
                contentType: a.contentType,
                body: a.body,
            };
        }).filter(Boolean);
    }
    _serializeAnnotations(annotations) {
        // Annotations can be pushed directly, with a wrong type.
        return annotations.map(a => ({
            type: a.type,
            description: a.description === undefined ? undefined : String(a.description),
            location: a.location ? {
                file: a.location.file,
                line: a.location.line,
                column: a.location.column,
            } : undefined,
        }));
    }
    _createTestResult(test, result) {
        return {
            duration: result.duration,
            startTime: result.startTime.toISOString(),
            retry: result.retry,
            steps: dedupeSteps(result.steps).map(s => this._createTestStep(s, result)),
            errors: formatResultFailure(internalScreen, test, result, '').map(error => {
                return {
                    message: error.message,
                    codeframe: error.location ? createErrorCodeframe(error.message, error.location) : undefined
                };
            }),
            status: result.status,
            annotations: this._serializeAnnotations(result.annotations),
            attachments: this._serializeAttachments([
                ...result.attachments,
                ...result.stdout.map(m => stdioAttachment(m, 'stdout')),
                ...result.stderr.map(m => stdioAttachment(m, 'stderr'))
            ]),
            workerIndex: result.workerIndex,
        };
    }
    _createTestStep(dedupedStep, result) {
        const { step, duration, count } = dedupedStep;
        const skipped = dedupedStep.step.annotations?.find(a => a.type === 'skip');
        let title = step.title;
        if (skipped)
            title = `${title} (skipped${skipped.description ? ': ' + skipped.description : ''})`;
        const testStep = {
            title,
            startTime: step.startTime.toISOString(),
            duration,
            steps: dedupeSteps(step.steps).map(s => this._createTestStep(s, result)),
            attachments: step.attachments.map(s => {
                const index = result.attachments.indexOf(s);
                if (index === -1)
                    throw new Error('Unexpected, attachment not found');
                return index;
            }),
            location: this._relativeLocation(step.location),
            error: step.error?.message,
            count,
            skipped: !!skipped,
        };
        if (step.location)
            this._stepsInFile.set(step.location.file, testStep);
        return testStep;
    }
    _relativeLocation(location) {
        if (!location)
            return undefined;
        const file = toPosixPath(path.relative(this._config.rootDir, location.file));
        return {
            file,
            line: location.line,
            column: location.column,
        };
    }
}
const emptyStats = () => {
    return {
        total: 0,
        expected: 0,
        unexpected: 0,
        flaky: 0,
        skipped: 0,
        ok: true,
    };
};
const addStats = (stats, delta) => {
    stats.total += delta.total;
    stats.skipped += delta.skipped;
    stats.expected += delta.expected;
    stats.unexpected += delta.unexpected;
    stats.flaky += delta.flaky;
    stats.ok = stats.ok && delta.ok;
    return stats;
};
class Base64Encoder extends Transform {
    _remainder;
    _transform(chunk, encoding, callback) {
        if (this._remainder) {
            chunk = Buffer.concat([this._remainder, chunk]);
            this._remainder = undefined;
        }
        const remaining = chunk.length % 3;
        if (remaining) {
            this._remainder = chunk.slice(chunk.length - remaining);
            chunk = chunk.slice(0, chunk.length - remaining);
        }
        chunk = chunk.toString('base64');
        this.push(Buffer.from(chunk));
        callback();
    }
    _flush(callback) {
        if (this._remainder)
            this.push(Buffer.from(this._remainder.toString('base64')));
        callback();
    }
}
function isTextContentType(contentType) {
    return contentType.startsWith('text/') || contentType.startsWith('application/json');
}
function stdioAttachment(chunk, type) {
    return {
        name: type,
        contentType: 'text/plain',
        body: typeof chunk === 'string' ? chunk : chunk.toString('utf-8')
    };
}
function dedupeSteps(steps) {
    const result = [];
    let lastResult = undefined;
    for (const step of steps) {
        const canDedupe = !step.error && step.duration >= 0 && step.location?.file && !step.steps.length;
        const lastStep = lastResult?.step;
        if (canDedupe && lastResult && lastStep && step.category === lastStep.category && step.title === lastStep.title && step.location?.file === lastStep.location?.file && step.location?.line === lastStep.location?.line && step.location?.column === lastStep.location?.column) {
            ++lastResult.count;
            lastResult.duration += step.duration;
            continue;
        }
        lastResult = { step, count: 1, duration: step.duration };
        result.push(lastResult);
        if (!canDedupe)
            lastResult = undefined;
    }
    return result;
}
function createSnippets(stepsInFile) {
    for (const file of stepsInFile.keys()) {
        let source;
        try {
            source = fs.readFileSync(file, 'utf-8') + '\n//';
        }
        catch (e) {
            continue;
        }
        const lines = source.split('\n').length;
        const highlighted = babel.codeFrameColumns(source, { start: { line: lines, column: 1 } }, { highlightCode: true, linesAbove: lines, linesBelow: 0 });
        const highlightedLines = highlighted.split('\n');
        const lineWithArrow = highlightedLines[highlightedLines.length - 1];
        for (const step of stepsInFile.get(file)) {
            // Don't bother with snippets that have less than 3 lines.
            if (step.location.line < 2 || step.location.line >= lines)
                continue;
            // Cut out snippet.
            const snippetLines = highlightedLines.slice(step.location.line - 2, step.location.line + 1);
            // Relocate arrow.
            const index = lineWithArrow.indexOf('^');
            const shiftedArrow = lineWithArrow.slice(0, index) + ' '.repeat(step.location.column - 1) + lineWithArrow.slice(index);
            // Insert arrow line.
            snippetLines.splice(2, 0, shiftedArrow);
            step.snippet = snippetLines.join('\n');
        }
    }
}
function createErrorCodeframe(message, location) {
    let source;
    try {
        source = fs.readFileSync(location.file, 'utf-8') + '\n//';
    }
    catch (e) {
        return;
    }
    return babel.codeFrameColumns(source, {
        start: {
            line: location.line,
            column: location.column,
        },
    }, {
        highlightCode: false,
        linesAbove: 100,
        linesBelow: 100,
        message: stripAnsiEscapes(message).split('\n')[0] || undefined,
    });
}
function writeLine(line) {
    // eslint-disable-next-line no-restricted-properties
    process.stdout.write(line + '\n');
}
export default HtmlReporter;
//# sourceMappingURL=html.js.map