| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- /* global global */
- const fs = require("fs");
- const path = require("path");
- const TestExclude = require("test-exclude");
- const { recorder, event, output } = require("codeceptjs");
- const Container = require("codeceptjs/lib/container");
- const { clearString } = require("codeceptjs/lib/utils");
- function hashCode(str) {
- let hash = 0;
- if (str.length === 0) {
- return `${hash}`;
- }
- for (let i = 0; i < str.length; i++) {
- const char = str.charCodeAt(i);
- hash = (hash << 5) - hash + char;
- hash = hash & hash; // Convert to 32bit integer
- }
- return hash.toString(16);
- }
- const defaultConfig = {
- coverageDir: "output/coverage",
- actionCoverage: false,
- uniqueFileName: true,
- };
- const defaultActionCoverageConfig = {
- enabled: true,
- beginActionName: "performActionBegin",
- endActionName: "performActionEnd",
- coverageDir: "output/actionCoverage",
- include: true,
- exclude: false,
- };
- const supportedHelpers = ["Puppeteer", "Playwright"];
- function buildFileName(test, uniqueFileName) {
- let fileName = clearString(test.title);
- const originalName = fileName;
- // This prevent data driven to be included in the failed screenshot file name
- if (fileName.indexOf("{") !== -1) {
- fileName = fileName.substr(0, fileName.indexOf("{") - 3).trim();
- }
- if (test.ctx && test.ctx.test && test.ctx.test.type === "hook") {
- fileName = clearString(`${test.title}_${test.ctx.test.title}`);
- }
- if (uniqueFileName) {
- const uuid = hashCode(originalName);
- fileName = `${fileName.substring(0, 15)}_${uuid}.coverage.json`;
- } else {
- fileName = `${fileName}.coverage.json`;
- }
- return fileName;
- }
- function prepareActionStepConfig(actionCoverageConfig) {
- const config =
- typeof actionCoverageConfig === "boolean"
- ? {
- enabled: actionCoverageConfig,
- }
- : actionCoverageConfig;
- return Object.assign({}, defaultActionCoverageConfig, config);
- }
- /**
- * Dumps code coverage from Playwright/Puppeteer after every test.
- *
- * #### Configuration
- *
- *
- * ```js
- * plugins: {
- * istanbulCoverage: {
- * require: "./path/to/istanbulCoverage"
- * enabled: true
- * }
- * }
- * ```
- *
- * Possible config options:
- *
- * * `coverageDir`: directory to dump coverage files
- * * `uniqueFileName`: generate a unique filename by adding uuid
- */
- module.exports = (config) => {
- const helpers = Container.helpers();
- let helper;
- for (const helperName of supportedHelpers) {
- if (Object.keys(helpers).indexOf(helperName) > -1) {
- helper = helpers[helperName];
- }
- }
- if (!helper) {
- console.error("Coverage is only supported in Puppeteer, Playwright");
- return;
- }
- const options = Object.assign(defaultConfig, helper.options, config);
- options.actionCoverage = prepareActionStepConfig(options.actionCoverage);
- const excludeTester = new TestExclude({
- ...options.actionCoverage,
- cwd: path.resolve("../"),
- });
- let lastCoverages = {};
- let lastActionKeys = new Set();
- let actionCoverages = {};
- const actionsStack = [];
- let prevActionsStack = [];
- const performActionBegin = (name) => {
- actionsStack.push(name);
- };
- const performActionEnd = () => {
- actionsStack.pop();
- };
- if (options.actionCoverage.enabled) {
- global[options.actionCoverage.beginActionName] = performActionBegin;
- global[options.actionCoverage.endActionName] = performActionEnd;
- } else {
- global[options.actionCoverage.beginActionName] = global[options.actionCoverage.endActionName] = () => {};
- }
- const getCoverage = async () => {
- const coverageInfo = await helper.page.evaluate(() => {
- const coverageInfo = window.__coverage__;
- return coverageInfo;
- });
- return coverageInfo;
- };
- function hasActionChanges() {
- return (
- actionsStack.length !== prevActionsStack.length || actionsStack.some((val, key) => val !== prevActionsStack[key])
- );
- }
- function filterActionCoverage(actionCoverage) {
- return Object.fromEntries(Object.entries(actionCoverage).filter(([path]) => excludeTester.shouldInstrument(path)));
- }
- async function collectLastCoverage(actionKeys, endOfTest = false) {
- const coverageInfo = await getCoverage();
- if (!coverageInfo) return {};
- const actionCoverageInfo = filterActionCoverage(coverageInfo);
- actionKeys.forEach((actionKey) => {
- if (!lastCoverages[actionKey]) {
- lastCoverages[actionKey] = actionCoverageInfo;
- }
- });
- for (const lastActionKey of lastActionKeys) {
- if (endOfTest || actionKeys.indexOf(lastActionKey) === -1) {
- const additionalCoverage = subCoverage(actionCoverageInfo, lastCoverages[lastActionKey]);
- if (!actionCoverages[lastActionKey]) {
- actionCoverages[lastActionKey] = additionalCoverage;
- } else {
- actionCoverages[lastActionKey] = addCoverage(actionCoverages[lastActionKey], additionalCoverage);
- }
- lastCoverages[lastActionKey] = undefined;
- delete lastCoverages[lastActionKey];
- }
- }
- lastActionKeys = [...actionKeys];
- return coverageInfo;
- }
- function operateCoverage(aCoverage, bCoverage, op) {
- const resultCoverage = {};
- for (const [filePath, aFileCoverage] of Object.entries(aCoverage)) {
- const bFileCoverage = bCoverage[filePath];
- const resultFileCoverage = { ...aFileCoverage, s: {}, f: {}, b: {} };
- for (const [key, value] of Object.entries(aFileCoverage.s)) {
- resultFileCoverage.s[key] = op(value, bFileCoverage.s[key]);
- }
- for (const [key, value] of Object.entries(aFileCoverage.f)) {
- resultFileCoverage.f[key] = op(value, bFileCoverage.f[key]);
- }
- for (const [key, values] of Object.entries(aFileCoverage.b)) {
- resultFileCoverage.b[key] = values.map((val, idx) => op(val, bFileCoverage.b[key][idx]));
- }
- resultCoverage[filePath] = resultFileCoverage;
- }
- return resultCoverage;
- }
- function subCoverage(aCoverage, bCoverage) {
- return operateCoverage(aCoverage, bCoverage, (a, b) => a - b);
- }
- function addCoverage(aCoverage, bCoverage) {
- return operateCoverage(aCoverage, bCoverage, (a, b) => a + b);
- }
- event.dispatcher.on(event.all.before, async () => {
- output.debug("*** Collecting istanbul coverage for tests ****");
- if (!options.actionCoverage.enabled) return;
- actionCoverages = {};
- });
- event.dispatcher.on(event.all.after, async () => {
- if (!options.actionCoverage.enabled) return;
- recorder.add(
- "saving action coverage",
- async () => {
- try {
- const coverageDir = path.resolve(process.cwd(), options.actionCoverage.coverageDir);
- if (!fs.existsSync(coverageDir)) {
- fs.mkdirSync(coverageDir, { recursive: true });
- }
- for (const [actionName, coverage] of Object.entries(actionCoverages)) {
- const coveragePath = path.resolve(coverageDir, `${actionName}.coverage.json`);
- output.print(`writing ${coveragePath}`);
- fs.writeFileSync(coveragePath, JSON.stringify(coverage));
- }
- } catch (err) {
- console.error(err);
- }
- },
- true,
- );
- });
- event.dispatcher.on(event.test.before, async () => {
- if (!options.actionCoverage.enabled) return;
- lastCoverages = {};
- lastActionKeys = new Set();
- prevActionsStack = [];
- });
- // Save coverage data after every test run
- event.dispatcher.on(event.test.after, async (test) => {
- recorder.add(
- "saving coverage",
- async () => {
- try {
- const coverageInfo = await collectLastCoverage(actionsStack, true);
- const coverageDir = path.resolve(process.cwd(), options.coverageDir);
- if (!fs.existsSync(coverageDir)) {
- fs.mkdirSync(coverageDir, { recursive: true });
- }
- const coveragePath = path.resolve(coverageDir, buildFileName(test, options.uniqueFileName));
- output.print(`writing ${coveragePath}`);
- fs.writeFileSync(coveragePath, JSON.stringify(coverageInfo));
- } catch (err) {
- console.error(err);
- }
- },
- true,
- );
- });
- event.dispatcher.on(event.step.before, async () => {
- if (!options.actionCoverage.enabled) return;
- if (!hasActionChanges()) return;
- prevActionsStack = [...actionsStack];
- const stack = [...actionsStack];
- recorder.add("collect last coverage", async () => {
- try {
- await collectLastCoverage(stack);
- } catch (err) {
- console.error(err);
- }
- });
- });
- };
|