npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

monocart-coverage-reports

v2.11.3

Published

A code coverage tool to generate native V8 reports or Istanbul reports.

Downloads

868,287

Readme

Monocart Coverage Reports

npm license build install size npm graph dependencies downloads

🌐 English | 简体中文

JS代码覆盖率工具,用来生成原生的V8或者Istanbul代码覆盖率报告

Usage

推荐使用 Node.js 20+.

  • 安装
npm install monocart-coverage-reports
  • API
const MCR = require('monocart-coverage-reports');
const mcr = MCR({
    name: 'My Coverage Report - 2024-02-28',
    outputDir: './coverage-reports',
    reports: ["v8", "console-details"],
    cleanCache: true
});
await mcr.add(coverageData);
await mcr.generate();

也可以使用ESM的 import 然后加载配置文件

import { CoverageReport } from 'monocart-coverage-reports';
const mcr = new CoverageReport();
await mcr.loadConfig();

参见 多进程支持

  • CLI
mcr node my-app.js -r v8,console-details

参见 命令行

Options

Available Reports

内置V8报告(仅V8格式数据支持):

  • v8
    • 推荐使用:
      • 全新的原生V8覆盖率报告界面,更好的用户体验
      • 支持原生的Bytes覆盖率指标
      • 支持高性能处理大数据
      • 支持任何运行时代码的覆盖率(压缩后的)
      • 支持CSS代码覆盖率(用于分析CSS的冗余代码)
      • 对Sourcemap转换有更好的支持
    • 预览: V8 and more

内置Istanbul报告 (V8和Istanbul格式数据都支持):

其他内置报告 (V8和Istanbul格式数据都支持):

  • codecov 保存覆盖率数据到 Codecov 专属的json文件 (默认是codecov.json), 见例子

  • codacy 保存覆盖率数据到 Codacy 专属的json文件 (默认是codacy.json)

  • console-summary 在控制台显示覆盖率概要

  • console-details 在控制台显示每个文件的覆盖率概要。如果是Github actions,可以使用环境变量FORCE_COLOR: true来强制开启颜色支持

  • markdown-summary 保存概要信息到markdown文件 (默认是coverage-summary.md)。 如果是Github actions, 可以把markdown的内容添加到a job summary
cat path-to/coverage-summary.md >> $GITHUB_STEP_SUMMARY

  • markdown-details 保存覆盖率详情到markdown文件 (默认是 coverage-details.md)

    • 预览运行结果 runs
  • raw 只是保存原始覆盖率数据, 用于使用inputDir参数来导入多个原始数据进行合并报告。参见 合并覆盖率报告

  • 自定义报告

    {
        reports: [
            [path.resolve('./test/custom-istanbul-reporter.js'), {
                type: 'istanbul',
                file: 'custom-istanbul-coverage.text'
            }],
            [path.resolve('./test/custom-v8-reporter.js'), {
                type: 'v8',
                outputFile: 'custom-v8-coverage.json'
            }],
            [path.resolve('./test/custom-v8-reporter.mjs'), {
                type: 'both'
            }]
        ]
    }
    • Istanbul自定义报告

    例子: ./test/custom-istanbul-reporter.js, see istanbul built-in reporters' implementation for reference.

    • V8自定义报告

    例子: ./test/custom-v8-reporter.js

Multiple Reports:

如何配置多个报告

const MCR = require('monocart-coverage-reports');
const coverageOptions = {
    outputDir: './coverage-reports',
    reports: [
        // build-in reports
        ['console-summary'],
        ['v8'],
        ['html', {
            subdir: 'istanbul'
        }],
        ['json', {
            file: 'my-json-file.json'
        }],
        'lcovonly',

        // custom reports
        // Specify reporter name with the NPM package
        ["custom-reporter-1"],
        ["custom-reporter-2", {
            type: "istanbul",
            key: "value"
        }],
        // Specify reporter name with local path
        ['/absolute/path/to/custom-reporter.js']
    ]
}
const mcr = MCR(coverageOptions);

