Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

monocart-coverage-reports

cenfun1mMIT2.12.2TypeScript support: included

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

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'
              }]
          ]
      }

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 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 // 开始和停止并收集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`
```js
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();
  • 结合使用 Playwright CDPSession

    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();
  • 结合使用 Puppeteer CDPSession

    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();
  • 结合使用 Selenium Webdriver WebSocket (仅支持Chrome/Edge浏览器)

    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

  • JavaScript V8代码覆盖官方说明
  • Playwright的覆盖率接口
  • Puppeteer的覆盖率接口
  • DevTools Protocol的覆盖率接口 参见 ScriptCoveragev8-coverage
    // 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 */
  • 忽略接下来一行或者多行 `js /* 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'); }

- 兼容支持 [c8 coverage](https://github.com/bcoe/c8/?tab=readme-ov-file#ignoring-all-lines-until-told) 或 [nodejs coverage](https://nodejs.org/docs/latest/api/test.html#collecting-code-coverage) 的语法格式
```js
/* c8 ignore start */
function uncovered() {
}
/* c8 ignore stop */

/* node:coverage disable */
function uncovered() {
}
/* node:coverage enable */

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报告来导出原始的覆盖率数据到指定的目录

  • 比如,单元测试保存到./coverage-reports/unit/raw,见例子

    • Jest + jest-monocart-coverage
    • Vitest + vitest-monocart-coverage
      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文件,通过下面新增的一些调试帮助信息来核对覆盖率

  • 生成额外的source和sourcemap文件到cache或raw文件夹

    const coverageOptions = {
      logging: 'debug',
      sourceMap: true
    };
  • 使用环境变量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

  • tsx
    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
  • ts-node
    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

  • Supports native codecov built-in report (specification)
    const coverageOptions = {
      outputDir: "./coverage-reports",
      reports: [
          ['codecov']
      ]
    };
  • Github actions: `yml
  • 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: `yml
  • 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: `yml
  • name: Coveralls uses: coverallsapp/github-action@v2 with:
      files: ./coverage-reports/lcov.info
    `

Sonar Cloud

Coverage

  • Using lcov report. Github actions example: `yml
  • 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) `sh npm install npx playwright install --with-deps

npm run build npm run test

npm run dev

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

npm run eol

Thanks

changelog

