@actualwave/traceability-matrices
Advanced tools
Comparing version 0.0.4 to 0.0.5
48
cli.js
@@ -22,6 +22,2 @@ #!/usr/bin/env node | ||
if (!parsed) { | ||
// FIXME for testing purposes, remove before publishing | ||
console.error("Oh, come on, useless pile of garbage. Git gud, bitch."); | ||
process.exit(1); | ||
break; | ||
@@ -77,5 +73,6 @@ } | ||
/** | ||
* serve --target-dir= --port= | ||
* serve --target-dir= --port= --https=true | ||
*/ | ||
const port = args.port ? parseInt(String(args.port), 10) : DEFAULT_PORT; | ||
const useHttps = String(args.https) === "true"; | ||
@@ -86,9 +83,11 @@ if (Number.isNaN(port)) { | ||
const { serve } = require("./serve.js"); | ||
const { serve } = require("./commands/serve.js"); | ||
serve(targetDirs, port); | ||
import("open").then(({ default: open }) => | ||
open(`http://localhost:${port}`) | ||
); | ||
serve(targetDirs, port, useHttps).then(() => { | ||
import("open").then(({ default: open }) => | ||
open( | ||
useHttps ? `https://localhost:${port}` : `http://localhost:${port}` | ||
) | ||
); | ||
}); | ||
} | ||
@@ -110,8 +109,8 @@ break; | ||
fs.rmSync(outputDir, { recursive: true, force: true }); | ||
} else { | ||
exitWithError(`"${outputDir}" exists.`); | ||
} | ||
} else { | ||
fs.mkdirSync(outputDir); | ||
} | ||
const { generateStatic } = require("./generate-static.js"); | ||
const { generateStatic } = require("./commands/generate-static.js"); | ||
@@ -121,2 +120,23 @@ generateStatic(targetDirs, outputDir); | ||
break; | ||
case "threshold": | ||
{ | ||
// threshold --total=80 --per-project=40 | ||
const total = | ||
args["total"] === undefined ? 100 : parseInt(args["total"], 10); | ||
const perProject = | ||
args["per-project"] === undefined | ||
? 100 | ||
: parseInt(args["per-project"], 10); | ||
if (isNaN(total) || isNaN(perProject)) { | ||
exitWithError( | ||
"Coverage thresholds should be positive integer values between 0 and 100." | ||
); | ||
} | ||
const { threshold } = require("./commands/threshold.js"); | ||
threshold(targetDirs, total, perProject); | ||
} | ||
break; | ||
case "help": | ||
@@ -123,0 +143,0 @@ // TODO |
@@ -1,8 +0,36 @@ | ||
export declare const createProject: (projectTitle: string) => { | ||
structure: (data?: Record<string, object>, headers?: string[]) => void; | ||
export type Project = { | ||
title: String; | ||
description: string; | ||
structure: Record<string, object>; | ||
headers: string[]; | ||
records: Record<string, object>; | ||
}; | ||
export type ProjectApi = { | ||
structure: ( | ||
data?: Record<string, object>, | ||
headers?: string[] | ||
) => { | ||
add: (...path: string[]) => Record<string, object>; | ||
get: (...path: string[]) => Record<string, object>; | ||
merge: (source: Record<string, object>) => void; | ||
clone: (...path: string[]) => Record<string, object>; | ||
branch: ( | ||
path: string[], | ||
projectTitle: string, | ||
projectDescription?: string | ||
) => ProjectApi; | ||
narrow: ( | ||
path: string[], | ||
projectTitle: string, | ||
projectDescription?: string | ||
) => ProjectApi; | ||
}; | ||
headers: (headers?: string[]) => { | ||
clone: () => string[]; | ||
get: (index: number) => string; | ||
set: (index: number, title: string) => void; | ||
}; | ||
trace: (requirement: string | string[], chainFn?: () => void) => void; | ||
requirement: (requirement: string | string[]) => { | ||
requirement: (...requirement: string[]) => { | ||
describe: (title: string, ...args: any[]) => any; | ||
@@ -16,2 +44,23 @@ context: (title: string, ...args: any[]) => any; | ||
}; | ||
valueOf: () => Project; | ||
}; | ||
export declare const createProject: ( | ||
projectTitle: string, | ||
projectDescription?: string | ||
) => ProjectApi; | ||
export declare const getStructureBranch: ( | ||
structure: Record<string, object>, | ||
path: string[] | ||
) => Record<string, object>; | ||
export declare const mergeStructure: ( | ||
source: Record<string, object>, | ||
target: Record<string, object> | ||
) => void; | ||
export declare const cloneStructure: ( | ||
source: Record<string, object>, | ||
target?: Record<string, object> | ||
) => Record<string, object>; |
111
cypress.js
@@ -35,2 +35,20 @@ const projects = []; | ||
const getStructureBranch = (structure, path) => { | ||
let index = 0; | ||
let parent = structure; | ||
while (index < path.length) { | ||
const name = path[index]; | ||
if (!parent[name]) { | ||
return null; | ||
} | ||
parent = parent[name]; | ||
index++; | ||
} | ||
return parent; | ||
}; | ||
const mergeStructure = (source, target) => { | ||
@@ -46,5 +64,18 @@ Object.entries(source).forEach(([title, children]) => { | ||
export const createProject = (projectTitle) => { | ||
const cloneStructure = (source, target = {}) => { | ||
for (let name in source) { | ||
if (!target[name]) { | ||
target[name] = {}; | ||
} | ||
cloneStructure(source[name], target[name]); | ||
} | ||
return target; | ||
}; | ||
const createProject = (projectTitle, projectDescription = "") => { | ||
const project = { | ||
title: projectTitle, | ||
description: projectDescription, | ||
structure: {}, | ||
@@ -57,2 +88,10 @@ headers: [], | ||
const clone = (projectTitle, projectDescription) => ({ | ||
title: projectTitle, | ||
description: projectDescription, | ||
structure: cloneStructure(project.structure), | ||
headers: [], | ||
records: {}, | ||
}); | ||
const structure = (data, columnHeaders) => { | ||
@@ -66,2 +105,63 @@ if (data) { | ||
} | ||
function cloneProjectStructure(...path) { | ||
const branch = getStructureBranch(project.structure, path); | ||
if (!branch) { | ||
throw new Error( | ||
`Structure path [${ | ||
path.length ? `"${path.join('", "')}"` : "" | ||
}] is not available in "${project.title}"` | ||
); | ||
} | ||
return cloneStructure(branch); | ||
} | ||
return { | ||
add: (...path) => { | ||
let index = 0; | ||
let parent = project.structure; | ||
while (index < path.length) { | ||
const name = path[index]; | ||
if (!parent[name]) { | ||
parent[name] = {}; | ||
} | ||
parent = parent[name]; | ||
index++; | ||
} | ||
return parent; | ||
}, | ||
get: (...path) => getStructureBranch(project.structure, path), | ||
merge: (struct) => mergeStructure(struct, project.structure), | ||
clone: cloneProjectStructure, | ||
branch: (path, projectTitle, projectDescription) => { | ||
const subProject = createProject(projectTitle, projectDescription); | ||
subProject.structure(cloneProjectStructure(path)); | ||
return subProject; | ||
}, | ||
narrow: (path, projectTitle, projectDescription) => { | ||
const subProject = createProject(projectTitle, projectDescription); | ||
subProject.valueOf().headers = project.headers.concat(); | ||
const sourceStruct = getStructureBranch(project.structure, path); | ||
if (!sourceStruct) { | ||
throw new Error( | ||
`Structure path [${ | ||
path.length ? `"${path.join('", "')}"` : "" | ||
}] is not available in "${project.title}"` | ||
); | ||
} | ||
cloneStructure(sourceStruct, subProject.structure().add(path)); | ||
return subProject; | ||
}, | ||
}; | ||
}; | ||
@@ -75,2 +175,4 @@ | ||
return { | ||
clone: () => project.headers.concat(), | ||
get: (index) => project.headers[index], | ||
set: (index, header) => { | ||
@@ -138,2 +240,3 @@ project.headers[index] = header; | ||
return { | ||
valueOf: () => project, | ||
structure, | ||
@@ -143,2 +246,3 @@ headers, | ||
requirement, | ||
clone, | ||
}; | ||
@@ -153,1 +257,6 @@ }; | ||
}); | ||
module.exports.getStructureBranch = getStructureBranch; | ||
module.exports.mergeStructure = mergeStructure; | ||
module.exports.cloneStructure = cloneStructure; | ||
module.exports.createProject = createProject; |
{ | ||
"name": "@actualwave/traceability-matrices", | ||
"version": "0.0.4", | ||
"version": "0.0.5", | ||
"types": "cypress.d.ts", | ||
@@ -39,3 +39,11 @@ "bin": { | ||
"pug": "^3.0.2" | ||
}, | ||
"scripts": { | ||
"certs": "mkcert -key-file key.pem -cert-file cert.pem localhost 127.0.0.1", | ||
"test": "jest", | ||
"test:watch": "jest --watch" | ||
}, | ||
"devDependencies": { | ||
"jest": "^29.6.3" | ||
} | ||
} |
@@ -18,3 +18,7 @@ const { readFile } = require("fs/promises"); | ||
const addEmptyRecordsFromStructure = (structure, records, requirements = []) => { | ||
const addEmptyRecordsFromStructure = ( | ||
structure, | ||
records, | ||
requirements = [] | ||
) => { | ||
Object.entries(structure).forEach(([name, children]) => { | ||
@@ -83,10 +87,14 @@ if (isPopulated(children)) { | ||
const getStructureDepth = (source, depth = 1) => { | ||
Object.values(source).forEach((value) => { | ||
if (isPopulatedStructure(value)) { | ||
depth = Math.max(depth, getStructureDepth(value, depth + 1)); | ||
const getStructureDepth = (source, depth = 0) => { | ||
let newDepth = depth; | ||
for (key in source) { | ||
const value = source[key]; | ||
if (value && typeof value === "object") { | ||
newDepth = Math.max(newDepth, getStructureDepth(value, depth + 1)); | ||
} | ||
}); | ||
} | ||
return depth; | ||
return newDepth; | ||
}; | ||
@@ -104,2 +112,3 @@ | ||
if (global) { | ||
global.description = global.description || source.description; | ||
mergeStructure(source.structure, global.structure); | ||
@@ -109,2 +118,3 @@ } else { | ||
title: source.title, | ||
description: source.description, | ||
structure: source.structure, | ||
@@ -111,0 +121,0 @@ records: {}, |
264
README.md
# @actualwave/traceability-matrices | ||
# Work in progress | ||
Integrate requirements into e2e/integration test code and generate traceability matrices for your project. Currently it has an adapter to work with [Cypress](https://www.cypress.io/) tests. | ||
Integrate requirements into e2e/integration test code and generate traceability matrices for your project. Currently it has an adapter to work with Cypress. | ||
![One file project](https://github.com/burdiuz/traceability-matrices/blob/master/screenshots/project_a.png?raw=true) | ||
![Multi-file project](https://github.com/burdiuz/traceability-matrices/blob/master/screenshots/project_c.png?raw=true) | ||
![One file project](https://github.com/burdiuz/traceability-matrices/blob/master/screenshots/project_a.png?raw=true) | ||
## How it works | ||
Work with this project starts with placing traces of requirements within a test file. | ||
![Multi-file project](https://github.com/burdiuz/traceability-matrices/blob/master/screenshots/project_c.png?raw=true) | ||
```js | ||
it("should do something according to requirement #1", () => { | ||
project.trace("requirement #1"); | ||
expect(something).toEqual(somethingElse); | ||
}); | ||
``` | ||
Once test is finished, coverage report will be stored in a coverage folder specified in cypress config or environment variable. Stored file is a JSON file and is not suitable for viewung, to generate viewable information and actual matrices/tables user should use command `traceability-matrices generate` to generate static HTML files with reports or `traceability-matrices serve` to start local HTTP server with reports. | ||
## Installation | ||
NPM | ||
First add the package via NPM | ||
``` | ||
npm install -D @actualwave/traceability-matrices | ||
``` | ||
Yarn | ||
or Yarn | ||
``` | ||
@@ -24,10 +37,81 @@ yarn add -D @actualwave/traceability-matrices | ||
## Usage | ||
Add a script to your package.json | ||
Then configure by defining a environment variable `TRACE_RECORDS_DATA_DIR`, this could be done in cypress config file | ||
```js | ||
const { defineConfig } = require("cypress"); | ||
module.exports = defineConfig({ | ||
e2e: { | ||
setupNodeEvents(on, config) { | ||
// implement node event listeners here | ||
}, | ||
env: { | ||
// will store coverage reports in <project-root>/cypress/coverage | ||
TRACE_RECORDS_DATA_DIR: "cypress/coverage", | ||
}, | ||
}, | ||
}); | ||
``` | ||
traceability-matrices serve --target-dir=<folder with coverage reports> | ||
Also, it might be useful to add commands to package.json | ||
```json | ||
"scripts": { | ||
"tm:serve": "traceability-matrices serve --target-dir=cypress/coverage", | ||
"tm:generate": "traceability-matrices generate --target-dir=cypress/coverage --output-dir=coverage-static" | ||
}, | ||
``` | ||
## Commands | ||
This package supports multiple commands to work with generated coverage reports after test run. All commands accept required parameter `--target-dir` which points at coverage reports root folder, it is the same folder defined in `TRACE_RECORDS_DATA_DIR` environment variable. This parameter could be provided multiple times to point at multiple coverage directories. | ||
### traceability-matrices serve | ||
Run HTTP/S server with coverage reports and open in default browser. | ||
Parameters: | ||
- `--target-dir` - required, path to directory with coverage reports. | ||
- `--port` - port for HTTP/S server, 8477 by default | ||
- `--https` - set to "true"(`--https=true`) to start HTTPS server, by default starts HTTP server | ||
Example: | ||
``` | ||
traceability-matrices serve --target-dir=cypress/coverage --https=true | ||
``` | ||
### traceability-matrices generate | ||
Generate static HTML files with coverage reports. | ||
Parameters: | ||
- `--target-dir` - required, path to directory with coverage reports. | ||
- `--output-dir` - required, path to folder where generated HTML files should be stored | ||
Example: | ||
``` | ||
traceability-matrices generate --target-dir=cypress/coverage --output-dir=coverage-static | ||
``` | ||
### traceability-matrices threshold | ||
Fails(exits with an error code) if coverage thresholds weren't met. | ||
Parameters: | ||
- `--target-dir` - required, path to directory with coverage reports. | ||
- `--total` - optional, defines global coverage threshold, value can be between 0 and 100. Fails command if combined coverage of all project does not meet threshold. | ||
- `--per-project` - optional, defines coverage threshold applies to every project, value can be between 0 and 100. Fails command if at least one project does not meet threshold. | ||
Example: | ||
``` | ||
traceability-matrices threshold --target-dir=cypress/coverage --total=80 --per-project=60 | ||
``` | ||
## Cypress integration | ||
Add `TRACE_RECORDS_DATA_DIR` environment variable to cypress config that will tell where to store coverage reports | ||
Start with adding `TRACE_RECORDS_DATA_DIR` environment variable to cypress config that will tell where to store coverage reports | ||
```js | ||
@@ -45,35 +129,159 @@ const { defineConfig } = require("cypress"); | ||
and defining a project in a test file | ||
This project has a part that integrates into Cypress test files and adds traces of covered requirements. To start import it | ||
```js | ||
import { createProject } from "@actualwave/traceability-matrices/cypress"; | ||
const project = createProject("My Project"); | ||
``` | ||
Then use imported function to create a project | ||
This project provides methods to match test specs with project requriements and genrates test records that will be stored in a JSON file after test run is finished. | ||
### project.trace() | ||
To match requirements with specs engineer should place traces within specs, like | ||
```js | ||
const Project = createProject("My Project"); | ||
// test spec | ||
it("should do something according to requirement #1", () => { | ||
// trace requirement | ||
project.trace("requirement #1"); | ||
expect(something).toEqual(somethingElse); | ||
}); | ||
``` | ||
And create traces for requirements within the test specs | ||
After running this test, coverage will contain a record that spec `should do something according to requirement #1` tests `requirement #1` requirement. One spec may contain multiple requriements and traces could contain expectations or be nested. | ||
```js | ||
it("should display entry point path", () => { | ||
Project.trace("Path to App.js should be visible"); | ||
cy.get(".App-header p > code").should("contain", "src/App.js"); | ||
it("should do something according to multiple requirements", () => { | ||
project.trace("requirement #1", () => { | ||
expect(something).toEqual(somethingElse); | ||
project.trace("requirement #3", () => { | ||
expect(something).toEqual(somethingElse); | ||
project.trace("requirement #4", () => { | ||
expect(something).toEqual(somethingElse); | ||
}); | ||
}); | ||
}); | ||
project.trace("requirement #2"); | ||
expect(something).toEqual(somethingElse); | ||
}); | ||
``` | ||
When test run is finished, coverage report for it will be stored in specified folder. Coverage is generated into a JSON file, to have a human-readable format, run `serve` command. | ||
`project.trace()` could be used to generate requirements tree by providing an array of strings | ||
Using only traces will generate flat requirements structure, if you want to add categories, priorities or groups to it, sefine a structure to the project. | ||
```js | ||
import { createProject } from "@actualwave/traceability-matrices/cypress"; | ||
it("should do something according to requirement #1", () => { | ||
project.trace(["High", "General", "PRD I", "requirement #1"]); | ||
project.trace(["High", "General", "PRD I", "requirement #2"]); | ||
project.trace(["High", "General", "PRD I", "requirement #3"]); | ||
project.trace(["Low", "PRD IVa", "requirement #45"]); | ||
project.trace(["Low", "PRD IVa", "requirement #46"]); | ||
project.trace(["optional requirement #1"]); | ||
const Project = createProject("My Project"); | ||
expect(something).toEqual(somethingElse); | ||
}); | ||
``` | ||
Project.structure({ | ||
Statics: { | ||
"Add header text": {}, | ||
"Path to App.js should be visible": {}, | ||
"Add welcome text": {}, | ||
} | ||
This will generate a structure of requirements | ||
- High | ||
- General | ||
- PRD I | ||
- requirement #1 | ||
- requirement #2 | ||
- requirement #3 | ||
- Low | ||
- PRD IVa | ||
- requirement #45 | ||
- requirement #46 | ||
- optional requirement #1 | ||
Such structure also could be created using `project.structure()` method | ||
```js | ||
project.structure({ | ||
High: { | ||
General: { | ||
"PRD I": { | ||
"requirement #1": {}, | ||
"requirement #2": {}, | ||
"requirement #3": {}, | ||
}, | ||
}, | ||
}, | ||
Low: { | ||
"PRD IVa": { | ||
"requirement #45": {}, | ||
"requirement #46": {}, | ||
}, | ||
}, | ||
"optional requirement #1": {}, | ||
}); | ||
``` | ||
When using a trace, it will be matched to a leaf node of the structure with same name. | ||
and with this just use requirement name in `project.trace()` call, | ||
```js | ||
it("should do something according to requirement #1", () => { | ||
project.trace("requirement #3"); | ||
expect(something).toEqual(somethingElse); | ||
}); | ||
``` | ||
it will be matched to leaf node of structure. If requirement not found in structure, it will be added to the root of structure when coverage is generated. | ||
### project.requirement() | ||
With `project.trace()`, engineer could use `project.requirement()` and specify requirement to use in multiple places. | ||
```js | ||
const req1 = project.requirement("requirement #1"); | ||
``` | ||
Once created, it could be used to replace test lifecycle hooks like `describe()` and `it()` | ||
```js | ||
req1.describe('When someting', () => { | ||
... | ||
}); | ||
req1.it('should do', () => { | ||
... | ||
}); | ||
``` | ||
Both will record requirement being tested in these places. | ||
### project.structure() | ||
`project.structure()` used to specify category tree, ther are no specification but structure should be built only with objects. Leaf objects of this structure will be identified as testable requirementes, other branches are categories and could not be tested. | ||
```js | ||
project.structure({ | ||
High: { | ||
General: { | ||
"PRD I": { | ||
"requirement #1": {}, | ||
"requirement #2": {}, | ||
"requirement #3": {}, | ||
}, | ||
}, | ||
}, | ||
}); | ||
``` | ||
If test will contain a trace to category, that record will be added as a leaf node to the root of the structure. | ||
`project.structure()` return an object with additional methods to work with structure. | ||
- `add(...path: string[]) => Record<string, object>` - add categories/requirements to the structure if not exist | ||
- `get(...path: string[]) => Record<string, object>` - retrieve a branch of the structure | ||
- `merge(source: Record<string, object>) => void` - merge external structure into | ||
- `clone(...path: string[]) => Record<string, object>` - clone and return whole or branch of the structure | ||
- `branch(path: string[], projectTitle: string, projectDescription?: string) => ProjectApi` - create a sub-project from structure branch. sub-project will have no connection to current project and structures will be copied. | ||
- `narrow(path: string[], projectTitle: string, projectDescription?: string) => ProjectApi` - create sub-project with a structure by removing branches out of provided path. sub-project will have no connection to current project and structures will be copied. |
@@ -7,5 +7,5 @@ const { renderProject } = require("./project"); | ||
*/ | ||
const renderFile = (file, state) => { | ||
const renderFile = (file, state, links) => { | ||
const list = Object.values(file.projects).map((source) => { | ||
return renderProject(source, state); | ||
return renderProject(source, state, links); | ||
}); | ||
@@ -12,0 +12,0 @@ |
@@ -15,3 +15,3 @@ const { compile } = require("pug"); | ||
div.file | ||
a(href=\`/file?id=\${file.id}\`) #{file.specName} | ||
a(href=self.links.getFileLink(file.id)) #{file.specName} | ||
div.file-projects | ||
@@ -29,5 +29,6 @@ each project in self.listFileProjects(file) | ||
*/ | ||
const renderFiles = (state) => { | ||
const renderFiles = (state, links) => { | ||
return fileStructureTemplate({ | ||
...state, | ||
links, | ||
@@ -34,0 +35,0 @@ // TODO CACHE totals per file and project |
@@ -5,26 +5,40 @@ const { basename } = require("path"); | ||
const projectTableTemplate = compile(` | ||
const projectTableTemplate = compile( | ||
` | ||
table.project | ||
if self.projectDescription | ||
tr | ||
td.project-description(colspan=self.requirementsDepth + self.totalSpecCount + 1) !{self.projectDescription} | ||
//- Header Rows | ||
tr.file-headers | ||
th.project-title(colspan=requirementsDepth, rowspan='2') #{projectTitle} | ||
th.project-title(colspan=self.requirementsDepth, rowspan=self.projectHeaders.length ? 1 : 2) #{self.projectTitle} (#{self.coveredRequirements} / #{self.totalRequirements}) | ||
th.spec-count(rowspan='2') Spec count | ||
each file in fileHeaders | ||
each file in self.fileHeaders | ||
th.file-name(colspan=file.colspan, title=file.title) #{file.name} | ||
tr.specs-headers | ||
each spec in specHeaders | ||
if self.projectHeaders.length | ||
- var index = 0; | ||
while index < self.requirementsDepth | ||
th.header(title=self.projectHeaders[index]) #{self.projectHeaders[index]} | ||
- index++; | ||
each spec in self.specHeaders | ||
th.spec-name(title=spec.title) #{spec.name} | ||
//- Data Rows | ||
each row, index in dataRows | ||
each row, index in self.dataRows | ||
tr(class=\`result \${row.class}\`) | ||
each header in dataHeaderRows[index] | ||
th(colspan=header.colspan, rowspan=header.rowspan, class=header.class, title=header.title) #{header.name} | ||
each header in self.dataHeaderRows[index] | ||
th(colspan=header.colspan, rowspan=header.rowspan, class=header.class, title=header.title) !{header.name} | ||
each data in row.cells | ||
td(title=data.title, class=data.class) #{data.name} | ||
//- Totals | ||
tr.totals | ||
td(colspan=requirementsDepth) Project Coverage | ||
td(colspan=totalSpecCount+1) #{coveredRequirements} / #{totalRequirements} | ||
`); | ||
td(colspan=self.requirementsDepth) Project Coverage | ||
td(colspan=self.totalSpecCount + 1) #{self.coveredRequirements} / #{self.totalRequirements} | ||
`, | ||
{ self: true } | ||
); | ||
@@ -116,3 +130,5 @@ /** | ||
name: requirement.title, | ||
title: requirement.title, | ||
// if title contains HTML -- strip tags | ||
title: requirement.title.replace(/<\/?[^>]+?>/gi, ""), | ||
colspan: 1, | ||
@@ -205,3 +221,3 @@ rowspan: 1, | ||
*/ | ||
const renderProject = (project, state) => { | ||
const renderProject = (project, state, links) => { | ||
/* | ||
@@ -229,2 +245,4 @@ * build headers | ||
projectTitle: project.title, | ||
projectDescription: project.description, | ||
projectHeaders: project.headers || [], | ||
fileHeaders: horizontal.filesRow, | ||
@@ -237,2 +255,3 @@ specHeaders: horizontal.specsRow, | ||
dataHeaderRows: vertical.rows, | ||
links, | ||
}); | ||
@@ -239,0 +258,0 @@ |
@@ -10,3 +10,3 @@ const { compile } = require("pug"); | ||
span.totals #{project.requirementsCovered} / #{project.requirementsTotal} | ||
a.title(href=\`/project?id=\${encodeURIComponent(project.title)}\`) #{project.title} | ||
a.title(href=self.links.getProjectLink(project.title)) #{project.title} | ||
`, | ||
@@ -20,3 +20,3 @@ { self: true } | ||
*/ | ||
const renderProjects = (state) => { | ||
const renderProjects = (state, links) => { | ||
const list = Object.values(state.projects).map((project) => { | ||
@@ -31,5 +31,5 @@ const stats = calculateProjectStats(project); | ||
return projectsStructureTemplate({ list }); | ||
return projectsStructureTemplate({ list, links }); | ||
}; | ||
module.exports.renderProjects = renderProjects; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
57375
26
1450
286
1
6
1