Compare Reports

如果是V8数据格式使用Istanbul的报告,将自动从V8转换到Istanbul

| | Istanbul | V8 | V8 to Istanbul | | :--------------| :------ | :------ | :---------------------- | | 数据格式 | Istanbul (Object) | V8 (Array) | V8 (Array) | | 输出报告 | Istanbul reports | V8 reports | Istanbul reports | | - Bytes 字节覆盖率 | ❌ | ✅ | ❌ | | - Statements 语句覆盖率 | ✅ | ✅ | ✅ | | - Branches 分支覆盖率 | ✅ | ✅ | ✅ | | - Functions 函数覆盖率 | ✅ | ✅ | ✅ | | - Lines 行覆盖率 | ✅ | ✅ | ✅ | | - Execution counts 函数执行数 | ✅ | ✅ | ✅ | | CSS 覆盖率 | ❌ | ✅ | ✅ | | 压缩过的代码 | ❌ | ✅ | ❌ |

Collecting Istanbul Coverage Data

Collecting V8 Coverage Data

Collecting V8 Coverage Data with Playwright

使用Playwright的覆盖接口收集覆盖率数据

await Promise.all([
    page.coverage.startJSCoverage({
        // reportAnonymousScripts: true,
        resetOnNavigation: false
    }),
    page.coverage.startCSSCoverage({
        // Note, anonymous styles (without sourceURLs) are not supported, alternatively, you can use CDPClient
        resetOnNavigation: false
    })
]);

await page.goto("your page url");

const [jsCoverage, cssCoverage] = await Promise.all([
    page.coverage.stopJSCoverage(),
    page.coverage.stopCSSCoverage()
]);

const coverageData = [... jsCoverage, ... cssCoverage];

使用 @playwright/testAutomatic fixtures收集覆盖率数据, 见例子: fixtures.ts 参见例子 ./test/test-v8.js, and anonymous, css

Collecting Raw V8 Coverage Data with Puppeteer

使用Puppeteer的覆盖接口收集覆盖率数据,注意Puppeteer默认不会提供原生V8的覆盖率数据,需要设置includeRawScriptCoverage

await Promise.all([
    page.coverage.startJSCoverage({
        // reportAnonymousScripts: true,
        resetOnNavigation: false,
        // provide raw v8 coverage data
        includeRawScriptCoverage: true
    }),
    page.coverage.startCSSCoverage({
        resetOnNavigation: false
    })
]);

await page.goto("your page url");

const [jsCoverage, cssCoverage] = await Promise.all([
    page.coverage.stopJSCoverage(),
    page.coverage.stopCSSCoverage()
]);

// to raw V8 script coverage
const coverageData = [... jsCoverage.map((it) => {
    return {
        source: it.text,
        ... it.rawScriptCoverage
    };
}), ... cssCoverage];

参见: ./test/test-puppeteer.js

Collecting V8 Coverage Data from Node.js

有多种方法可以从Node.js收集V8覆盖率数据:

  • NODE_V8_COVERAGE=dir

    • 使用Node.js环境变量NODE_V8_COVERAGE=dir来启动程序, 然后在进程正常结束之后,覆盖率数据将自动保存到指定的dir目录.
    • dir目录读取所有的JSON文件,来生成覆盖率报告
    • 参见例子:

    cross-env NODE_V8_COVERAGE=.temp/v8-coverage-env node ./test/test-node-env.js && node ./test/generate-report.js

  • V8 API + NODE_V8_COVERAGE

    • 如果进程不能正常结束,比如被强制关闭,或者压根就不结束,比如启动了一个服务类的,那么需要手动写入覆盖率数据,这里需要调用接口v8.takeCoverage()
    • 参见例子:

    cross-env NODE_V8_COVERAGE=.temp/v8-coverage-api node ./test/test-node-api.js

  • Inspector API

    • 首先连接到Node.js的V8 inspector
    • 然后使用inspector的覆盖相关API来开启和收集覆盖率数据
    • 参见例子:

    node ./test/test-node-ins.js

    • vm的例子 (注意这里需要使用scriptOffset,因为vm里一般都会加一层包裹代码,需要这个偏移位置来修正覆盖率数据块的位置):

    node ./test/test-node-vm.js

  • CDP API

    • 开启Node调试
    • 使用CDP的覆盖率接口开启和收集覆盖率数据
    • 参见例子:

    node --inspect=9229 ./test/test-node-cdp.js

  • Node Debugging + CDP + NODE_V8_COVERAGE + V8 API

    • 如果启动了一个Node服务,可以手动调用v8.takeCoverage()接口来保存覆盖率数据,开启Node调试就可以远程通过CDP连接的Runtime.evaluate,来调用这个接口.
    • 参见koa的例子:

    node ./test/test-node-koa.js

  • Child Process + NODE_V8_COVERAGE

