diff options
Diffstat (limited to 'node_modules/istanbul-reports/lib/html-spa/src')
9 files changed, 825 insertions, 0 deletions
diff --git a/node_modules/istanbul-reports/lib/html-spa/src/fileBreadcrumbs.js b/node_modules/istanbul-reports/lib/html-spa/src/fileBreadcrumbs.js new file mode 100644 index 0000000..b123d37 --- /dev/null +++ b/node_modules/istanbul-reports/lib/html-spa/src/fileBreadcrumbs.js @@ -0,0 +1,31 @@ +const React = require('react'); + +module.exports = function FileBreadcrumbs({ fileFilter = '', setFileFilter }) { + const parts = fileFilter.split('/'); + const breadcrumbs = [ + { + path: '', + name: 'all files' + }, + ...parts.map((part, i) => ({ + path: parts.slice(0, i + 1).join('/'), + name: part + })) + ]; + + return breadcrumbs.map(({ path, name }) => + path === fileFilter ? ( + name + ) : ( + <> + <a + href="javascript:void(0)" + onClick={() => setFileFilter(path)} + > + {name} + </a> + / + </> + ) + ); +}; diff --git a/node_modules/istanbul-reports/lib/html-spa/src/filterToggle.js b/node_modules/istanbul-reports/lib/html-spa/src/filterToggle.js new file mode 100644 index 0000000..b636b53 --- /dev/null +++ b/node_modules/istanbul-reports/lib/html-spa/src/filterToggle.js @@ -0,0 +1,50 @@ +const React = require('react'); + +function ToggleOption({ children, filter, activeFilters, setFilters }) { + return ( + <button + className={ + 'toggle__option ' + (activeFilters[filter] ? 'is-toggled' : '') + } + onClick={() => + setFilters({ + ...activeFilters, + [filter]: !activeFilters[filter] + }) + } + > + {children} + </button> + ); +} + +module.exports = function FilterToggle({ activeFilters, setFilters }) { + return ( + <div className="toggle"> + <div className="toggle__label">Filter:</div> + <div className="toggle__options"> + <ToggleOption + filter="low" + activeFilters={activeFilters} + setFilters={setFilters} + > + Low + </ToggleOption> + <ToggleOption + filter="medium" + activeFilters={activeFilters} + setFilters={setFilters} + > + Medium + </ToggleOption> + <ToggleOption + filter="high" + activeFilters={activeFilters} + setFilters={setFilters} + > + High + </ToggleOption> + </div> + </div> + ); +}; diff --git a/node_modules/istanbul-reports/lib/html-spa/src/flattenToggle.js b/node_modules/istanbul-reports/lib/html-spa/src/flattenToggle.js new file mode 100644 index 0000000..8bb6ea3 --- /dev/null +++ b/node_modules/istanbul-reports/lib/html-spa/src/flattenToggle.js @@ -0,0 +1,25 @@ +const React = require('react'); + +module.exports = function FlattenButton({ setIsFlat, isFlat }) { + return ( + <div className="toggle"> + <div className="toggle__label">Files:</div> + <div className="toggle__options"> + <button + onClick={() => setIsFlat(!isFlat)} + className={ + 'toggle__option ' + (!isFlat ? 'is-toggled' : '') + } + > + Tree + </button> + <button + onClick={() => setIsFlat(!isFlat)} + className={'toggle__option ' + (isFlat ? 'is-toggled' : '')} + > + Flat + </button> + </div>{' '} + </div> + ); +}; diff --git a/node_modules/istanbul-reports/lib/html-spa/src/getChildData.js b/node_modules/istanbul-reports/lib/html-spa/src/getChildData.js new file mode 100644 index 0000000..eff5d31 --- /dev/null +++ b/node_modules/istanbul-reports/lib/html-spa/src/getChildData.js @@ -0,0 +1,155 @@ +function addPath(node, parentPath) { + if (!parentPath) { + return node; + } + return { ...node, file: parentPath + '/' + node.file }; +} + +function flatten(nodes, parentPath) { + let children = []; + for (let i = 0; i < nodes.length; i++) { + const child = nodes[i]; + if (child.children) { + children = [ + ...children, + ...flatten( + child.children, + (parentPath ? parentPath + '/' : '') + child.file + ) + ]; + } else { + children.push(addPath(child, parentPath)); + } + } + return children; +} + +function filterByFile(nodes, fileFilter, parentPath) { + let children = []; + + for (let i = 0; i < nodes.length; i++) { + const child = nodes[i]; + const childFullPath = (parentPath ? parentPath + '/' : '') + child.file; + + const isChildUnderFilter = + fileFilter === childFullPath || + fileFilter.indexOf(childFullPath + '/') === 0; + const isChildAboveFilter = + childFullPath.indexOf(fileFilter + '/') === 0; + + if (isChildUnderFilter) { + // flatten and continue looking underneath + children = [ + ...children, + ...filterByFile(child.children, fileFilter, childFullPath) + ]; + } else if (isChildAboveFilter) { + // remove the parent path and add everything underneath + const charsToRemoveFromFile = + fileFilter.length - (parentPath ? parentPath.length : 0); + let childFilename = child.file.slice(charsToRemoveFromFile); + if (childFilename[0] === '/') { + childFilename = childFilename.slice(1); + } + children.push({ + ...child, + file: childFilename + }); + } + } + return children; +} + +function sort(childData, activeSort) { + const top = activeSort.order === 'asc' ? 1 : -1; + const bottom = activeSort.order === 'asc' ? -1 : 1; + childData.sort((a, b) => { + let valueA; + let valueB; + if (activeSort.sortKey === 'file') { + valueA = a.file; + valueB = b.file; + } else { + const [metricType, valueType] = activeSort.sortKey.split('.'); + valueA = a.metrics[metricType][valueType]; + valueB = b.metrics[metricType][valueType]; + } + + if (valueA === valueB) { + return 0; + } + return valueA < valueB ? top : bottom; + }); + + for (let i = 0; i < childData.length; i++) { + const child = childData[i]; + if (child.children) { + childData[i] = { + ...child, + children: sort(child.children, activeSort) + }; + } + } + return childData; +} + +function filter(nodes, metricsMap, activeFilters) { + const children = []; + for (let i = 0; i < nodes.length; i++) { + let child = nodes[i]; + if (child.children) { + const newSubChildren = filter( + child.children, + metricsMap, + activeFilters + ); + if (newSubChildren.length) { + child = { ...child, children: newSubChildren }; + children.push(child); + } + } else { + if ( + (metricsMap.statements && + activeFilters[child.metrics.statements.classForPercent]) || + (metricsMap.branches && + activeFilters[child.metrics.branches.classForPercent]) || + (metricsMap.functions && + activeFilters[child.metrics.functions.classForPercent]) || + (metricsMap.lines && + activeFilters[child.metrics.lines.classForPercent]) + ) { + children.push(child); + } + } + } + return children; +} + +module.exports = function getChildData( + sourceData, + metricsToShow, + activeSort, + isFlat, + activeFilters, + fileFilter +) { + let childData = sourceData.children; + + if (isFlat) { + childData = flatten(childData.slice(0)); + } + + if (fileFilter) { + childData = filterByFile(childData, fileFilter); + } + + if (activeFilters.low) { + activeFilters = { ...activeFilters, empty: true }; + } + childData = filter(childData, metricsToShow, activeFilters); + + if (activeSort) { + childData = sort(childData, activeSort); + } + return childData; +}; diff --git a/node_modules/istanbul-reports/lib/html-spa/src/index.js b/node_modules/istanbul-reports/lib/html-spa/src/index.js new file mode 100644 index 0000000..c89c416 --- /dev/null +++ b/node_modules/istanbul-reports/lib/html-spa/src/index.js @@ -0,0 +1,160 @@ +// The index file for the spa running on the summary page +const React = require('react'); +const ReactDOM = require('react-dom'); +const SummaryTableHeader = require('./summaryTableHeader'); +const SummaryTableLine = require('./summaryTableLine'); +const SummaryHeader = require('./summaryHeader'); +const getChildData = require('./getChildData'); +const FlattenToggle = require('./flattenToggle'); +const FilterToggle = require('./filterToggle'); +const FileBreadcrumbs = require('./fileBreadcrumbs'); +const { setLocation, decodeLocation } = require('./routing'); + +const { useState, useMemo, useEffect } = React; + +const sourceData = window.data; +const metricsToShow = {}; +for (let i = 0; i < window.metricsToShow.length; i++) { + metricsToShow[window.metricsToShow[i]] = true; +} + +let firstMount = true; + +function App() { + const routingDefaults = decodeLocation(); + + const [activeSort, setSort] = useState( + (routingDefaults && routingDefaults.activeSort) || { + sortKey: 'file', + order: 'desc' + } + ); + const [isFlat, setIsFlat] = useState( + (routingDefaults && routingDefaults.isFlat) || false + ); + const [activeFilters, setFilters] = useState( + (routingDefaults && routingDefaults.activeFilters) || { + low: true, + medium: true, + high: true + } + ); + const [expandedLines, setExpandedLines] = useState( + (routingDefaults && routingDefaults.expandedLines) || [] + ); + const [fileFilter, setFileFilter] = useState( + (routingDefaults && routingDefaults.fileFilter) || '' + ); + const childData = useMemo( + () => + getChildData( + sourceData, + metricsToShow, + activeSort, + isFlat, + activeFilters, + fileFilter + ), + [activeSort, isFlat, activeFilters, fileFilter] + ); + const overallMetrics = sourceData.metrics; + + useEffect(() => { + setLocation( + firstMount, + activeSort, + isFlat, + activeFilters, + fileFilter, + expandedLines + ); + firstMount = false; + }, [activeSort, isFlat, activeFilters, fileFilter, expandedLines]); + + useEffect(() => { + window.onpopstate = () => { + const routingState = decodeLocation(); + if (routingState) { + // make sure all the state is set before rendering to avoid url updates + // alternative is to merge all the states into one so it can be set in one go + // https://github.com/facebook/react/issues/14259 + ReactDOM.unstable_batchedUpdates(() => { + setFilters(routingState.activeFilters); + setSort(routingState.activeSort); + setIsFlat(routingState.isFlat); + setExpandedLines(routingState.expandedLines); + setFileFilter(routingState.fileFilter); + }); + } + }; + }, []); + + return ( + <div className="layout"> + <div className="layout__section"> + <SummaryHeader + metrics={overallMetrics} + metricsToShow={metricsToShow} + /> + </div> + <div className="layout__section"> + <div className="toolbar"> + <div className="toolbar__item"> + <FlattenToggle setIsFlat={setIsFlat} isFlat={isFlat} /> + </div> + <div className="toolbar__item"> + <FilterToggle + activeFilters={activeFilters} + setFilters={setFilters} + /> + </div> + </div> + </div> + <div className="layout__section"> + <h1> + <FileBreadcrumbs + fileFilter={fileFilter} + setFileFilter={setFileFilter} + /> + </h1> + </div> + <div className="layout__section layout__section--fill"> + <table className="coverage-summary"> + <SummaryTableHeader + onSort={newSort => { + setSort(newSort); + }} + activeSort={activeSort} + metricsToShow={metricsToShow} + /> + <tbody> + {childData.map(child => ( + <SummaryTableLine + {...child} + key={child.file} + metricsToShow={metricsToShow} + expandedLines={expandedLines} + setExpandedLines={setExpandedLines} + fileFilter={fileFilter} + setFileFilter={setFileFilter} + /> + ))} + </tbody> + </table> + </div> + <div className="layout__section center small quiet"> + Code coverage generated by{' '} + <a + href="https://istanbul.js.org/" + target="_blank" + rel="noopener noreferrer" + > + istanbul + </a>{' '} + at {window.generatedDatetime} + </div> + </div> + ); +} + +ReactDOM.render(<App />, document.getElementById('app')); diff --git a/node_modules/istanbul-reports/lib/html-spa/src/routing.js b/node_modules/istanbul-reports/lib/html-spa/src/routing.js new file mode 100644 index 0000000..a4d0bc7 --- /dev/null +++ b/node_modules/istanbul-reports/lib/html-spa/src/routing.js @@ -0,0 +1,52 @@ +exports.setLocation = function setLocation( + isReplace, + activeSort, + isFlat, + activeFilters, + fileFilter, + expandedLines +) { + const params = [ + activeSort.sortKey, + activeSort.order, + isFlat, + activeFilters.low, + activeFilters.medium, + activeFilters.high, + encodeURIComponent(fileFilter), + expandedLines.map(encodeURIComponent).join(',') + ]; + const newUrl = `#${params.join('/')}`; + + if (newUrl === location.hash) { + return; + } + + window.history[isReplace ? 'replaceState' : 'pushState'](null, '', newUrl); +}; + +exports.decodeLocation = function decodeLocation() { + const items = location.hash.substr(1).split('/'); + if (items.length !== 8) { + return null; + } + + try { + return { + activeSort: { + sortKey: items[0], + order: items[1] + }, + isFlat: JSON.parse(items[2]), + activeFilters: { + low: JSON.parse(items[3]), + medium: JSON.parse(items[4]), + high: JSON.parse(items[5]) + }, + fileFilter: decodeURIComponent(items[6]), + expandedLines: items[7].split(',').map(decodeURIComponent) + }; + } catch (e) { + return null; + } +}; diff --git a/node_modules/istanbul-reports/lib/html-spa/src/summaryHeader.js b/node_modules/istanbul-reports/lib/html-spa/src/summaryHeader.js new file mode 100644 index 0000000..3bdd8ff --- /dev/null +++ b/node_modules/istanbul-reports/lib/html-spa/src/summaryHeader.js @@ -0,0 +1,63 @@ +const React = require('react'); + +function Ignores({ metrics, metricsToShow }) { + const metricKeys = Object.keys(metricsToShow); + const result = []; + + for (let i = 0; i < metricKeys.length; i++) { + const metricKey = metricKeys[i]; + if (metricsToShow[metricKey]) { + const skipped = metrics[metricKey].skipped; + if (skipped > 0) { + result.push( + `${skipped} ${metricKey}${ + skipped === 1 ? '' : metricKey === 'branch' ? 'es' : 's' + }` + ); + } + } + } + + if (result.length === 0) { + return false; + } + + return ( + <div className="toolbar__item"> + <span className="strong">{result.join(', ')}</span> + <span className="quiet">Ignored</span> + </div> + ); +} + +function StatusMetric({ data, name }) { + return ( + <div className="toolbar__item"> + <span className="strong">{data.pct}%</span>{' '} + <span className="quiet">{name}</span>{' '} + <span className={'fraction ' + data.classForPercent}> + {data.covered}/{data.total} + </span> + </div> + ); +} + +module.exports = function SummaryHeader({ metrics, metricsToShow }) { + return ( + <div className="toolbar"> + {metricsToShow.statements && ( + <StatusMetric data={metrics.statements} name="Statements" /> + )} + {metricsToShow.branches && ( + <StatusMetric data={metrics.branches} name="Branches" /> + )} + {metricsToShow.functions && ( + <StatusMetric data={metrics.functions} name="Functions" /> + )} + {metricsToShow.lines && ( + <StatusMetric data={metrics.lines} name="Lines" /> + )} + <Ignores metrics={metrics} metricsToShow={metricsToShow} /> + </div> + ); +}; diff --git a/node_modules/istanbul-reports/lib/html-spa/src/summaryTableHeader.js b/node_modules/istanbul-reports/lib/html-spa/src/summaryTableHeader.js new file mode 100644 index 0000000..7cf964e --- /dev/null +++ b/node_modules/istanbul-reports/lib/html-spa/src/summaryTableHeader.js @@ -0,0 +1,130 @@ +const React = require('react'); + +function getSortDetails(sortKey, activeSort) { + let newSort = { sortKey, order: 'desc' }; + let sortClass = ''; + if (activeSort && activeSort.sortKey === sortKey) { + sortClass = 'sorted'; + if (activeSort.order === 'desc') { + sortClass += '-desc'; + newSort.order = 'asc'; + } else { + if (sortKey !== 'file') { + newSort = { sortKey: 'file', order: 'desc' }; + } + } + } + + return { + newSort, + sortClass + }; +} + +function SummaryTableHeaderCell({ name, onSort, sortKey, activeSort }) { + const { newSort, sortClass } = getSortDetails(sortKey, activeSort); + return ( + <th + className={'sortable headercell ' + sortClass} + onClick={() => onSort(newSort)} + > + {name} + <span className="sorter" /> + </th> + ); +} + +function FileHeaderCell({ onSort, activeSort }) { + const { newSort, sortClass } = getSortDetails('file', activeSort); + + return ( + <th + className={'sortable file ' + sortClass} + onClick={() => onSort(newSort)} + > + File + <span className="sorter" /> + </th> + ); +} + +function SubHeadings({ sortKeyPrefix, onSort, activeSort }) { + return ( + <> + <SummaryTableHeaderCell + name="%" + onSort={onSort} + sortKey={sortKeyPrefix + '.pct'} + activeSort={activeSort} + /> + <th className="headercell"></th> + <SummaryTableHeaderCell + name="Covered" + onSort={onSort} + sortKey={sortKeyPrefix + '.covered'} + activeSort={activeSort} + /> + <SummaryTableHeaderCell + name="Missed" + onSort={onSort} + sortKey={sortKeyPrefix + '.missed'} + activeSort={activeSort} + /> + <SummaryTableHeaderCell + name="Total" + onSort={onSort} + sortKey={sortKeyPrefix + '.total'} + activeSort={activeSort} + /> + </> + ); +} + +module.exports = function SummaryTableHeader({ + onSort, + activeSort, + metricsToShow +}) { + return ( + <thead> + <tr className="topheading"> + <th></th> + {metricsToShow.statements && <th colSpan={4}>Statements</th>} + {metricsToShow.branches && <th colSpan={4}>Branches</th>} + {metricsToShow.functions && <th colSpan={4}>Functions</th>} + {metricsToShow.lines && <th colSpan={4}>Lines</th>} + </tr> + <tr className="subheading"> + <FileHeaderCell onSort={onSort} activeSort={activeSort} /> + {metricsToShow.statements && ( + <SubHeadings + sortKeyPrefix="statements" + onSort={onSort} + activeSort={activeSort} + /> + )} + {metricsToShow.branches && ( + <SubHeadings + sortKeyPrefix="branches" + onSort={onSort} + activeSort={activeSort} + /> + )} + {metricsToShow.functions && ( + <SubHeadings + sortKeyPrefix="functions" + onSort={onSort} + activeSort={activeSort} + /> + )} + {metricsToShow.lines && ( + <SubHeadings + sortKeyPrefix="lines" + onSort={onSort} + activeSort={activeSort} + /> + )} + </tr> + </thead> + ); +}; diff --git a/node_modules/istanbul-reports/lib/html-spa/src/summaryTableLine.js b/node_modules/istanbul-reports/lib/html-spa/src/summaryTableLine.js new file mode 100644 index 0000000..6dc56e6 --- /dev/null +++ b/node_modules/istanbul-reports/lib/html-spa/src/summaryTableLine.js @@ -0,0 +1,159 @@ +const React = require('react'); + +function MetricCells({ metrics }) { + const { classForPercent, pct, covered, missed, total } = metrics; + + return ( + <> + <td className={'pct ' + classForPercent}>{Math.round(pct)}% </td> + <td className={classForPercent}> + <div className="bar"> + <div + className={`bar__data ${classForPercent} ${classForPercent}--dark`} + style={{ width: pct + '%' }} + ></div> + </div> + </td> + <td className={'abs ' + classForPercent}>{covered}</td> + <td className={'abs ' + classForPercent}>{missed}</td> + <td className={'abs ' + classForPercent}>{total}</td> + </> + ); +} + +function FileCell({ + file, + prefix, + expandedLines, + setExpandedLines, + hasChildren, + setFileFilter +}) { + if (hasChildren) { + const expandedIndex = expandedLines.indexOf(prefix + file); + const isExpanded = expandedIndex >= 0; + const newExpandedLines = isExpanded + ? [ + ...expandedLines.slice(0, expandedIndex), + ...expandedLines.slice(expandedIndex + 1) + ] + : [...expandedLines, prefix + file]; + + return ( + <> + <button + type="button" + onClick={() => setExpandedLines(newExpandedLines)} + className="expandbutton" + > + {isExpanded ? String.fromCharCode(0x2013) : '+'} + </button> + <a + href="javascript:void(0)" + onClick={() => setFileFilter(prefix + file)} + > + {file} + </a> + </> + ); + } else { + return <a href={`./${prefix}${file}.html`}>{file}</a>; + } +} + +function getWorstMetricClassForPercent(metricsToShow, metrics) { + let classForPercent = 'none'; + for (const metricToShow in metricsToShow) { + if (metricsToShow[metricToShow]) { + const metricClassForPercent = metrics[metricToShow].classForPercent; + + // ignore none metrics so they don't change whats shown + if (metricClassForPercent === 'none') { + continue; + } + + // if the metric low or lower than whats currently being used, replace it + if ( + metricClassForPercent == 'low' || + (metricClassForPercent === 'medium' && + classForPercent !== 'low') || + (metricClassForPercent === 'high' && + classForPercent !== 'low' && + classForPercent !== 'medium') + ) { + classForPercent = metricClassForPercent; + } + } + } + return classForPercent; +} + +module.exports = function SummaryTableLine({ + prefix, + metrics, + file, + children, + tabSize, + metricsToShow, + expandedLines, + setExpandedLines, + fileFilter, + setFileFilter +}) { + tabSize = tabSize || 0; + if (children && tabSize > 0) { + tabSize--; + } + prefix = (fileFilter ? fileFilter + '/' : '') + (prefix || ''); + + return ( + <> + <tr> + <td + className={ + 'file ' + + getWorstMetricClassForPercent(metricsToShow, metrics) + } + > + {/* eslint-disable-line prefer-spread */ Array.apply(null, { + length: tabSize + }).map((nothing, index) => ( + <span className="filetab" key={index} /> + ))} + <FileCell + file={file} + prefix={prefix} + expandedLines={expandedLines} + setExpandedLines={setExpandedLines} + hasChildren={Boolean(children)} + setFileFilter={setFileFilter} + /> + </td> + {metricsToShow.statements && ( + <MetricCells metrics={metrics.statements} /> + )} + {metricsToShow.branches && ( + <MetricCells metrics={metrics.branches} /> + )} + {metricsToShow.functions && ( + <MetricCells metrics={metrics.functions} /> + )} + {metricsToShow.lines && <MetricCells metrics={metrics.lines} />} + </tr> + {children && + expandedLines.indexOf(prefix + file) >= 0 && + children.map(child => ( + <SummaryTableLine + {...child} + tabSize={tabSize + 2} + key={child.file} + prefix={prefix + file + '/'} + metricsToShow={metricsToShow} + expandedLines={expandedLines} + setExpandedLines={setExpandedLines} + setFileFilter={setFileFilter} + /> + ))} + </> + ); +}; |