├── .browserslistrc ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── babel.config.js ├── icon-16x16.png ├── icon-240x240.svg ├── icon-32x32.png ├── icon-48x48.png ├── icon-config.json ├── jest.base-config.js ├── jest.config.js ├── jest ├── babelTransform.js ├── fileTransform.js └── rawLoader.js ├── lerna.json ├── package.json ├── packages ├── app │ ├── .gitignore │ ├── LICENSE │ ├── index.html │ ├── jest.config.js │ ├── mocks │ │ └── lcov │ │ │ ├── index.js │ │ │ ├── unix_absolute.info │ │ │ ├── unix_relative_multi_root.info │ │ │ ├── unix_relative_single_root.info │ │ │ ├── win_absolute.info │ │ │ ├── win_relative_multi_root.info │ │ │ └── win_relative_single_root.info │ ├── package.json │ ├── public │ │ └── _redirects │ ├── src │ │ ├── components │ │ │ ├── App.js │ │ │ ├── App.module.less │ │ │ ├── App.test.js │ │ │ ├── CoverageDataProvider │ │ │ │ ├── CoverageDataContext.js │ │ │ │ ├── CoverageDataProvider.js │ │ │ │ ├── useCoverageData.js │ │ │ │ ├── useCoverageDataControl.js │ │ │ │ └── useCoverageDate.js │ │ │ ├── Footer │ │ │ │ └── Footer.js │ │ │ ├── Header │ │ │ │ ├── Header.js │ │ │ │ └── Header.module.css │ │ │ ├── LcovImport │ │ │ │ ├── LcovImport.js │ │ │ │ └── LcovImport.module.less │ │ │ ├── Redirect │ │ │ │ └── Redirect.js │ │ │ └── Report │ │ │ │ ├── Report.js │ │ │ │ └── Report.module.less │ │ ├── images │ │ │ └── demo.png │ │ ├── index.js │ │ ├── index.less │ │ └── utils │ │ │ └── readLcov.js │ └── webpack.config.js ├── cli │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── commands │ │ └── lcov.js │ ├── package.json │ ├── rollup.config.mjs │ └── src │ │ └── index.js ├── components │ ├── CoverageIndicator │ │ ├── CoverageIndicator.js │ │ └── CoverageIndicator.module.less │ ├── Footer │ │ ├── Footer.js │ │ └── Footer.module.less │ ├── LICENSE │ ├── LinkButton │ │ ├── LinkButton.js │ │ └── LinkButton.module.less │ ├── Summary │ │ ├── Summary.js │ │ ├── Summary.module.less │ │ ├── SummaryLine.js │ │ └── SummaryLine.module.less │ ├── TreeView │ │ ├── TreeView.js │ │ ├── TreeView.module.less │ │ ├── TreeView.test.js │ │ └── useCollapse.js │ ├── index.js │ ├── jest.config.js │ ├── package.json │ └── render │ │ ├── buildTableRowsData.js │ │ ├── collapse.js │ │ ├── renderRow.js │ │ ├── renderRow.module.less │ │ ├── renderTable.js │ │ └── renderTable.module.less ├── core │ ├── LICENSE │ ├── index.js │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── build.coverage.test.js │ │ ├── buildCoverage.js │ │ ├── buildCoverageTree.js │ │ ├── calculatePercentage.js │ │ ├── clsx.js │ │ ├── coverageLevel.js │ │ ├── parseLCOV.js │ │ └── treeNodeTypes.js │ └── styles │ │ ├── baseline.less │ │ ├── colors.less │ │ ├── layout.less │ │ └── typography.less ├── demo │ └── tree-view │ │ ├── App.js │ │ ├── App.module.less │ │ ├── LICENSE │ │ ├── index.html │ │ ├── index.js │ │ ├── package.json │ │ ├── sample.json │ │ └── webpack.config.js ├── istanbul-report │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── rollup.config.mjs │ └── src │ │ └── index.js ├── report │ ├── LICENSE │ ├── index.js │ ├── package.json │ ├── src │ │ ├── components │ │ │ ├── App.js │ │ │ ├── App.module.less │ │ │ ├── Details.js │ │ │ ├── Details.module.less │ │ │ ├── Error.js │ │ │ ├── Error.module.less │ │ │ ├── Report.js │ │ │ ├── Report.module.less │ │ │ └── TokenStream.js │ │ ├── highlight │ │ │ ├── filterTagTokens.js │ │ │ ├── tokenize.js │ │ │ └── tokens.js │ │ ├── index.html │ │ ├── index.js │ │ └── utilities │ │ │ └── generateUniqueId.js │ └── webpack.config.js └── rollup-copy │ ├── LICENSE │ ├── index.js │ └── package.json ├── postcss.config.js ├── webpack.base-config.js └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | last 2 versions 2 | not dead 3 | not op_mini all 4 | not android > 0 5 | not and_chr > 0 6 | not and_ff > 0 7 | not and_uc > 0 8 | not ios_saf > 0 9 | not and_qq > 0 10 | not op_mob > 0 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | versioning-strategy: increase 13 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Build & Test 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [16.x, 18.x, 20.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | 30 | - name: Install and Test 31 | run: | 32 | yarn 33 | yarn build 34 | yarn test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | /.idea/ 4 | /packages/demo/*/coverage/ 5 | /packages/*/coverage/ 6 | /coverage/ 7 | yarn-error.log 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## [Unreleased] 9 | 10 | ## [1.4.0] - 2023-08-21 11 | ### Added 12 | * Better branding for the Lcov Viewer application 13 | * Dependabot configuration 14 | * Syncpack for better dependency management 15 | * Added CHANGELOG.md 16 | 17 | ### Changed 18 | * Updated dependencies with security fixes 19 | * "noopener noreferrer" for footer links 20 | * Moved Github Actions from 14.x, 16.x, 17.x to 16.x, 18.x, 20.x 21 | * Updated legacy Github Actions to new versions 22 | * Updated the root README.md with package descriptions and local development instructions 23 | * Replaced react-prism with a custom TokenStream component for better control 24 | * Explicitly switched Prism to manual mode 25 | * Switched lerna to independent versioning to avoid publishing unchanged CLI package 26 | 27 | ### Removed 28 | * Removed react-prism dependency 29 | 30 | ## [1.3.0] - 2022-07-03 31 | ### Added 32 | * Collapse/expand all buttons 33 | 34 | ### Changed 35 | * Updated dependencies 36 | * Replaced favicons and favicons-webpack-plugin packages with static icons for faster build 37 | * Moved Github Actions from 12.x, 14.x, 16.x to 14.x, 16.x, 17.x 38 | 39 | ## [1.2.1] - 2022-03-28 40 | ### Added 41 | * First release od the CLI package 42 | 43 | ### Fixed 44 | * Fixed issue with tree nodes being collapsed when parent node has path partially matching the collapsing node path 45 | 46 | ## [1.2.0] - 2022-03-28 47 | ### Added 48 | * Netlify redirect 49 | * CLI package 50 | 51 | ### Changed 52 | * Updated dependencies 53 | * Changed lerna configuration to use unified versioning 54 | 55 | ## [1.1.0] - 2021-10-28 56 | ### Added 57 | * Github Actions 58 | * Badges in README.md 59 | * Report details page with source code and coverage 60 | * First deployable version of Lcov Viewer application and Netlify deploy 61 | 62 | ### Changed 63 | * Replaced react-router with preact-router for Lcov Viewer application 64 | * Updated Istanbul report to support nyc 65 | 66 | ## [1.0.1] - 2021-10-24 67 | ### Changed 68 | * Moved license-webpack-plugin and terser-webpack-plugin to devDependencies 69 | 70 | ## [1.0.0] - 2021-10-24 71 | ### Added 72 | * Initial implementation of the tree view 73 | * Lcov Viewer application 74 | * Istanbul coverage report 75 | * Initialize monorepo 76 | 77 | [//]: # (Reference links) 78 | 79 | [Unreleased]: https://github.com/eugenezinovyev/lcov-viewer/compare/v1.4.0...HEAD 80 | [1.4.0]: https://github.com/eugenezinovyev/lcov-viewer/compare/v1.3.0...v1.4.0 81 | [1.3.0]: https://github.com/eugenezinovyev/lcov-viewer/compare/v1.2.1...v1.3.0 82 | [1.2.1]: https://github.com/eugenezinovyev/lcov-viewer/compare/v1.2.0...v1.2.1 83 | [1.2.0]: https://github.com/eugenezinovyev/lcov-viewer/compare/v1.1.0...v1.2.0 84 | [1.1.0]: https://github.com/eugenezinovyev/lcov-viewer/compare/v1.0.1...v1.1.0 85 | [1.0.1]: https://github.com/eugenezinovyev/lcov-viewer/compare/v1.0.0...v1.0.1 86 | [1.0.0]: https://github.com/eugenezinovyev/lcov-viewer/releases/tag/v1.0.0 87 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2021 Eugene Zinovyev 2 | 3 | Permission is hereby granted, 4 | free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice 12 | (including the next paragraph) shall be included in all copies or substantial 13 | portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 18 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 19 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![build](https://github.com/eugenezinovyev/lcov-viewer/actions/workflows/main.yml/badge.svg) 2 | [![Known Vulnerabilities](https://snyk.io/test/github/eugenezinovyev/lcov-viewer/badge.svg?targetFile=package.json)](https://snyk.io/test/github/eugenezinovyev/lcov-viewer?targetFile=package.json) 3 | 4 | # LCOV viewer 5 | 6 | Live: **[lcov-viewer.netlify.app](https://lcov-viewer.netlify.app/)** 7 | 8 | LCOV code coverage report viewer. Parses lcov report files and generates HTML report grouped by directory. 9 | 10 | This repository provides two packages: 11 | 12 | | Package | Description | 13 | |-------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------| 14 | | [![npm version](https://badge.fury.io/js/@lcov-viewer%2Fistanbul-report.svg)](https://www.npmjs.com/package/@lcov-viewer/istanbul-report) | Istanbul grouped HTML report. Generates code coverage report grouped by directory. | 15 | | [![npm version](https://badge.fury.io/js/@lcov-viewer%2Fcli.svg)](https://www.npmjs.com/package/@lcov-viewer/cli) | CLI to convert coverage in LCOV format to a grouped HTML report. | 16 | 17 | ## Istanbul Report 18 | 19 | Istanbul reporter for Jest and nyc. Generates code coverage report grouped by directory. 20 | 21 | Documentation: [/packages/istanbul-report](/packages/istanbul-report) 22 | 23 | ```shell 24 | yarn add -D @lcov-viewer/istanbul-report 25 | npm install -D @lcov-viewer/istanbul-report 26 | ``` 27 | 28 | ## CLI 29 | 30 | CLI to convert coverage in LCOV format to a grouped HTML report. 31 | 32 | Documentation: [/packages/cli](/packages/cli) 33 | 34 | ```shell 35 | yarn global add @lcov-viewer/cli 36 | npm install -g @lcov-viewer/cli 37 | ``` 38 | 39 | -- OR locally in your project -- 40 | 41 | ```shell 42 | yarn add -D @lcov-viewer/cli 43 | npm install -D @lcov-viewer/cli 44 | ``` 45 | 46 | ## Local Development Setup 47 | 48 | This repository uses Yarn workspaces. Please use Yarn as a package manager. 49 | 50 | ```shell 51 | git clone https://github.com/eugenezinovyev/lcov-viewer.git 52 | cd lcov-viewer 53 | yarn 54 | yarn start 55 | ``` 56 | 57 | The open http://localhost:8080/ in your browser and drop `lcov.info` file into the dropzone. 58 | 59 | ![viewer](https://user-images.githubusercontent.com/1678896/139163904-5f845791-2af5-4cdd-b044-98406b2963b7.gif) 60 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [], 3 | presets: [ 4 | [ 5 | '@babel/preset-env', 6 | { 7 | useBuiltIns: 'usage', 8 | corejs: { version: '3' }, 9 | }, 10 | ], 11 | '@babel/preset-react', 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /icon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eugenezinovyev/lcov-viewer/da1e1675509e6cae8acb3ab57470be479049c4ac/icon-16x16.png -------------------------------------------------------------------------------- /icon-240x240.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eugenezinovyev/lcov-viewer/da1e1675509e6cae8acb3ab57470be479049c4ac/icon-32x32.png -------------------------------------------------------------------------------- /icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eugenezinovyev/lcov-viewer/da1e1675509e6cae8acb3ab57470be479049c4ac/icon-48x48.png -------------------------------------------------------------------------------- /icon-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "shape": "squircle", 3 | "shapeProps": { 4 | "n": 5, 5 | "resolution": 20, 6 | "width": 240, 7 | "height": 240, 8 | "fill": "#d461b0" 9 | }, 10 | "fontFamily": "Oxygen", 11 | "fontSize": 160, 12 | "fontVariant": "regular", 13 | "text": "LV", 14 | "fontColor": "#ffffff" 15 | } -------------------------------------------------------------------------------- /jest.base-config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const configurator = (customization = {}) => { 4 | return { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // The directory where Jest should store its cached dependency information 12 | // cacheDirectory: "", 13 | 14 | // Automatically clear mock calls and instances between every test 15 | clearMocks: true, 16 | 17 | // Indicates whether the coverage information should be collected while executing the test 18 | collectCoverage: true, 19 | 20 | // An array of glob patterns indicating a set of files for which coverage information should be collected 21 | // collectCoverageFrom: undefined, 22 | 23 | // The directory where Jest should output its coverage files 24 | coverageDirectory: 'coverage', 25 | 26 | // An array of regexp pattern strings used to skip coverage collection 27 | // coveragePathIgnorePatterns: [ 28 | // "\\\\node_modules\\\\" 29 | // ], 30 | 31 | // Indicates which provider should be used to instrument code for coverage 32 | coverageProvider: 'v8', 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | coverageReporters: [ 36 | // 'json', 37 | // 'text', 38 | // 'lcov', 39 | // 'lcovonly', 40 | // 'clover', 41 | ], 42 | 43 | // An object that configures minimum threshold enforcement for coverage results 44 | // coverageThreshold: undefined, 45 | 46 | // A path to a custom dependency extractor 47 | // dependencyExtractor: undefined, 48 | 49 | // Make calling deprecated APIs throw helpful error messages 50 | // errorOnDeprecated: false, 51 | 52 | // Force coverage collection from ignored files using an array of glob patterns 53 | // forceCoverageMatch: [], 54 | 55 | // A path to a module which exports an async function that is triggered once before all test suites 56 | // globalSetup: undefined, 57 | 58 | // A path to a module which exports an async function that is triggered once after all test suites 59 | // globalTeardown: undefined, 60 | 61 | // A set of global variables that need to be available in all test environments 62 | // globals: {}, 63 | 64 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 65 | // maxWorkers: "50%", 66 | 67 | // An array of directory names to be searched recursively up from the requiring module's location 68 | // moduleDirectories: [ 69 | // "node_modules" 70 | // ], 71 | 72 | // An array of file extensions your modules use 73 | // moduleFileExtensions: [ 74 | // "js", 75 | // "jsx", 76 | // "ts", 77 | // "tsx", 78 | // "json", 79 | // "node" 80 | // ], 81 | 82 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 83 | // moduleNameMapper: {}, 84 | 85 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 86 | // modulePathIgnorePatterns: [], 87 | 88 | // Activates notifications for test results 89 | // notify: false, 90 | 91 | // An enum that specifies notification mode. Requires { notify: true } 92 | // notifyMode: "failure-change", 93 | 94 | // A preset that is used as a base for Jest's configuration 95 | // preset: undefined, 96 | 97 | // Run tests from one or more projects 98 | // projects: undefined, 99 | 100 | // Use this configuration option to add custom reporters to Jest 101 | // reporters: undefined, 102 | 103 | // Automatically reset mock state between every test 104 | // resetMocks: false, 105 | 106 | // Reset the module registry before running each individual test 107 | // resetModules: false, 108 | 109 | // A path to a custom resolver 110 | // resolver: undefined, 111 | 112 | // Automatically restore mock state between every test 113 | // restoreMocks: false, 114 | 115 | // The root directory that Jest should scan for tests and modules within 116 | // rootDir: undefined, 117 | 118 | // A list of paths to directories that Jest should use to search for files in 119 | // roots: [ 120 | // "" 121 | // ], 122 | 123 | // Allows you to use a custom runner instead of Jest's default test runner 124 | // runner: "jest-runner", 125 | 126 | // The paths to modules that run some code to configure or set up the testing environment before each test 127 | // setupFiles: [], 128 | 129 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 130 | // setupFilesAfterEnv: [], 131 | 132 | // The number of seconds after which a test is considered as slow and reported as such in the results. 133 | // slowTestThreshold: 5, 134 | 135 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 136 | // snapshotSerializers: [], 137 | 138 | // The test environment that will be used for testing 139 | testEnvironment: 'jsdom', 140 | 141 | // Options that will be passed to the testEnvironment 142 | // testEnvironmentOptions: {}, 143 | 144 | // Adds a location field to test results 145 | // testLocationInResults: false, 146 | 147 | // The glob patterns Jest uses to detect test files 148 | testMatch: [ 149 | "**/__tests__/**/*.[jt]s?(x)", 150 | "**/?(*.)+(spec|test).[tj]s?(x)" 151 | ], 152 | 153 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 154 | // testPathIgnorePatterns: [ 155 | // "\\\\node_modules\\\\" 156 | // ], 157 | 158 | // The regexp pattern or array of patterns that Jest uses to detect test files 159 | // testRegex: [], 160 | 161 | // This option allows the use of a custom results processor 162 | // testResultsProcessor: undefined, 163 | 164 | // This option allows use of a custom test runner 165 | // testRunner: "jest-circus/runner", 166 | 167 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 168 | // testURL: "http://localhost", 169 | 170 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 171 | // timers: "real", 172 | 173 | // A map from regular expressions to paths to transformers 174 | transform: { 175 | '^.+\\.(js|jsx|mjs|cjs|ts|tsx)$': path.resolve(__dirname, 'jest', 'babelTransform.js'), 176 | '^.+\\.(css|less)$': 'jest-css-modules-transform', 177 | '^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json|info)$)': path.resolve(__dirname, 'jest', 'fileTransform.js'), 178 | '^.+\\.info$': path.resolve(__dirname, 'jest', 'rawLoader.js'), 179 | }, 180 | 181 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 182 | transformIgnorePatterns: [ 183 | "\\\\node_modules\\\\(?!preact|@testing-library\\preact)", 184 | "\\.pnp\\.[^\\\\]+$", 185 | ], 186 | 187 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 188 | // unmockedModulePathPatterns: undefined, 189 | 190 | // Indicates whether each individual test should be reported during the run 191 | // verbose: undefined, 192 | 193 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 194 | // watchPathIgnorePatterns: [], 195 | 196 | // Whether to use watchman for file crawling 197 | // watchman: true, 198 | ...customization, 199 | }; 200 | }; 201 | 202 | module.exports = configurator; 203 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const configurator = require('./jest.base-config'); 2 | 3 | const config = configurator({ 4 | coverageReporters: [ 5 | 'lcov', 6 | 'json', 7 | '@lcov-viewer/istanbul-report', 8 | ], 9 | moduleNameMapper: { 10 | '^react$': 'preact/compat', 11 | '^react-dom/test-utils$': 'preact/test-utils', 12 | '^react-dom$': 'preact/compat', 13 | '^react/jsx-runtime$': 'preact/jsx-runtime', 14 | }, 15 | coveragePathIgnorePatterns: [ 16 | '/node_modules/', 17 | '/mocks/', 18 | '\.less$', 19 | ], 20 | }); 21 | 22 | module.exports = config; 23 | -------------------------------------------------------------------------------- /jest/babelTransform.js: -------------------------------------------------------------------------------- 1 | const babelJestMd = require('babel-jest'); 2 | const babelJest = babelJestMd.__esModule ? babelJestMd.default : babelJestMd; 3 | 4 | module.exports = babelJest.createTransformer({ 5 | presets: [ 6 | require.resolve('babel-preset-react-app'), 7 | ], 8 | babelrc: false, 9 | configFile: false, 10 | }); 11 | -------------------------------------------------------------------------------- /jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | process(src, filename) { 5 | const assetFilename = JSON.stringify(path.basename(filename)); 6 | 7 | return { 8 | code: `module.exports = ${assetFilename};`, 9 | }; 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /jest/rawLoader.js: -------------------------------------------------------------------------------- 1 | // jest-raw-loader compatibility with Jest version 28. 2 | // See: https://github.com/keplersj/jest-raw-loader/pull/239 3 | module.exports = { 4 | process: (content) => { 5 | return { 6 | code: 'module.exports = ' + JSON.stringify(content), 7 | }; 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmClient": "yarn", 3 | "packages": [ 4 | "packages/*", 5 | "packages/demo/*" 6 | ], 7 | "version": "independent" 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lcov-viewer", 3 | "description": "LCOV viewer", 4 | "version": "1.2.1", 5 | "author": "Eugene Zinovyev ", 6 | "license": "MIT", 7 | "private": true, 8 | "workspaces": [ 9 | "packages/*", 10 | "packages/demo/*" 11 | ], 12 | "scripts": { 13 | "build": "lerna run build --stream", 14 | "test": "lerna run test --stream", 15 | "test:all": "jest --colors --passWithNoTests", 16 | "version:update": "lerna version --no-push --amend", 17 | "syncpack": "syncpack list-mismatches", 18 | "start" : "lerna run start --scope @lcov-viewer/app --stream" 19 | }, 20 | "devDependencies": { 21 | "@babel/core": "^7.18.6", 22 | "@babel/preset-env": "^7.20.2", 23 | "@babel/preset-react": "^7.18.6", 24 | "autoprefixer": "^10.4.7", 25 | "babel-jest": "^29.6.2", 26 | "babel-loader": "^8.2.5", 27 | "babel-preset-react-app": "^10.0.1", 28 | "browserslist": "^4.21.1", 29 | "clean-webpack-plugin": "^4.0.0", 30 | "copy-webpack-plugin": "^11.0.0", 31 | "core-js": "^3.23.3", 32 | "css-loader": "^6.7.3", 33 | "css-minimizer-webpack-plugin": "^4.0.0", 34 | "html-webpack-plugin": "^5.5.0", 35 | "jest": "^29.6.2", 36 | "jest-css-modules-transform": "^4.4.2", 37 | "jest-environment-jsdom": "^28.1.2", 38 | "json-minimizer-webpack-plugin": "^4.0.0", 39 | "lerna": "^7.1.5", 40 | "less": "^4.1.3", 41 | "less-loader": "^11.0.0", 42 | "mini-css-extract-plugin": "^2.6.1", 43 | "postcss": "^8.4.14", 44 | "postcss-flexbugs-fixes": "^5.0.2", 45 | "postcss-loader": "^7.1.0", 46 | "postcss-normalize": "^10.0.1", 47 | "postcss-preset-env": "^9.1.1", 48 | "react-test-renderer": "^18.2.0", 49 | "style-loader": "^3.3.1", 50 | "syncpack": "^11.2.1", 51 | "webpack": "^5.76.2", 52 | "webpack-bundle-analyzer": "^4.9.0", 53 | "webpack-cli": "^5.0.1", 54 | "webpack-dev-server": "^4.9.3" 55 | }, 56 | "main": "index.js", 57 | "repository": { 58 | "type": "git", 59 | "url": "git+https://github.com/eugenezinovyev/lcov-viewer.git" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/app/.gitignore: -------------------------------------------------------------------------------- 1 | /jest-coverage -------------------------------------------------------------------------------- /packages/app/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2021 Eugene Zinovyev 2 | 3 | Permission is hereby granted, 4 | free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice 12 | (including the next paragraph) shall be included in all copies or substantial 13 | portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 18 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 19 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /packages/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | LCOV Viewer 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | -------------------------------------------------------------------------------- /packages/app/jest.config.js: -------------------------------------------------------------------------------- 1 | const configurator = require('../../jest.base-config'); 2 | 3 | const config = configurator({ 4 | coverageReporters: [ 5 | 'lcov', 6 | 'json', 7 | '@lcov-viewer/istanbul-report', 8 | ], 9 | moduleNameMapper: { 10 | '^react$': 'preact/compat', 11 | '^react-dom/test-utils$': 'preact/test-utils', 12 | '^react-dom$': 'preact/compat', 13 | '^react/jsx-runtime$': 'preact/jsx-runtime', 14 | }, 15 | coveragePathIgnorePatterns: [ 16 | '/node_modules/', 17 | '/mocks/', 18 | '\.less$', 19 | ], 20 | }); 21 | 22 | module.exports = config; 23 | -------------------------------------------------------------------------------- /packages/app/mocks/lcov/index.js: -------------------------------------------------------------------------------- 1 | import unix_relative_multi_root from './unix_relative_multi_root.info'; 2 | import unix_relative_single_root from './unix_relative_single_root.info'; 3 | import unix_absolute from './unix_absolute.info'; 4 | import win_relative_multi_root from './win_relative_multi_root.info'; 5 | import win_relative_single_root from './win_relative_single_root.info'; 6 | import win_absolute from './win_absolute.info'; 7 | 8 | const mocks = { 9 | unixRelativeMultiRoot: unix_relative_multi_root, 10 | unixRelativeSingleRoot: unix_relative_single_root, 11 | unixAbsolute: unix_absolute, 12 | winRelativeMultiRoot: win_relative_multi_root, 13 | winRelativeSingleRoot: win_relative_single_root, 14 | winAbsolute: win_absolute, 15 | }; 16 | 17 | export default mocks; 18 | -------------------------------------------------------------------------------- /packages/app/mocks/lcov/unix_absolute.info: -------------------------------------------------------------------------------- 1 | TN: 2 | SF:/home/projects/src/CollapseProvider/CollapseContext.js 3 | FNF:0 4 | FNH:0 5 | DA:1,1 6 | DA:2,1 7 | DA:3,1 8 | DA:4,1 9 | DA:5,1 10 | LF:5 11 | LH:5 12 | BRF:0 13 | BRH:0 14 | end_of_record 15 | TN: 16 | SF:/home/projects/src/CollapseProvider/CollapseProvider.js 17 | FN:5,buildState 18 | FN:5,Object.keys.reduce.children 19 | FN:13,CollapseProvider 20 | FNF:3 21 | FNH:3 22 | FNDA:9,buildState 23 | FNDA:12,Object.keys.reduce.children 24 | FNDA:3,CollapseProvider 25 | DA:1,1 26 | DA:2,1 27 | DA:3,1 28 | DA:4,1 29 | DA:5,1 30 | DA:6,12 31 | DA:7,6 32 | DA:8,6 33 | DA:9,12 34 | DA:10,12 35 | DA:11,1 36 | DA:12,1 37 | DA:13,1 38 | DA:14,3 39 | DA:15,3 40 | DA:16,3 41 | DA:17,3 42 | DA:18,3 43 | DA:19,3 44 | DA:20,3 45 | DA:21,3 46 | DA:22,1 47 | DA:23,1 48 | LF:23 49 | LH:23 50 | BRDA:5,0,0,9 51 | BRDA:5,1,0,12 52 | BRDA:6,2,0,6 53 | BRDA:13,3,0,3 54 | BRF:4 55 | BRH:4 56 | end_of_record 57 | TN: 58 | SF:/home/projects/src/CollapseProvider/findNode.js 59 | FN:1,findNode 60 | FNF:1 61 | FNH:1 62 | FNDA:22,findNode 63 | DA:1,1 64 | DA:2,1 65 | DA:3,1 66 | LF:3 67 | LH:3 68 | BRDA:1,0,0,22 69 | BRDA:1,1,0,7 70 | BRDA:1,2,0,15 71 | BRDA:1,3,0,22 72 | BRF:4 73 | BRH:4 74 | end_of_record 75 | TN: 76 | SF:/home/projects/src/CollapseProvider/useCollapse.js 77 | FN:5,useCollapse 78 | FNF:1 79 | FNH:1 80 | FNDA:17,useCollapse 81 | DA:1,1 82 | DA:2,1 83 | DA:3,1 84 | DA:4,1 85 | DA:5,1 86 | DA:6,17 87 | DA:7,17 88 | DA:8,17 89 | DA:9,5 90 | DA:10,5 91 | DA:11,5 92 | DA:12,5 93 | DA:13,5 94 | DA:14,5 95 | DA:15,17 96 | DA:16,17 97 | DA:17,1 98 | DA:18,1 99 | LF:18 100 | LH:18 101 | BRDA:5,0,0,17 102 | BRDA:8,1,0,5 103 | BRDA:13,2,0,5 104 | BRF:3 105 | BRH:3 106 | end_of_record 107 | TN: 108 | SF:/home/projects/src/CollapseProvider/useCollapsedState.js 109 | FN:5,useCollapsedState 110 | FN:15,notify 111 | FNF:2 112 | FNH:2 113 | FNDA:17,useCollapsedState 114 | FNDA:5,notify 115 | DA:1,1 116 | DA:2,1 117 | DA:3,1 118 | DA:4,1 119 | DA:5,1 120 | DA:6,17 121 | DA:7,17 122 | DA:8,17 123 | DA:9,17 124 | DA:10,17 125 | DA:11,12 126 | DA:12,9 127 | DA:13,9 128 | DA:14,12 129 | DA:15,12 130 | DA:16,12 131 | DA:17,12 132 | DA:18,12 133 | DA:19,12 134 | DA:20,12 135 | DA:21,12 136 | DA:22,12 137 | DA:23,12 138 | DA:24,12 139 | DA:25,17 140 | DA:26,17 141 | DA:27,17 142 | DA:28,17 143 | DA:29,1 144 | DA:30,1 145 | LF:30 146 | LH:30 147 | BRDA:5,0,0,17 148 | BRDA:10,1,0,12 149 | BRDA:11,2,0,9 150 | BRDA:15,3,0,5 151 | BRDA:18,4,0,12 152 | BRDA:19,5,0,12 153 | BRF:6 154 | BRH:6 155 | end_of_record 156 | TN: 157 | SF:/home/projects/src/TreeView/Node.js 158 | FN:10,calculatePadding 159 | FN:18,LeafNode 160 | FN:27,BranchNode 161 | FN:47,Node 162 | FNF:4 163 | FNH:4 164 | FNDA:26,calculatePadding 165 | FNDA:9,LeafNode 166 | FNDA:17,BranchNode 167 | FNDA:21,Node 168 | DA:1,1 169 | DA:2,1 170 | DA:3,1 171 | DA:4,1 172 | DA:5,1 173 | DA:6,1 174 | DA:7,1 175 | DA:8,1 176 | DA:9,1 177 | DA:10,1 178 | DA:11,26 179 | DA:12,4 180 | DA:13,4 181 | DA:14,26 182 | DA:15,26 183 | DA:16,26 184 | DA:17,1 185 | DA:18,1 186 | DA:19,9 187 | DA:20,9 188 | DA:21,9 189 | DA:22,9 190 | DA:23,1 191 | DA:24,1 192 | DA:25,1 193 | DA:26,1 194 | DA:27,1 195 | DA:28,17 196 | DA:29,17 197 | DA:30,17 198 | DA:31,17 199 | DA:32,17 200 | DA:33,17 201 | DA:34,17 202 | DA:35,17 203 | DA:36,17 204 | DA:37,17 205 | DA:38,17 206 | DA:39,17 207 | DA:40,17 208 | DA:41,17 209 | DA:42,17 210 | DA:43,17 211 | DA:44,17 212 | DA:45,17 213 | DA:46,1 214 | DA:47,1 215 | DA:48,21 216 | DA:49,21 217 | DA:50,21 218 | DA:51,21 219 | DA:52,1 220 | DA:53,1 221 | DA:54,1 222 | DA:55,1 223 | DA:56,1 224 | DA:57,1 225 | DA:58,1 226 | LF:58 227 | LH:58 228 | BRDA:10,0,0,26 229 | BRDA:11,1,0,4 230 | BRDA:18,2,0,9 231 | BRDA:27,3,0,17 232 | BRDA:30,4,0,13 233 | BRDA:40,5,0,13 234 | BRDA:41,6,0,18 235 | BRDA:47,7,0,21 236 | BRDA:48,8,0,12 237 | BRDA:49,9,0,9 238 | BRF:10 239 | BRH:10 240 | end_of_record 241 | TN: 242 | SF:/home/projects/src/TreeView/NodeStats.js 243 | FN:16,calculateCoverage 244 | FN:17,aggregateStats 245 | FN:34,StatColumns 246 | FN:51,NodeStats 247 | FNF:4 248 | FNH:4 249 | FNDA:21,calculateCoverage 250 | FNDA:240,aggregateStats 251 | FNDA:78,StatColumns 252 | FNDA:26,NodeStats 253 | DA:1,1 254 | DA:2,1 255 | DA:3,1 256 | DA:4,1 257 | DA:5,1 258 | DA:6,1 259 | DA:7,1 260 | DA:8,1 261 | DA:9,1 262 | DA:10,1 263 | DA:11,1 264 | DA:12,1 265 | DA:13,1 266 | DA:14,1 267 | DA:15,1 268 | DA:16,1 269 | DA:17,21 270 | DA:18,60 271 | DA:19,219 272 | DA:20,219 273 | DA:21,219 274 | DA:22,219 275 | DA:23,219 276 | DA:24,219 277 | DA:25,219 278 | DA:26,219 279 | DA:27,219 280 | DA:28,240 281 | DA:29,180 282 | DA:30,21 283 | DA:31,21 284 | DA:32,21 285 | DA:33,1 286 | DA:34,1 287 | DA:35,78 288 | DA:36,78 289 | DA:37,78 290 | DA:38,78 291 | DA:39,78 292 | DA:40,78 293 | DA:41,78 294 | DA:42,78 295 | DA:43,78 296 | DA:44,78 297 | DA:45,78 298 | DA:46,78 299 | DA:47,78 300 | DA:48,78 301 | DA:49,78 302 | DA:50,1 303 | DA:51,1 304 | DA:52,26 305 | DA:53,26 306 | DA:54,26 307 | DA:55,26 308 | DA:56,26 309 | DA:57,26 310 | DA:58,26 311 | DA:59,26 312 | DA:60,26 313 | DA:61,26 314 | DA:62,1 315 | DA:63,1 316 | LF:63 317 | LH:63 318 | BRDA:16,0,0,21 319 | BRDA:17,1,0,240 320 | BRDA:17,2,0,60 321 | BRDA:28,3,0,180 322 | BRDA:18,4,0,73 323 | BRDA:18,5,0,219 324 | BRDA:34,6,0,78 325 | BRDA:35,7,0,68 326 | BRDA:35,8,0,10 327 | BRDA:39,9,0,31 328 | BRDA:40,10,0,47 329 | BRDA:40,11,0,10 330 | BRDA:51,12,0,26 331 | BRDA:52,13,0,21 332 | BRF:14 333 | BRH:14 334 | end_of_record 335 | TN: 336 | SF:/home/projects/src/TreeView/TreeView.js 337 | FN:7,TreeView 338 | FNF:1 339 | FNH:1 340 | FNDA:3,TreeView 341 | DA:1,1 342 | DA:2,1 343 | DA:3,1 344 | DA:4,1 345 | DA:5,1 346 | DA:6,1 347 | DA:7,1 348 | DA:8,3 349 | DA:9,3 350 | DA:10,3 351 | DA:11,3 352 | DA:12,3 353 | DA:13,3 354 | DA:14,3 355 | DA:15,3 356 | DA:16,3 357 | DA:17,3 358 | DA:18,3 359 | DA:19,3 360 | DA:20,3 361 | DA:21,3 362 | DA:22,3 363 | DA:23,3 364 | DA:24,3 365 | DA:25,3 366 | DA:26,3 367 | DA:27,3 368 | DA:28,3 369 | DA:29,3 370 | DA:30,3 371 | DA:31,3 372 | DA:32,3 373 | DA:33,3 374 | DA:34,3 375 | DA:35,3 376 | DA:36,1 377 | DA:37,1 378 | LF:37 379 | LH:37 380 | BRDA:7,0,0,3 381 | BRF:1 382 | BRH:1 383 | end_of_record 384 | TN: 385 | SF:/home/projects/src/TreeView/coverageTree.js 386 | FN:4,buildCoverageTree 387 | FNF:1 388 | FNH:1 389 | FNDA:3,buildCoverageTree 390 | DA:1,1 391 | DA:2,1 392 | DA:3,1 393 | DA:4,1 394 | DA:5,3 395 | DA:6,3 396 | DA:7,3 397 | DA:8,6 398 | DA:9,6 399 | DA:10,6 400 | DA:11,6 401 | DA:12,15 402 | DA:13,15 403 | DA:14,12 404 | DA:15,12 405 | DA:16,12 406 | DA:17,12 407 | DA:18,12 408 | DA:19,12 409 | DA:20,12 410 | DA:21,12 411 | DA:22,15 412 | DA:23,15 413 | DA:24,15 414 | DA:25,6 415 | DA:26,3 416 | DA:27,3 417 | DA:28,3 418 | LF:28 419 | LH:28 420 | BRDA:4,0,0,3 421 | BRDA:7,1,0,6 422 | BRDA:11,2,0,15 423 | BRDA:12,3,0,9 424 | BRDA:12,4,0,6 425 | BRDA:13,5,0,12 426 | BRDA:16,6,0,6 427 | BRDA:16,7,0,6 428 | BRDA:18,8,0,6 429 | BRDA:18,9,0,6 430 | BRF:10 431 | BRH:10 432 | end_of_record 433 | -------------------------------------------------------------------------------- /packages/app/mocks/lcov/unix_relative_multi_root.info: -------------------------------------------------------------------------------- 1 | TN: 2 | SF:CollapseProvider/CollapseContext.js 3 | FNF:0 4 | FNH:0 5 | DA:1,1 6 | DA:2,1 7 | DA:3,1 8 | DA:4,1 9 | DA:5,1 10 | LF:5 11 | LH:5 12 | BRF:0 13 | BRH:0 14 | end_of_record 15 | TN: 16 | SF:CollapseProvider/CollapseProvider.js 17 | FN:5,buildState 18 | FN:5,Object.keys.reduce.children 19 | FN:13,CollapseProvider 20 | FNF:3 21 | FNH:3 22 | FNDA:9,buildState 23 | FNDA:12,Object.keys.reduce.children 24 | FNDA:3,CollapseProvider 25 | DA:1,1 26 | DA:2,1 27 | DA:3,1 28 | DA:4,1 29 | DA:5,1 30 | DA:6,12 31 | DA:7,6 32 | DA:8,6 33 | DA:9,12 34 | DA:10,12 35 | DA:11,1 36 | DA:12,1 37 | DA:13,1 38 | DA:14,3 39 | DA:15,3 40 | DA:16,3 41 | DA:17,3 42 | DA:18,3 43 | DA:19,3 44 | DA:20,3 45 | DA:21,3 46 | DA:22,1 47 | DA:23,1 48 | LF:23 49 | LH:23 50 | BRDA:5,0,0,9 51 | BRDA:5,1,0,12 52 | BRDA:6,2,0,6 53 | BRDA:13,3,0,3 54 | BRF:4 55 | BRH:4 56 | end_of_record 57 | TN: 58 | SF:CollapseProvider/findNode.js 59 | FN:1,findNode 60 | FNF:1 61 | FNH:1 62 | FNDA:22,findNode 63 | DA:1,1 64 | DA:2,1 65 | DA:3,1 66 | LF:3 67 | LH:3 68 | BRDA:1,0,0,22 69 | BRDA:1,1,0,7 70 | BRDA:1,2,0,15 71 | BRDA:1,3,0,22 72 | BRF:4 73 | BRH:4 74 | end_of_record 75 | TN: 76 | SF:CollapseProvider/useCollapse.js 77 | FN:5,useCollapse 78 | FNF:1 79 | FNH:1 80 | FNDA:17,useCollapse 81 | DA:1,1 82 | DA:2,1 83 | DA:3,1 84 | DA:4,1 85 | DA:5,1 86 | DA:6,17 87 | DA:7,17 88 | DA:8,17 89 | DA:9,5 90 | DA:10,5 91 | DA:11,5 92 | DA:12,5 93 | DA:13,5 94 | DA:14,5 95 | DA:15,17 96 | DA:16,17 97 | DA:17,1 98 | DA:18,1 99 | LF:18 100 | LH:18 101 | BRDA:5,0,0,17 102 | BRDA:8,1,0,5 103 | BRDA:13,2,0,5 104 | BRF:3 105 | BRH:3 106 | end_of_record 107 | TN: 108 | SF:CollapseProvider/useCollapsedState.js 109 | FN:5,useCollapsedState 110 | FN:15,notify 111 | FNF:2 112 | FNH:2 113 | FNDA:17,useCollapsedState 114 | FNDA:5,notify 115 | DA:1,1 116 | DA:2,1 117 | DA:3,1 118 | DA:4,1 119 | DA:5,1 120 | DA:6,17 121 | DA:7,17 122 | DA:8,17 123 | DA:9,17 124 | DA:10,17 125 | DA:11,12 126 | DA:12,9 127 | DA:13,9 128 | DA:14,12 129 | DA:15,12 130 | DA:16,12 131 | DA:17,12 132 | DA:18,12 133 | DA:19,12 134 | DA:20,12 135 | DA:21,12 136 | DA:22,12 137 | DA:23,12 138 | DA:24,12 139 | DA:25,17 140 | DA:26,17 141 | DA:27,17 142 | DA:28,17 143 | DA:29,1 144 | DA:30,1 145 | LF:30 146 | LH:30 147 | BRDA:5,0,0,17 148 | BRDA:10,1,0,12 149 | BRDA:11,2,0,9 150 | BRDA:15,3,0,5 151 | BRDA:18,4,0,12 152 | BRDA:19,5,0,12 153 | BRF:6 154 | BRH:6 155 | end_of_record 156 | TN: 157 | SF:TreeView/Node.js 158 | FN:10,calculatePadding 159 | FN:18,LeafNode 160 | FN:27,BranchNode 161 | FN:47,Node 162 | FNF:4 163 | FNH:4 164 | FNDA:26,calculatePadding 165 | FNDA:9,LeafNode 166 | FNDA:17,BranchNode 167 | FNDA:21,Node 168 | DA:1,1 169 | DA:2,1 170 | DA:3,1 171 | DA:4,1 172 | DA:5,1 173 | DA:6,1 174 | DA:7,1 175 | DA:8,1 176 | DA:9,1 177 | DA:10,1 178 | DA:11,26 179 | DA:12,4 180 | DA:13,4 181 | DA:14,26 182 | DA:15,26 183 | DA:16,26 184 | DA:17,1 185 | DA:18,1 186 | DA:19,9 187 | DA:20,9 188 | DA:21,9 189 | DA:22,9 190 | DA:23,1 191 | DA:24,1 192 | DA:25,1 193 | DA:26,1 194 | DA:27,1 195 | DA:28,17 196 | DA:29,17 197 | DA:30,17 198 | DA:31,17 199 | DA:32,17 200 | DA:33,17 201 | DA:34,17 202 | DA:35,17 203 | DA:36,17 204 | DA:37,17 205 | DA:38,17 206 | DA:39,17 207 | DA:40,17 208 | DA:41,17 209 | DA:42,17 210 | DA:43,17 211 | DA:44,17 212 | DA:45,17 213 | DA:46,1 214 | DA:47,1 215 | DA:48,21 216 | DA:49,21 217 | DA:50,21 218 | DA:51,21 219 | DA:52,1 220 | DA:53,1 221 | DA:54,1 222 | DA:55,1 223 | DA:56,1 224 | DA:57,1 225 | DA:58,1 226 | LF:58 227 | LH:58 228 | BRDA:10,0,0,26 229 | BRDA:11,1,0,4 230 | BRDA:18,2,0,9 231 | BRDA:27,3,0,17 232 | BRDA:30,4,0,13 233 | BRDA:40,5,0,13 234 | BRDA:41,6,0,18 235 | BRDA:47,7,0,21 236 | BRDA:48,8,0,12 237 | BRDA:49,9,0,9 238 | BRF:10 239 | BRH:10 240 | end_of_record 241 | TN: 242 | SF:TreeView/NodeStats.js 243 | FN:16,calculateCoverage 244 | FN:17,aggregateStats 245 | FN:34,StatColumns 246 | FN:51,NodeStats 247 | FNF:4 248 | FNH:4 249 | FNDA:21,calculateCoverage 250 | FNDA:240,aggregateStats 251 | FNDA:78,StatColumns 252 | FNDA:26,NodeStats 253 | DA:1,1 254 | DA:2,1 255 | DA:3,1 256 | DA:4,1 257 | DA:5,1 258 | DA:6,1 259 | DA:7,1 260 | DA:8,1 261 | DA:9,1 262 | DA:10,1 263 | DA:11,1 264 | DA:12,1 265 | DA:13,1 266 | DA:14,1 267 | DA:15,1 268 | DA:16,1 269 | DA:17,21 270 | DA:18,60 271 | DA:19,219 272 | DA:20,219 273 | DA:21,219 274 | DA:22,219 275 | DA:23,219 276 | DA:24,219 277 | DA:25,219 278 | DA:26,219 279 | DA:27,219 280 | DA:28,240 281 | DA:29,180 282 | DA:30,21 283 | DA:31,21 284 | DA:32,21 285 | DA:33,1 286 | DA:34,1 287 | DA:35,78 288 | DA:36,78 289 | DA:37,78 290 | DA:38,78 291 | DA:39,78 292 | DA:40,78 293 | DA:41,78 294 | DA:42,78 295 | DA:43,78 296 | DA:44,78 297 | DA:45,78 298 | DA:46,78 299 | DA:47,78 300 | DA:48,78 301 | DA:49,78 302 | DA:50,1 303 | DA:51,1 304 | DA:52,26 305 | DA:53,26 306 | DA:54,26 307 | DA:55,26 308 | DA:56,26 309 | DA:57,26 310 | DA:58,26 311 | DA:59,26 312 | DA:60,26 313 | DA:61,26 314 | DA:62,1 315 | DA:63,1 316 | LF:63 317 | LH:63 318 | BRDA:16,0,0,21 319 | BRDA:17,1,0,240 320 | BRDA:17,2,0,60 321 | BRDA:28,3,0,180 322 | BRDA:18,4,0,73 323 | BRDA:18,5,0,219 324 | BRDA:34,6,0,78 325 | BRDA:35,7,0,68 326 | BRDA:35,8,0,10 327 | BRDA:39,9,0,31 328 | BRDA:40,10,0,47 329 | BRDA:40,11,0,10 330 | BRDA:51,12,0,26 331 | BRDA:52,13,0,21 332 | BRF:14 333 | BRH:14 334 | end_of_record 335 | TN: 336 | SF:TreeView/TreeView.js 337 | FN:7,TreeView 338 | FNF:1 339 | FNH:1 340 | FNDA:3,TreeView 341 | DA:1,1 342 | DA:2,1 343 | DA:3,1 344 | DA:4,1 345 | DA:5,1 346 | DA:6,1 347 | DA:7,1 348 | DA:8,3 349 | DA:9,3 350 | DA:10,3 351 | DA:11,3 352 | DA:12,3 353 | DA:13,3 354 | DA:14,3 355 | DA:15,3 356 | DA:16,3 357 | DA:17,3 358 | DA:18,3 359 | DA:19,3 360 | DA:20,3 361 | DA:21,3 362 | DA:22,3 363 | DA:23,3 364 | DA:24,3 365 | DA:25,3 366 | DA:26,3 367 | DA:27,3 368 | DA:28,3 369 | DA:29,3 370 | DA:30,3 371 | DA:31,3 372 | DA:32,3 373 | DA:33,3 374 | DA:34,3 375 | DA:35,3 376 | DA:36,1 377 | DA:37,1 378 | LF:37 379 | LH:37 380 | BRDA:7,0,0,3 381 | BRF:1 382 | BRH:1 383 | end_of_record 384 | TN: 385 | SF:TreeView/coverageTree.js 386 | FN:4,buildCoverageTree 387 | FNF:1 388 | FNH:1 389 | FNDA:3,buildCoverageTree 390 | DA:1,1 391 | DA:2,1 392 | DA:3,1 393 | DA:4,1 394 | DA:5,3 395 | DA:6,3 396 | DA:7,3 397 | DA:8,6 398 | DA:9,6 399 | DA:10,6 400 | DA:11,6 401 | DA:12,15 402 | DA:13,15 403 | DA:14,12 404 | DA:15,12 405 | DA:16,12 406 | DA:17,12 407 | DA:18,12 408 | DA:19,12 409 | DA:20,12 410 | DA:21,12 411 | DA:22,15 412 | DA:23,15 413 | DA:24,15 414 | DA:25,6 415 | DA:26,3 416 | DA:27,3 417 | DA:28,3 418 | LF:28 419 | LH:28 420 | BRDA:4,0,0,3 421 | BRDA:7,1,0,6 422 | BRDA:11,2,0,15 423 | BRDA:12,3,0,9 424 | BRDA:12,4,0,6 425 | BRDA:13,5,0,12 426 | BRDA:16,6,0,6 427 | BRDA:16,7,0,6 428 | BRDA:18,8,0,6 429 | BRDA:18,9,0,6 430 | BRF:10 431 | BRH:10 432 | end_of_record 433 | -------------------------------------------------------------------------------- /packages/app/mocks/lcov/unix_relative_single_root.info: -------------------------------------------------------------------------------- 1 | TN: 2 | SF:src/CollapseProvider/CollapseContext.js 3 | FNF:0 4 | FNH:0 5 | DA:1,1 6 | DA:2,1 7 | DA:3,1 8 | DA:4,1 9 | DA:5,1 10 | LF:5 11 | LH:5 12 | BRF:0 13 | BRH:0 14 | end_of_record 15 | TN: 16 | SF:src/CollapseProvider/CollapseProvider.js 17 | FN:5,buildState 18 | FN:5,Object.keys.reduce.children 19 | FN:13,CollapseProvider 20 | FNF:3 21 | FNH:3 22 | FNDA:9,buildState 23 | FNDA:12,Object.keys.reduce.children 24 | FNDA:3,CollapseProvider 25 | DA:1,1 26 | DA:2,1 27 | DA:3,1 28 | DA:4,1 29 | DA:5,1 30 | DA:6,12 31 | DA:7,6 32 | DA:8,6 33 | DA:9,12 34 | DA:10,12 35 | DA:11,1 36 | DA:12,1 37 | DA:13,1 38 | DA:14,3 39 | DA:15,3 40 | DA:16,3 41 | DA:17,3 42 | DA:18,3 43 | DA:19,3 44 | DA:20,3 45 | DA:21,3 46 | DA:22,1 47 | DA:23,1 48 | LF:23 49 | LH:23 50 | BRDA:5,0,0,9 51 | BRDA:5,1,0,12 52 | BRDA:6,2,0,6 53 | BRDA:13,3,0,3 54 | BRF:4 55 | BRH:4 56 | end_of_record 57 | TN: 58 | SF:src/CollapseProvider/findNode.js 59 | FN:1,findNode 60 | FNF:1 61 | FNH:1 62 | FNDA:22,findNode 63 | DA:1,1 64 | DA:2,1 65 | DA:3,1 66 | LF:3 67 | LH:3 68 | BRDA:1,0,0,22 69 | BRDA:1,1,0,7 70 | BRDA:1,2,0,15 71 | BRDA:1,3,0,22 72 | BRF:4 73 | BRH:4 74 | end_of_record 75 | TN: 76 | SF:src/CollapseProvider/useCollapse.js 77 | FN:5,useCollapse 78 | FNF:1 79 | FNH:1 80 | FNDA:17,useCollapse 81 | DA:1,1 82 | DA:2,1 83 | DA:3,1 84 | DA:4,1 85 | DA:5,1 86 | DA:6,17 87 | DA:7,17 88 | DA:8,17 89 | DA:9,5 90 | DA:10,5 91 | DA:11,5 92 | DA:12,5 93 | DA:13,5 94 | DA:14,5 95 | DA:15,17 96 | DA:16,17 97 | DA:17,1 98 | DA:18,1 99 | LF:18 100 | LH:18 101 | BRDA:5,0,0,17 102 | BRDA:8,1,0,5 103 | BRDA:13,2,0,5 104 | BRF:3 105 | BRH:3 106 | end_of_record 107 | TN: 108 | SF:src/CollapseProvider/useCollapsedState.js 109 | FN:5,useCollapsedState 110 | FN:15,notify 111 | FNF:2 112 | FNH:2 113 | FNDA:17,useCollapsedState 114 | FNDA:5,notify 115 | DA:1,1 116 | DA:2,1 117 | DA:3,1 118 | DA:4,1 119 | DA:5,1 120 | DA:6,17 121 | DA:7,17 122 | DA:8,17 123 | DA:9,17 124 | DA:10,17 125 | DA:11,12 126 | DA:12,9 127 | DA:13,9 128 | DA:14,12 129 | DA:15,12 130 | DA:16,12 131 | DA:17,12 132 | DA:18,12 133 | DA:19,12 134 | DA:20,12 135 | DA:21,12 136 | DA:22,12 137 | DA:23,12 138 | DA:24,12 139 | DA:25,17 140 | DA:26,17 141 | DA:27,17 142 | DA:28,17 143 | DA:29,1 144 | DA:30,1 145 | LF:30 146 | LH:30 147 | BRDA:5,0,0,17 148 | BRDA:10,1,0,12 149 | BRDA:11,2,0,9 150 | BRDA:15,3,0,5 151 | BRDA:18,4,0,12 152 | BRDA:19,5,0,12 153 | BRF:6 154 | BRH:6 155 | end_of_record 156 | TN: 157 | SF:src/TreeView/Node.js 158 | FN:10,calculatePadding 159 | FN:18,LeafNode 160 | FN:27,BranchNode 161 | FN:47,Node 162 | FNF:4 163 | FNH:4 164 | FNDA:26,calculatePadding 165 | FNDA:9,LeafNode 166 | FNDA:17,BranchNode 167 | FNDA:21,Node 168 | DA:1,1 169 | DA:2,1 170 | DA:3,1 171 | DA:4,1 172 | DA:5,1 173 | DA:6,1 174 | DA:7,1 175 | DA:8,1 176 | DA:9,1 177 | DA:10,1 178 | DA:11,26 179 | DA:12,4 180 | DA:13,4 181 | DA:14,26 182 | DA:15,26 183 | DA:16,26 184 | DA:17,1 185 | DA:18,1 186 | DA:19,9 187 | DA:20,9 188 | DA:21,9 189 | DA:22,9 190 | DA:23,1 191 | DA:24,1 192 | DA:25,1 193 | DA:26,1 194 | DA:27,1 195 | DA:28,17 196 | DA:29,17 197 | DA:30,17 198 | DA:31,17 199 | DA:32,17 200 | DA:33,17 201 | DA:34,17 202 | DA:35,17 203 | DA:36,17 204 | DA:37,17 205 | DA:38,17 206 | DA:39,17 207 | DA:40,17 208 | DA:41,17 209 | DA:42,17 210 | DA:43,17 211 | DA:44,17 212 | DA:45,17 213 | DA:46,1 214 | DA:47,1 215 | DA:48,21 216 | DA:49,21 217 | DA:50,21 218 | DA:51,21 219 | DA:52,1 220 | DA:53,1 221 | DA:54,1 222 | DA:55,1 223 | DA:56,1 224 | DA:57,1 225 | DA:58,1 226 | LF:58 227 | LH:58 228 | BRDA:10,0,0,26 229 | BRDA:11,1,0,4 230 | BRDA:18,2,0,9 231 | BRDA:27,3,0,17 232 | BRDA:30,4,0,13 233 | BRDA:40,5,0,13 234 | BRDA:41,6,0,18 235 | BRDA:47,7,0,21 236 | BRDA:48,8,0,12 237 | BRDA:49,9,0,9 238 | BRF:10 239 | BRH:10 240 | end_of_record 241 | TN: 242 | SF:src/TreeView/NodeStats.js 243 | FN:16,calculateCoverage 244 | FN:17,aggregateStats 245 | FN:34,StatColumns 246 | FN:51,NodeStats 247 | FNF:4 248 | FNH:4 249 | FNDA:21,calculateCoverage 250 | FNDA:240,aggregateStats 251 | FNDA:78,StatColumns 252 | FNDA:26,NodeStats 253 | DA:1,1 254 | DA:2,1 255 | DA:3,1 256 | DA:4,1 257 | DA:5,1 258 | DA:6,1 259 | DA:7,1 260 | DA:8,1 261 | DA:9,1 262 | DA:10,1 263 | DA:11,1 264 | DA:12,1 265 | DA:13,1 266 | DA:14,1 267 | DA:15,1 268 | DA:16,1 269 | DA:17,21 270 | DA:18,60 271 | DA:19,219 272 | DA:20,219 273 | DA:21,219 274 | DA:22,219 275 | DA:23,219 276 | DA:24,219 277 | DA:25,219 278 | DA:26,219 279 | DA:27,219 280 | DA:28,240 281 | DA:29,180 282 | DA:30,21 283 | DA:31,21 284 | DA:32,21 285 | DA:33,1 286 | DA:34,1 287 | DA:35,78 288 | DA:36,78 289 | DA:37,78 290 | DA:38,78 291 | DA:39,78 292 | DA:40,78 293 | DA:41,78 294 | DA:42,78 295 | DA:43,78 296 | DA:44,78 297 | DA:45,78 298 | DA:46,78 299 | DA:47,78 300 | DA:48,78 301 | DA:49,78 302 | DA:50,1 303 | DA:51,1 304 | DA:52,26 305 | DA:53,26 306 | DA:54,26 307 | DA:55,26 308 | DA:56,26 309 | DA:57,26 310 | DA:58,26 311 | DA:59,26 312 | DA:60,26 313 | DA:61,26 314 | DA:62,1 315 | DA:63,1 316 | LF:63 317 | LH:63 318 | BRDA:16,0,0,21 319 | BRDA:17,1,0,240 320 | BRDA:17,2,0,60 321 | BRDA:28,3,0,180 322 | BRDA:18,4,0,73 323 | BRDA:18,5,0,219 324 | BRDA:34,6,0,78 325 | BRDA:35,7,0,68 326 | BRDA:35,8,0,10 327 | BRDA:39,9,0,31 328 | BRDA:40,10,0,47 329 | BRDA:40,11,0,10 330 | BRDA:51,12,0,26 331 | BRDA:52,13,0,21 332 | BRF:14 333 | BRH:14 334 | end_of_record 335 | TN: 336 | SF:src/TreeView/TreeView.js 337 | FN:7,TreeView 338 | FNF:1 339 | FNH:1 340 | FNDA:3,TreeView 341 | DA:1,1 342 | DA:2,1 343 | DA:3,1 344 | DA:4,1 345 | DA:5,1 346 | DA:6,1 347 | DA:7,1 348 | DA:8,3 349 | DA:9,3 350 | DA:10,3 351 | DA:11,3 352 | DA:12,3 353 | DA:13,3 354 | DA:14,3 355 | DA:15,3 356 | DA:16,3 357 | DA:17,3 358 | DA:18,3 359 | DA:19,3 360 | DA:20,3 361 | DA:21,3 362 | DA:22,3 363 | DA:23,3 364 | DA:24,3 365 | DA:25,3 366 | DA:26,3 367 | DA:27,3 368 | DA:28,3 369 | DA:29,3 370 | DA:30,3 371 | DA:31,3 372 | DA:32,3 373 | DA:33,3 374 | DA:34,3 375 | DA:35,3 376 | DA:36,1 377 | DA:37,1 378 | LF:37 379 | LH:37 380 | BRDA:7,0,0,3 381 | BRF:1 382 | BRH:1 383 | end_of_record 384 | TN: 385 | SF:src/TreeView/coverageTree.js 386 | FN:4,buildCoverageTree 387 | FNF:1 388 | FNH:1 389 | FNDA:3,buildCoverageTree 390 | DA:1,1 391 | DA:2,1 392 | DA:3,1 393 | DA:4,1 394 | DA:5,3 395 | DA:6,3 396 | DA:7,3 397 | DA:8,6 398 | DA:9,6 399 | DA:10,6 400 | DA:11,6 401 | DA:12,15 402 | DA:13,15 403 | DA:14,12 404 | DA:15,12 405 | DA:16,12 406 | DA:17,12 407 | DA:18,12 408 | DA:19,12 409 | DA:20,12 410 | DA:21,12 411 | DA:22,15 412 | DA:23,15 413 | DA:24,15 414 | DA:25,6 415 | DA:26,3 416 | DA:27,3 417 | DA:28,3 418 | LF:28 419 | LH:28 420 | BRDA:4,0,0,3 421 | BRDA:7,1,0,6 422 | BRDA:11,2,0,15 423 | BRDA:12,3,0,9 424 | BRDA:12,4,0,6 425 | BRDA:13,5,0,12 426 | BRDA:16,6,0,6 427 | BRDA:16,7,0,6 428 | BRDA:18,8,0,6 429 | BRDA:18,9,0,6 430 | BRF:10 431 | BRH:10 432 | end_of_record 433 | -------------------------------------------------------------------------------- /packages/app/mocks/lcov/win_absolute.info: -------------------------------------------------------------------------------- 1 | TN: 2 | SF:C:\projects\src\CollapseProvider\CollapseContext.js 3 | FNF:0 4 | FNH:0 5 | DA:1,1 6 | DA:2,1 7 | DA:3,1 8 | DA:4,1 9 | DA:5,1 10 | LF:5 11 | LH:5 12 | BRF:0 13 | BRH:0 14 | end_of_record 15 | TN: 16 | SF:C:\projects\src\CollapseProvider\CollapseProvider.js 17 | FN:5,buildState 18 | FN:5,Object.keys.reduce.children 19 | FN:13,CollapseProvider 20 | FNF:3 21 | FNH:3 22 | FNDA:9,buildState 23 | FNDA:12,Object.keys.reduce.children 24 | FNDA:3,CollapseProvider 25 | DA:1,1 26 | DA:2,1 27 | DA:3,1 28 | DA:4,1 29 | DA:5,1 30 | DA:6,12 31 | DA:7,6 32 | DA:8,6 33 | DA:9,12 34 | DA:10,12 35 | DA:11,1 36 | DA:12,1 37 | DA:13,1 38 | DA:14,3 39 | DA:15,3 40 | DA:16,3 41 | DA:17,3 42 | DA:18,3 43 | DA:19,3 44 | DA:20,3 45 | DA:21,3 46 | DA:22,1 47 | DA:23,1 48 | LF:23 49 | LH:23 50 | BRDA:5,0,0,9 51 | BRDA:5,1,0,12 52 | BRDA:6,2,0,6 53 | BRDA:13,3,0,3 54 | BRF:4 55 | BRH:4 56 | end_of_record 57 | TN: 58 | SF:C:\projects\src\CollapseProvider\findNode.js 59 | FN:1,findNode 60 | FNF:1 61 | FNH:1 62 | FNDA:22,findNode 63 | DA:1,1 64 | DA:2,1 65 | DA:3,1 66 | LF:3 67 | LH:3 68 | BRDA:1,0,0,22 69 | BRDA:1,1,0,7 70 | BRDA:1,2,0,15 71 | BRDA:1,3,0,22 72 | BRF:4 73 | BRH:4 74 | end_of_record 75 | TN: 76 | SF:C:\projects\src\CollapseProvider\useCollapse.js 77 | FN:5,useCollapse 78 | FNF:1 79 | FNH:1 80 | FNDA:17,useCollapse 81 | DA:1,1 82 | DA:2,1 83 | DA:3,1 84 | DA:4,1 85 | DA:5,1 86 | DA:6,17 87 | DA:7,17 88 | DA:8,17 89 | DA:9,5 90 | DA:10,5 91 | DA:11,5 92 | DA:12,5 93 | DA:13,5 94 | DA:14,5 95 | DA:15,17 96 | DA:16,17 97 | DA:17,1 98 | DA:18,1 99 | LF:18 100 | LH:18 101 | BRDA:5,0,0,17 102 | BRDA:8,1,0,5 103 | BRDA:13,2,0,5 104 | BRF:3 105 | BRH:3 106 | end_of_record 107 | TN: 108 | SF:C:\projects\src\CollapseProvider\useCollapsedState.js 109 | FN:5,useCollapsedState 110 | FN:15,notify 111 | FNF:2 112 | FNH:2 113 | FNDA:17,useCollapsedState 114 | FNDA:5,notify 115 | DA:1,1 116 | DA:2,1 117 | DA:3,1 118 | DA:4,1 119 | DA:5,1 120 | DA:6,17 121 | DA:7,17 122 | DA:8,17 123 | DA:9,17 124 | DA:10,17 125 | DA:11,12 126 | DA:12,9 127 | DA:13,9 128 | DA:14,12 129 | DA:15,12 130 | DA:16,12 131 | DA:17,12 132 | DA:18,12 133 | DA:19,12 134 | DA:20,12 135 | DA:21,12 136 | DA:22,12 137 | DA:23,12 138 | DA:24,12 139 | DA:25,17 140 | DA:26,17 141 | DA:27,17 142 | DA:28,17 143 | DA:29,1 144 | DA:30,1 145 | LF:30 146 | LH:30 147 | BRDA:5,0,0,17 148 | BRDA:10,1,0,12 149 | BRDA:11,2,0,9 150 | BRDA:15,3,0,5 151 | BRDA:18,4,0,12 152 | BRDA:19,5,0,12 153 | BRF:6 154 | BRH:6 155 | end_of_record 156 | TN: 157 | SF:C:\projects\src\TreeView\Node.js 158 | FN:10,calculatePadding 159 | FN:18,LeafNode 160 | FN:27,BranchNode 161 | FN:47,Node 162 | FNF:4 163 | FNH:4 164 | FNDA:26,calculatePadding 165 | FNDA:9,LeafNode 166 | FNDA:17,BranchNode 167 | FNDA:21,Node 168 | DA:1,1 169 | DA:2,1 170 | DA:3,1 171 | DA:4,1 172 | DA:5,1 173 | DA:6,1 174 | DA:7,1 175 | DA:8,1 176 | DA:9,1 177 | DA:10,1 178 | DA:11,26 179 | DA:12,4 180 | DA:13,4 181 | DA:14,26 182 | DA:15,26 183 | DA:16,26 184 | DA:17,1 185 | DA:18,1 186 | DA:19,9 187 | DA:20,9 188 | DA:21,9 189 | DA:22,9 190 | DA:23,1 191 | DA:24,1 192 | DA:25,1 193 | DA:26,1 194 | DA:27,1 195 | DA:28,17 196 | DA:29,17 197 | DA:30,17 198 | DA:31,17 199 | DA:32,17 200 | DA:33,17 201 | DA:34,17 202 | DA:35,17 203 | DA:36,17 204 | DA:37,17 205 | DA:38,17 206 | DA:39,17 207 | DA:40,17 208 | DA:41,17 209 | DA:42,17 210 | DA:43,17 211 | DA:44,17 212 | DA:45,17 213 | DA:46,1 214 | DA:47,1 215 | DA:48,21 216 | DA:49,21 217 | DA:50,21 218 | DA:51,21 219 | DA:52,1 220 | DA:53,1 221 | DA:54,1 222 | DA:55,1 223 | DA:56,1 224 | DA:57,1 225 | DA:58,1 226 | LF:58 227 | LH:58 228 | BRDA:10,0,0,26 229 | BRDA:11,1,0,4 230 | BRDA:18,2,0,9 231 | BRDA:27,3,0,17 232 | BRDA:30,4,0,13 233 | BRDA:40,5,0,13 234 | BRDA:41,6,0,18 235 | BRDA:47,7,0,21 236 | BRDA:48,8,0,12 237 | BRDA:49,9,0,9 238 | BRF:10 239 | BRH:10 240 | end_of_record 241 | TN: 242 | SF:C:\projects\src\TreeView\NodeStats.js 243 | FN:16,calculateCoverage 244 | FN:17,aggregateStats 245 | FN:34,StatColumns 246 | FN:51,NodeStats 247 | FNF:4 248 | FNH:4 249 | FNDA:21,calculateCoverage 250 | FNDA:240,aggregateStats 251 | FNDA:78,StatColumns 252 | FNDA:26,NodeStats 253 | DA:1,1 254 | DA:2,1 255 | DA:3,1 256 | DA:4,1 257 | DA:5,1 258 | DA:6,1 259 | DA:7,1 260 | DA:8,1 261 | DA:9,1 262 | DA:10,1 263 | DA:11,1 264 | DA:12,1 265 | DA:13,1 266 | DA:14,1 267 | DA:15,1 268 | DA:16,1 269 | DA:17,21 270 | DA:18,60 271 | DA:19,219 272 | DA:20,219 273 | DA:21,219 274 | DA:22,219 275 | DA:23,219 276 | DA:24,219 277 | DA:25,219 278 | DA:26,219 279 | DA:27,219 280 | DA:28,240 281 | DA:29,180 282 | DA:30,21 283 | DA:31,21 284 | DA:32,21 285 | DA:33,1 286 | DA:34,1 287 | DA:35,78 288 | DA:36,78 289 | DA:37,78 290 | DA:38,78 291 | DA:39,78 292 | DA:40,78 293 | DA:41,78 294 | DA:42,78 295 | DA:43,78 296 | DA:44,78 297 | DA:45,78 298 | DA:46,78 299 | DA:47,78 300 | DA:48,78 301 | DA:49,78 302 | DA:50,1 303 | DA:51,1 304 | DA:52,26 305 | DA:53,26 306 | DA:54,26 307 | DA:55,26 308 | DA:56,26 309 | DA:57,26 310 | DA:58,26 311 | DA:59,26 312 | DA:60,26 313 | DA:61,26 314 | DA:62,1 315 | DA:63,1 316 | LF:63 317 | LH:63 318 | BRDA:16,0,0,21 319 | BRDA:17,1,0,240 320 | BRDA:17,2,0,60 321 | BRDA:28,3,0,180 322 | BRDA:18,4,0,73 323 | BRDA:18,5,0,219 324 | BRDA:34,6,0,78 325 | BRDA:35,7,0,68 326 | BRDA:35,8,0,10 327 | BRDA:39,9,0,31 328 | BRDA:40,10,0,47 329 | BRDA:40,11,0,10 330 | BRDA:51,12,0,26 331 | BRDA:52,13,0,21 332 | BRF:14 333 | BRH:14 334 | end_of_record 335 | TN: 336 | SF:C:\projects\src\TreeView\TreeView.js 337 | FN:7,TreeView 338 | FNF:1 339 | FNH:1 340 | FNDA:3,TreeView 341 | DA:1,1 342 | DA:2,1 343 | DA:3,1 344 | DA:4,1 345 | DA:5,1 346 | DA:6,1 347 | DA:7,1 348 | DA:8,3 349 | DA:9,3 350 | DA:10,3 351 | DA:11,3 352 | DA:12,3 353 | DA:13,3 354 | DA:14,3 355 | DA:15,3 356 | DA:16,3 357 | DA:17,3 358 | DA:18,3 359 | DA:19,3 360 | DA:20,3 361 | DA:21,3 362 | DA:22,3 363 | DA:23,3 364 | DA:24,3 365 | DA:25,3 366 | DA:26,3 367 | DA:27,3 368 | DA:28,3 369 | DA:29,3 370 | DA:30,3 371 | DA:31,3 372 | DA:32,3 373 | DA:33,3 374 | DA:34,3 375 | DA:35,3 376 | DA:36,1 377 | DA:37,1 378 | LF:37 379 | LH:37 380 | BRDA:7,0,0,3 381 | BRF:1 382 | BRH:1 383 | end_of_record 384 | TN: 385 | SF:C:\projects\src\TreeView\coverageTree.js 386 | FN:4,buildCoverageTree 387 | FNF:1 388 | FNH:1 389 | FNDA:3,buildCoverageTree 390 | DA:1,1 391 | DA:2,1 392 | DA:3,1 393 | DA:4,1 394 | DA:5,3 395 | DA:6,3 396 | DA:7,3 397 | DA:8,6 398 | DA:9,6 399 | DA:10,6 400 | DA:11,6 401 | DA:12,15 402 | DA:13,15 403 | DA:14,12 404 | DA:15,12 405 | DA:16,12 406 | DA:17,12 407 | DA:18,12 408 | DA:19,12 409 | DA:20,12 410 | DA:21,12 411 | DA:22,15 412 | DA:23,15 413 | DA:24,15 414 | DA:25,6 415 | DA:26,3 416 | DA:27,3 417 | DA:28,3 418 | LF:28 419 | LH:28 420 | BRDA:4,0,0,3 421 | BRDA:7,1,0,6 422 | BRDA:11,2,0,15 423 | BRDA:12,3,0,9 424 | BRDA:12,4,0,6 425 | BRDA:13,5,0,12 426 | BRDA:16,6,0,6 427 | BRDA:16,7,0,6 428 | BRDA:18,8,0,6 429 | BRDA:18,9,0,6 430 | BRF:10 431 | BRH:10 432 | end_of_record 433 | -------------------------------------------------------------------------------- /packages/app/mocks/lcov/win_relative_multi_root.info: -------------------------------------------------------------------------------- 1 | TN: 2 | SF:CollapseProvider\CollapseContext.js 3 | FNF:0 4 | FNH:0 5 | DA:1,1 6 | DA:2,1 7 | DA:3,1 8 | DA:4,1 9 | DA:5,1 10 | LF:5 11 | LH:5 12 | BRF:0 13 | BRH:0 14 | end_of_record 15 | TN: 16 | SF:CollapseProvider\CollapseProvider.js 17 | FN:5,buildState 18 | FN:5,Object.keys.reduce.children 19 | FN:13,CollapseProvider 20 | FNF:3 21 | FNH:3 22 | FNDA:9,buildState 23 | FNDA:12,Object.keys.reduce.children 24 | FNDA:3,CollapseProvider 25 | DA:1,1 26 | DA:2,1 27 | DA:3,1 28 | DA:4,1 29 | DA:5,1 30 | DA:6,12 31 | DA:7,6 32 | DA:8,6 33 | DA:9,12 34 | DA:10,12 35 | DA:11,1 36 | DA:12,1 37 | DA:13,1 38 | DA:14,3 39 | DA:15,3 40 | DA:16,3 41 | DA:17,3 42 | DA:18,3 43 | DA:19,3 44 | DA:20,3 45 | DA:21,3 46 | DA:22,1 47 | DA:23,1 48 | LF:23 49 | LH:23 50 | BRDA:5,0,0,9 51 | BRDA:5,1,0,12 52 | BRDA:6,2,0,6 53 | BRDA:13,3,0,3 54 | BRF:4 55 | BRH:4 56 | end_of_record 57 | TN: 58 | SF:CollapseProvider\findNode.js 59 | FN:1,findNode 60 | FNF:1 61 | FNH:1 62 | FNDA:22,findNode 63 | DA:1,1 64 | DA:2,1 65 | DA:3,1 66 | LF:3 67 | LH:3 68 | BRDA:1,0,0,22 69 | BRDA:1,1,0,7 70 | BRDA:1,2,0,15 71 | BRDA:1,3,0,22 72 | BRF:4 73 | BRH:4 74 | end_of_record 75 | TN: 76 | SF:CollapseProvider\useCollapse.js 77 | FN:5,useCollapse 78 | FNF:1 79 | FNH:1 80 | FNDA:17,useCollapse 81 | DA:1,1 82 | DA:2,1 83 | DA:3,1 84 | DA:4,1 85 | DA:5,1 86 | DA:6,17 87 | DA:7,17 88 | DA:8,17 89 | DA:9,5 90 | DA:10,5 91 | DA:11,5 92 | DA:12,5 93 | DA:13,5 94 | DA:14,5 95 | DA:15,17 96 | DA:16,17 97 | DA:17,1 98 | DA:18,1 99 | LF:18 100 | LH:18 101 | BRDA:5,0,0,17 102 | BRDA:8,1,0,5 103 | BRDA:13,2,0,5 104 | BRF:3 105 | BRH:3 106 | end_of_record 107 | TN: 108 | SF:CollapseProvider\useCollapsedState.js 109 | FN:5,useCollapsedState 110 | FN:15,notify 111 | FNF:2 112 | FNH:2 113 | FNDA:17,useCollapsedState 114 | FNDA:5,notify 115 | DA:1,1 116 | DA:2,1 117 | DA:3,1 118 | DA:4,1 119 | DA:5,1 120 | DA:6,17 121 | DA:7,17 122 | DA:8,17 123 | DA:9,17 124 | DA:10,17 125 | DA:11,12 126 | DA:12,9 127 | DA:13,9 128 | DA:14,12 129 | DA:15,12 130 | DA:16,12 131 | DA:17,12 132 | DA:18,12 133 | DA:19,12 134 | DA:20,12 135 | DA:21,12 136 | DA:22,12 137 | DA:23,12 138 | DA:24,12 139 | DA:25,17 140 | DA:26,17 141 | DA:27,17 142 | DA:28,17 143 | DA:29,1 144 | DA:30,1 145 | LF:30 146 | LH:30 147 | BRDA:5,0,0,17 148 | BRDA:10,1,0,12 149 | BRDA:11,2,0,9 150 | BRDA:15,3,0,5 151 | BRDA:18,4,0,12 152 | BRDA:19,5,0,12 153 | BRF:6 154 | BRH:6 155 | end_of_record 156 | TN: 157 | SF:TreeView\Node.js 158 | FN:10,calculatePadding 159 | FN:18,LeafNode 160 | FN:27,BranchNode 161 | FN:47,Node 162 | FNF:4 163 | FNH:4 164 | FNDA:26,calculatePadding 165 | FNDA:9,LeafNode 166 | FNDA:17,BranchNode 167 | FNDA:21,Node 168 | DA:1,1 169 | DA:2,1 170 | DA:3,1 171 | DA:4,1 172 | DA:5,1 173 | DA:6,1 174 | DA:7,1 175 | DA:8,1 176 | DA:9,1 177 | DA:10,1 178 | DA:11,26 179 | DA:12,4 180 | DA:13,4 181 | DA:14,26 182 | DA:15,26 183 | DA:16,26 184 | DA:17,1 185 | DA:18,1 186 | DA:19,9 187 | DA:20,9 188 | DA:21,9 189 | DA:22,9 190 | DA:23,1 191 | DA:24,1 192 | DA:25,1 193 | DA:26,1 194 | DA:27,1 195 | DA:28,17 196 | DA:29,17 197 | DA:30,17 198 | DA:31,17 199 | DA:32,17 200 | DA:33,17 201 | DA:34,17 202 | DA:35,17 203 | DA:36,17 204 | DA:37,17 205 | DA:38,17 206 | DA:39,17 207 | DA:40,17 208 | DA:41,17 209 | DA:42,17 210 | DA:43,17 211 | DA:44,17 212 | DA:45,17 213 | DA:46,1 214 | DA:47,1 215 | DA:48,21 216 | DA:49,21 217 | DA:50,21 218 | DA:51,21 219 | DA:52,1 220 | DA:53,1 221 | DA:54,1 222 | DA:55,1 223 | DA:56,1 224 | DA:57,1 225 | DA:58,1 226 | LF:58 227 | LH:58 228 | BRDA:10,0,0,26 229 | BRDA:11,1,0,4 230 | BRDA:18,2,0,9 231 | BRDA:27,3,0,17 232 | BRDA:30,4,0,13 233 | BRDA:40,5,0,13 234 | BRDA:41,6,0,18 235 | BRDA:47,7,0,21 236 | BRDA:48,8,0,12 237 | BRDA:49,9,0,9 238 | BRF:10 239 | BRH:10 240 | end_of_record 241 | TN: 242 | SF:TreeView\NodeStats.js 243 | FN:16,calculateCoverage 244 | FN:17,aggregateStats 245 | FN:34,StatColumns 246 | FN:51,NodeStats 247 | FNF:4 248 | FNH:4 249 | FNDA:21,calculateCoverage 250 | FNDA:240,aggregateStats 251 | FNDA:78,StatColumns 252 | FNDA:26,NodeStats 253 | DA:1,1 254 | DA:2,1 255 | DA:3,1 256 | DA:4,1 257 | DA:5,1 258 | DA:6,1 259 | DA:7,1 260 | DA:8,1 261 | DA:9,1 262 | DA:10,1 263 | DA:11,1 264 | DA:12,1 265 | DA:13,1 266 | DA:14,1 267 | DA:15,1 268 | DA:16,1 269 | DA:17,21 270 | DA:18,60 271 | DA:19,219 272 | DA:20,219 273 | DA:21,219 274 | DA:22,219 275 | DA:23,219 276 | DA:24,219 277 | DA:25,219 278 | DA:26,219 279 | DA:27,219 280 | DA:28,240 281 | DA:29,180 282 | DA:30,21 283 | DA:31,21 284 | DA:32,21 285 | DA:33,1 286 | DA:34,1 287 | DA:35,78 288 | DA:36,78 289 | DA:37,78 290 | DA:38,78 291 | DA:39,78 292 | DA:40,78 293 | DA:41,78 294 | DA:42,78 295 | DA:43,78 296 | DA:44,78 297 | DA:45,78 298 | DA:46,78 299 | DA:47,78 300 | DA:48,78 301 | DA:49,78 302 | DA:50,1 303 | DA:51,1 304 | DA:52,26 305 | DA:53,26 306 | DA:54,26 307 | DA:55,26 308 | DA:56,26 309 | DA:57,26 310 | DA:58,26 311 | DA:59,26 312 | DA:60,26 313 | DA:61,26 314 | DA:62,1 315 | DA:63,1 316 | LF:63 317 | LH:63 318 | BRDA:16,0,0,21 319 | BRDA:17,1,0,240 320 | BRDA:17,2,0,60 321 | BRDA:28,3,0,180 322 | BRDA:18,4,0,73 323 | BRDA:18,5,0,219 324 | BRDA:34,6,0,78 325 | BRDA:35,7,0,68 326 | BRDA:35,8,0,10 327 | BRDA:39,9,0,31 328 | BRDA:40,10,0,47 329 | BRDA:40,11,0,10 330 | BRDA:51,12,0,26 331 | BRDA:52,13,0,21 332 | BRF:14 333 | BRH:14 334 | end_of_record 335 | TN: 336 | SF:TreeView\TreeView.js 337 | FN:7,TreeView 338 | FNF:1 339 | FNH:1 340 | FNDA:3,TreeView 341 | DA:1,1 342 | DA:2,1 343 | DA:3,1 344 | DA:4,1 345 | DA:5,1 346 | DA:6,1 347 | DA:7,1 348 | DA:8,3 349 | DA:9,3 350 | DA:10,3 351 | DA:11,3 352 | DA:12,3 353 | DA:13,3 354 | DA:14,3 355 | DA:15,3 356 | DA:16,3 357 | DA:17,3 358 | DA:18,3 359 | DA:19,3 360 | DA:20,3 361 | DA:21,3 362 | DA:22,3 363 | DA:23,3 364 | DA:24,3 365 | DA:25,3 366 | DA:26,3 367 | DA:27,3 368 | DA:28,3 369 | DA:29,3 370 | DA:30,3 371 | DA:31,3 372 | DA:32,3 373 | DA:33,3 374 | DA:34,3 375 | DA:35,3 376 | DA:36,1 377 | DA:37,1 378 | LF:37 379 | LH:37 380 | BRDA:7,0,0,3 381 | BRF:1 382 | BRH:1 383 | end_of_record 384 | TN: 385 | SF:TreeView\coverageTree.js 386 | FN:4,buildCoverageTree 387 | FNF:1 388 | FNH:1 389 | FNDA:3,buildCoverageTree 390 | DA:1,1 391 | DA:2,1 392 | DA:3,1 393 | DA:4,1 394 | DA:5,3 395 | DA:6,3 396 | DA:7,3 397 | DA:8,6 398 | DA:9,6 399 | DA:10,6 400 | DA:11,6 401 | DA:12,15 402 | DA:13,15 403 | DA:14,12 404 | DA:15,12 405 | DA:16,12 406 | DA:17,12 407 | DA:18,12 408 | DA:19,12 409 | DA:20,12 410 | DA:21,12 411 | DA:22,15 412 | DA:23,15 413 | DA:24,15 414 | DA:25,6 415 | DA:26,3 416 | DA:27,3 417 | DA:28,3 418 | LF:28 419 | LH:28 420 | BRDA:4,0,0,3 421 | BRDA:7,1,0,6 422 | BRDA:11,2,0,15 423 | BRDA:12,3,0,9 424 | BRDA:12,4,0,6 425 | BRDA:13,5,0,12 426 | BRDA:16,6,0,6 427 | BRDA:16,7,0,6 428 | BRDA:18,8,0,6 429 | BRDA:18,9,0,6 430 | BRF:10 431 | BRH:10 432 | end_of_record 433 | -------------------------------------------------------------------------------- /packages/app/mocks/lcov/win_relative_single_root.info: -------------------------------------------------------------------------------- 1 | TN: 2 | SF:src\CollapseProvider\CollapseContext.js 3 | FNF:0 4 | FNH:0 5 | DA:1,1 6 | DA:2,1 7 | DA:3,1 8 | DA:4,1 9 | DA:5,1 10 | LF:5 11 | LH:5 12 | BRF:0 13 | BRH:0 14 | end_of_record 15 | TN: 16 | SF:src\CollapseProvider\CollapseProvider.js 17 | FN:5,buildState 18 | FN:5,Object.keys.reduce.children 19 | FN:13,CollapseProvider 20 | FNF:3 21 | FNH:3 22 | FNDA:9,buildState 23 | FNDA:12,Object.keys.reduce.children 24 | FNDA:3,CollapseProvider 25 | DA:1,1 26 | DA:2,1 27 | DA:3,1 28 | DA:4,1 29 | DA:5,1 30 | DA:6,12 31 | DA:7,6 32 | DA:8,6 33 | DA:9,12 34 | DA:10,12 35 | DA:11,1 36 | DA:12,1 37 | DA:13,1 38 | DA:14,3 39 | DA:15,3 40 | DA:16,3 41 | DA:17,3 42 | DA:18,3 43 | DA:19,3 44 | DA:20,3 45 | DA:21,3 46 | DA:22,1 47 | DA:23,1 48 | LF:23 49 | LH:23 50 | BRDA:5,0,0,9 51 | BRDA:5,1,0,12 52 | BRDA:6,2,0,6 53 | BRDA:13,3,0,3 54 | BRF:4 55 | BRH:4 56 | end_of_record 57 | TN: 58 | SF:src\CollapseProvider\findNode.js 59 | FN:1,findNode 60 | FNF:1 61 | FNH:1 62 | FNDA:22,findNode 63 | DA:1,1 64 | DA:2,1 65 | DA:3,1 66 | LF:3 67 | LH:3 68 | BRDA:1,0,0,22 69 | BRDA:1,1,0,7 70 | BRDA:1,2,0,15 71 | BRDA:1,3,0,22 72 | BRF:4 73 | BRH:4 74 | end_of_record 75 | TN: 76 | SF:src\CollapseProvider\useCollapse.js 77 | FN:5,useCollapse 78 | FNF:1 79 | FNH:1 80 | FNDA:17,useCollapse 81 | DA:1,1 82 | DA:2,1 83 | DA:3,1 84 | DA:4,1 85 | DA:5,1 86 | DA:6,17 87 | DA:7,17 88 | DA:8,17 89 | DA:9,5 90 | DA:10,5 91 | DA:11,5 92 | DA:12,5 93 | DA:13,5 94 | DA:14,5 95 | DA:15,17 96 | DA:16,17 97 | DA:17,1 98 | DA:18,1 99 | LF:18 100 | LH:18 101 | BRDA:5,0,0,17 102 | BRDA:8,1,0,5 103 | BRDA:13,2,0,5 104 | BRF:3 105 | BRH:3 106 | end_of_record 107 | TN: 108 | SF:src\CollapseProvider\useCollapsedState.js 109 | FN:5,useCollapsedState 110 | FN:15,notify 111 | FNF:2 112 | FNH:2 113 | FNDA:17,useCollapsedState 114 | FNDA:5,notify 115 | DA:1,1 116 | DA:2,1 117 | DA:3,1 118 | DA:4,1 119 | DA:5,1 120 | DA:6,17 121 | DA:7,17 122 | DA:8,17 123 | DA:9,17 124 | DA:10,17 125 | DA:11,12 126 | DA:12,9 127 | DA:13,9 128 | DA:14,12 129 | DA:15,12 130 | DA:16,12 131 | DA:17,12 132 | DA:18,12 133 | DA:19,12 134 | DA:20,12 135 | DA:21,12 136 | DA:22,12 137 | DA:23,12 138 | DA:24,12 139 | DA:25,17 140 | DA:26,17 141 | DA:27,17 142 | DA:28,17 143 | DA:29,1 144 | DA:30,1 145 | LF:30 146 | LH:30 147 | BRDA:5,0,0,17 148 | BRDA:10,1,0,12 149 | BRDA:11,2,0,9 150 | BRDA:15,3,0,5 151 | BRDA:18,4,0,12 152 | BRDA:19,5,0,12 153 | BRF:6 154 | BRH:6 155 | end_of_record 156 | TN: 157 | SF:src\TreeView\Node.js 158 | FN:10,calculatePadding 159 | FN:18,LeafNode 160 | FN:27,BranchNode 161 | FN:47,Node 162 | FNF:4 163 | FNH:4 164 | FNDA:26,calculatePadding 165 | FNDA:9,LeafNode 166 | FNDA:17,BranchNode 167 | FNDA:21,Node 168 | DA:1,1 169 | DA:2,1 170 | DA:3,1 171 | DA:4,1 172 | DA:5,1 173 | DA:6,1 174 | DA:7,1 175 | DA:8,1 176 | DA:9,1 177 | DA:10,1 178 | DA:11,26 179 | DA:12,4 180 | DA:13,4 181 | DA:14,26 182 | DA:15,26 183 | DA:16,26 184 | DA:17,1 185 | DA:18,1 186 | DA:19,9 187 | DA:20,9 188 | DA:21,9 189 | DA:22,9 190 | DA:23,1 191 | DA:24,1 192 | DA:25,1 193 | DA:26,1 194 | DA:27,1 195 | DA:28,17 196 | DA:29,17 197 | DA:30,17 198 | DA:31,17 199 | DA:32,17 200 | DA:33,17 201 | DA:34,17 202 | DA:35,17 203 | DA:36,17 204 | DA:37,17 205 | DA:38,17 206 | DA:39,17 207 | DA:40,17 208 | DA:41,17 209 | DA:42,17 210 | DA:43,17 211 | DA:44,17 212 | DA:45,17 213 | DA:46,1 214 | DA:47,1 215 | DA:48,21 216 | DA:49,21 217 | DA:50,21 218 | DA:51,21 219 | DA:52,1 220 | DA:53,1 221 | DA:54,1 222 | DA:55,1 223 | DA:56,1 224 | DA:57,1 225 | DA:58,1 226 | LF:58 227 | LH:58 228 | BRDA:10,0,0,26 229 | BRDA:11,1,0,4 230 | BRDA:18,2,0,9 231 | BRDA:27,3,0,17 232 | BRDA:30,4,0,13 233 | BRDA:40,5,0,13 234 | BRDA:41,6,0,18 235 | BRDA:47,7,0,21 236 | BRDA:48,8,0,12 237 | BRDA:49,9,0,9 238 | BRF:10 239 | BRH:10 240 | end_of_record 241 | TN: 242 | SF:src\TreeView\NodeStats.js 243 | FN:16,calculateCoverage 244 | FN:17,aggregateStats 245 | FN:34,StatColumns 246 | FN:51,NodeStats 247 | FNF:4 248 | FNH:4 249 | FNDA:21,calculateCoverage 250 | FNDA:240,aggregateStats 251 | FNDA:78,StatColumns 252 | FNDA:26,NodeStats 253 | DA:1,1 254 | DA:2,1 255 | DA:3,1 256 | DA:4,1 257 | DA:5,1 258 | DA:6,1 259 | DA:7,1 260 | DA:8,1 261 | DA:9,1 262 | DA:10,1 263 | DA:11,1 264 | DA:12,1 265 | DA:13,1 266 | DA:14,1 267 | DA:15,1 268 | DA:16,1 269 | DA:17,21 270 | DA:18,60 271 | DA:19,219 272 | DA:20,219 273 | DA:21,219 274 | DA:22,219 275 | DA:23,219 276 | DA:24,219 277 | DA:25,219 278 | DA:26,219 279 | DA:27,219 280 | DA:28,240 281 | DA:29,180 282 | DA:30,21 283 | DA:31,21 284 | DA:32,21 285 | DA:33,1 286 | DA:34,1 287 | DA:35,78 288 | DA:36,78 289 | DA:37,78 290 | DA:38,78 291 | DA:39,78 292 | DA:40,78 293 | DA:41,78 294 | DA:42,78 295 | DA:43,78 296 | DA:44,78 297 | DA:45,78 298 | DA:46,78 299 | DA:47,78 300 | DA:48,78 301 | DA:49,78 302 | DA:50,1 303 | DA:51,1 304 | DA:52,26 305 | DA:53,26 306 | DA:54,26 307 | DA:55,26 308 | DA:56,26 309 | DA:57,26 310 | DA:58,26 311 | DA:59,26 312 | DA:60,26 313 | DA:61,26 314 | DA:62,1 315 | DA:63,1 316 | LF:63 317 | LH:63 318 | BRDA:16,0,0,21 319 | BRDA:17,1,0,240 320 | BRDA:17,2,0,60 321 | BRDA:28,3,0,180 322 | BRDA:18,4,0,73 323 | BRDA:18,5,0,219 324 | BRDA:34,6,0,78 325 | BRDA:35,7,0,68 326 | BRDA:35,8,0,10 327 | BRDA:39,9,0,31 328 | BRDA:40,10,0,47 329 | BRDA:40,11,0,10 330 | BRDA:51,12,0,26 331 | BRDA:52,13,0,21 332 | BRF:14 333 | BRH:14 334 | end_of_record 335 | TN: 336 | SF:src\TreeView\TreeView.js 337 | FN:7,TreeView 338 | FNF:1 339 | FNH:1 340 | FNDA:3,TreeView 341 | DA:1,1 342 | DA:2,1 343 | DA:3,1 344 | DA:4,1 345 | DA:5,1 346 | DA:6,1 347 | DA:7,1 348 | DA:8,3 349 | DA:9,3 350 | DA:10,3 351 | DA:11,3 352 | DA:12,3 353 | DA:13,3 354 | DA:14,3 355 | DA:15,3 356 | DA:16,3 357 | DA:17,3 358 | DA:18,3 359 | DA:19,3 360 | DA:20,3 361 | DA:21,3 362 | DA:22,3 363 | DA:23,3 364 | DA:24,3 365 | DA:25,3 366 | DA:26,3 367 | DA:27,3 368 | DA:28,3 369 | DA:29,3 370 | DA:30,3 371 | DA:31,3 372 | DA:32,3 373 | DA:33,3 374 | DA:34,3 375 | DA:35,3 376 | DA:36,1 377 | DA:37,1 378 | LF:37 379 | LH:37 380 | BRDA:7,0,0,3 381 | BRF:1 382 | BRH:1 383 | end_of_record 384 | TN: 385 | SF:src\TreeView\coverageTree.js 386 | FN:4,buildCoverageTree 387 | FNF:1 388 | FNH:1 389 | FNDA:3,buildCoverageTree 390 | DA:1,1 391 | DA:2,1 392 | DA:3,1 393 | DA:4,1 394 | DA:5,3 395 | DA:6,3 396 | DA:7,3 397 | DA:8,6 398 | DA:9,6 399 | DA:10,6 400 | DA:11,6 401 | DA:12,15 402 | DA:13,15 403 | DA:14,12 404 | DA:15,12 405 | DA:16,12 406 | DA:17,12 407 | DA:18,12 408 | DA:19,12 409 | DA:20,12 410 | DA:21,12 411 | DA:22,15 412 | DA:23,15 413 | DA:24,15 414 | DA:25,6 415 | DA:26,3 416 | DA:27,3 417 | DA:28,3 418 | LF:28 419 | LH:28 420 | BRDA:4,0,0,3 421 | BRDA:7,1,0,6 422 | BRDA:11,2,0,15 423 | BRDA:12,3,0,9 424 | BRDA:12,4,0,6 425 | BRDA:13,5,0,12 426 | BRDA:16,6,0,6 427 | BRDA:16,7,0,6 428 | BRDA:18,8,0,6 429 | BRDA:18,9,0,6 430 | BRF:10 431 | BRH:10 432 | end_of_record 433 | -------------------------------------------------------------------------------- /packages/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lcov-viewer/app", 3 | "version": "1.3.0", 4 | "author": "Eugene Zinovyev ", 5 | "main": "src/index.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "webpack --mode=production --node-env=production", 9 | "analyze": "webpack --mode=production --analyze", 10 | "start": "webpack serve", 11 | "test": "../../node_modules/.bin/jest --colors --passWithNoTests" 12 | }, 13 | "devDependencies": { 14 | "@lcov-viewer/istanbul-report": "^1.4.0", 15 | "@testing-library/jest-dom": "^5.16.4", 16 | "@testing-library/preact": "^3.2.3" 17 | }, 18 | "dependencies": { 19 | "@fortawesome/fontawesome-svg-core": "^6.1.1", 20 | "@fortawesome/free-brands-svg-icons": "^6.4.2", 21 | "@lcov-viewer/components": "^1.3.0", 22 | "@lcov-viewer/core": "^1.3.0", 23 | "preact": "^10.8.2", 24 | "preact-router": "^4.1.2", 25 | "react-dropzone": "^14.2.3", 26 | "react-fontawesome-svg-icon": "^1.1.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/app/public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /packages/app/src/components/App.js: -------------------------------------------------------------------------------- 1 | import Router from 'preact-router'; 2 | import React from 'react'; 3 | import CoverageDataProvider from './CoverageDataProvider/CoverageDataProvider'; 4 | import Footer from './Footer/Footer'; 5 | import Header from './Header/Header'; 6 | import LcovImport from './LcovImport/LcovImport'; 7 | import Redirect from './Redirect/Redirect'; 8 | import Report from './Report/Report'; 9 | import classes from './App.module.less'; 10 | 11 | const App = () => ( 12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 |
23 | ); 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /packages/app/src/components/App.module.less: -------------------------------------------------------------------------------- 1 | @import "@fortawesome/fontawesome-svg-core/styles.css"; 2 | 3 | .root { 4 | flex-grow: 1; 5 | display: flex; 6 | flex-direction: column; 7 | padding: 0 1rem; 8 | height: 100%; 9 | max-width: 1920px; 10 | } -------------------------------------------------------------------------------- /packages/app/src/components/App.test.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import { fireEvent, render } from '@testing-library/preact'; 3 | import React from 'react'; 4 | import mockLcov from '../../mocks/lcov'; 5 | import App from './App'; 6 | 7 | function dispatchEvt(node, type, data) { 8 | const event = new Event(type, { bubbles: true }); 9 | Object.assign(event, data); 10 | fireEvent(node, event); 11 | } 12 | 13 | const pauseFor = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds)); 14 | 15 | const mockData = files => ({ 16 | dataTransfer: { 17 | files, 18 | items: files.map(file => ({ 19 | kind: 'file', 20 | type: file.type, 21 | getAsFile: () => file, 22 | })), 23 | types: ['Files'], 24 | }, 25 | }); 26 | 27 | const queryTableContent = container => [...container.querySelectorAll('tbody > tr')] 28 | .map(row => [...row.querySelectorAll('td')].map(cell => cell.textContent)); 29 | 30 | describe('', () => { 31 | let container = null; 32 | let queryByText = null; 33 | const importFile = async (content) => { 34 | const file = new File([content], 'lcov.info', { type: 'plain/text' }); 35 | const data = mockData([file]); 36 | 37 | dispatchEvt(container.querySelector('input[type="file"]'), 'drop', data); 38 | await pauseFor(100); 39 | 40 | return queryTableContent(container); 41 | }; 42 | 43 | beforeEach(() => { 44 | const result = render(); 45 | container = result.container; 46 | queryByText = result.queryByText; 47 | }); 48 | 49 | describe('Absolute path', () => { 50 | it('should process LCOV with windows paths', async () => { 51 | const tableContent = await importFile(mockLcov.winAbsolute); 52 | 53 | //@formatter:off 54 | expect(tableContent).toEqual([ 55 | ['All Files', '265/265', '100.0%', '17/17', '100.0%', '52/52', '100.0%'], 56 | ['C', '265/265', '100.0%', '17/17', '100.0%', '52/52', '100.0%'], 57 | ['projects', '265/265', '100.0%', '17/17', '100.0%', '52/52', '100.0%'], 58 | ['src', '265/265', '100.0%', '17/17', '100.0%', '52/52', '100.0%'], 59 | ['CollapseProvider', '79/79', '100.0%', '7/7', '100.0%', '17/17', '100.0%'], 60 | ['CollapseContext.js', '5/5', '100.0%', '0/0', '100.0%', '0/0', '100.0%'], 61 | ['CollapseProvider.js', '23/23', '100.0%', '3/3', '100.0%', '4/4', '100.0%'], 62 | ['findNode.js', '3/3', '100.0%', '1/1', '100.0%', '4/4', '100.0%'], 63 | ['useCollapse.js', '18/18', '100.0%', '1/1', '100.0%', '3/3', '100.0%'], 64 | ['useCollapsedState.js', '30/30', '100.0%', '2/2', '100.0%', '6/6', '100.0%'], 65 | ['TreeView', '186/186', '100.0%', '10/10', '100.0%', '35/35', '100.0%'], 66 | ['Node.js', '58/58', '100.0%', '4/4', '100.0%', '10/10', '100.0%'], 67 | ['NodeStats.js', '63/63', '100.0%', '4/4', '100.0%', '14/14', '100.0%'], 68 | ['TreeView.js', '37/37', '100.0%', '1/1', '100.0%', '1/1', '100.0%'], 69 | ['coverageTree.js', '28/28', '100.0%', '1/1', '100.0%', '10/10', '100.0%'], 70 | ]); 71 | //@formatter:on 72 | }); 73 | 74 | it('should process LCOV with unix paths', async () => { 75 | const tableContent = await importFile(mockLcov.unixAbsolute); 76 | 77 | //@formatter:off 78 | expect(tableContent).toEqual([ 79 | ['All Files', '265/265', '100.0%', '17/17', '100.0%', '52/52', '100.0%'], 80 | ['home', '265/265', '100.0%', '17/17', '100.0%', '52/52', '100.0%'], 81 | ['projects', '265/265', '100.0%', '17/17', '100.0%', '52/52', '100.0%'], 82 | ['src', '265/265', '100.0%', '17/17', '100.0%', '52/52', '100.0%'], 83 | ['CollapseProvider', '79/79', '100.0%', '7/7', '100.0%', '17/17', '100.0%'], 84 | ['CollapseContext.js', '5/5', '100.0%', '0/0', '100.0%', '0/0', '100.0%'], 85 | ['CollapseProvider.js', '23/23', '100.0%', '3/3', '100.0%', '4/4', '100.0%'], 86 | ['findNode.js', '3/3', '100.0%', '1/1', '100.0%', '4/4', '100.0%'], 87 | ['useCollapse.js', '18/18', '100.0%', '1/1', '100.0%', '3/3', '100.0%'], 88 | ['useCollapsedState.js', '30/30', '100.0%', '2/2', '100.0%', '6/6', '100.0%'], 89 | ['TreeView', '186/186', '100.0%', '10/10', '100.0%', '35/35', '100.0%'], 90 | ['Node.js', '58/58', '100.0%', '4/4', '100.0%', '10/10', '100.0%'], 91 | ['NodeStats.js', '63/63', '100.0%', '4/4', '100.0%', '14/14', '100.0%'], 92 | ['TreeView.js', '37/37', '100.0%', '1/1', '100.0%', '1/1', '100.0%'], 93 | ['coverageTree.js', '28/28', '100.0%', '1/1', '100.0%', '10/10', '100.0%'], 94 | ]); 95 | //@formatter:on 96 | }); 97 | }); 98 | 99 | describe('Multiple root folders', () => { 100 | //@formatter:off 101 | const multiRootTableData = [ 102 | ['All Files', '265/265', '100.0%', '17/17', '100.0%', '52/52', '100.0%'], 103 | ['CollapseProvider', '79/79', '100.0%', '7/7', '100.0%', '17/17', '100.0%'], 104 | ['CollapseContext.js', '5/5', '100.0%', '0/0', '100.0%', '0/0', '100.0%'], 105 | ['CollapseProvider.js', '23/23', '100.0%', '3/3', '100.0%', '4/4', '100.0%'], 106 | ['findNode.js', '3/3', '100.0%', '1/1', '100.0%', '4/4', '100.0%'], 107 | ['useCollapse.js', '18/18', '100.0%', '1/1', '100.0%', '3/3', '100.0%'], 108 | ['useCollapsedState.js', '30/30', '100.0%', '2/2', '100.0%', '6/6', '100.0%'], 109 | ['TreeView', '186/186', '100.0%', '10/10', '100.0%', '35/35', '100.0%'], 110 | ['Node.js', '58/58', '100.0%', '4/4', '100.0%', '10/10', '100.0%'], 111 | ['NodeStats.js', '63/63', '100.0%', '4/4', '100.0%', '14/14', '100.0%'], 112 | ['TreeView.js', '37/37', '100.0%', '1/1', '100.0%', '1/1', '100.0%'], 113 | ['coverageTree.js', '28/28', '100.0%', '1/1', '100.0%', '10/10', '100.0%'], 114 | ]; 115 | //@formatter:on 116 | 117 | it('should process LCOV with relative unix paths', async () => { 118 | const tableContent = await importFile(mockLcov.unixRelativeMultiRoot); 119 | expect(tableContent).toEqual(multiRootTableData); 120 | }); 121 | 122 | it('should process LCOV with relative windows paths', async () => { 123 | const tableContent = await importFile(mockLcov.winRelativeMultiRoot); 124 | expect(tableContent).toEqual(multiRootTableData); 125 | }); 126 | }); 127 | 128 | describe('Single root folder', () => { 129 | //@formatter:off 130 | const singleRootTableData = [ 131 | ['All Files', '265/265', '100.0%', '17/17', '100.0%', '52/52', '100.0%'], 132 | ['src', '265/265', '100.0%', '17/17', '100.0%', '52/52', '100.0%'], 133 | ['CollapseProvider', '79/79', '100.0%', '7/7', '100.0%', '17/17', '100.0%'], 134 | ['CollapseContext.js', '5/5', '100.0%', '0/0', '100.0%', '0/0', '100.0%'], 135 | ['CollapseProvider.js', '23/23', '100.0%', '3/3', '100.0%', '4/4', '100.0%'], 136 | ['findNode.js', '3/3', '100.0%', '1/1', '100.0%', '4/4', '100.0%'], 137 | ['useCollapse.js', '18/18', '100.0%', '1/1', '100.0%', '3/3', '100.0%'], 138 | ['useCollapsedState.js', '30/30', '100.0%', '2/2', '100.0%', '6/6', '100.0%'], 139 | ['TreeView', '186/186', '100.0%', '10/10', '100.0%', '35/35', '100.0%'], 140 | ['Node.js', '58/58', '100.0%', '4/4', '100.0%', '10/10', '100.0%'], 141 | ['NodeStats.js', '63/63', '100.0%', '4/4', '100.0%', '14/14', '100.0%'], 142 | ['TreeView.js', '37/37', '100.0%', '1/1', '100.0%', '1/1', '100.0%'], 143 | ['coverageTree.js', '28/28', '100.0%', '1/1', '100.0%', '10/10', '100.0%'], 144 | ]; 145 | //@formatter:on 146 | 147 | it('should process LCOV with relative unix paths', async () => { 148 | const tableContent = await importFile(mockLcov.unixRelativeSingleRoot); 149 | expect(tableContent).toEqual(singleRootTableData); 150 | }); 151 | 152 | it('should process LCOV with relative windows paths', async () => { 153 | const tableContent = await importFile(mockLcov.winRelativeSingleRoot); 154 | expect(tableContent).toEqual(singleRootTableData); 155 | }); 156 | }); 157 | 158 | it('should reset footer when navigated back to import page', async () => { 159 | expect(queryByText(/^Report generated at/)).not.toBeInTheDocument(); 160 | 161 | await importFile(mockLcov.winAbsolute); 162 | expect(queryByText(/^Report generated at/)).toBeInTheDocument(); 163 | 164 | fireEvent.click(queryByText(/Back/), { role: 'a' }); 165 | expect(queryByText(/^Report generated at/)).not.toBeInTheDocument(); 166 | }); 167 | }); -------------------------------------------------------------------------------- /packages/app/src/components/CoverageDataProvider/CoverageDataContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | const CoverageDataContext = createContext(); 4 | 5 | export default CoverageDataContext; 6 | -------------------------------------------------------------------------------- /packages/app/src/components/CoverageDataProvider/CoverageDataProvider.js: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import CoverageDataContext from './CoverageDataContext'; 3 | 4 | const CoverageDataProvider = ({ children }) => { 5 | const value = useRef({ data: null, date: '', subscriptions: [] }); 6 | 7 | return ( 8 | 9 | {children} 10 | 11 | ); 12 | }; 13 | 14 | export default CoverageDataProvider; 15 | -------------------------------------------------------------------------------- /packages/app/src/components/CoverageDataProvider/useCoverageData.js: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useState } from 'react'; 2 | import CoverageDataContext from './CoverageDataContext'; 3 | 4 | const useCoverageData = () => { 5 | const [, forceUpdate] = useState(); 6 | const context = useContext(CoverageDataContext).current; 7 | 8 | useEffect(() => { 9 | const notify = () => forceUpdate({}); 10 | context.subscriptions.push(notify); 11 | 12 | return () => { 13 | const index = context.subscriptions.findIndex(cb => cb === notify); 14 | 15 | if (index > -1) { 16 | context.subscriptions.splice(index, 1); 17 | } 18 | }; 19 | }, [context]); 20 | 21 | return context; 22 | }; 23 | 24 | export default useCoverageData; 25 | -------------------------------------------------------------------------------- /packages/app/src/components/CoverageDataProvider/useCoverageDataControl.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useContext } from 'react'; 2 | import CoverageDataContext from './CoverageDataContext'; 3 | 4 | const useCoverageDataControl = () => { 5 | const context = useContext(CoverageDataContext).current; 6 | const dispatch = useCallback((type, payload) => { 7 | switch (type) { 8 | case 'set': 9 | context.data = payload.data; 10 | context.date = payload.date; 11 | context.subscriptions.forEach(cb => cb()); 12 | break; 13 | case 'unset': 14 | context.data = null; 15 | context.date = ''; 16 | context.subscriptions.forEach(cb => cb()); 17 | break; 18 | } 19 | }, [context]); 20 | const set = useCallback((data, date) => dispatch('set', { data, date }), [dispatch]); 21 | const unset = useCallback(() => dispatch('unset'), [dispatch]); 22 | 23 | return [set, unset]; 24 | }; 25 | 26 | export default useCoverageDataControl; 27 | -------------------------------------------------------------------------------- /packages/app/src/components/CoverageDataProvider/useCoverageDate.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eugenezinovyev/lcov-viewer/da1e1675509e6cae8acb3ab57470be479049c4ac/packages/app/src/components/CoverageDataProvider/useCoverageDate.js -------------------------------------------------------------------------------- /packages/app/src/components/Footer/Footer.js: -------------------------------------------------------------------------------- 1 | import { Footer as CommonFooter } from '@lcov-viewer/components'; 2 | import React from 'react'; 3 | import useCoverageData from '../CoverageDataProvider/useCoverageData'; 4 | 5 | const Footer = () => { 6 | const { date } = useCoverageData(); 7 | 8 | return ( 9 | 10 | {date && `Report generated at ${date}`} 11 | 12 | ); 13 | }; 14 | 15 | export default Footer; 16 | -------------------------------------------------------------------------------- /packages/app/src/components/Header/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FontAwesomeSvgIcon } from 'react-fontawesome-svg-icon'; 3 | import { faGithub, faNpm } from '@fortawesome/free-brands-svg-icons'; 4 | import classes from './Header.module.css'; 5 | import icon from '../../../../../icon-240x240.svg'; 6 | 7 | const Header = () => { 8 | return ( 9 |
10 | icon 11 |

LCOV Viewer

12 | 20 |
21 | ); 22 | }; 23 | 24 | export default Header; 25 | -------------------------------------------------------------------------------- /packages/app/src/components/Header/Header.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | align-items: center; 4 | padding: 1rem 0; 5 | } 6 | 7 | .icon { 8 | margin-right: 1rem; 9 | } 10 | 11 | .title { 12 | margin: 0; 13 | flex: 1; 14 | } 15 | 16 | .links { 17 | display: flex; 18 | flex-direction: row; 19 | } 20 | 21 | .links a { 22 | font-size: 2.5rem; 23 | margin-left: 1.5rem; 24 | } 25 | 26 | .github, .github:visited, .github:active { 27 | color: #24292f; 28 | } 29 | 30 | .npm, .npm:visited, .npm:active { 31 | color: #cb0000; 32 | } 33 | -------------------------------------------------------------------------------- /packages/app/src/components/LcovImport/LcovImport.js: -------------------------------------------------------------------------------- 1 | import { buildCoverage, buildCoverageTree, clsx } from '@lcov-viewer/core'; 2 | import { route } from 'preact-router'; 3 | import React, { useCallback, useState } from 'react'; 4 | import { useDropzone } from 'react-dropzone'; 5 | import readLcov from '../../utils/readLcov'; 6 | import useCoverageDataControl from '../CoverageDataProvider/useCoverageDataControl'; 7 | import classes from './LcovImport.module.less'; 8 | import demoImage from '../../images/demo.png'; 9 | 10 | const LcovImport = () => { 11 | const [error, setError] = useState(undefined); 12 | const [set] = useCoverageDataControl(); 13 | const onDrop = useCallback((files) => { 14 | if (files.length <= 0) { 15 | return; 16 | } 17 | 18 | readLcov(files[0]).then( 19 | ([date, coverage]) => { 20 | set(buildCoverageTree(buildCoverage(coverage)), date); 21 | route('/report', true); 22 | }, 23 | ([error, msg]) => { 24 | console.error(error); 25 | setError(msg); 26 | }, 27 | ); 28 | }, [set]); 29 | const { isDragActive, getRootProps, getInputProps } = useDropzone({ onDrop }); 30 | 31 | return ( 32 |
33 |
34 |

More convenient way to review your test coverage

35 |

Group coverage by directory, collapse inessential, expand what's important.

36 | demo 37 |

Other ways to get report

38 |
39 | @lcov-viewer/istanbul-report 40 | Istanbul reporter with grouped HTML report output including per-file line coverage. 41 | @lcov-viewer/cli 42 | CLI to convert lcov.info file into grouped HTML report. 43 |
44 |
45 |
46 |
47 | 48 | Drop your LCOV file here 49 | — or — 50 | Click to open file 51 | {error && {error}} 52 |
53 |
54 |
55 | ); 56 | }; 57 | 58 | export default LcovImport; 59 | -------------------------------------------------------------------------------- /packages/app/src/components/LcovImport/LcovImport.module.less: -------------------------------------------------------------------------------- 1 | @import "@lcov-viewer/core/styles/colors"; 2 | @import "@lcov-viewer/core/styles/layout"; 3 | 4 | .root { 5 | flex-grow: 1; 6 | display: flex; 7 | flex-direction: row; 8 | width: 100%; 9 | height: 100%; 10 | font-size: 1.25rem; 11 | } 12 | 13 | .packages { 14 | display: grid; 15 | grid-template-columns: auto 1fr; 16 | grid-row-gap: 0.5rem; 17 | grid-column-gap: 0.5rem; 18 | font-size: 1.125rem; 19 | 20 | a { 21 | white-space: nowrap; 22 | } 23 | 24 | span { 25 | white-space: break-spaces; 26 | } 27 | } 28 | 29 | .column { 30 | display: flex; 31 | flex-direction: column; 32 | justify-content: center; 33 | 34 | &.infoColumn { 35 | padding: 0 1rem; 36 | flex: 0 1 0; 37 | 38 | h3, h4 { 39 | margin: 0 0 0.625rem; 40 | } 41 | 42 | p { 43 | margin: 0 0 0.5rem; 44 | } 45 | 46 | img { 47 | margin: 2.5rem 0 2.5rem; 48 | } 49 | } 50 | 51 | &.dropColumn { 52 | flex: 1; 53 | align-items: center; 54 | } 55 | } 56 | 57 | .dropper { 58 | .shadow; 59 | cursor: pointer; 60 | flex: 0 1 50%; 61 | width: 50%; 62 | box-sizing: border-box; 63 | border-radius: @border-radius; 64 | user-select: none; 65 | display: flex; 66 | flex-direction: column; 67 | align-items: center; 68 | justify-content: center; 69 | text-align: center; 70 | 71 | @media only screen and (min-width: 1300px) { 72 | font-size: 1.375rem; 73 | flex: 0 1 60%; 74 | width: 60%; 75 | } 76 | 77 | @media only screen and (min-width: 1600px) { 78 | font-size: 1.5rem; 79 | flex: 0 1 75%; 80 | width: 75%; 81 | } 82 | 83 | &.active { 84 | background-color: fade(@color-main, @color-fade-0); 85 | } 86 | } 87 | 88 | .error { 89 | color: @color-bad; 90 | font-size: 1rem; 91 | } 92 | -------------------------------------------------------------------------------- /packages/app/src/components/Redirect/Redirect.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'preact/hooks'; 2 | import { route } from 'preact-router'; 3 | 4 | const Redirect = ({ to }) => { 5 | useEffect(() => { 6 | route(to, true); 7 | }, [to]); 8 | 9 | return null; 10 | }; 11 | 12 | export default Redirect; 13 | -------------------------------------------------------------------------------- /packages/app/src/components/Report/Report.js: -------------------------------------------------------------------------------- 1 | import { CoverageIndicator, Summary, TreeView } from '@lcov-viewer/components'; 2 | import React from 'react'; 3 | import useCoverageData from '../CoverageDataProvider/useCoverageData'; 4 | import useCoverageDataControl from '../CoverageDataProvider/useCoverageDataControl'; 5 | import Redirect from '../Redirect/Redirect'; 6 | import classes from './Report.module.less'; 7 | 8 | const Report = () => { 9 | const { data: coverage } = useCoverageData(); 10 | const [, unset] = useCoverageDataControl(); 11 | 12 | if (!coverage) { 13 | return ; 14 | } 15 | 16 | return ( 17 |
18 | 19 | 🠔 Back 20 | | 21 | All Files 22 | 23 | 24 | 25 |
26 | ); 27 | }; 28 | 29 | export default Report; 30 | -------------------------------------------------------------------------------- /packages/app/src/components/Report/Report.module.less: -------------------------------------------------------------------------------- 1 | .root { 2 | flex-grow: 1; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | .separator { 8 | margin: 0 0.5rem; 9 | } -------------------------------------------------------------------------------- /packages/app/src/images/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eugenezinovyev/lcov-viewer/da1e1675509e6cae8acb3ab57470be479049c4ac/packages/app/src/images/demo.png -------------------------------------------------------------------------------- /packages/app/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import '@lcov-viewer/core/styles/baseline.less'; 4 | import './index.less'; 5 | import App from './components/App'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | -------------------------------------------------------------------------------- /packages/app/src/index.less: -------------------------------------------------------------------------------- 1 | #root { 2 | flex-direction: row; 3 | justify-content: center; 4 | } -------------------------------------------------------------------------------- /packages/app/src/utils/readLcov.js: -------------------------------------------------------------------------------- 1 | import { parseLCOV } from '@lcov-viewer/core'; 2 | 3 | const readLcov = (file) => new Promise((resolve, reject) => { 4 | const date = new Date(file.lastModified || file.lastModifiedDate || new Date().getTime()).toString(); 5 | const reader = new FileReader(); 6 | reader.onload = async () => { 7 | try { 8 | const data = await parseLCOV(reader.result); 9 | resolve([date, data]); 10 | } catch (error) { 11 | reject([error, 'Failed to parse file']); 12 | } 13 | }; 14 | reader.onerror = () => reject([reader.error, 'Failed to read file']); 15 | reader.readAsText(file); 16 | }); 17 | 18 | export default readLcov; 19 | -------------------------------------------------------------------------------- /packages/app/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const baseConfig = require('../../webpack.base-config'); 3 | const CopyPlugin = require('copy-webpack-plugin'); 4 | 5 | module.exports = (env, argv) => { 6 | const outputPath = path.resolve(__dirname, './dist'); 7 | const config = { 8 | ...baseConfig(env, argv), 9 | entry: './src/index.js', 10 | output: { path: outputPath }, 11 | }; 12 | 13 | config.plugins.push(new CopyPlugin({ 14 | patterns: [ 15 | { from: 'public', to: outputPath }, 16 | ], 17 | })); 18 | 19 | config.resolve.fallback = { 20 | fs: false, 21 | path: false, 22 | }; 23 | 24 | return config; 25 | }; 26 | -------------------------------------------------------------------------------- /packages/cli/.gitignore: -------------------------------------------------------------------------------- 1 | /lib/ -------------------------------------------------------------------------------- /packages/cli/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2021 Eugene Zinovyev 2 | 3 | Permission is hereby granted, 4 | free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice 12 | (including the next paragraph) shall be included in all copies or substantial 13 | portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 18 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 19 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /packages/cli/README.md: -------------------------------------------------------------------------------- 1 | ![build](https://github.com/eugenezinovyev/lcov-viewer/actions/workflows/main.yml/badge.svg) 2 | [![Known Vulnerabilities](https://snyk.io/test/github/eugenezinovyev/lcov-viewer/badge.svg?targetFile=packages%2Fcli%2Fpackage.json)](https://snyk.io/test/github/eugenezinovyev/lcov-viewer?targetFile=packages%2Fcli%2Fpackage.json) 3 | [![npm version](https://badge.fury.io/js/@lcov-viewer%2Fcli.svg)](https://www.npmjs.com/package/@lcov-viewer/cli) 4 | 5 | # LCOV viewer CLI 6 | 7 | CLI to convert coverage to grouped HTML report. Generates code coverage report grouped by directory. 8 | 9 | ## Installation 10 | 11 | ``` 12 | npm install -g @lcov-viewer/cli 13 | yarn global add @lcov-viewer/cli 14 | ``` 15 | 16 | ## Usage 17 | ``` 18 | lcov-viewer lcov -o ./report-output-directory ./lcov.info 19 | ``` 20 | 21 | # Other packages 22 | An Istanbul report package: [@lcov-viewer/istanbul-report](https://www.npmjs.com/package/@lcov-viewer/istanbul-report) 23 | 24 | 25 | ## Demo 26 | ![report](https://user-images.githubusercontent.com/1678896/160290486-4474bd0a-c3e5-4e0e-90dc-6ac72a1e76f7.gif) 27 | -------------------------------------------------------------------------------- /packages/cli/commands/lcov.js: -------------------------------------------------------------------------------- 1 | import { buildCoverage, parseLCOV } from '@lcov-viewer/core'; 2 | import { promises as fs } from 'fs'; 3 | import path from 'path'; 4 | 5 | async function lcov(src, { output }) { 6 | const [stats, lcovContent] = await Promise.all([fs.stat(src), fs.readFile(src, { encoding: 'utf8' })]); 7 | const date = stats.mtime.toString(); 8 | const lcov = await parseLCOV(lcovContent); 9 | const coverage = buildCoverage(lcov); 10 | const assetsDirectory = path.resolve(__dirname, 'assets'); 11 | await fs.mkdir(output, { recursive: true }); 12 | const files = await fs.readdir(assetsDirectory); 13 | files.map(file => { 14 | const dest = path.resolve(output, file); 15 | return fs.copyFile(path.resolve(assetsDirectory, file), dest); 16 | }); 17 | await fs.writeFile( 18 | path.resolve(output, 'report-data.js'), 19 | `window.REPORT_DATE = '${date.toString()}';window.COVERAGE_DATA = ${JSON.stringify(coverage)};`, 20 | { encoding: 'utf8' }, 21 | ); 22 | } 23 | 24 | export default lcov; 25 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lcov-viewer/cli", 3 | "keywords": [ 4 | "coverage", 5 | "code coverage", 6 | "cli", 7 | "lcov", 8 | "report" 9 | ], 10 | "version": "1.3.0", 11 | "private": false, 12 | "author": "Eugene Zinovyev ", 13 | "main": "lib/index.js", 14 | "license": "MIT", 15 | "scripts": { 16 | "build": "rollup -c" 17 | }, 18 | "dependencies": { 19 | "commander": "^9.3.0" 20 | }, 21 | "bin": { 22 | "lcov-viewer": "lib/index.js" 23 | }, 24 | "devDependencies": { 25 | "@lcov-viewer/core": "^1.3.0", 26 | "@lcov-viewer/report": "^1.4.0", 27 | "@lcov-viewer/rollup-copy": "^1.3.0", 28 | "@rollup/plugin-commonjs": "^25.0.4", 29 | "@rollup/plugin-node-resolve": "^15.1.0", 30 | "@rollup/plugin-terser": "^0.4.3", 31 | "rollup": "^3.28.0", 32 | "rollup-plugin-preserve-shebang": "^1.0.1" 33 | }, 34 | "files": [ 35 | "lib/**/*.*" 36 | ], 37 | "directories": { 38 | "lib": "lib" 39 | }, 40 | "description": "LCOV viewer CLI to convert coverage to grouped HTML report. Generates code coverage report grouped by directory.", 41 | "repository": { 42 | "type": "git", 43 | "url": "https://github.com/eugenezinovyev/lcov-viewer.git", 44 | "directory": "packages/cli" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/cli/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import report from '@lcov-viewer/report'; 2 | import copy from '@lcov-viewer/rollup-copy'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 5 | import shebang from 'rollup-plugin-preserve-shebang'; 6 | import terser from '@rollup/plugin-terser'; 7 | 8 | export default [ 9 | { 10 | input: './src/index.js', 11 | output: { 12 | file: './lib/index.js', 13 | format: 'cjs', 14 | }, 15 | plugins: [ 16 | terser(), 17 | shebang(), 18 | copy({ 19 | files: report.getAssets(), 20 | dest: './lib/assets', 21 | }), 22 | nodeResolve({ preferBuiltins: true }), 23 | commonjs(), 24 | ], 25 | }, 26 | ]; -------------------------------------------------------------------------------- /packages/cli/src/index.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | import { program } from 'commander'; 4 | import lcov from '../commands/lcov'; 5 | 6 | program 7 | .name('lcov-viewer') 8 | .command('lcov ') 9 | .description('Converts lcov.info file into an LCOV viewer HTML report.') 10 | .option('-o, --output ', 'The output directory.', './lcov-viewer') 11 | .action(lcov); 12 | 13 | program.parse(); 14 | -------------------------------------------------------------------------------- /packages/components/CoverageIndicator/CoverageIndicator.js: -------------------------------------------------------------------------------- 1 | import { calculatePercentage, clsx, resolveCoverageClass } from '@lcov-viewer/core'; 2 | import React from 'react'; 3 | import classes from './CoverageIndicator.module.less'; 4 | 5 | const CoverageIndicator = ({ metrics }) =>
; 6 | 7 | export default CoverageIndicator; 8 | -------------------------------------------------------------------------------- /packages/components/CoverageIndicator/CoverageIndicator.module.less: -------------------------------------------------------------------------------- 1 | @import "@lcov-viewer/core/styles/colors"; 2 | @import "@lcov-viewer/core/styles/layout"; 3 | 4 | .root { 5 | flex-shrink: 0; 6 | height: 1rem; 7 | margin-bottom: 0.5rem; 8 | border-radius: @border-radius; 9 | 10 | &.bad { 11 | background-color: @color-bad; 12 | } 13 | 14 | &.normal { 15 | background-color: @color-normal; 16 | } 17 | 18 | &.good { 19 | background-color: @color-good; 20 | } 21 | } -------------------------------------------------------------------------------- /packages/components/Footer/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classes from './Footer.module.less'; 3 | 4 | const Footer = ({ linkHref, children }) => ( 5 |
6 | LCOV Viewer 7 | {children && {children}} 8 |
9 | ); 10 | 11 | Footer.defaultProps = { 12 | linkHref: 'https://github.com/eugenezinovyev/lcov-viewer', 13 | }; 14 | 15 | export default Footer; 16 | -------------------------------------------------------------------------------- /packages/components/Footer/Footer.module.less: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | padding: 1rem 0; 6 | 7 | span, a { 8 | white-space: nowrap; 9 | } 10 | 11 | span { 12 | &:before { 13 | content: '\00a0|\00a0'; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /packages/components/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2021 Eugene Zinovyev 2 | 3 | Permission is hereby granted, 4 | free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice 12 | (including the next paragraph) shall be included in all copies or substantial 13 | portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 18 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 19 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /packages/components/LinkButton/LinkButton.js: -------------------------------------------------------------------------------- 1 | import { clsx } from '@lcov-viewer/core'; 2 | import React from 'react'; 3 | import classes from './LinkButton.module.less'; 4 | 5 | const LinkButton = ({ children, className, ...props }) => ( 6 | 7 | ); 8 | 9 | export default LinkButton; 10 | -------------------------------------------------------------------------------- /packages/components/LinkButton/LinkButton.module.less: -------------------------------------------------------------------------------- 1 | @import "@lcov-viewer/core/styles/colors"; 2 | 3 | .root { 4 | border: none; 5 | outline: none; 6 | background: none; 7 | color: @color-main; 8 | text-decoration: none; 9 | margin: 0; 10 | padding: 0; 11 | font-family: inherit; 12 | font-size: inherit; 13 | cursor: pointer; 14 | 15 | &:hover { 16 | color: darken(@color-main, 20%); 17 | } 18 | } -------------------------------------------------------------------------------- /packages/components/Summary/Summary.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SummaryLine from './SummaryLine'; 3 | import classes from './Summary.module.less'; 4 | 5 | const Summary = ({ children, metrics }) => ( 6 |
7 |

8 | {children} 9 |

10 |
11 | 12 | 13 | 14 |
15 |
16 | ); 17 | 18 | export default Summary; 19 | -------------------------------------------------------------------------------- /packages/components/Summary/Summary.module.less: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | margin: 0.5rem 0; 6 | } 7 | 8 | .title { 9 | flex-grow: 1; 10 | flex-shrink: 1; 11 | margin: 0; 12 | font-size: 1.5rem; 13 | font-weight: normal; 14 | white-space: nowrap; 15 | } 16 | 17 | .summary { 18 | display: flex; 19 | flex-direction: column; 20 | } -------------------------------------------------------------------------------- /packages/components/Summary/SummaryLine.js: -------------------------------------------------------------------------------- 1 | import { calculatePercentage, clsx, resolveCoverageClass } from '@lcov-viewer/core'; 2 | import React from 'react'; 3 | import classes from './SummaryLine.module.less'; 4 | 5 | const SummaryLine = ({ name, metrics }) => { 6 | const percentage = calculatePercentage(metrics); 7 | 8 | return ( 9 |
10 | {percentage.toFixed(1)}% 11 | {name} 12 | {metrics.covered}/{metrics.total} 13 |
14 | ); 15 | }; 16 | 17 | export default SummaryLine; 18 | -------------------------------------------------------------------------------- /packages/components/Summary/SummaryLine.module.less: -------------------------------------------------------------------------------- 1 | @import "@lcov-viewer/core/styles/colors"; 2 | @import "@lcov-viewer/core/styles/layout"; 3 | 4 | .root { 5 | display: flex; 6 | flex-direction: row; 7 | white-space: nowrap; 8 | align-items: center; 9 | 10 | & + & { 11 | margin-top: 0.25rem; 12 | } 13 | } 14 | 15 | .percentage { 16 | font-weight: bolder; 17 | min-width: 5em; 18 | font-size: 1.125rem; 19 | display: inline-block; 20 | text-align: right; 21 | margin-right: 0.5rem; 22 | } 23 | 24 | .name { 25 | display: inline-block; 26 | flex: 1 0 auto; 27 | } 28 | 29 | .badge { 30 | display: inline-block; 31 | margin-left: 1rem; 32 | text-align: right; 33 | padding: 0.125em 0.25em; 34 | background: rgba(0,0,0,0.19); 35 | border-radius: @border-radius; 36 | 37 | &.bad { 38 | background-color: fade(@color-bad, @color-fade-1); 39 | color: @color-bad; 40 | } 41 | 42 | &.normal { 43 | background-color: fade(@color-normal, @color-fade-1); 44 | color: @color-normal; 45 | } 46 | 47 | &.good { 48 | background-color: fade(@color-good, @color-fade-1); 49 | color: @color-good; 50 | } 51 | } -------------------------------------------------------------------------------- /packages/components/TreeView/TreeView.js: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import buildTableRowsData from '../render/buildTableRowsData'; 3 | import LinkButton from '../LinkButton/LinkButton'; 4 | import renderRow from '../render/renderRow'; 5 | import renderTable from '../render/renderTable'; 6 | import useCollapse from './useCollapse'; 7 | import classes from './TreeView.module.less'; 8 | 9 | const TreeView = ({ coverage, linkSelector }) => { 10 | const [collapseContainer, onCollapseAll, onExpandAll] = useCollapse(); 11 | const tableHTML = useMemo(() => { 12 | const rows = buildTableRowsData(coverage); 13 | const bodyContent = rows.map(row => renderRow(row, { linkSelector })); 14 | 15 | return { __html: renderTable(bodyContent) }; 16 | }, [coverage, linkSelector]); 17 | 18 | 19 | return ( 20 |
21 |
22 | Collapse All 23 | Expand All 24 |
25 |
26 |
27 | ); 28 | }; 29 | 30 | export default TreeView; 31 | -------------------------------------------------------------------------------- /packages/components/TreeView/TreeView.module.less: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .controls { 7 | display: flex; 8 | flex-direction: row; 9 | margin-bottom: 0.5rem; 10 | 11 | & > * + * { 12 | margin-left: 1rem; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/components/TreeView/TreeView.test.js: -------------------------------------------------------------------------------- 1 | import { buildCoverageTree } from '@lcov-viewer/core'; 2 | import '@testing-library/jest-dom'; 3 | import { fireEvent, render } from '@testing-library/preact'; 4 | import React from 'react'; 5 | import TreeView from './TreeView'; 6 | 7 | const coverage = buildCoverageTree({ 8 | 'src/index.js': { 9 | metrics: { 10 | lines: { total: 6, covered: 0 }, 11 | functions: { total: 0, covered: 0 }, 12 | branches: { total: 0, covered: 0 }, 13 | }, 14 | path: 'src/index.js', 15 | filename: 'index.js', 16 | }, 17 | 'src/components/App.js': { 18 | metrics: { 19 | lines: { total: 14, covered: 9 }, 20 | functions: { total: 2, covered: 0 }, 21 | branches: { total: 2, covered: 2 }, 22 | }, 23 | path: 'src/components/App.js', 24 | filename: 'App.js', 25 | }, 26 | }); 27 | 28 | const queryTableContent = container => [...container.querySelectorAll('tbody > tr')] 29 | .filter(row => !row.classList.contains('hidden')) 30 | .map(row => [...row.querySelectorAll('td')].map(cell => cell.textContent)); 31 | const queryRows = container => [...container.querySelectorAll('tbody > tr > td:first-child')] 32 | .filter(cell => !cell.parentElement.classList.contains('hidden')) 33 | .map(cell => cell.textContent); 34 | 35 | const clickRow = (container, title) => fireEvent.click([...container.querySelectorAll('tbody > tr')] 36 | .find(row => row.querySelector('td:first-child').textContent === title)); 37 | 38 | describe('', () => { 39 | it('should render coverage for each directory and file', () => { 40 | const { container } = render(); 41 | 42 | //@formatter:off 43 | expect(queryTableContent(container)).toEqual([ 44 | ['All Files', '9/20', '45.0%', '0/2', '0.0%', '2/2', '100.0%'], 45 | ['src', '9/20', '45.0%', '0/2', '0.0%', '2/2', '100.0%'], 46 | ['index.js', '0/6', '0.0%', '0/0', '100.0%', '0/0', '100.0%'], 47 | ['components', '9/14', '64.3%', '0/2', '0.0%', '2/2', '100.0%'], 48 | ['App.js', '9/14', '64.3%', '0/2', '0.0%', '2/2', '100.0%'], 49 | ]); 50 | //@formatter:on 51 | }); 52 | 53 | it('should toggle child rows visibility on directory row click', () => { 54 | const { container } = render(); 55 | expect(queryRows(container)).toEqual(['All Files', 'src', 'index.js', 'components', 'App.js']); 56 | 57 | clickRow(container, 'All Files'); 58 | expect(queryRows(container)).toEqual(['All Files']); 59 | 60 | clickRow(container, 'All Files'); 61 | expect(queryRows(container)).toEqual(['All Files', 'src', 'index.js', 'components', 'App.js']); 62 | }); 63 | 64 | it('should persist child row visibility on parent row visibility change', () => { 65 | const { container } = render(); 66 | expect(queryRows(container)).toEqual(['All Files', 'src', 'index.js', 'components', 'App.js']); 67 | 68 | clickRow(container, 'components'); 69 | expect(queryRows(container)).toEqual(['All Files', 'src', 'index.js', 'components']); 70 | 71 | clickRow(container, 'src'); 72 | expect(queryRows(container)).toEqual(['All Files', 'src']); 73 | 74 | clickRow(container, 'src'); 75 | expect(queryRows(container)).toEqual(['All Files', 'src', 'index.js', 'components']); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /packages/components/TreeView/useCollapse.js: -------------------------------------------------------------------------------- 1 | import { BRANCH_NODE } from '@lcov-viewer/core'; 2 | import { useCallback, useEffect, useRef } from 'react'; 3 | import { collapseRow, expandRow, hideRow, showRow } from '../render/collapse'; 4 | 5 | const findRow = (element) => { 6 | let current = element; 7 | 8 | while (current && current.tagName.toUpperCase() !== 'TR') { 9 | current = current.parentElement; 10 | } 11 | 12 | return current; 13 | }; 14 | 15 | const makeParentPath = path => path && `${path}/`; 16 | 17 | const useCollapse = () => { 18 | const ref = useRef(); 19 | const state = useRef({}); 20 | 21 | useEffect(() => { 22 | if (!ref.current) { 23 | return; 24 | } 25 | 26 | Array.prototype.forEach.call(ref.current.querySelectorAll('tbody tr'), row => { 27 | const path = row.getAttribute('data-node'); 28 | const type = row.getAttribute('data-node-type'); 29 | state.current[path] = { path, type, row, collapsed: false, visible: true }; 30 | }); 31 | 32 | const handleClick = (event) => { 33 | const toggledRow = findRow(event.target); 34 | 35 | if (!toggledRow || toggledRow.getAttribute('data-node-type') !== BRANCH_NODE) { 36 | return; 37 | } 38 | 39 | const toggledPath = toggledRow.getAttribute('data-node'); 40 | const rowState = state.current[toggledPath]; 41 | 42 | if (rowState.collapsed) { 43 | expandRow(rowState.row); 44 | } else { 45 | collapseRow(rowState.row); 46 | } 47 | 48 | const childrenPaths = Object.keys(state.current).filter(path => path.startsWith(makeParentPath(rowState.path)) && path !== rowState.path); 49 | const collapsedChildrenPaths = childrenPaths.filter(path => state.current[path].collapsed); 50 | const affectedChildrenPaths = childrenPaths 51 | .filter(path => !collapsedChildrenPaths 52 | .find(collapsedChildPath => path.startsWith(makeParentPath(collapsedChildPath)) && path !== collapsedChildPath)); 53 | 54 | affectedChildrenPaths.forEach(path => { 55 | if (rowState.collapsed) { 56 | showRow(state.current[path].row); 57 | state.current[path].visible = true; 58 | } else { 59 | hideRow(state.current[path].row); 60 | state.current[path].visible = false; 61 | } 62 | }); 63 | 64 | rowState.collapsed = !rowState.collapsed; 65 | }; 66 | 67 | ref.current.addEventListener('click', handleClick, true); 68 | 69 | return () => { 70 | ref.current && ref.current.removeEventListener('click', handleClick, true); 71 | state.current = {}; 72 | }; 73 | }, []); 74 | 75 | const onCollapseAll = useCallback(() => { 76 | Object.values(state.current).forEach(rowState => { 77 | if (rowState.type === BRANCH_NODE) { 78 | if (!rowState.collapsed) { 79 | collapseRow(rowState.row); 80 | rowState.collapsed = true; 81 | } 82 | } 83 | 84 | if (rowState.path !== '') { 85 | hideRow(rowState.row); 86 | rowState.visible = false; 87 | } 88 | }); 89 | 90 | }, []); 91 | 92 | const onExpandAll = useCallback(() => { 93 | Object.values(state.current).forEach(rowState => { 94 | if (rowState.collapsed) { 95 | expandRow(rowState.row); 96 | rowState.collapsed = false; 97 | } 98 | 99 | if (!rowState.visible) { 100 | showRow(rowState.row); 101 | rowState.visible = true; 102 | } 103 | }); 104 | }, []); 105 | 106 | return [ref, onCollapseAll, onExpandAll]; 107 | }; 108 | 109 | export default useCollapse; 110 | -------------------------------------------------------------------------------- /packages/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as TreeView } from './TreeView/TreeView'; 2 | export { default as Footer } from './Footer/Footer'; 3 | export { default as CoverageIndicator } from './CoverageIndicator/CoverageIndicator'; 4 | export { default as Summary } from './Summary/Summary'; 5 | export { default as LinkButton } from './LinkButton/LinkButton'; 6 | -------------------------------------------------------------------------------- /packages/components/jest.config.js: -------------------------------------------------------------------------------- 1 | const configurator = require('../../jest.base-config'); 2 | 3 | const config = configurator({ 4 | coverageReporters: [ 5 | 'lcov', 6 | 'json', 7 | '@lcov-viewer/istanbul-report' 8 | ], 9 | moduleNameMapper: { 10 | '^react$': 'preact/compat', 11 | '^react-dom/test-utils$': 'preact/test-utils', 12 | '^react-dom$': 'preact/compat', 13 | '^react/jsx-runtime$': 'preact/jsx-runtime', 14 | }, 15 | coveragePathIgnorePatterns: [ 16 | '/node_modules/', 17 | '/mocks/', 18 | '\.less$', 19 | ], 20 | }); 21 | 22 | module.exports = config; 23 | -------------------------------------------------------------------------------- /packages/components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lcov-viewer/components", 3 | "version": "1.3.0", 4 | "author": "Eugene Zinovyev ", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "test": "../../node_modules/.bin/jest --colors --passWithNoTests" 9 | }, 10 | "dependencies": { 11 | "@lcov-viewer/core": "^1.3.0" 12 | }, 13 | "peerDependencies": { 14 | "react": "^17.0.2" 15 | }, 16 | "devDependencies": { 17 | "@lcov-viewer/istanbul-report": "^1.4.0", 18 | "@testing-library/jest-dom": "^5.16.4", 19 | "@testing-library/preact": "^3.2.3", 20 | "react": "^18.2.0", 21 | "react-dom": "^18.2.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/components/render/buildTableRowsData.js: -------------------------------------------------------------------------------- 1 | const buildTableRowsData = (node, depth = 0, result = []) => { 2 | result.push({ node, depth }); 3 | 4 | node.children && Object.keys(node.children).forEach(key => buildTableRowsData(node.children[key], depth + 1, result)); 5 | 6 | return result; 7 | }; 8 | 9 | export default buildTableRowsData; -------------------------------------------------------------------------------- /packages/components/render/collapse.js: -------------------------------------------------------------------------------- 1 | import rowClasses from './renderRow.module.less'; 2 | 3 | export const collapseRow = (tr) => { tr && tr.classList.remove(rowClasses.open) }; 4 | 5 | export const expandRow = (tr) => { tr && tr.classList.add(rowClasses.open) }; 6 | 7 | export const showRow = (tr) => { tr && tr.classList.remove(rowClasses.hidden) }; 8 | 9 | export const hideRow = (tr) => { tr && tr.classList.add(rowClasses.hidden) }; 10 | -------------------------------------------------------------------------------- /packages/components/render/renderRow.js: -------------------------------------------------------------------------------- 1 | import { BRANCH_NODE, calculatePercentage, clsx, resolveCoverageClass } from '@lcov-viewer/core'; 2 | import classes from './renderRow.module.less'; 3 | 4 | const rowClasses = clsx(classes.root, classes.folder, classes.open); 5 | 6 | const coverageClasses = { 7 | good: clsx(classes.numbers, classes.coverage, classes.good), 8 | normal: clsx(classes.numbers, classes.coverage, classes.normal), 9 | bad: clsx(classes.numbers, classes.coverage, classes.bad), 10 | }; 11 | 12 | const renderTitleCell = (node, depth, linkSelector) => { 13 | const link = linkSelector && linkSelector(node); 14 | const title = link ? `${node.name}` : node.name; 15 | const offset = node.type === BRANCH_NODE ? 0.5 : 1.75; // 1.75 = 0.5 + 1 (close icon) + 0.25 (close icon margin) 16 | 17 | return `${title}`; 18 | }; 19 | 20 | const renderMetricCells = (stats, category) => { 21 | const rowCoverage = calculatePercentage(stats[category]); 22 | const coverageClassName = resolveCoverageClass(rowCoverage, coverageClasses); 23 | 24 | return [ 25 | `${stats[category].covered}/${stats[category].total}`, 26 | `${rowCoverage.toFixed(1)}%`, 27 | ]; 28 | }; 29 | 30 | const renderRow = (row, { linkSelector }) => { 31 | const cells = [ 32 | renderTitleCell(row.node, row.depth, linkSelector), 33 | ...renderMetricCells(row.node.metrics, 'lines'), 34 | ...renderMetricCells(row.node.metrics, 'functions'), 35 | ...renderMetricCells(row.node.metrics, 'branches'), 36 | ]; 37 | const className = row.node.type === BRANCH_NODE ? rowClasses : classes.root; 38 | 39 | return `${cells.join('')}`; 40 | }; 41 | 42 | export default renderRow; 43 | -------------------------------------------------------------------------------- /packages/components/render/renderRow.module.less: -------------------------------------------------------------------------------- 1 | @import "@lcov-viewer/core/styles/colors"; 2 | 3 | .root { 4 | cursor: default; 5 | 6 | &:hover { 7 | background-color: fade(@color-main, @color-fade-1); 8 | } 9 | } 10 | 11 | .folder { 12 | .title { 13 | white-space: nowrap; 14 | 15 | &:before { 16 | color: @color-main; 17 | cursor: pointer; 18 | display: inline-block; 19 | content: "\002B"; 20 | width: 1rem; 21 | margin-right: 0.25rem; 22 | text-align: center; 23 | } 24 | } 25 | 26 | &.open { 27 | .title { 28 | &:before { 29 | content: "\2013"; 30 | } 31 | } 32 | } 33 | } 34 | 35 | .hidden { 36 | display: none; 37 | } 38 | 39 | .coverage { 40 | &.bad { 41 | color: @color-bad; 42 | } 43 | 44 | &.normal { 45 | color: @color-normal; 46 | } 47 | 48 | &.good { 49 | color: @color-good; 50 | } 51 | } 52 | 53 | .numbers { 54 | text-align: right; 55 | } 56 | -------------------------------------------------------------------------------- /packages/components/render/renderTable.js: -------------------------------------------------------------------------------- 1 | import classes from './renderTable.module.less'; 2 | 3 | const headCells = [ 4 | `File`, 5 | `Lines`, 6 | `Line Coverage`, 7 | `Functions`, 8 | `Function Coverage`, 9 | `Branches`, 10 | `Branch Coverage`, 11 | ]; 12 | 13 | const header = `${headCells.join('')}`; 14 | const footer = ``; 15 | 16 | export const renderTableContents = (bodyCells) => `${header}${bodyCells.join('')}${footer}`; 17 | 18 | const renderTable = (bodyCells) => `${renderTableContents(bodyCells)}
`; 19 | 20 | export default renderTable; 21 | -------------------------------------------------------------------------------- /packages/components/render/renderTable.module.less: -------------------------------------------------------------------------------- 1 | @import "@lcov-viewer/core/styles/colors"; 2 | 3 | .root { 4 | width: 100%; 5 | 6 | .title { 7 | width: 50%; 8 | } 9 | 10 | .numbers { 11 | text-align: right; 12 | } 13 | } -------------------------------------------------------------------------------- /packages/core/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2021 Eugene Zinovyev 2 | 3 | Permission is hereby granted, 4 | free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice 12 | (including the next paragraph) shall be included in all copies or substantial 13 | portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 18 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 19 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /packages/core/index.js: -------------------------------------------------------------------------------- 1 | export { default as clsx } from './src/clsx'; 2 | export { default as calculatePercentage } from './src/calculatePercentage'; 3 | export { resolveCoverageClass, resolveCoverageLevel } from './src/coverageLevel'; 4 | export { default as buildCoverageTree } from './src/buildCoverageTree'; 5 | export { default as buildCoverage } from './src/buildCoverage'; 6 | export { default as parseLCOV } from './src/parseLCOV'; 7 | export { BRANCH_NODE, LEAF_NODE } from './src/treeNodeTypes'; 8 | -------------------------------------------------------------------------------- /packages/core/jest.config.js: -------------------------------------------------------------------------------- 1 | const configurator = require('../../jest.base-config'); 2 | 3 | const config = configurator({ 4 | coverageReporters: [ 5 | 'lcov', 6 | 'json', 7 | '@lcov-viewer/istanbul-report' 8 | ], 9 | coveragePathIgnorePatterns: [ 10 | '/node_modules/', 11 | '/mocks/', 12 | '\.less$', 13 | ], 14 | }); 15 | 16 | module.exports = config; 17 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lcov-viewer/core", 3 | "version": "1.3.0", 4 | "author": "Eugene Zinovyev ", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "test": "../../node_modules/.bin/jest --colors --passWithNoTests" 9 | }, 10 | "dependencies": { 11 | "lcov-parse": "^1.0.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/src/build.coverage.test.js: -------------------------------------------------------------------------------- 1 | import buildCoverage from './buildCoverage'; 2 | 3 | const createRecord = (path) => ({ 4 | lines: { hit: 0, found: 0, details: [] }, 5 | functions: { hit: 0, found: 0, details: [] }, 6 | branches: { hit: 0, found: 0, details: [] }, 7 | title: '', 8 | file: path, 9 | }); 10 | 11 | describe('buildCoverage', () => { 12 | it('should process windows absolute path', () => { 13 | expect(buildCoverage([createRecord('C:\\src\\index.js')])).toEqual({ 14 | 'C/src/index.js': { 15 | filename: 'index.js', 16 | metrics: { 17 | branches: { covered: 0, total: 0 }, 18 | functions: { covered: 0, total: 0 }, 19 | lines: { covered: 0, total: 0 }, 20 | }, 21 | path: 'C/src/index.js', 22 | }, 23 | }); 24 | }); 25 | 26 | it('should process unix absolute path', () => { 27 | expect(buildCoverage([createRecord('/src/index.js')])).toEqual({ 28 | 'src/index.js': { 29 | filename: 'index.js', 30 | metrics: { 31 | branches: { covered: 0, total: 0 }, 32 | functions: { covered: 0, total: 0 }, 33 | lines: { covered: 0, total: 0 }, 34 | }, 35 | path: 'src/index.js', 36 | }, 37 | }); 38 | }); 39 | 40 | it('should process windows relative path', () => { 41 | expect(buildCoverage([createRecord('src\\index.js')])).toEqual({ 42 | 'src/index.js': { 43 | filename: 'index.js', 44 | metrics: { 45 | branches: { covered: 0, total: 0 }, 46 | functions: { covered: 0, total: 0 }, 47 | lines: { covered: 0, total: 0 }, 48 | }, 49 | path: 'src/index.js', 50 | }, 51 | }); 52 | }); 53 | 54 | it('should process unix relative path', () => { 55 | expect(buildCoverage([createRecord('src/index.js')])).toEqual({ 56 | 'src/index.js': { 57 | filename: 'index.js', 58 | metrics: { 59 | branches: { covered: 0, total: 0 }, 60 | functions: { covered: 0, total: 0 }, 61 | lines: { covered: 0, total: 0 }, 62 | }, 63 | path: 'src/index.js', 64 | }, 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /packages/core/src/buildCoverage.js: -------------------------------------------------------------------------------- 1 | const buildCoverage = (records) => records.reduce((aggregate, record) => { 2 | const path = record.file.replace(/\\/g, '/').replace(/^(.):/, '/$1').replace(/^\//, ''); 3 | 4 | aggregate[path] = { 5 | metrics: { 6 | lines: { total: record.lines.found, covered: record.lines.hit }, 7 | functions: { total: record.functions.found, covered: record.functions.hit }, 8 | branches: { total: record.branches.found, covered: record.branches.hit }, 9 | }, 10 | path: path, 11 | filename: path.split('/').pop(), 12 | }; 13 | 14 | return aggregate; 15 | }, {}); 16 | 17 | export default buildCoverage; 18 | -------------------------------------------------------------------------------- /packages/core/src/buildCoverageTree.js: -------------------------------------------------------------------------------- 1 | import { BRANCH_NODE, LEAF_NODE } from './treeNodeTypes'; 2 | 3 | const defaultMetrics = () => ({ 4 | lines: { total: 0, covered: 0 }, 5 | functions: { total: 0, covered: 0 }, 6 | branches: { total: 0, covered: 0 }, 7 | }); 8 | 9 | const statCategories = Object.keys(defaultMetrics()); 10 | 11 | const addMetrics = (aggregate, metrics) => { 12 | statCategories.forEach(category => { 13 | aggregate[category].total += metrics[category].total; 14 | aggregate[category].covered += metrics[category].covered; 15 | }); 16 | }; 17 | 18 | const buildCoverageTree = (coverage) => { 19 | const root = { type: BRANCH_NODE, name: 'All Files', children: {}, path: '', metrics: defaultMetrics() }; 20 | 21 | Object.keys(coverage).forEach((path) => { 22 | let current = root; 23 | let currentPath = root.path; 24 | const segments = path.split('/'); 25 | segments.forEach((segment, index) => { 26 | const isLeaf = index === segments.length - 1; 27 | currentPath = currentPath.length > 0 ? `${currentPath}/${segment}` : segment; 28 | 29 | if (!Object.hasOwnProperty.call(current.children, segment)) { 30 | current.children[segment] = { 31 | type: isLeaf ? LEAF_NODE : BRANCH_NODE, 32 | name: segment, 33 | children: {}, 34 | path: currentPath, 35 | metrics: isLeaf ? coverage[path].metrics : defaultMetrics(), 36 | parent: current, 37 | }; 38 | } 39 | 40 | if (isLeaf) { 41 | let currentAggregate = current; 42 | 43 | while(currentAggregate) { 44 | addMetrics(currentAggregate.metrics, coverage[path].metrics); 45 | currentAggregate = currentAggregate.parent; 46 | } 47 | } else { 48 | current = current.children[segment]; 49 | } 50 | }); 51 | }); 52 | 53 | return root; 54 | }; 55 | 56 | export default buildCoverageTree; 57 | -------------------------------------------------------------------------------- /packages/core/src/calculatePercentage.js: -------------------------------------------------------------------------------- 1 | const calculatePercentage = ({ covered, total }) => (total > 0 ? covered / total : 1) * 100; 2 | 3 | export default calculatePercentage; 4 | -------------------------------------------------------------------------------- /packages/core/src/clsx.js: -------------------------------------------------------------------------------- 1 | const clsx = (...classes) => classes.filter(Boolean).join(' '); 2 | 3 | export default clsx; 4 | -------------------------------------------------------------------------------- /packages/core/src/coverageLevel.js: -------------------------------------------------------------------------------- 1 | export const GOOD_COVERAGE_THRESHOLD = 80; 2 | export const NORMAL_COVERAGE_THRESHOLD = 60; 3 | 4 | export const COVERAGE_QUALITY_LEVEL_BAD = 0; 5 | export const COVERAGE_QUALITY_LEVEL_NORMAL = 1; 6 | export const COVERAGE_QUALITY_LEVEL_GOOD = 2; 7 | 8 | export const resolveCoverageLevel = (coverage) => { 9 | if (coverage >= GOOD_COVERAGE_THRESHOLD) { 10 | return COVERAGE_QUALITY_LEVEL_GOOD; 11 | } else if (coverage >= NORMAL_COVERAGE_THRESHOLD) { 12 | return COVERAGE_QUALITY_LEVEL_NORMAL; 13 | } else { 14 | return COVERAGE_QUALITY_LEVEL_BAD; 15 | } 16 | }; 17 | 18 | export const resolveCoverageClass = (coverage, { good, normal, bad }) => { 19 | const level = resolveCoverageLevel(coverage); 20 | 21 | switch (level) { 22 | case COVERAGE_QUALITY_LEVEL_GOOD: return good; 23 | case COVERAGE_QUALITY_LEVEL_NORMAL: return normal; 24 | case COVERAGE_QUALITY_LEVEL_BAD: return bad; 25 | default: return null; 26 | } 27 | }; -------------------------------------------------------------------------------- /packages/core/src/parseLCOV.js: -------------------------------------------------------------------------------- 1 | import { source } from 'lcov-parse'; 2 | 3 | const parseLCOV = content => new Promise((resolve, reject) => { 4 | source(content, (error, data) => { 5 | if (error) { 6 | reject(error); 7 | } else { 8 | resolve(data); 9 | } 10 | }); 11 | }); 12 | 13 | export default parseLCOV; 14 | -------------------------------------------------------------------------------- /packages/core/src/treeNodeTypes.js: -------------------------------------------------------------------------------- 1 | export const BRANCH_NODE = 'branch'; 2 | export const LEAF_NODE = 'leaf'; 3 | -------------------------------------------------------------------------------- /packages/core/styles/baseline.less: -------------------------------------------------------------------------------- 1 | @import "colors"; 2 | @import "layout"; 3 | @import "typography"; 4 | 5 | html { 6 | font-size: @base-font-size; 7 | } 8 | 9 | body { 10 | margin: 0; 11 | padding: 0; 12 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 13 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 14 | sans-serif; 15 | -webkit-font-smoothing: antialiased; 16 | -moz-osx-font-smoothing: grayscale; 17 | width: 100%; 18 | height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | color: @color-black; 22 | } 23 | 24 | ::selection { 25 | .selection; 26 | } 27 | 28 | #root { 29 | flex-grow: 1; 30 | display: flex; 31 | flex-direction: column; 32 | } 33 | 34 | a, a:visited { 35 | color: @color-main; 36 | text-decoration: none; 37 | } 38 | 39 | a:hover { 40 | color: darken(@color-main, 20%); 41 | } 42 | 43 | table { 44 | border: none; 45 | border-spacing: 0; 46 | } 47 | 48 | thead { 49 | tr:first-child { 50 | th { 51 | padding: 0.5rem; 52 | white-space: nowrap; 53 | text-align: left; 54 | font-weight: normal; 55 | background-color: fade(@color-main, @color-fade-7); 56 | color: contrast(fade(@color-main, @color-fade-7)); 57 | 58 | &:first-child { 59 | padding-left: 1rem; 60 | border-top-left-radius: @border-radius; 61 | } 62 | 63 | &:last-child { 64 | padding-right: 1rem; 65 | border-top-right-radius: @border-radius; 66 | } 67 | } 68 | } 69 | } 70 | 71 | tbody { 72 | td { 73 | padding: 0.5rem; 74 | cursor: inherit; 75 | border-bottom: 1px solid fade(@color-black, @color-fade-0); 76 | 77 | &:first-child { 78 | border-left: 1px solid fade(@color-main, @color-fade-2); 79 | } 80 | 81 | &:last-child { 82 | border-right: 1px solid fade(@color-main, @color-fade-2); 83 | } 84 | } 85 | } 86 | 87 | tfoot { 88 | tr:first-child { 89 | td { 90 | padding: 0.5rem; 91 | background-color: fade(@color-main, @color-fade-7); 92 | 93 | &:first-child { 94 | border-bottom-left-radius: @border-radius; 95 | } 96 | 97 | &:last-child { 98 | border-bottom-right-radius: @border-radius; 99 | } 100 | } 101 | } 102 | } 103 | 104 | pre.code { 105 | .code-font; 106 | margin: 0; 107 | padding: 0; 108 | background: none; 109 | overflow: visible; 110 | 111 | ::selection { 112 | .selection; 113 | } 114 | 115 | &::selection { 116 | .selection; 117 | } 118 | } -------------------------------------------------------------------------------- /packages/core/styles/colors.less: -------------------------------------------------------------------------------- 1 | @color-fade-0: 9%; 2 | @color-fade-1: 19%; 3 | @color-fade-2: 29%; 4 | @color-fade-5: 59%; 5 | @color-fade-7: 79%; 6 | 7 | @color-black: #4a4a4a; 8 | @color-main: #c9369b; 9 | 10 | @color-bad: #d3343c; 11 | @color-normal: #d3b334; 12 | @color-good: #248f29; 13 | @color-inactive: #7f7f7f; 14 | -------------------------------------------------------------------------------- /packages/core/styles/layout.less: -------------------------------------------------------------------------------- 1 | @import "colors"; 2 | 3 | @border-radius: 2px; 4 | 5 | .shadow { 6 | box-shadow: 0 0 3px 1px fade(@color-main, @color-fade-2), 0 0 2px 0 fade(@color-main, @color-fade-5); 7 | 8 | &:hover { 9 | box-shadow: 0 0 5px 2px fade(@color-main, @color-fade-2), 0 0 1px 1px fade(@color-main, @color-fade-5); 10 | } 11 | } 12 | 13 | .selection { 14 | background: fade(@color-main, @color-fade-1); 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/styles/typography.less: -------------------------------------------------------------------------------- 1 | @base-font-size: 14px; 2 | 3 | .code-font { 4 | font-size: 1rem; 5 | line-height: 1.5; 6 | } -------------------------------------------------------------------------------- /packages/demo/tree-view/App.js: -------------------------------------------------------------------------------- 1 | import { CoverageIndicator, Footer, Summary, TreeView } from '@lcov-viewer/components'; 2 | import { buildCoverageTree } from '@lcov-viewer/core'; 3 | import React from 'react'; 4 | import sample from './sample.json'; 5 | 6 | import classes from './App.module.less'; 7 | 8 | const coverageTree = buildCoverageTree(sample); 9 | 10 | const App = () => ( 11 |
12 | 13 | All Files 14 | 15 | 16 | 17 |
18 | Report generated at {window.REPORT_DATE} 19 |
20 |
21 | ); 22 | 23 | export default App; 24 | -------------------------------------------------------------------------------- /packages/demo/tree-view/App.module.less: -------------------------------------------------------------------------------- 1 | .root { 2 | padding: 0 1rem; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | -------------------------------------------------------------------------------- /packages/demo/tree-view/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2021 Eugene Zinovyev 2 | 3 | Permission is hereby granted, 4 | free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice 12 | (including the next paragraph) shall be included in all copies or substantial 13 | portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 18 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 19 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /packages/demo/tree-view/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | LCOV Viewer Tree View 8 | 9 | 10 | 11 | 12 | 13 |
14 | 17 | 18 | -------------------------------------------------------------------------------- /packages/demo/tree-view/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import '@lcov-viewer/core/styles/baseline.less'; 4 | import App from './App'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /packages/demo/tree-view/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lcov-viewer/demo-tree-view", 3 | "version": "1.3.0", 4 | "author": "Eugene Zinovyev ", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "webpack --mode=production --node-env=production", 9 | "analyze": "webpack --mode=production --analyze", 10 | "start": "webpack serve" 11 | }, 12 | "dependencies": { 13 | "@lcov-viewer/components": "^1.3.0", 14 | "@lcov-viewer/core": "^1.3.0", 15 | "preact": "^10.8.2" 16 | }, 17 | "devDependencies": { 18 | "html-webpack-plugin": "^5.5.0", 19 | "json-loader": "^0.5.7" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/demo/tree-view/sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "components/App.js": { 3 | "metrics": { "lines": { "total": 25, "covered": 25 }, "functions": { "total": 1, "covered": 1 }, "branches": { "total": 1, "covered": 1 } }, 4 | "path": "components/App.js", 5 | "filename": "App.js", 6 | "details": { 7 | "lines": [ 8 | { "line": 1, "hits": 1, "text": "import React from 'react';" }, 9 | { "line": 2, "hits": 1, "text": "import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom';" }, 10 | { "line": 3, "hits": 1, "text": "import CoverageDataProvider from './CoverageDataProvider/CoverageDataProvider';" }, 11 | { "line": 4, "hits": 1, "text": "import LcovImport from './LcovImport/LcovImport';" }, 12 | { "line": 5, "hits": 1, "text": "import Report from './Report/Report';" }, 13 | { "line": 6, "hits": 1, "text": "" }, 14 | { "line": 7, "hits": 1, "text": "const App = () => (" }, 15 | { "line": 8, "hits": 6, "text": " " }, 16 | { "line": 9, "hits": 6, "text": " " }, 17 | { "line": 10, "hits": 6, "text": " " }, 18 | { "line": 11, "hits": 6, "text": " " }, 19 | { "line": 12, "hits": 6, "text": " " }, 20 | { "line": 13, "hits": 6, "text": " " }, 21 | { "line": 14, "hits": 6, "text": " " }, 22 | { "line": 15, "hits": 6, "text": " " }, 23 | { "line": 16, "hits": 6, "text": " " }, 24 | { "line": 17, "hits": 6, "text": " " }, 25 | { "line": 18, "hits": 1, "text": " " }, 26 | { "line": 19, "hits": 1, "text": " " }, 27 | { "line": 20, "hits": 1, "text": " " }, 28 | { "line": 21, "hits": 1, "text": " " }, 29 | { "line": 22, "hits": 1, "text": " " }, 30 | { "line": 23, "hits": 1, "text": ");" }, 31 | { "line": 24, "hits": 1, "text": "" }, 32 | { "line": 25, "hits": 1, "text": "export default App;" }, 33 | { "line": 26, "hits": 0, "text": "" } 34 | ] 35 | } 36 | }, 37 | "components/CoverageDataProvider/CoverageDataContext.js": { 38 | "metrics": { "lines": { "total": 5, "covered": 5 }, "functions": { "total": 0, "covered": 0 }, "branches": { "total": 0, "covered": 0 } }, 39 | "path": "components/CoverageDataProvider/CoverageDataContext.js", 40 | "filename": "CoverageDataContext.js", 41 | "details": { 42 | "lines": [ 43 | { "line": 1, "hits": 1, "text": "import { createContext } from 'react';" }, 44 | { "line": 2, "hits": 1, "text": "" }, 45 | { "line": 3, "hits": 1, "text": "const CoverageDataContext = createContext();" }, 46 | { "line": 4, "hits": 1, "text": "" }, 47 | { "line": 5, "hits": 1, "text": "export default CoverageDataContext;" }, 48 | { "line": 6, "hits": 0, "text": "" } 49 | ] 50 | } 51 | }, 52 | "components/CoverageDataProvider/CoverageDataProvider.js": { 53 | "metrics": { "lines": { "total": 14, "covered": 14 }, "functions": { "total": 1, "covered": 1 }, "branches": { "total": 1, "covered": 1 } }, 54 | "path": "components/CoverageDataProvider/CoverageDataProvider.js", 55 | "filename": "CoverageDataProvider.js", 56 | "details": { 57 | "lines": [ 58 | { "line": 1, "hits": 1, "text": "import React, { useRef } from 'react';" }, 59 | { "line": 2, "hits": 1, "text": "import CoverageDataContext from './CoverageDataContext';" }, 60 | { "line": 3, "hits": 1, "text": "" }, 61 | { "line": 4, "hits": 1, "text": "const CoverageDataProvider = ({ children }) => {" }, 62 | { "line": 5, "hits": 6, "text": " const value = useRef({ data: null, subscriptions: [] });" }, 63 | { "line": 6, "hits": 6, "text": "" }, 64 | { "line": 7, "hits": 6, "text": " return (" }, 65 | { "line": 8, "hits": 6, "text": " " }, 66 | { "line": 9, "hits": 6, "text": " {children}" }, 67 | { "line": 10, "hits": 6, "text": " " }, 68 | { "line": 11, "hits": 6, "text": " );" }, 69 | { "line": 12, "hits": 6, "text": "};" }, 70 | { "line": 13, "hits": 1, "text": "" }, 71 | { "line": 14, "hits": 1, "text": "export default CoverageDataProvider;" }, 72 | { "line": 15, "hits": 0, "text": "" } 73 | ] 74 | } 75 | }, 76 | "components/CoverageDataProvider/useCoverageData.js": { 77 | "metrics": { "lines": { "total": 24, "covered": 24 }, "functions": { "total": 2, "covered": 1 }, "branches": { "total": 4, "covered": 4 } }, 78 | "path": "components/CoverageDataProvider/useCoverageData.js", 79 | "filename": "useCoverageData.js", 80 | "details": { 81 | "lines": [ 82 | { "line": 1, "hits": 1, "text": "import { useContext, useEffect, useState } from 'react';" }, 83 | { "line": 2, "hits": 1, "text": "import CoverageDataContext from './CoverageDataContext';" }, 84 | { "line": 3, "hits": 1, "text": "" }, 85 | { "line": 4, "hits": 1, "text": "const useCoverageData = () => {" }, 86 | { "line": 5, "hits": 11, "text": " const [, forceUpdate] = useState();" }, 87 | { "line": 6, "hits": 11, "text": " const context = useContext(CoverageDataContext).current;" }, 88 | { "line": 7, "hits": 11, "text": "" }, 89 | { "line": 8, "hits": 11, "text": " useEffect(() => {" }, 90 | { "line": 9, "hits": 6, "text": " const notify = () => forceUpdate({});" }, 91 | { "line": 10, "hits": 6, "text": " context.subscriptions.push(notify);" }, 92 | { "line": 11, "hits": 6, "text": "" }, 93 | { "line": 12, "hits": 6, "text": " return () => {" }, 94 | { "line": 13, "hits": 6, "text": " const index = context.subscriptions.findIndex(cb => cb === notify);" }, 95 | { "line": 14, "hits": 6, "text": "" }, 96 | { "line": 15, "hits": 6, "text": " if (index > -1) {" }, 97 | { "line": 16, "hits": 6, "text": " context.subscriptions.splice(index, 1);" }, 98 | { "line": 17, "hits": 6, "text": " }" }, 99 | { "line": 18, "hits": 6, "text": " };" }, 100 | { "line": 19, "hits": 11, "text": " }, [context]);" }, 101 | { "line": 20, "hits": 11, "text": "" }, 102 | { "line": 21, "hits": 11, "text": " return context.data;" }, 103 | { "line": 22, "hits": 11, "text": "};" }, 104 | { "line": 23, "hits": 1, "text": "" }, 105 | { "line": 24, "hits": 1, "text": "export default useCoverageData;" }, 106 | { "line": 25, "hits": 0, "text": "" } 107 | ] 108 | } 109 | }, 110 | "components/CoverageDataProvider/useCoverageDataControl.js": { 111 | "metrics": { "lines": { "total": 24, "covered": 21 }, "functions": { "total": 1, "covered": 1 }, "branches": { "total": 4, "covered": 3 } }, 112 | "path": "components/CoverageDataProvider/useCoverageDataControl.js", 113 | "filename": "useCoverageDataControl.js", 114 | "details": { 115 | "lines": [ 116 | { "line": 1, "hits": 1, "text": "import { useCallback, useContext } from 'react';" }, 117 | { "line": 2, "hits": 1, "text": "import CoverageDataContext from './CoverageDataContext';" }, 118 | { "line": 3, "hits": 1, "text": "" }, 119 | { "line": 4, "hits": 1, "text": "const useCoverageDataControl = () => {" }, 120 | { "line": 5, "hits": 18, "text": " const context = useContext(CoverageDataContext).current;" }, 121 | { "line": 6, "hits": 18, "text": " const dispatch = useCallback((type, payload) => {" }, 122 | { "line": 7, "hits": 6, "text": " switch (type) {" }, 123 | { "line": 8, "hits": 6, "text": " case 'set':" }, 124 | { "line": 9, "hits": 6, "text": " context.data = payload;" }, 125 | { "line": 10, "hits": 6, "text": " context.subscriptions.forEach(cb => cb());" }, 126 | { "line": 11, "hits": 6, "text": " break;" }, 127 | { "line": 12, "hits": 6, "text": " case 'unset':" }, 128 | { "line": 13, "hits": 0, "text": " context.data = null;" }, 129 | { "line": 14, "hits": 0, "text": " context.subscriptions.forEach(cb => cb());" }, 130 | { "line": 15, "hits": 0, "text": " break;" }, 131 | { "line": 16, "hits": 6, "text": " }" }, 132 | { "line": 17, "hits": 18, "text": " }, [context]);" }, 133 | { "line": 18, "hits": 18, "text": " const set = useCallback((data) => dispatch('set', data), [context]);" }, 134 | { "line": 19, "hits": 18, "text": " const unset = useCallback(() => dispatch('unset'), [context]);" }, 135 | { "line": 20, "hits": 18, "text": "" }, 136 | { "line": 21, "hits": 18, "text": " return [set, unset];" }, 137 | { "line": 22, "hits": 18, "text": "};" }, 138 | { "line": 23, "hits": 1, "text": "" }, 139 | { "line": 24, "hits": 1, "text": "export default useCoverageDataControl;" }, 140 | { "line": 25, "hits": 0, "text": "" } 141 | ] 142 | } 143 | }, 144 | "components/LcovImport/LcovImport.js": { 145 | "metrics": { "lines": { "total": 43, "covered": 38 }, "functions": { "total": 1, "covered": 1 }, "branches": { "total": 6, "covered": 3 } }, 146 | "path": "components/LcovImport/LcovImport.js", 147 | "filename": "LcovImport.js", 148 | "details": { 149 | "lines": [ 150 | { "line": 1, "hits": 1, "text": "import clsx from 'clsx';" }, 151 | { "line": 2, "hits": 1, "text": "import React, { useCallback, useState } from 'react';" }, 152 | { "line": 3, "hits": 1, "text": "import { useDropzone } from 'react-dropzone';" }, 153 | { "line": 4, "hits": 1, "text": "import { useHistory } from 'react-router-dom';" }, 154 | { "line": 5, "hits": 1, "text": "import buildCoverage from '../../utils/buildCoverage';" }, 155 | { "line": 6, "hits": 1, "text": "import readLcov from '../../utils/readLcov';" }, 156 | { "line": 7, "hits": 1, "text": "import useCoverageDataControl from '../CoverageDataProvider/useCoverageDataControl';" }, 157 | { "line": 8, "hits": 1, "text": "import classes from './LcovImport.module.less';" }, 158 | { "line": 9, "hits": 1, "text": "" }, 159 | { "line": 10, "hits": 1, "text": "const LcovImport = () => {" }, 160 | { "line": 11, "hits": 18, "text": " const [error, setError] = useState(undefined);" }, 161 | { "line": 12, "hits": 18, "text": " const [set] = useCoverageDataControl();" }, 162 | { "line": 13, "hits": 18, "text": " const history = useHistory();" }, 163 | { "line": 14, "hits": 18, "text": " const onDrop = useCallback((files) => {" }, 164 | { "line": 15, "hits": 6, "text": " if (files.length <= 0) {" }, 165 | { "line": 16, "hits": 0, "text": " return;" }, 166 | { "line": 17, "hits": 0, "text": " }" }, 167 | { "line": 18, "hits": 6, "text": "" }, 168 | { "line": 19, "hits": 6, "text": " readLcov(files[0]).then(" }, 169 | { "line": 20, "hits": 6, "text": " (coverage) => {" }, 170 | { "line": 21, "hits": 6, "text": " set(buildCoverage(coverage));" }, 171 | { "line": 22, "hits": 6, "text": " history.push('/report');" }, 172 | { "line": 23, "hits": 6, "text": " }," }, 173 | { "line": 24, "hits": 6, "text": " ([error, msg]) => {" }, 174 | { "line": 25, "hits": 0, "text": " console.error(error);" }, 175 | { "line": 26, "hits": 0, "text": " setError(msg);" }, 176 | { "line": 27, "hits": 0, "text": " }," }, 177 | { "line": 28, "hits": 6, "text": " );" }, 178 | { "line": 29, "hits": 18, "text": " }, [set, history]);" }, 179 | { "line": 30, "hits": 18, "text": " const { isDragActive, getRootProps, getInputProps } = useDropzone({ onDrop });" }, 180 | { "line": 31, "hits": 18, "text": "" }, 181 | { "line": 32, "hits": 18, "text": " return (" }, 182 | { "line": 33, "hits": 18, "text": "
" }, 183 | { "line": 34, "hits": 18, "text": "
" }, 184 | { "line": 35, "hits": 18, "text": " " }, 185 | { "line": 36, "hits": 18, "text": " Drop your LCOV file here" }, 186 | { "line": 37, "hits": 18, "text": " {error && {error}}" }, 187 | { "line": 38, "hits": 18, "text": "
" }, 188 | { "line": 39, "hits": 18, "text": "
" }, 189 | { "line": 40, "hits": 18, "text": " );" }, 190 | { "line": 41, "hits": 18, "text": "};" }, 191 | { "line": 42, "hits": 1, "text": "" }, 192 | { "line": 43, "hits": 1, "text": "export default LcovImport;" }, 193 | { "line": 44, "hits": 0, "text": "" } 194 | ] 195 | } 196 | }, 197 | "components/LcovImport/LcovImport.module.less": { 198 | "metrics": { "lines": { "total": 36, "covered": 36 }, "functions": { "total": 1, "covered": 1 }, "branches": { "total": 2, "covered": 2 } }, 199 | "path": "components/LcovImport/LcovImport.module.less", 200 | "filename": "LcovImport.module.less", 201 | "details": { 202 | "lines": [ 203 | { "line": 1, "hits": 1, "text": ".root {" }, 204 | { "line": 2, "hits": 1, "text": " flex: 1;" }, 205 | { "line": 3, "hits": 1, "text": " display: flex;" }, 206 | { "line": 4, "hits": 1, "text": " flex-direction: column;" }, 207 | { "line": 5, "hits": 1, "text": " width: 100%;" }, 208 | { "line": 6, "hits": 1, "text": " height: 100%;" }, 209 | { "line": 7, "hits": 1, "text": " align-items: center;" }, 210 | { "line": 8, "hits": 1, "text": " justify-content: center;" }, 211 | { "line": 9, "hits": 1, "text": "}" }, 212 | { "line": 10, "hits": 1, "text": "" }, 213 | { "line": 11, "hits": 1, "text": ".dropper {" }, 214 | { "line": 12, "hits": 1, "text": " cursor: pointer;" }, 215 | { "line": 13, "hits": 1, "text": " flex: 0 1 50%;" }, 216 | { "line": 14, "hits": 1, "text": " width: 50%;" }, 217 | { "line": 15, "hits": 1, "text": " box-sizing: border-box;" }, 218 | { "line": 16, "hits": 1, "text": " box-shadow: 0 0 3px 1px rgba(211, 71, 166, 0.29), 0 0 2px 0 rgba(211, 71, 166, 0.59);" }, 219 | { "line": 17, "hits": 1, "text": " border-radius: 3px;" }, 220 | { "line": 18, "hits": 1, "text": " color: #4a4a4a;" }, 221 | { "line": 19, "hits": 1, "text": " user-select: none;" }, 222 | { "line": 20, "hits": 1, "text": " display: flex;" }, 223 | { "line": 21, "hits": 1, "text": " flex-direction: column;" }, 224 | { "line": 22, "hits": 1, "text": " align-items: center;" }, 225 | { "line": 23, "hits": 1, "text": " justify-content: center;" }, 226 | { "line": 24, "hits": 1, "text": "" }, 227 | { "line": 25, "hits": 1, "text": " &.active {" }, 228 | { "line": 26, "hits": 1, "text": " background-color: rgba(211, 71, 166, 0.09);" }, 229 | { "line": 27, "hits": 1, "text": " }" }, 230 | { "line": 28, "hits": 1, "text": "" }, 231 | { "line": 29, "hits": 1, "text": " &:hover {" }, 232 | { "line": 30, "hits": 1, "text": " box-shadow: 0 0 5px 2px rgba(211, 71, 166, 0.29), 0 0 1px 1px rgba(211, 71, 166, 0.59);" }, 233 | { "line": 31, "hits": 1, "text": " }" }, 234 | { "line": 32, "hits": 1, "text": "}" }, 235 | { "line": 33, "hits": 1, "text": "" }, 236 | { "line": 34, "hits": 1, "text": ".error {" }, 237 | { "line": 35, "hits": 1, "text": " color: #d3343c;" }, 238 | { "line": 36, "hits": 1, "text": "}" }, 239 | { "line": 37, "hits": 0, "text": "" } 240 | ] 241 | } 242 | }, 243 | "components/Report/Report.js": { 244 | "metrics": { "lines": { "total": 17, "covered": 17 }, "functions": { "total": 1, "covered": 1 }, "branches": { "total": 3, "covered": 3 } }, 245 | "path": "components/Report/Report.js", 246 | "filename": "Report.js", 247 | "details": { 248 | "lines": [ 249 | { "line": 1, "hits": 1, "text": "import TreeView from '@lcov-viewer/tree-view';" }, 250 | { "line": 2, "hits": 1, "text": "import React from 'react';" }, 251 | { "line": 3, "hits": 1, "text": "import { Redirect } from 'react-router-dom';" }, 252 | { "line": 4, "hits": 1, "text": "import useCoverageData from '../CoverageDataProvider/useCoverageData';" }, 253 | { "line": 5, "hits": 1, "text": "import classes from './Report.module.less';" }, 254 | { "line": 6, "hits": 1, "text": "" }, 255 | { "line": 7, "hits": 1, "text": "const Report = () => {" }, 256 | { "line": 8, "hits": 11, "text": " const coverage = useCoverageData();" }, 257 | { "line": 9, "hits": 11, "text": "" }, 258 | { "line": 10, "hits": 11, "text": " return (" }, 259 | { "line": 11, "hits": 11, "text": "
" }, 260 | { "line": 12, "hits": 11, "text": " {coverage ? : }" }, 261 | { "line": 13, "hits": 11, "text": "
" }, 262 | { "line": 14, "hits": 11, "text": " );" }, 263 | { "line": 15, "hits": 11, "text": "};" }, 264 | { "line": 16, "hits": 1, "text": "" }, 265 | { "line": 17, "hits": 1, "text": "export default Report;" }, 266 | { "line": 18, "hits": 0, "text": "" } 267 | ] 268 | } 269 | }, 270 | "components/Report/Report.module.less": { 271 | "metrics": { "lines": { "total": 7, "covered": 7 }, "functions": { "total": 1, "covered": 1 }, "branches": { "total": 2, "covered": 2 } }, 272 | "path": "components/Report/Report.module.less", 273 | "filename": "Report.module.less", 274 | "details": { 275 | "lines": [ 276 | { "line": 1, "hits": 1, "text": ".root {" }, 277 | { "line": 2, "hits": 1, "text": " flex: 1;" }, 278 | { "line": 3, "hits": 1, "text": " display: flex;" }, 279 | { "line": 4, "hits": 1, "text": " flex-direction: column;" }, 280 | { "line": 5, "hits": 1, "text": " overflow: auto;" }, 281 | { "line": 6, "hits": 1, "text": " padding: 16px;" }, 282 | { "line": 7, "hits": 1, "text": "}" } 283 | ] 284 | } 285 | }, 286 | "utils/buildCoverage.js": { 287 | "metrics": { "lines": { "total": 17, "covered": 12 }, "functions": { "total": 1, "covered": 1 }, "branches": { "total": 2, "covered": 2 } }, 288 | "path": "utils/buildCoverage.js", 289 | "filename": "buildCoverage.js", 290 | "details": { 291 | "lines": [ 292 | { "line": 1, "hits": 1, "text": "const buildCoverage = (records) => records.reduce((aggregate, record) => {" }, 293 | { "line": 2, "hits": 58, "text": " const path = record.file.replace(/\\\\/g, '/').replace(/^(.):/, '/$1').replace(/^\\//, '');" }, 294 | { "line": 3, "hits": 58, "text": "" }, 295 | { "line": 4, "hits": 58, "text": " aggregate[path] = {" }, 296 | { "line": 5, "hits": 58, "text": " metrics: {" }, 297 | { "line": 6, "hits": 58, "text": " lines: { total: record.lines.found, covered: record.lines.hit }," }, 298 | { "line": 7, "hits": 58, "text": " functions: { total: record.functions.found, covered: record.functions.hit }," }, 299 | { "line": 8, "hits": 58, "text": " branches: { total: record.branches.found, covered: record.branches.hit }," }, 300 | { "line": 9, "hits": 58, "text": " }," }, 301 | { "line": 10, "hits": 58, "text": " path: path," }, 302 | { "line": 11, "hits": 58, "text": " filename: path.split('/').pop()," }, 303 | { "line": 12, "hits": 58, "text": " };" }, 304 | { "line": 13, "hits": 58, "text": "" }, 305 | { "line": 14, "hits": 58, "text": " return aggregate;" }, 306 | { "line": 15, "hits": 1, "text": "}, {});" }, 307 | { "line": 16, "hits": 1, "text": "" }, 308 | { "line": 17, "hits": 1, "text": "export default buildCoverage;" }, 309 | { "line": 18, "hits": 0, "text": "" } 310 | ] 311 | } 312 | }, 313 | "utils/readLcov.js": { 314 | "metrics": { "lines": { "total": 19, "covered": 10 }, "functions": { "total": 3, "covered": 2 }, "branches": { "total": 5, "covered": 4 } }, 315 | "path": "utils/readLcov.js", 316 | "filename": "readLcov.js", 317 | "details": { 318 | "lines": [ 319 | { "line": 1, "hits": 1, "text": "import { source as parseLCOV } from 'lcov-parse';" }, 320 | { "line": 2, "hits": 1, "text": "" }, 321 | { "line": 3, "hits": 1, "text": "const readLcov = (file) => new Promise((resolve, reject) => {" }, 322 | { "line": 4, "hits": 6, "text": " const reader = new FileReader();" }, 323 | { "line": 5, "hits": 6, "text": " reader.onload = () => {" }, 324 | { "line": 6, "hits": 0, "text": " const lcovContent = reader.result;" }, 325 | { "line": 7, "hits": 0, "text": " parseLCOV(lcovContent, (error, data) => {" }, 326 | { "line": 8, "hits": 0, "text": " if (error) {" }, 327 | { "line": 9, "hits": 0, "text": " reject([error, 'Failed to parse file']);" }, 328 | { "line": 10, "hits": 0, "text": " } else {" }, 329 | { "line": 11, "hits": 0, "text": " resolve(data);" }, 330 | { "line": 12, "hits": 0, "text": " }" }, 331 | { "line": 13, "hits": 0, "text": " });" }, 332 | { "line": 14, "hits": 6, "text": " };" }, 333 | { "line": 15, "hits": 6, "text": " reader.onerror = () => reject([reader.error, 'Failed to read file']);" }, 334 | { "line": 16, "hits": 6, "text": " reader.readAsText(file);" }, 335 | { "line": 17, "hits": 6, "text": "});" }, 336 | { "line": 18, "hits": 1, "text": "" }, 337 | { "line": 19, "hits": 1, "text": "export default readLcov;" }, 338 | { "line": 20, "hits": 0, "text": "" } 339 | ] 340 | } 341 | } 342 | } -------------------------------------------------------------------------------- /packages/demo/tree-view/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const baseConfig = require('../../../webpack.base-config'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | 5 | module.exports = (env, argv) => { 6 | const config = { 7 | ...baseConfig(env, argv), 8 | entry: './index.js', 9 | output: { 10 | path: path.resolve(__dirname, 'dist'), 11 | }, 12 | }; 13 | 14 | const htmlWebpackPlugin = config.plugins.find(plugin => plugin instanceof HtmlWebpackPlugin) || new HtmlWebpackPlugin(); 15 | htmlWebpackPlugin.userOptions.reportDate = new Date().toString(); 16 | 17 | config.module.rules.push({ 18 | test: /sample\.json$/i, 19 | use: { loader: 'json-loader' }, // I gave up trying to minify asset/source 20 | type: 'javascript/auto', 21 | }); 22 | 23 | config.resolve.fallback = { 24 | fs: false, 25 | path: false, 26 | }; 27 | 28 | return config; 29 | }; 30 | -------------------------------------------------------------------------------- /packages/istanbul-report/.gitignore: -------------------------------------------------------------------------------- 1 | /lib/ -------------------------------------------------------------------------------- /packages/istanbul-report/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2021 Eugene Zinovyev 2 | 3 | Permission is hereby granted, 4 | free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice 12 | (including the next paragraph) shall be included in all copies or substantial 13 | portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 18 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 19 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /packages/istanbul-report/README.md: -------------------------------------------------------------------------------- 1 | ![build](https://github.com/eugenezinovyev/lcov-viewer/actions/workflows/main.yml/badge.svg) 2 | [![Known Vulnerabilities](https://snyk.io/test/github/eugenezinovyev/lcov-viewer/badge.svg?targetFile=packages%2Fistanbul-report%2Fpackage.json)](https://snyk.io/test/github/eugenezinovyev/lcov-viewer?targetFile=packages%2Fistanbul-report%2Fpackage.json) 3 | [![npm version](https://badge.fury.io/js/@lcov-viewer%2Fistanbul-report.svg)](https://www.npmjs.com/package/@lcov-viewer/istanbul-report) 4 | 5 | # LCOV viewer Istanbul report 6 | 7 | Istanbul grouped HTML report. Generates code coverage report grouped by directory. 8 | 9 | ## Installation and Usage 10 | 11 | Install development dependency using NPM 12 | ``` 13 | npm install -D @lcov-viewer/istanbul-report 14 | ``` 15 | OR using YARN 16 | ``` 17 | yarn add -D @lcov-viewer/istanbul-report 18 | ``` 19 | 20 | Include reporter in Jest config 21 | ```js 22 | module.exports = { 23 | // ... 24 | coverageReporters: [ 25 | '@lcov-viewer/istanbul-report' 26 | ], 27 | // ... 28 | }; 29 | ``` 30 | 31 | or specify as [nyc](https://github.com/istanbuljs/nyc) reporter 32 | ``` 33 | nyc --reporter=@lcov-viewer/istanbul-report npm run test 34 | nyc --reporter=@lcov-viewer/istanbul-report yarn test 35 | ``` 36 | 37 | # Other packages 38 | A CLI conversion package: [@lcov-viewer/cli](https://www.npmjs.com/package/@lcov-viewer/cli) 39 | 40 | 41 | ## Demo 42 | ![report](https://user-images.githubusercontent.com/1678896/139162954-4e062a3a-9776-4b15-9700-2a63dbdd58c3.gif) 43 | -------------------------------------------------------------------------------- /packages/istanbul-report/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lcov-viewer/istanbul-report", 3 | "keywords": [ 4 | "coverage", 5 | "code coverage", 6 | "istanbul", 7 | "lcov", 8 | "report" 9 | ], 10 | "version": "1.4.0", 11 | "private": false, 12 | "author": "Eugene Zinovyev ", 13 | "main": "lib/index.js", 14 | "license": "MIT", 15 | "scripts": { 16 | "build": "rollup -c" 17 | }, 18 | "dependencies": { 19 | "istanbul-lib-report": "^3.0.0" 20 | }, 21 | "devDependencies": { 22 | "@lcov-viewer/report": "^1.4.0", 23 | "@lcov-viewer/rollup-copy": "^1.3.0", 24 | "@rollup/plugin-terser": "^0.4.3", 25 | "rollup": "^3.28.0" 26 | }, 27 | "files": [ 28 | "lib/**/*.*" 29 | ], 30 | "directories": { 31 | "lib": "lib" 32 | }, 33 | "description": "Istanbul grouped HTML report. Generates code coverage report grouped by directory.", 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/eugenezinovyev/lcov-viewer.git", 37 | "directory": "packages/istanbul-report" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/istanbul-report/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import report from '@lcov-viewer/report'; 2 | import copy from '@lcov-viewer/rollup-copy'; 3 | import terser from '@rollup/plugin-terser'; 4 | 5 | export default [ 6 | { 7 | input: './src/index.js', 8 | output: { 9 | file: './lib/index.js', 10 | format: 'cjs', 11 | }, 12 | plugins: [ 13 | terser(), 14 | copy({ 15 | files: report.getAssets(), 16 | dest: './lib/assets', 17 | }), 18 | ], 19 | }, 20 | ]; -------------------------------------------------------------------------------- /packages/istanbul-report/src/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { ReportBase } = require('istanbul-lib-report'); 4 | 5 | const buildMetrics = ({ lines, functions, branches }) => ({ 6 | lines: { total: lines.total, covered: lines.covered }, 7 | functions: { total: functions.total, covered: functions.covered }, 8 | branches: { total: branches.total, covered: branches.covered }, 9 | }); 10 | 11 | const assetHeaders = { 12 | '.js': '/* eslint-disable */\n', 13 | }; 14 | 15 | class IstanbulReport extends ReportBase { 16 | constructor(opts) { 17 | super(); 18 | this.subdir = opts.subdir || 'lcov-viewer'; 19 | this.verbose = opts.verbose; 20 | this.srcDir = path.resolve(__dirname, 'assets'); 21 | } 22 | 23 | getWriter(context) { 24 | return !this.subdir ? context.writer : context.writer.writerForDir(this.subdir); 25 | } 26 | 27 | onStart(root, context) { 28 | this.writer = this.getWriter(context); 29 | this.collectedData = {}; 30 | this.reportDataWriter = this.writer.writeFile('report-data.js'); 31 | 32 | const self = this; 33 | fs.readdirSync(self.srcDir).forEach(file => { 34 | const resolvedSource = path.resolve(self.srcDir, file); 35 | const dest = `./${file}`; 36 | 37 | self.verbose && console.log(`Write asset: ${dest}`); 38 | self.writer.copyFile(resolvedSource, dest, assetHeaders[path.extname(file)]); 39 | }); 40 | } 41 | 42 | onDetail(node, context) { 43 | const qualifiedName = node.getQualifiedName(); 44 | const metrics = node.getCoverageSummary(); 45 | this.collectedData[qualifiedName] = { 46 | metrics: buildMetrics(metrics), 47 | path: qualifiedName, 48 | filename: node.getRelativeName(), 49 | details: { 50 | lines: undefined, 51 | branchCoverage: undefined, 52 | branchMap: undefined, 53 | }, 54 | }; 55 | 56 | const fileCoverage = node.getFileCoverage(); 57 | const sourceText = context.getSource(fileCoverage.path); 58 | const lineStats = fileCoverage.getLineCoverage(); 59 | 60 | this.collectedData[qualifiedName].details.branchCoverage = fileCoverage.b; 61 | this.collectedData[qualifiedName].details.branchMap = fileCoverage.branchMap; 62 | 63 | if (lineStats) { 64 | this.collectedData[qualifiedName].details.lines = sourceText.split(/\r?\n|\r/) 65 | .map((line, index) => { 66 | const lineNumber = index + 1; 67 | 68 | return ({ 69 | line: lineNumber, 70 | hits: lineStats[lineNumber.toString()] || 0, 71 | text: line, 72 | }); 73 | }); 74 | } 75 | } 76 | 77 | onEnd() { 78 | this.reportDataWriter.write(`window.REPORT_DATE = '${new Date().toString()}';`); 79 | this.reportDataWriter.write(`window.COVERAGE_DATA = ${JSON.stringify(this.collectedData)};`); 80 | this.reportDataWriter.close(); 81 | } 82 | } 83 | 84 | module.exports = IstanbulReport; 85 | -------------------------------------------------------------------------------- /packages/report/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2021 Eugene Zinovyev 2 | 3 | Permission is hereby granted, 4 | free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice 12 | (including the next paragraph) shall be included in all copies or substantial 13 | portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 18 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 19 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /packages/report/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | 4 | module.exports = { 5 | async getAssets() { 6 | const dist = path.resolve(__dirname, './dist'); 7 | const files = await fs.promises.readdir(dist); 8 | 9 | return files.map(file => path.resolve(dist, file)); 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /packages/report/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lcov-viewer/report", 3 | "version": "1.4.0", 4 | "private": false, 5 | "author": "Eugene Zinovyev ", 6 | "main": "index.js", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "webpack --mode=production --node-env=production", 10 | "analyze": "webpack --mode=production --analyze", 11 | "start": "webpack serve" 12 | }, 13 | "devDependencies": { 14 | "@lcov-viewer/components": "^1.3.0", 15 | "@lcov-viewer/core": "^1.3.0", 16 | "history": "^5.3.0", 17 | "html-inline-css-webpack-plugin": "^1.11.1", 18 | "html-webpack-plugin": "^5.5.0", 19 | "license-webpack-plugin": "^4.0.2", 20 | "nanoid": "^4.0.2", 21 | "preact": "^10.8.2", 22 | "preact-router": "^4.1.2", 23 | "prismjs": "^1.29.0", 24 | "terser-webpack-plugin": "^5.3.3" 25 | }, 26 | "description": "LCOV viewer report assets." 27 | } 28 | -------------------------------------------------------------------------------- /packages/report/src/components/App.js: -------------------------------------------------------------------------------- 1 | import { Footer } from '@lcov-viewer/components'; 2 | import { createHashHistory } from 'history'; 3 | import Router from 'preact-router'; 4 | import React from 'react'; 5 | import Details from './Details'; 6 | import Report from './Report'; 7 | import classes from './App.module.less'; 8 | 9 | const App = ({ date, coverage }) => ( 10 |
11 | 12 | 13 |
14 | 15 |
16 | Report generated at {date} 17 |
18 |
19 | ); 20 | 21 | export default App; 22 | -------------------------------------------------------------------------------- /packages/report/src/components/App.module.less: -------------------------------------------------------------------------------- 1 | .root { 2 | flex-grow: 1; 3 | display: flex; 4 | flex-direction: column; 5 | padding: 0 1rem; 6 | height: 100%; 7 | } -------------------------------------------------------------------------------- /packages/report/src/components/Details.js: -------------------------------------------------------------------------------- 1 | import { CoverageIndicator, Summary } from '@lcov-viewer/components'; 2 | import Prism from 'prismjs'; 3 | import React from 'react'; 4 | import tokenize from '../highlight/tokenize'; 5 | import TokenStream from './TokenStream'; 6 | import classes from './Details.module.less'; 7 | 8 | Prism.manual = true; 9 | Prism.disableWorkerMessageHandler = true; 10 | 11 | const Details = ({ coverage, file }) => { 12 | const { metrics, details } = coverage[file] || {}; 13 | 14 | if (!details || !details.lines || !details.lines.length) { 15 | return null; 16 | } 17 | 18 | return ( 19 |
20 | 21 | All Files 22 | / 23 | {file} 24 | 25 | 26 |
27 |
28 | {details.lines.map(({ line, hits }) => ( 29 | 0 ? classes.hit : classes.notHit)}> 30 | {line} 31 | 32 | ))} 33 |
34 |
35 | {details.lines.map(({ line, hits }) => ( 36 | 0 ? classes.hit : classes.notHit)}> 37 | {hits > 0 ? `${hits}x` : '\u00A0'} 38 | 39 | ))} 40 |
41 |
42 | {/* TODO: resolve language from file extension */} 43 |
44 |             
45 |           
46 |
47 |
48 |
49 | ); 50 | }; 51 | 52 | export default Details; 53 | -------------------------------------------------------------------------------- /packages/report/src/components/Details.module.less: -------------------------------------------------------------------------------- 1 | @import "@lcov-viewer/core/styles/layout"; 2 | @import "@lcov-viewer/core/styles/colors"; 3 | @import "@lcov-viewer/core/styles/typography"; 4 | @import 'prismjs/themes/prism.css'; 5 | 6 | :global(.token.uncovered-branch) { 7 | background-color: fade(@color-bad, @color-fade-0); 8 | outline: @color-bad solid 1px; 9 | outline-offset: 1px; 10 | border-radius: @border-radius; 11 | } 12 | 13 | .root { 14 | display: flex; 15 | flex-direction: column; 16 | flex-grow: 1; 17 | } 18 | 19 | .codeContainer { 20 | display: flex; 21 | flex-direction: row; 22 | } 23 | 24 | .lineNumbers, .hits { 25 | display: flex; 26 | flex-direction: column; 27 | 28 | span { 29 | .code-font; 30 | padding: 0 0.5rem; 31 | background: fade(@color-inactive, @color-fade-1); 32 | 33 | &.hit { 34 | background: fade(@color-good, @color-fade-1); 35 | } 36 | 37 | &.notHit { 38 | background: fade(@color-bad, @color-fade-1); 39 | } 40 | } 41 | } 42 | 43 | .code { 44 | flex-grow: 1; 45 | padding-left: 0.5rem; 46 | 47 | .pre { 48 | margin: 0; 49 | padding: 0 0.5rem; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/report/src/components/Error.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classes from './Error.module.less'; 3 | 4 | const Error = ({ children }) => ( 5 |
6 | {children} 7 |
8 | ); 9 | 10 | export default Error; 11 | -------------------------------------------------------------------------------- /packages/report/src/components/Error.module.less: -------------------------------------------------------------------------------- 1 | @import "@lcov-viewer/core/styles/colors"; 2 | 3 | .root { 4 | flex-grow: 1; 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: center; 9 | color: @color-bad; 10 | } 11 | -------------------------------------------------------------------------------- /packages/report/src/components/Report.js: -------------------------------------------------------------------------------- 1 | import { CoverageIndicator, Summary, TreeView } from '@lcov-viewer/components'; 2 | import { buildCoverageTree, LEAF_NODE } from '@lcov-viewer/core'; 3 | import React from 'react'; 4 | import Error from './Error'; 5 | import classes from './Report.module.less'; 6 | 7 | const Report = ({ coverage }) => { 8 | const tree = buildCoverageTree(coverage); 9 | const linkSelector = node => { 10 | if (node.type !== LEAF_NODE) { 11 | return null; 12 | } 13 | 14 | const { details } = coverage[node.path] || {}; 15 | 16 | return details && details.lines && details.lines.length 17 | ? `/details/${encodeURIComponent(node.path)}` 18 | : null; 19 | }; 20 | 21 | return ( 22 |
23 | 24 | All Files 25 | 26 | 27 | {coverage 28 | ? 29 | : No coverage data provided.} 30 |
31 | ); 32 | }; 33 | 34 | export default Report; 35 | -------------------------------------------------------------------------------- /packages/report/src/components/Report.module.less: -------------------------------------------------------------------------------- 1 | .root { 2 | flex-grow: 1; 3 | } -------------------------------------------------------------------------------- /packages/report/src/components/TokenStream.js: -------------------------------------------------------------------------------- 1 | import { clsx } from '@lcov-viewer/core'; 2 | import React, { Fragment } from 'react'; 3 | 4 | const buildClassName = (token) => { 5 | const aliases = token.alias ? (Array.isArray(token.alias) ? token.alias : [token.alias]) : []; 6 | 7 | return clsx('token', token.type, ...aliases); 8 | }; 9 | 10 | const TokenStream = ({ tokens }) => ( 11 | 12 | {tokens.map((token, index) => typeof token === 'string' 13 | ? token 14 | : ( 15 | 16 | {token.content && Array.isArray(token.content) ? : token.content} 17 | 18 | ))} 19 | 20 | ); 21 | 22 | export default TokenStream; 23 | -------------------------------------------------------------------------------- /packages/report/src/highlight/filterTagTokens.js: -------------------------------------------------------------------------------- 1 | export default function filterTagTokens(tokens, types) { 2 | return tokens 3 | .map(token => { 4 | if (types.includes(token.type)) { 5 | return null; 6 | } 7 | 8 | if (token.content && Array.isArray(token.content)) { 9 | token.content = filterTagTokens(token.content, types); 10 | } 11 | 12 | return token; 13 | }) 14 | .filter(token => token !== null); 15 | } 16 | -------------------------------------------------------------------------------- /packages/report/src/highlight/tokenize.js: -------------------------------------------------------------------------------- 1 | import Prism from 'prismjs'; 2 | import generateUniqueId from '../utilities/generateUniqueId'; 3 | import filterTagTokens from './filterTagTokens'; 4 | import { UncoveredBranch, UncoveredBranchEnd, UncoveredBranchStart } from './tokens'; 5 | 6 | const TagsToFilter = [ 7 | UncoveredBranchStart, 8 | UncoveredBranchEnd, 9 | ]; 10 | 11 | function createTokenize(uncoveredBranchTag, language) { 12 | const startTag = `start-${uncoveredBranchTag}`; 13 | const endTag = `end-${uncoveredBranchTag}`; 14 | const uncoveredBranch = { 15 | pattern: new RegExp(`${startTag}((?!${endTag}).)*?${endTag}`, 's'), 16 | greedy: true, 17 | inside: { 18 | [UncoveredBranchStart]: new RegExp(startTag), 19 | [UncoveredBranchEnd]: new RegExp(endTag), 20 | // we can either make them greedy and add base grammar after 21 | // OR we can remove them from token stream and run a separate highlight on the rest of the 'uncovered-branch' content 22 | // OR we can leave them as is to have no highlighting inside the 'uncovered-branch' content (the option I went with because it lloks 23 | // slightly better to have no highlighting for the 'uncovered-branch' content) 24 | }, 25 | }; 26 | 27 | uncoveredBranch.inside[UncoveredBranch] = uncoveredBranch; 28 | 29 | const grammar = { 30 | ...Prism.languages[language], 31 | [UncoveredBranch]: uncoveredBranch, 32 | }; 33 | 34 | return { 35 | startTag, 36 | endTag, 37 | tokenize: (text) => { 38 | const tokens = Prism.tokenize(text, grammar); 39 | 40 | return filterTagTokens(tokens, TagsToFilter); 41 | }, 42 | }; 43 | } 44 | 45 | // merges nested and intersecting insertions 46 | function mergeInsertions(insertions) { 47 | // sort insertions by line then by col ASC 48 | insertions.sort((a, b) => a.line !== b.line ? a.line - b.line : a.col - b.col); 49 | 50 | const mergedInsertions = []; 51 | let lastInsertion = null; 52 | let openBranches = 0; 53 | 54 | insertions.forEach(insertion => { 55 | if (!lastInsertion && insertion.type === 'start') { // just a silly check to make sure we start with 'start' tag 56 | lastInsertion = insertion; 57 | mergedInsertions.push(insertion); 58 | openBranches++; 59 | 60 | return; 61 | } 62 | 63 | if (insertion.type === 'start') { 64 | if (openBranches === 0) { 65 | mergedInsertions.push(insertion); 66 | lastInsertion = insertion; 67 | } 68 | 69 | openBranches++; 70 | } 71 | 72 | if (insertion.type === 'end') { 73 | if (openBranches === 1) { 74 | mergedInsertions.push(insertion); 75 | lastInsertion = insertion; 76 | } 77 | 78 | openBranches--; 79 | } 80 | }); 81 | 82 | // just for security, we don't want to do anything else if we don't have any insertions 83 | if (!mergedInsertions.length) { 84 | return mergedInsertions; 85 | } 86 | 87 | // another silly check to make sure we don't end with 'start' tag 88 | if (mergedInsertions[mergedInsertions.length - 1].type === 'start') { 89 | mergedInsertions.pop(); 90 | } 91 | 92 | return mergedInsertions; 93 | } 94 | 95 | function insertBranchTags(lines, branchMap, branchCoverage, startTag, endTag) { 96 | if (!branchCoverage || !branchCoverage) { 97 | return; 98 | } 99 | 100 | const insertions = []; 101 | 102 | Object.entries(branchCoverage).forEach(([branchId, hitsArray]) => { 103 | // BTW, I don't know how hitsArray can contain more than 1 element, never seen it happen. 104 | // However, I'll treat it as an array just in case. 105 | hitsArray.forEach((hits, index) => { 106 | if (hits > 0) { 107 | return; 108 | } 109 | 110 | const branch = branchMap[branchId]; 111 | const { start, end } = branch.locations[index]; 112 | const startLineIndex = start.line - 1; 113 | const endLineIndex = end.line - 1; 114 | const startCol = Math.max(start.column, 0); // sometimes start.column is less than 0 (e.g. when there is an empty line) 115 | const endCol = end.column + 1; 116 | 117 | insertions.push({ line: startLineIndex, col: startCol, tag: startTag, type: 'start' }); 118 | insertions.push({ line: endLineIndex, col: endCol, tag: endTag, type: 'end' }); 119 | }); 120 | }); 121 | 122 | // we don't want to display uncovered branches inside other uncovered branches 123 | const mergedInsertions = mergeInsertions(insertions); 124 | 125 | // sorting is important because we need to insert tags from the end of the file to the beginning 126 | // sort by line first, then by col DESC 127 | mergedInsertions 128 | .sort((a, b) => a.line !== b.line ? a.line - b.line : b.col - a.col) 129 | .forEach(({ line, col, tag }) => { 130 | const lineTextBefore = lines[line].slice(0, col); 131 | const lineTextAfter = lines[line].slice(col); 132 | 133 | lines[line] = `${lineTextBefore}${tag}${lineTextAfter}`; 134 | }); 135 | } 136 | 137 | export default function tokenize(details) { 138 | const language = 'javascript'; // TODO: get language from file extension 139 | const { branchMap, branchCoverage } = details; 140 | const lines = details.lines.map(l => l.text); // do not mess with the existing lines array 141 | const [uncoveredTag, successfullyGeneratedId] = generateUniqueId(lines); 142 | 143 | /* 144 | * if we failed to generate unique id, then something is terribly wrong with the code 145 | * and we should not try to insert tags into it 146 | */ 147 | if (!successfullyGeneratedId) { 148 | return Prism.tokenize(lines.join('\n'), Prism.languages[language]); 149 | } 150 | 151 | const { startTag, endTag, tokenize: tokenizer } = createTokenize(uncoveredTag, language); 152 | 153 | insertBranchTags(lines, branchMap, branchCoverage, startTag, endTag); 154 | 155 | return tokenizer(lines.join('\n')); 156 | } 157 | -------------------------------------------------------------------------------- /packages/report/src/highlight/tokens.js: -------------------------------------------------------------------------------- 1 | export const UncoveredBranch = 'uncovered-branch'; 2 | export const UncoveredBranchStart = `${UncoveredBranch}-start`; 3 | export const UncoveredBranchEnd = `${UncoveredBranch}-end`; 4 | -------------------------------------------------------------------------------- /packages/report/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Code coverage report by LCOV Viewer 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/report/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import '@lcov-viewer/core/styles/baseline.less'; 4 | import App from './components/App'; 5 | 6 | const date = REPORT_DATE; 7 | const coverage = COVERAGE_DATA; 8 | 9 | ReactDOM.render( 10 | , 11 | document.getElementById('root') 12 | ); 13 | -------------------------------------------------------------------------------- /packages/report/src/utilities/generateUniqueId.js: -------------------------------------------------------------------------------- 1 | import { nanoid } from 'nanoid'; 2 | 3 | /** 4 | * Generates unique id which is not present in the lines array. 5 | * If id is not unique, it will retry generating it up to retryLimit times. 6 | * If id is not unique after retryLimit retries, it will return false as second element of the array. 7 | * Uses nanoid to generate unique id, retry logic is implemented for ridiculously low chance of collision. 8 | * @param lines - array of lines of text. 9 | * @param retryLimit - how many times to retry generating unique id. Default is 3. 10 | * @returns {(string|boolean)[]} - array with unique id and boolean indicating if id was generated in less than retryLimit retries. 11 | */ 12 | function generateUniqueId(lines, retryLimit = 3) { 13 | let retries = 0; 14 | let id = nanoid(); 15 | 16 | while (retries < retryLimit && lines.find(line => line.includes(id))) { 17 | id = nanoid(); 18 | retries++; 19 | } 20 | 21 | // we need at least one dash to make it work with Prism 22 | // we also cannot have id end with dash 23 | 24 | return [`id-${id}-tag`, retries < retryLimit]; 25 | } 26 | 27 | export default generateUniqueId; 28 | -------------------------------------------------------------------------------- /packages/report/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const TerserPlugin = require('terser-webpack-plugin'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const { LicenseWebpackPlugin } = require('license-webpack-plugin'); 5 | const HTMLInlineCSSWebpackPlugin = require('html-inline-css-webpack-plugin').default; 6 | const baseConfig = require('../../webpack.base-config'); 7 | 8 | const licenseExcludedPackages = [ 9 | 'preact-compat', 10 | 'preact-hooks', 11 | ]; 12 | 13 | module.exports = (env, argv) => { 14 | const config = { 15 | ...baseConfig(env, argv), 16 | entry: { 17 | app: './src/index.js', 18 | }, 19 | output: { 20 | filename: '[name].js', 21 | path: path.resolve(__dirname, 'dist'), 22 | }, 23 | }; 24 | 25 | const htmlWebpackPlugin = config.plugins.find(plugin => plugin instanceof HtmlWebpackPlugin) || new HtmlWebpackPlugin(); 26 | htmlWebpackPlugin.userOptions.template = 'src/index.html'; 27 | htmlWebpackPlugin.userOptions.inject = 'body'; 28 | 29 | config.plugins.push(new HTMLInlineCSSWebpackPlugin()); 30 | 31 | config.plugins.push(new LicenseWebpackPlugin({ 32 | addBanner: true, 33 | perChunkOutput: false, 34 | licenseFileOverrides: { 35 | '@lcov-viewer/report': path.resolve('LICENSE'), 36 | }, 37 | excludedPackageTest: (packageName) => licenseExcludedPackages.includes(packageName), 38 | })); 39 | 40 | config.optimization.minimizer = [ 41 | ...config.optimization.minimizer, 42 | new TerserPlugin({ 43 | extractComments: false, // prevents TerserPlugin from extracting a [chunkName].js.LICENSE.txt file 44 | terserOptions: { 45 | format: { 46 | // Tell terser to remove all comments except for the banner added via LicenseWebpackPlugin. 47 | // This can be customized further to allow other types of comments to show up in the final js file as well. 48 | // See the terser documentation for format.comments options for more details. 49 | comments: (astNode, comment) => { 50 | const startsWith = comment.value.startsWith('! licenses are at '); 51 | console.log(comment.value, startsWith); 52 | return startsWith; 53 | }, 54 | }, 55 | }, 56 | }), 57 | ]; 58 | 59 | config.resolve.fallback = { 60 | fs: false, 61 | path: false, 62 | }; 63 | 64 | return config; 65 | }; 66 | -------------------------------------------------------------------------------- /packages/rollup-copy/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2021 Eugene Zinovyev 2 | 3 | Permission is hereby granted, 4 | free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice 12 | (including the next paragraph) shall be included in all copies or substantial 13 | portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 18 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 19 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /packages/rollup-copy/index.js: -------------------------------------------------------------------------------- 1 | const { promises: fs } = require('fs'); 2 | const path = require('path'); 3 | 4 | module.exports = ({ files: maybeFilesPromise, dest }) => ({ 5 | name: 'copy', 6 | async buildEnd() { 7 | await fs.mkdir(dest, { recursive: true }); 8 | const files = await Promise.resolve(maybeFilesPromise); 9 | await Promise.all(files.map(file => fs.copyFile(file, path.resolve(dest, path.basename(file))))); 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/rollup-copy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lcov-viewer/rollup-copy", 3 | "version": "1.3.0", 4 | "private": false, 5 | "author": "Eugene Zinovyev ", 6 | "main": "index.js", 7 | "license": "MIT", 8 | "description": "Rollup plugin for copying files." 9 | } 10 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | 'autoprefixer', 4 | 'postcss-flexbugs-fixes', 5 | [ 6 | 'postcss-preset-env', 7 | { 8 | 'autoprefixer': { 9 | 'flexbox': 'no-2009', 10 | }, 11 | 'stage': 3 12 | }, 13 | ], 14 | 'postcss-normalize' 15 | ], 16 | }; 17 | -------------------------------------------------------------------------------- /webpack.base-config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 5 | const JsonMinimizerPlugin = require('json-minimizer-webpack-plugin'); 6 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); 7 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); 8 | const CopyPlugin = require('copy-webpack-plugin'); 9 | 10 | module.exports = (env, argv) => { 11 | const mode = argv.mode || 'development'; 12 | const analyze = argv.analyze === true; 13 | const isProduction = mode === 'production'; 14 | return { 15 | mode, 16 | entry: './index.js', 17 | output: { 18 | path: path.resolve(__dirname, 'dist'), 19 | }, 20 | devServer: { 21 | open: true, 22 | compress: true, 23 | historyApiFallback: true, 24 | host: 'localhost', 25 | }, 26 | resolve: { 27 | alias: { 28 | 'react': 'preact/compat', 29 | 'react-dom': 'preact/compat', 30 | 'react/jsx-runtime': 'preact/jsx-runtime', 31 | }, 32 | }, 33 | plugins: [ 34 | new CleanWebpackPlugin(), 35 | new HtmlWebpackPlugin({ template: 'index.html' }), 36 | isProduction && new MiniCssExtractPlugin(), 37 | analyze && new BundleAnalyzerPlugin(), 38 | new CopyPlugin({ 39 | patterns: [ 40 | path.resolve(__dirname, 'icon-16x16.png'), 41 | path.resolve(__dirname, 'icon-32x32.png'), 42 | path.resolve(__dirname, 'icon-48x48.png'), 43 | ], 44 | }), 45 | ].filter(Boolean), 46 | optimization: { 47 | minimize: isProduction, 48 | minimizer: [ 49 | `...`, 50 | isProduction && new JsonMinimizerPlugin(), 51 | isProduction && new CssMinimizerPlugin(), 52 | ].filter(Boolean), 53 | }, 54 | module: { 55 | rules: [ 56 | { 57 | test: /\.(js|jsx|cjs|mjs)$/i, 58 | exclude: /node_modules[\\/](?!(react-dropzone)).*/, 59 | use: { 60 | loader: 'babel-loader', 61 | options: { 62 | rootMode: 'upward', 63 | }, 64 | }, 65 | }, 66 | { 67 | test: /\.(less|css)$/i, 68 | use: [ 69 | isProduction ? MiniCssExtractPlugin.loader : 'style-loader', 70 | 'css-loader', 71 | 'postcss-loader', 72 | 'less-loader', 73 | ], 74 | }, 75 | { 76 | test: /\.(eot|svg|ttf|woff|woff2|png|jpg|gif)$/i, 77 | type: 'asset', 78 | }, 79 | ], 80 | }, 81 | }; 82 | }; 83 | --------------------------------------------------------------------------------