Collecting V8 Coverage Data with CDPClient API

  • CDPClientMCR提供的内置接口类,用来更便捷的处理覆盖率相关数据,所有的API如下
// 开始和停止并收集JS的覆盖率数据
startJSCoverage: () => Promise<void>;
stopJSCoverage: () => Promise<V8CoverageEntry[]>;

// 开始和停止并收集CSS的覆盖率数据,支持匿名文件(比如style里的css)
startCSSCoverage: () => Promise<void>;
stopCSSCoverage: () => Promise<V8CoverageEntry[]>;

// 开始和停止并收集JS和CSS的覆盖率数据
startCoverage: () => Promise<void>;
stopCoverage: () => Promise<V8CoverageEntry[]>;

/** 如果开启了NODE_V8_COVERAGE,这个接口用来手动保存当前覆盖率数据 */
writeCoverage: () => Promise<string>;

/** 收集istanbul覆盖率数据 */
getIstanbulCoverage: (coverageKey?: string) => Promise<any>;
  • 结合使用Node调试端口--inspect=9229 或者浏览器调试端口 --remote-debugging-port=9229
const MCR = require('monocart-coverage-reports');
const client = await MCR.CDPClient({
    port: 9229
});
await client.startJSCoverage();
// run your test here
const coverageData = await client.stopJSCoverage();
const { chromium } = require('playwright');
const MCR = require('monocart-coverage-reports');
const browser = await chromium.launch();
const page = await browser.newPage();
const session = await page.context().newCDPSession(page);
const client = await MCR.CDPClient({
    session
});
// both js and css coverage
await client.startCoverage();
// run your test page here
await page.goto("your page url");
const coverageData = await client.stopCoverage();
const puppeteer = require('puppeteer');
const MCR = require('monocart-coverage-reports');
const browser = await puppeteer.launch({});
const page = await browser.newPage();
const session = await page.target().createCDPSession();
const client = await MCR.CDPClient({
    session
});
// both js and css coverage
await client.startCoverage();
// run your test page here
await page.goto("your page url");
const coverageData = await client.stopCoverage();
const { Builder, Browser } = require('selenium-webdriver');
const MCR = require('monocart-coverage-reports');
const driver = await new Builder().forBrowser(Browser.CHROME).build();
const pageCdpConnection = await driver.createCDPConnection('page');
const session = new MCR.WSSession(pageCdpConnection._wsConnection);
const client = await MCR.CDPClient({
    session
})

V8 Coverage Data API

// Coverage data for a source range.
export interface CoverageRange {
    // JavaScript script source offset for the range start.
    startOffset: integer;
    // JavaScript script source offset for the range end.
    endOffset: integer;
    // Collected execution count of the source range.
    count: integer;
}
// Coverage data for a JavaScript function.
/**
 * @functionName can be an empty string.
 * @ranges is always non-empty. The first range is called the "root range".
 * @isBlockCoverage indicates if the function has block coverage information.
    If this is false, it usually means that the functions was never called.
    It seems to be equivalent to ranges.length === 1 && ranges[0].count === 0.
*/
export interface FunctionCoverage {
    // JavaScript function name.
    functionName: string;
    // Source ranges inside the function with coverage data.
    ranges: CoverageRange[];
    // Whether coverage data for this function has block granularity.
    isBlockCoverage: boolean;
}
// Coverage data for a JavaScript script.
export interface ScriptCoverage {
    // JavaScript script id.
    scriptId: Runtime.ScriptId;
    // JavaScript script name or url.
    url: string;
    // Functions contained in the script that has coverage data.
    functions: FunctionCoverage[];
}
export type V8CoverageData = ScriptCoverage[];