Changelog

  • 2.12.2

    • fixed #104
    • fixed #106
  • 2.12.1

    • fixed a css coverage issue for tailwindcss v4: Cannot read properties of undefined (reading 'forEach')
  • 2.12.0

    • fixed source path normalization policy (localhost-8130 => localhost:8130)
  • 2.11.6

    • added close() to CoverageClient type definition (#102)
  • 2.11.5

    • fixed sourcemap file JSON error (#97)
  • 2.11.4

    • fixed encoded source url issue
    • fixed filter for encoded url
  • 2.11.3

    • fixed cli to support merge command
  • 2.11.2

    • fixed log style
  • 2.11.1

    • added option gc for running gc manually
  • 2.11.0

    • added zip and merge option for raw report
  • 2.10.9

    • fixed empty coverage issue
  • 2.10.8

    • fixed source path for untested files
  • 2.10.7

    • fixed source filter for untested files
  • 2.10.6

    • fixed untested file coverage
  • 2.10.5

    • added all.transformer option to transform untested files
    • fixed merging issue for statements coverage
    • fixed merging issue for uncovered bytes if range not in sourcemap
  • 2.10.4

    • fixed sourcemap issue for jsx file (#72)
  • 2.10.3

    • fixed branch coverage issue for switch/case (#68)
  • 2.10.2

    • fixed branch coverage issue for static async function (#67)
  • 2.10.1

    • fixed hits UI issue (skip indent)
    • fixed ranges for original bytes
  • 2.10.0

    • (breaking snapshot) the report's files will be sorted by default
    • (breaking assets) the report assets have been changed from multiple files to a single compressed file
    • added maxCols for markdown-details
    • added filter for markdown-details and console-details
    • added name for function coverage
  • 2.9.3

    • fixed minimatch version to support Nodejs 18
  • 2.9.2

    • supported indexed sourcemaps (#57)
  • 2.9.1

    • improved performance
    • refactor for vendor
  • 2.9.0

    • performance improvement for comment parser (monocart-locator)
    • added new build-in report: markdown-summary (coverage-summary.md)
    • added new build-in report: markdown-details (coverage-details.md)
    • added new option: dataDir alternative to method addFromDir()
    • added new data extras for blank/comment/ignored lines
  • 2.8.7

    • fixed OOM with better GC
    • added new build-in report: codacy (codacy.json)
  • 2.8.6

    • improved performance
    • fixed warning message for adding empty istanbul coverage
    • fixed time duration formatter
  • 2.8.5

    • fixed a sourcemap content issue
  • 2.8.4

    • updated UI: highlight keywords for searching results
    • fixed coverage for static initialization blocks (#37)
    • fixed coverage for function defined after return (#38)
    • fixed ignored ranges
    • fixed CoverageReportOptions#reports type
    • fixed v8 report UI for hits
  • 2.8.3

    • fixed error occurred in electron (#27)
  • 2.8.2

    • fixed source path issue on Windows (#12)
  • 2.8.1

    • added baseDir option for normalizing the relative source path
  • 2.8.0

    • added filter option as combined filter for entryFilter and sourceFilter
    • added --all option for CLI
    • removed default config file .mcrrc.js and .mcrrc
    • added find-up for default config file
    • fixed type style for UI
    • fixed string to multiple patterns "{...}" for minimatch filter
    • added README.zh-Hans.md
  • 2.7.10

    • fixed undefined error when reading mappings
  • 2.7.9

    • fixed UI assets path
  • 2.7.8

    • fixed node import issue
    • fixed sourceRoot issue
    • fixed debug message
    • fixed Maximum call stack issue for cleanCache
    • updated formatter and locator
  • 2.7.7

    • fixed coverage undefined for CDPClient
    • added API getIstanbulCoverage for CDPClient
  • 2.7.6

    • fixed CLI arguments for -- separator
    • fixed sourcemap if range in a comment
    • added cache for sourcemap conversion
    • fixed ECONNREFUSED on NodeJS 18
    • added new API WSSession for custom CDPSession
  • 2.7.5

    • fixed special generated codes for original coverage
    • fixed generated bytes twice
    • fixed performance issue for diff matching
    • fixed loading sourcemap file if Content-Type is not json
  • 2.7.4

    • fixed sourcemap matching (diff algorithm)
  • 2.7.3

    • fixed a parser error for inline sourcemap
    • fixed error for loading config default
    • fixed sourcemap for more accurate matching
  • 2.7.2

    • fixed the switch between original and generated
    • fixed source url
    • added register to export source for v8 coverage
    • added support to read source from v8 coverage dir
    • added new API addFromDir
  • 2.7.1

    • fixed invalid sourcesContent
    • added new argument info for sourcePath
  • 2.7.0

    • added new API CDPClient for collecting v8 coverage more easily
    • added debug message for invalid coverage
    • fixed snapshot without debug file
    • fixed range mapping for debug
  • 2.6.5

    • fixed sourcemap issue for multiple lines of original text
    • fixed UI issues, next selection
  • 2.6.4

    • fixed summary name color for console-details
    • fixed uncaughtException for CLI
    • added loadConfig API
  • 2.6.3

    • fixed the path of sourcemap sources
    • fixed the deletion for node v8 coverage dir
    • fixed tbe end offset do not > source.length
    • added onReady hook for CLI
    • added onEntry hook
    • added default config file mcr.config.ts and better supports for ts-node
  • 2.6.2

    • fixed cli sub arguments
    • fixed report data for single codecov report
  • 2.6.1

    • fixed ts source issue
    • added getEntryFilter API
  • 2.6.0

    • fixed coverage for invalid original code
    • fixed coverage for empty bytes
    • added getSnapshot and diffSnapshot API for snapshot testing
    • added istanbul support for codecov and console-details
  • 2.5.9

    • fixed issue for istanbul source and source path replacement
    • fixed issue for original file when fully uncovered
    • added new option cleanCache
  • 2.5.8

    • added new option all for adding empty coverage for all files
    • added multiple patterns support for entryFilter and sourceFilter
    • fixed ast parser for empty coverage file
    • fixed percentage for empty css
    • fixed UI selection range error
  • 2.5.7

    • added object key/value replacement support for sourcePath
  • 2.5.6

    • added minimatch support for entryFilter and sourceFilter
    • added supports for loading default configuration file
  • 2.5.5

    • added switch button for count
    • fixed UI performance issue
    • fixed sourcemap content issue
  • 2.5.4

    • fixed empty coverage
  • 2.5.3

    • fixed color issue for console-details
    • fixed file url
  • 2.5.2

    • removed URL.canParse to support node 14
  • 2.5.1

    • added new option clean
  • 2.5.0

    • (New Feature) added new report console-details
  • 2.4.4

    • fixed branch count
    • fixed function range for class method
  • 2.4.3

    • fixed original offset if reversed
  • 2.4.2

    • fixed default reports with lcov
  • 2.4.1

    • fixed issue if uncovered in indent or ends with ";"
    • supports decorations for uncovered none else branches
  • 2.4.0

    • (New Feature) added statements to report
    • optimized source mapping related codes
  • 2.3.4

    • fixed ignore lines
    • fixed AssignmentPattern branches
    • added skip lines
  • 2.3.3

    • supports c8 ignore
    • fixed ignore range
  • 2.3.2

    • fixed issue for rmSync
    • fixed issue for AssignmentPattern
    • fixed url issue if entry url is a path
    • added inputDir option for CLI
  • 2.3.1

    • fixed issue for a source file fully uncovered
  • 2.3.0

    • added new feature: custom reporter
    • added new reporter: raw
    • added new option: inputDir
    • added support for vm scriptOffset
    • fixed branches if exists in wrapper function
    • fixed performance for decoding source mappings
    • fixed branch locations range error
  • 2.2.2

    • added codecov report
  • 2.2.1

    • fixed anonymous url for linux
  • 2.2.0

    • (New Feature) supports mcr CLI
  • 2.1.0 (Breaking Change)

    • defaults to export createCoverageReport function instead of CoverageReport class `diff
    • import CoverageReport, { createCoverageReport } from 'monocart-coverage-reports';
    • import createCoverageReport, { CoverageReport } from 'monocart-coverage-reports'; `
    • fixed format for files of coverage results
    • fixed types for commonjs
  • 2.0.10

    • updated dependencies
  • 2.0.9

    • fixed ignore for istanbul (#3)
    • fixed duplicate source file
    • fixed UI issues
  • 2.0.8

    • added new feature v8 ignore next N/start/stop
  • 2.0.7

    • added metrics option for v8 and console-summary report
  • 2.0.6

    • added console-summary report
  • 2.0.5

    • fixed AST parsing issue
    • fixed worker issue
    • updated formatter for worker issue
  • 2.0.4

    • fixed source mapping issue
  • 2.0.3

    • fixed branches count if no function ranges
  • 2.0.2

    • fixed TypeError: Cannot read properties of undefined (reading 'startOffset')
    • updated css parser
  • 2.0.0

    • added functions coverage for v8 (typescript)
    • added lines coverage for v8 (blanks, comments)
    • added node.js coverage test
  • 1.1.0

    • added onEnd hook
  • 1.0.9

    • added v8-json report
  • 1.0.8

    • fixed module path for pnpm
  • 1.0.7

    • fixed report path
    • added types for v8 options
  • 1.0.6

    • fixed reading json file
  • 1.0.4

    • added reportPath option