| JavaScript Runtime | V8 Coverage | | | :--------------| :----: | :---------------------- | | Chrome (65%) | ✅ | Chromium-based | | Safari (18%) | ❌ | | | Edge (5%) | ✅ | Chromium-based | | Firefox (2%) | ❌ | | | Node.js | ✅ | | | Deno | ❌ | issue | | Bun | ❌ | |

Filtering Results

Using entryFilter and sourceFilter to filter the results for V8 report

当收集到V8的覆盖数据时,它实际上包含了所有的入口文件的覆盖率数据, 比如有以下3个文件:

  • dist/main.js
  • dist/vendor.js
  • dist/something-else.js

这个时候可以使用entryFilter来过滤这些入口文件. 比如我们不需要看到vendor.jssomething-else.js的覆盖率,就可以过滤掉,只剩下1个文件

  • dist/main.js

如果一个入口文件存在行内或者链接的sourcemap文件,那么我们会尝试读取并解析sourcemap,以获取入口文件包含的所有源文件,并添加到列表。此时如果logging没有设置成debug,那么这个入口文件在成功解出源文件后会被移除

  • src/index.js
  • src/components/app.js
  • node_modules/dependency/dist/dependency.js

这个时候可以使用sourceFilter来过滤这些源文件。比如我们不需要看到源文件dependency.js的覆盖率,就可以过滤掉,最后只剩下如下文件

  • src/index.js
  • src/components/app.js

过滤可以使用函数:

const coverageOptions = {
    entryFilter: (entry) => entry.url.indexOf("main.js") !== -1,
    sourceFilter: (sourcePath) => sourcePath.search(/src\//) !== -1
};

也可以使用便捷的minimatch来匹配(推荐):

const coverageOptions = {
    entryFilter: "**/main.js",
    sourceFilter: "**/src/**"
};

支持多个匹配:

const coverageOptions = {
    entryFilter: {
        '**/node_modules/**': false,
        '**/vendor.js': false,
        '**/src/**': true
    },
    sourceFilter: {
        '**/node_modules/**': false,
        '**/**': true
    }
};

作为CLI参数(JSON字符串,Added in: v2.8):

mcr --sourceFilter "{'**/node_modules/**':false,'**/**':true}"

注意,这些匹配实际上会转换成一个过滤函数(如下),所以如果一个匹配成功则会直接返回,后面的将不再继续匹配。请注意先后顺序,如果存在包含关系的,可以调整上下顺序,最后如果都未匹配,则默认返回false

const coverageOptions = {
    entryFilter: (entry) => {
        if (minimatch(entry.url, '**/node_modules/**')) { return false; }
        if (minimatch(entry.url, '**/vendor.js')) { return false; }
        if (minimatch(entry.url, '**/src/**')) { return true; }
        return false; // else unmatched
    }
};

Using filter instead of entryFilter and sourceFilter

如果你不想定义两个过滤器,可以使用 filter 选项代替,可以将多个匹配合并在一起. (Added in: v2.8)

const coverageOptions = {
    // combined patterns
    filter: {
        '**/node_modules/**': false,
        '**/vendor.js': false,
        '**/src/**': true
        '**/**': true
    }
};

Resolve sourcePath for the Source Files

当一个文件从sourcemap解包,它的路径可能是个虚拟路径, 此时可以使用sourcePath选项来修改文件路径。比如,我们测试了多个dist包的入口文件,它们的源文件可能包含了一些共同的文件,但路径可能不同,如果我们需要相同的文件覆盖率数据可以自动合并,那么需要使用sourcePath来统一这些相同文件的路径

const coverageOptions = {
    sourcePath: (filePath) => {
        // Remove the virtual prefix
        const list = ['my-dist-file1/', 'my-dist-file2/'];
        for (const str of list) {
            if (filePath.startsWith(str)) {
                return filePath.slice(str.length);
            }
        }
        return filePath;
    }
};

它也支持简单key/value的替换:

const coverageOptions = {
    sourcePath: {
        'my-dist-file1/': '', 
        'my-dist-file2/': ''
    }
};

解决文件路径不完整的问题:

const path = require("path")

// MCR coverage options
const coverageOptions = {
    sourcePath: (filePath, info)=> {
        if (!filePath.includes('/') && info.distFile) {
            return `${path.dirname(info.distFile)}/${filePath}`;
        }
        return filePath;
    }
}

Adding Empty Coverage for Untested Files

默认,未测试的文件是不会包含到覆盖率报告的,需要使用all选项来为这些文件添加一个空的覆盖率,也就是0%

const coverageOptions = {
    all: './src',

    // 支持多个目录
    all: ['./src', './lib'],
};

未测试的文件也适用于sourceFilter过滤器. 而且也可以指定自己的filter过滤器 (可以返回文件类型来支持js或css的覆盖率格式):

const coverageOptions = {
    all: {
        dir: ['./src'],
        filter: {
            // exclude files
            '**/ignored-*.js': false,
            '**/*.html': false,
            // empty css coverage
            '**/*.scss': "css",
            '**/*': true
        }
    }
};

我们可能需要编译.ts, .jsx, .vue等等这样的文件, 这样才能被默认的AST解析器解析,以得到更多的覆盖率指标的数据

const path = require("path");
const swc = require("@swc/core");
const coverageOptions = {
    all: {
        dir: ['./src'],
        transformer: async (entry) => {
            const { code, map } = await swc.transform(entry.source, {
                filename: path.basename(entry.url),
                sourceMaps: true,
                isModule: true,
                jsc: {
                    parser: {
                        syntax: "typescript",
                        jsx: true
                    },
                    transform: {}
                }
            });
            entry.source = code;
            entry.sourceMap = JSON.parse(map);
        }
    }
};

onEnd Hook

结束回调可以用来自定义业务需求,比如检测覆盖率是否达标,对比每个指标的thresholds,如果低于要求的值则可以抛出一个错误退出

const EC = require('eight-colors');
const coverageOptions = {
    name: 'My Coverage Report',
    outputDir: './coverage-reports',
    onEnd: (coverageResults) => {
        const thresholds = {
            bytes: 80,
            lines: 60
        };
        console.log('check thresholds ...', thresholds);
        const errors = [];
        const { summary } = coverageResults;
        Object.keys(thresholds).forEach((k) => {
            const pct = summary[k].pct;
            if (pct < thresholds[k]) {
                errors.push(`Coverage threshold for ${k} (${pct} %) not met: ${thresholds[k]} %`);
            }
        });
        if (errors.length) {
            const errMsg = errors.join('\n');
            console.log(EC.red(errMsg));
            // throw new Error(errMsg);
            // process.exit(1);
        }
    }
}

Ignoring Uncovered Codes

使用特定的注释,以v8 ignore 开头可以忽略未覆盖的代码:

  • 忽略开始到结束
/* v8 ignore start */
function uncovered() {
}
/* v8 ignore stop */
  • 忽略接下来一行或者多行
/* v8 ignore next */
const os = platform === 'wind32' ? 'Windows' : 'Other';

const os = platform === 'wind32' ? 'Windows' /* v8 ignore next */ : 'Other';

// v8 ignore next 3
if (platform === 'linux') {
    console.log('hello linux');
}

Multiprocessing Support

多进程支持可以很好的解决异步并行的情况。所有的覆盖率数据会保存到[outputDir]/.cache,在报告生成之后,这些缓存数据会被清除。除非开启了调试模式,或者使用了raw报告

  • 主进程,初始化,清理之前的缓存
const MCR = require('monocart-coverage-reports');
const coverageOptions = require('path-to/same-options.js');
const mcr = MCR(coverageOptions);
// clean previous cache before the start of testing
// unless the running environment is new and no cache
mcr.cleanCache();
  • 子进程1, 测试业务1
const MCR = require('monocart-coverage-reports');
const coverageOptions = require('path-to/same-options.js');
const mcr = MCR(coverageOptions);
await mcr.add(coverageData1);
  • 子进程2, 测试业务2
const MCR = require('monocart-coverage-reports');
const coverageOptions = require('path-to/same-options.js');
const mcr = MCR(coverageOptions);
await mcr.add(coverageData2);
  • 主进程,所有测试完成之后
// generate coverage reports after the completion of testing
const MCR = require('monocart-coverage-reports');
const coverageOptions = require('path-to/same-options.js');
const mcr = MCR(coverageOptions);
await mcr.generate();

Command Line

使用mcr命令行将使用NODE_V8_COVERAGE=dir来启动一个子进程运行程序,直到正常退出,然后自动从dir目录来读取覆盖率数据,并生成覆盖率报告

  • 全局安装
npm i monocart-coverage-reports -g
mcr node ./test/specs/node.test.js -r v8,console-details --lcov
  • 本地项目安装
npm i monocart-coverage-reports
npx mcr node ./test/specs/node.test.js -r v8,console-details --lcov
  • 命令行参数 直接运行 mcrmcr --help 查看所有CLI的参数

  • 使用 -- 可以隔离子程序参数,以免两种参数混淆

mcr -c mcr.config.js -- sub-cli -c sub-cli.config.js

Config File

根据以下优先级加载配置文件

  • 自定义配置文件(如果没有指定则加载后面的默认配置文件):
    • CLI: mcr --config <my-config-file-path>
    • API: await mcr.loadConfig("my-config-file-path")
  • mcr.config.js
  • mcr.config.cjs
  • mcr.config.mjs
  • mcr.config.json - json format
  • mcr.config.ts (requires preloading the ts execution module)

Merge Coverage Reports

以下这些使用场景可能需要使用合并覆盖率报告:

  • 多个执行环境,比如Node.js服务端,以及浏览器客户端,比如Next.js
  • 多种测试类型,比如Jest单元测试,以及Playwright的端到端自动化测试
  • 分布式测试,测试结果保存到了多台机器或不同的容器中

Automatic Merging

  • 默认MCR在执行generate()时会自动合并覆盖率数据。所以可以在多进程支持下,多次添加覆盖率数据,最后将自动合并
  • 比如Next.js就可以同时添加前后端覆盖率数据,最后再执行generate()生成覆盖率报告,见例子nextjs-with-playwright
  • 使用Codecov在线覆盖率报告服务,请设置输出codecov报告, 它会生成专属的codecov.json,如果有多个codecov.json文件上传,它们会自动合并数据,参见Codecov合并报告说明

Manual Merging

手动合并覆盖率报告需要使用raw报告来导出原始的覆盖率数据到指定的目录

const coverageOptions = {
    name: 'My Unit Test Coverage Report',
    outputDir: "./coverage-reports/unit",
    reports: [
        ['raw', {
            // relative path will be "./coverage-reports/unit/raw"
            // defaults to raw
            outputDir: "raw"
        }],
        ['v8'],
        ['console-details']
    ]
};
  • 同样的,E2E测试保存到./coverage-reports/e2e/raw. 见例子:

  • 然后创建一个merge-coverage.js文件,使用inputDir参数导入raw数据,来生成合并的覆盖率报告.

// merge-coverage.js
const fs = require('fs');
const { CoverageReport } = require('monocart-coverage-reports');
const inputDir = [
    './coverage-reports/unit/raw',
    './coverage-reports/e2e/raw'
];
const coverageOptions = {
    name: 'My Merged Coverage Report',
    inputDir,
    outputDir: './coverage-reports/merged',

    // filter for both unit and e2e
    entryFilter: {
        '**/node_modules/**': false,
        '**/*': true
    },
    sourceFilter: {
        '**/node_modules/**': false,
        '**/src/**': true
    },
    
    sourcePath: (filePath, info) => {
        // Unify the file path for the same files
        // For example, the file index.js has different paths:
        // unit: unit-dist/src/index.js
        // e2e: e2e-dist/src/index.js
        // return filePath.replace("unit-dist/", "").replace("e2e-dist/", "")
        return filePath;
    },

    reports: [
        ['v8'],
        ['console-details']
    ],
    
    onEnd: () => {
        // remove the raw files if it useless
        // inputDir.forEach((p) => {
        //     fs.rmSync(p, {
        //         recursive: true,
        //         force: true
        //     });
        // });
    }
};
await new CoverageReport(coverageOptions).generate();
  • 最后在所有测试完成后运行node path/to/merge-coverage.js. 所有的执行脚本大概如下:
{
    "scripts": {
        "test:unit": "jest",
        "test:e2e": "playwright test",
        "merge-coverage": "node path/to/merge-coverage.js",
        "test": "npm run test:unit && npm run test:e2e && npm run merge-coverage"
    }
}

参见例子: merge-code-coverage

Common issues

常见问题

Unexpected coverage

覆盖率看起来不正确,多数情况是因为sourcemap转换的问题导致的. 可以先尝试设置构建工具的 minify=false 也就是不要压缩代码来解决。下面来看看sourcemap存在问题的具体原因:

const a = tf ? 'true' : 'false';
               ^     ^  ^
              m1     p  m2

上面是经过构建工具编译过的代码,通过AST分析,位置p对应的原始位置是我们要找的,而从sourcemap里仅能找到离p最近的位置映射m1m2,也就是位置p并没有精确的映射保存到sourcemap里,从而无法直接获取精确的原始位置,但我们能知道p的原始位置应该在m1m2之间。

MCR如何解决这个问题:

  • 1, 首先会尝试使用diff-sequences工具来比较m1m2之间的生成代码和原始代码,找到p对应的字符位置,可以解决绝大多数问题。但是如果代码是非JS格式的,比如Vue模板是HTML,或JSX这些,不管怎么比较也是很难精确找到对应位置的,甚至此时的sourcemap本身都比较乱。
  • 2, 然后就是通过分析AST,找到所有的functions, statements 和 branches,因为V8覆盖率本身不提供这些指标的覆盖率. (对于分支覆盖暂不支持AssignmentPattern类型,因为即使分析AST也无法从V8覆盖率找到它的数据)。

Unparsable source

源码无法解析问题。由上面我们知道MCR通过分析源码的AST获取更多指标的覆盖率信息,但源码如果不是标准的 ECMAScript,比如ts, jsx这些,那么分析的时候就会报错,此时我们可以手动来编译这些文件(可行但不推荐).

import * as fs from "fs";
import * as path from "path";
import { fileURLToPath } from "url";
import * as TsNode from 'ts-node';
const coverageOptions = {
    onEntry: async (entry) => {
        const filePath = fileURLToPath(entry.url)
        const originalSource = fs.readFileSync(filePath).toString("utf-8");
        const fileName = path.basename(filePath);
        const tn = TsNode.create({});
        const source = tn.compile(originalSource, fileName);
        entry.fake = false;
        entry.source = source;
    }
}

JavaScript heap out of memory

内存溢出问题可能出现在有太多的原生V8覆盖率文件要处理. 我们可以使用Node.js的一个选项来增加内存使用:

- run: npm run test:coverage
    env:
        NODE_OPTIONS: --max-old-space-size=8192

Debug for Coverage and Sourcemap

当你觉得覆盖率存在问题的时候,MCR支持自行调试来核验覆盖率的准确性

  • 首先打开调试设置logging: 'debug'
const coverageOptions = {
    logging: 'debug',
    reports: [
        ['v8'],
        ['console-details']
    ]
};

调试模式下,也就是loggingdebug的时候, 原始的覆盖率数据将保留在[outputDir]/.cache缓存目录下,不会删除,如果使用了raw报告,那么位置变为[outputDir]/raw下,这样我们可以打开v8报告的html文件,通过下面新增的一些调试帮助信息来核对覆盖率

  • 使用环境变量MCR_LOG_TIME显示时间日志
process.env.MCR_LOG_TIME = true

Integration with Any Testing Framework

通用集成方案

  • 通过API接口在程序集成
    • 首先,要自行收集覆盖率数据,然后,添加到报告实例 await mcr.add(coverageData)
    • 最后,生成覆盖率报告 await mcr.generate()
    • 参见 多进程支持
  • 通过CLI命令行与其他命令行集成
    • 直接在其他命令行前面添加mcr的命令行即可 mcr your-cli --your-arguments
    • 参见 命令行

Integration Examples

Playwright

c8

  • c8 has integrated MCR as an experimental feature since v10.1.0
c8 --experimental-monocart --reporter=v8 --reporter=console-details node foo.js

CodeceptJS

VSCode

Jest

Vitest

Node Test Runner

Puppeteer

Cypress

WebdriverIO

Storybook Test Runner

TestCafe

Selenium Webdriver

Mocha

mcr mocha ./test/**/*.js

TypeScript

cross-env NODE_OPTIONS="--import tsx" npx mcr tsx ./src/example.ts
cross-env NODE_OPTIONS="--import tsx" npx mcr mocha ./test/**/*.ts
# Node.js v18.19.0 +
mcr --import tsx tsx ./src/example.ts
mcr --import tsx mocha ./test/**/*.ts
cross-env NODE_OPTIONS="--loader ts-node/esm --no-warnings" npx mcr ts-node ./src/example.ts
cross-env NODE_OPTIONS="--loader ts-node/esm --no-warnings" npx mcr mocha ./test/**/*.ts

AVA

mcr ava

Codecov

codecov

const coverageOptions = {
    outputDir: "./coverage-reports",
    reports: [
        ['codecov']
    ]
};
  • Github actions:
- name: Codecov
    uses: codecov/codecov-action@v4
    with:
        token: ${{ secrets.CODECOV_TOKEN }}
        files: ./coverage-reports/codecov.json

Codacy

Codacy

  • Using lcov report:
const coverageOptions = {
    outputDir: "./coverage-reports",
    lcov: true
};
  • Github actions:
- name: Codacy Coverage Reporter
    uses: codacy/codacy-coverage-reporter-action@v1
    with:
        project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
        coverage-reports: ./docs/mcr/lcov.info

Coveralls

Coverage Status

  • Using lcov report:
const coverageOptions = {
    outputDir: "./coverage-reports",
    lcov: true
};
  • Github actions:
- name: Coveralls
    uses: coverallsapp/github-action@v2
    with:
        files: ./coverage-reports/lcov.info

Sonar Cloud

Coverage

  • Using lcov report. Github actions example:
- name: Analyze with SonarCloud
    uses: sonarsource/sonarcloud-github-action@master
    env: 
        SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
    with:
        projectBaseDir: ./
        args: >
        -Dsonar.organization=cenfun
        -Dsonar.projectKey=monocart-coverage-reports
        -Dsonar.projectName=monocart-coverage-reports
        -Dsonar.javascript.lcov.reportPaths=docs/mcr/lcov.info
        -Dsonar.sources=lib
        -Dsonar.tests=test
        -Dsonar.exclusions=dist/*,packages/*

Contributing

  • Node.js 20+
  • VSCode (extensions: eslint/stylelint/vue)
npm install
npx playwright install --with-deps

npm run build
npm run test

npm run dev
  • Refreshing eol=lf for snapshot of test (Windows)
git add . -u
git commit -m "Saving files before refreshing line endings"

npm run eol

Thanks