@govtechsg/oobee 0.10.88 → 0.10.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/bump-package-version.yml +10 -1
- package/.github/workflows/docker-push-ghcr.yml +5 -1
- package/.github/workflows/publish.yml +8 -3
- package/README.md +2 -0
- package/dist/crawlers/commonCrawlerFunc.js +4 -2
- package/dist/mergeAxeResults/itemsStore.js +51 -0
- package/dist/mergeAxeResults/jsonArtifacts.js +122 -132
- package/dist/mergeAxeResults/scanPages.js +2 -2
- package/dist/mergeAxeResults/writeCsv.js +115 -96
- package/dist/mergeAxeResults.js +66 -38
- package/fb85adb0-5db6-4a09-8c80-05f030115004.txt +0 -0
- package/oobee-client-scanner.js +6914 -6754
- package/package.json +1 -1
- package/src/crawlers/commonCrawlerFunc.ts +4 -2
- package/src/mergeAxeResults/itemsStore.ts +73 -0
- package/src/mergeAxeResults/jsonArtifacts.ts +135 -138
- package/src/mergeAxeResults/scanPages.ts +2 -2
- package/src/mergeAxeResults/writeCsv.ts +129 -100
- package/src/mergeAxeResults.ts +73 -46
|
@@ -28,19 +28,28 @@ jobs:
|
|
|
28
28
|
current_version=$(node -p "require('./package.json').version")
|
|
29
29
|
echo "version=$current_version" >> "$GITHUB_OUTPUT"
|
|
30
30
|
|
|
31
|
+
- name: Install dependencies
|
|
32
|
+
run: npm ci
|
|
33
|
+
|
|
34
|
+
- name: Build
|
|
35
|
+
run: npm run build
|
|
36
|
+
|
|
31
37
|
- name: Bump patch version
|
|
32
38
|
id: bump
|
|
33
39
|
run: |
|
|
34
40
|
new_version=$(npm version patch --no-git-tag-version)
|
|
35
41
|
echo "version=$new_version" >> "$GITHUB_OUTPUT"
|
|
36
42
|
|
|
43
|
+
- name: Generate oobee-client-scanner.js
|
|
44
|
+
run: node dist/generateOobeeClientScanner.js
|
|
45
|
+
|
|
37
46
|
- name: Create branch and commit changes
|
|
38
47
|
run: |
|
|
39
48
|
BRANCH="bump/version-${{ steps.bump.outputs.version }}"
|
|
40
49
|
git config user.name "github-actions[bot]"
|
|
41
50
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
42
51
|
git checkout -b "$BRANCH"
|
|
43
|
-
git add package.json package-lock.json
|
|
52
|
+
git add package.json package-lock.json oobee-client-scanner.js
|
|
44
53
|
git commit -m "chore: bump version ${{ steps.current.outputs.version }} → ${{ steps.bump.outputs.version }}"
|
|
45
54
|
git push origin "$BRANCH"
|
|
46
55
|
echo "BRANCH=$BRANCH" >> $GITHUB_ENV
|
|
@@ -7,6 +7,8 @@ on:
|
|
|
7
7
|
description: 'Release tag for the image (e.g. 0.10.87)'
|
|
8
8
|
required: true
|
|
9
9
|
type: string
|
|
10
|
+
release:
|
|
11
|
+
types: [published]
|
|
10
12
|
|
|
11
13
|
permissions:
|
|
12
14
|
contents: read
|
|
@@ -15,6 +17,8 @@ permissions:
|
|
|
15
17
|
jobs:
|
|
16
18
|
build-and-push:
|
|
17
19
|
runs-on: ubuntu-latest
|
|
20
|
+
env:
|
|
21
|
+
RELEASE_TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || inputs.release_tag }}
|
|
18
22
|
steps:
|
|
19
23
|
- name: Checkout code
|
|
20
24
|
uses: actions/checkout@v4
|
|
@@ -43,7 +47,7 @@ jobs:
|
|
|
43
47
|
platforms: linux/amd64,linux/arm64
|
|
44
48
|
push: true
|
|
45
49
|
tags: |
|
|
46
|
-
ghcr.io/${{ steps.repo.outputs.name }}:${{
|
|
50
|
+
ghcr.io/${{ steps.repo.outputs.name }}:${{ env.RELEASE_TAG }}
|
|
47
51
|
ghcr.io/${{ steps.repo.outputs.name }}:latest
|
|
48
52
|
cache-from: type=gha
|
|
49
53
|
cache-to: type=gha,mode=max
|
|
@@ -22,13 +22,18 @@ jobs:
|
|
|
22
22
|
- run: npm run build
|
|
23
23
|
continue-on-error: false
|
|
24
24
|
|
|
25
|
-
- name:
|
|
25
|
+
- name: Generate oobee-client-scanner.js
|
|
26
|
+
run: node dist/generateOobeeClientScanner.js
|
|
27
|
+
|
|
28
|
+
- name: Create and push git tags
|
|
26
29
|
run: |
|
|
27
30
|
VERSION=$(node -p "require('./package.json').version")
|
|
28
31
|
git config user.name "github-actions[bot]"
|
|
29
32
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
30
|
-
git tag -af "
|
|
31
|
-
git push origin "
|
|
33
|
+
git tag -af "${VERSION}" -m "Version ${VERSION}"
|
|
34
|
+
git push origin "${VERSION}" --force
|
|
35
|
+
git tag -af "latest" -m "Latest release ${VERSION}"
|
|
36
|
+
git push origin "latest" --force
|
|
32
37
|
|
|
33
38
|
- run: npm publish
|
|
34
39
|
env:
|
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ This is the engine and command-line interface (CLI) for Oobee. The official appl
|
|
|
6
6
|
|
|
7
7
|
For the **user-friendly desktop application**, check out [Oobee](https://go.gov.sg/oobee).
|
|
8
8
|
|
|
9
|
+
[](https://digitalpublicgoods.net/r/oobee)
|
|
10
|
+
|
|
9
11
|
## Technology Stack
|
|
10
12
|
|
|
11
13
|
1. [Crawlee](https://crawlee.dev/)
|
|
@@ -409,8 +409,10 @@ const buildColorContrastMessage = (node) => {
|
|
|
409
409
|
data.expectedContrastRatio;
|
|
410
410
|
if (!hasContrastData)
|
|
411
411
|
return;
|
|
412
|
-
|
|
413
|
-
|
|
412
|
+
if (!data.fgColor || !data.bgColor)
|
|
413
|
+
return;
|
|
414
|
+
const fgColor = data.fgColor;
|
|
415
|
+
const bgColor = data.bgColor;
|
|
414
416
|
const contrastRatio = String(data.contrastRatio ?? 'unknown');
|
|
415
417
|
const fontSize = formatContrastFontSize(data.fontSize);
|
|
416
418
|
const fontWeight = data.fontWeight || 'normal';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import readline from 'readline';
|
|
4
|
+
export class ItemsStore {
|
|
5
|
+
constructor(storagePath) {
|
|
6
|
+
this.ensuredDirs = new Set();
|
|
7
|
+
this.basePath = path.join(storagePath, 'tmp-items');
|
|
8
|
+
}
|
|
9
|
+
sanitizeRuleId(ruleId) {
|
|
10
|
+
return ruleId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
11
|
+
}
|
|
12
|
+
getRuleFilePath(category, ruleId) {
|
|
13
|
+
return path.join(this.basePath, category, `${this.sanitizeRuleId(ruleId)}.jsonl`);
|
|
14
|
+
}
|
|
15
|
+
async ensureDir(category) {
|
|
16
|
+
const dirPath = path.join(this.basePath, category);
|
|
17
|
+
if (!this.ensuredDirs.has(dirPath)) {
|
|
18
|
+
await fs.ensureDir(dirPath);
|
|
19
|
+
this.ensuredDirs.add(dirPath);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async appendPageItems(category, ruleId, entry) {
|
|
23
|
+
await this.ensureDir(category);
|
|
24
|
+
const filePath = this.getRuleFilePath(category, ruleId);
|
|
25
|
+
const line = JSON.stringify(entry) + '\n';
|
|
26
|
+
await fs.appendFile(filePath, line, 'utf8');
|
|
27
|
+
}
|
|
28
|
+
async *readRuleItems(category, ruleId) {
|
|
29
|
+
const filePath = this.getRuleFilePath(category, ruleId);
|
|
30
|
+
if (!fs.existsSync(filePath))
|
|
31
|
+
return;
|
|
32
|
+
const fileStream = fs.createReadStream(filePath, { encoding: 'utf8' });
|
|
33
|
+
const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
34
|
+
for await (const line of rl) {
|
|
35
|
+
if (line.trim()) {
|
|
36
|
+
yield JSON.parse(line);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async readRuleItemsMap(category, ruleId) {
|
|
41
|
+
const map = new Map();
|
|
42
|
+
for await (const entry of this.readRuleItems(category, ruleId)) {
|
|
43
|
+
const key = entry.pageIndex != null ? String(entry.pageIndex) : entry.url;
|
|
44
|
+
map.set(key, entry);
|
|
45
|
+
}
|
|
46
|
+
return map;
|
|
47
|
+
}
|
|
48
|
+
async cleanup() {
|
|
49
|
+
await fs.rm(this.basePath, { recursive: true, force: true });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -67,140 +67,130 @@ function writeLargeJsonToFile(obj, filePath) {
|
|
|
67
67
|
write();
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
|
-
const writeLargeScanItemsJsonToFile = async (obj, filePath) => {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
catch (error) {
|
|
90
|
-
writeStream.destroy(error);
|
|
91
|
-
return;
|
|
70
|
+
const writeLargeScanItemsJsonToFile = async (obj, filePath, itemsStore) => {
|
|
71
|
+
const writeStream = fs.createWriteStream(filePath, { flags: 'a', encoding: 'utf8' });
|
|
72
|
+
const write = (data) => {
|
|
73
|
+
if (!writeStream.write(data)) {
|
|
74
|
+
return new Promise(resolve => writeStream.once('drain', resolve));
|
|
75
|
+
}
|
|
76
|
+
return Promise.resolve();
|
|
77
|
+
};
|
|
78
|
+
try {
|
|
79
|
+
await write('{\n');
|
|
80
|
+
const keys = Object.keys(obj);
|
|
81
|
+
for (let i = 0; i < keys.length; i++) {
|
|
82
|
+
const key = keys[i];
|
|
83
|
+
const value = obj[key];
|
|
84
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
85
|
+
await write(` "${key}": ${JSON.stringify(value)}`);
|
|
92
86
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const value = obj[key];
|
|
113
|
-
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
114
|
-
queueWrite(` "${key}": ${JSON.stringify(value)}`);
|
|
87
|
+
else {
|
|
88
|
+
await write(` "${key}": {\n`);
|
|
89
|
+
const { rules, ...otherProperties } = value;
|
|
90
|
+
const otherKeys = Object.keys(otherProperties);
|
|
91
|
+
for (let j = 0; j < otherKeys.length; j++) {
|
|
92
|
+
const propKey = otherKeys[j];
|
|
93
|
+
const propValue = otherProperties[propKey];
|
|
94
|
+
const propValueString = propValue === null ||
|
|
95
|
+
typeof propValue === 'function' ||
|
|
96
|
+
typeof propValue === 'undefined'
|
|
97
|
+
? 'null'
|
|
98
|
+
: JSON.stringify(propValue);
|
|
99
|
+
await write(` "${propKey}": ${propValueString}`);
|
|
100
|
+
if (j < otherKeys.length - 1 || (rules && rules.length >= 0)) {
|
|
101
|
+
await write(',\n');
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
await write('\n');
|
|
105
|
+
}
|
|
115
106
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
107
|
+
if (rules && Array.isArray(rules)) {
|
|
108
|
+
await write(' "rules": [\n');
|
|
109
|
+
for (let j = 0; j < rules.length; j++) {
|
|
110
|
+
const rule = rules[j];
|
|
111
|
+
await write(' {\n');
|
|
112
|
+
const { pagesAffected, ...otherRuleProperties } = rule;
|
|
113
|
+
const ruleKeys = Object.keys(otherRuleProperties);
|
|
114
|
+
for (let k = 0; k < ruleKeys.length; k++) {
|
|
115
|
+
const ruleKey = ruleKeys[k];
|
|
116
|
+
const ruleValue = otherRuleProperties[ruleKey];
|
|
117
|
+
const ruleValueString = ruleValue === null ||
|
|
118
|
+
typeof ruleValue === 'function' ||
|
|
119
|
+
typeof ruleValue === 'undefined'
|
|
120
|
+
? 'null'
|
|
121
|
+
: JSON.stringify(ruleValue);
|
|
122
|
+
await write(` "${ruleKey}": ${ruleValueString}`);
|
|
123
|
+
if (k < ruleKeys.length - 1 || pagesAffected) {
|
|
124
|
+
await write(',\n');
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
await write('\n');
|
|
128
|
+
}
|
|
131
129
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
130
|
+
if (pagesAffected && Array.isArray(pagesAffected)) {
|
|
131
|
+
// Load items from disk for this rule if itemsStore is available
|
|
132
|
+
let itemsMap = null;
|
|
133
|
+
if (itemsStore && rule.rule) {
|
|
134
|
+
itemsMap = await itemsStore.readRuleItemsMap(key, rule.rule);
|
|
135
|
+
}
|
|
136
|
+
await write(' "pagesAffected": [\n');
|
|
137
|
+
for (let p = 0; p < pagesAffected.length; p++) {
|
|
138
|
+
const page = pagesAffected[p];
|
|
139
|
+
let fullPage = page;
|
|
140
|
+
if (itemsMap) {
|
|
141
|
+
const lookupKey = page.pageIndex != null ? String(page.pageIndex) : page.url;
|
|
142
|
+
const entry = itemsMap.get(lookupKey);
|
|
143
|
+
if (entry) {
|
|
144
|
+
// Strip itemsCount to match original scanItems.json format
|
|
145
|
+
const { itemsCount: _ic, ...pageWithoutCount } = page;
|
|
146
|
+
fullPage = { ...pageWithoutCount, items: entry.items };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const pageJson = JSON.stringify(fullPage, null, 2)
|
|
150
|
+
.split('\n')
|
|
151
|
+
.map(line => ` ${line}`)
|
|
152
|
+
.join('\n');
|
|
153
|
+
await write(pageJson);
|
|
154
|
+
if (p < pagesAffected.length - 1) {
|
|
155
|
+
await write(',\n');
|
|
147
156
|
}
|
|
148
157
|
else {
|
|
149
|
-
|
|
158
|
+
await write('\n');
|
|
150
159
|
}
|
|
151
|
-
});
|
|
152
|
-
if (pagesAffected && Array.isArray(pagesAffected)) {
|
|
153
|
-
queueWrite(' "pagesAffected": [\n');
|
|
154
|
-
pagesAffected.forEach((page, p) => {
|
|
155
|
-
const pageJson = JSON.stringify(page, null, 2)
|
|
156
|
-
.split('\n')
|
|
157
|
-
.map(line => ` ${line}`)
|
|
158
|
-
.join('\n');
|
|
159
|
-
queueWrite(pageJson);
|
|
160
|
-
if (p < pagesAffected.length - 1) {
|
|
161
|
-
queueWrite(',\n');
|
|
162
|
-
}
|
|
163
|
-
else {
|
|
164
|
-
queueWrite('\n');
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
queueWrite(' ]');
|
|
168
|
-
}
|
|
169
|
-
queueWrite('\n }');
|
|
170
|
-
if (j < rules.length - 1) {
|
|
171
|
-
queueWrite(',\n');
|
|
172
|
-
}
|
|
173
|
-
else {
|
|
174
|
-
queueWrite('\n');
|
|
175
160
|
}
|
|
176
|
-
|
|
177
|
-
|
|
161
|
+
await write(' ]');
|
|
162
|
+
}
|
|
163
|
+
await write('\n }');
|
|
164
|
+
if (j < rules.length - 1) {
|
|
165
|
+
await write(',\n');
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
await write('\n');
|
|
169
|
+
}
|
|
178
170
|
}
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
if (i < keys.length - 1) {
|
|
182
|
-
queueWrite(',\n');
|
|
183
|
-
}
|
|
184
|
-
else {
|
|
185
|
-
queueWrite('\n');
|
|
171
|
+
await write(' ]');
|
|
186
172
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
checkQueueAndEnd();
|
|
198
|
-
}
|
|
199
|
-
catch (err) {
|
|
200
|
-
writeStream.destroy(err);
|
|
201
|
-
reject(err);
|
|
173
|
+
await write('\n }');
|
|
174
|
+
}
|
|
175
|
+
if (i < keys.length - 1) {
|
|
176
|
+
await write(',\n');
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
await write('\n');
|
|
180
|
+
}
|
|
202
181
|
}
|
|
203
|
-
|
|
182
|
+
await write('}\n');
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
writeStream.end();
|
|
186
|
+
await new Promise((resolve, reject) => {
|
|
187
|
+
writeStream.on('finish', () => {
|
|
188
|
+
consoleLogger.info(`JSON file written successfully: ${filePath}`);
|
|
189
|
+
resolve();
|
|
190
|
+
});
|
|
191
|
+
writeStream.on('error', reject);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
204
194
|
};
|
|
205
195
|
async function compressJsonFileStreaming(inputPath, outputPath) {
|
|
206
196
|
const readStream = fs.createReadStream(inputPath);
|
|
@@ -210,12 +200,12 @@ async function compressJsonFileStreaming(inputPath, outputPath) {
|
|
|
210
200
|
await pipeline(readStream, gzip, base64Encode, writeStream);
|
|
211
201
|
consoleLogger.info(`File successfully compressed and saved to ${outputPath}`);
|
|
212
202
|
}
|
|
213
|
-
const writeJsonFileAndCompressedJsonFile = async (data, storagePath, filename) => {
|
|
203
|
+
const writeJsonFileAndCompressedJsonFile = async (data, storagePath, filename, itemsStore) => {
|
|
214
204
|
try {
|
|
215
205
|
consoleLogger.info(`Writing JSON to ${filename}.json`);
|
|
216
206
|
const jsonFilePath = path.join(storagePath, `${filename}.json`);
|
|
217
207
|
if (filename === 'scanItems') {
|
|
218
|
-
await writeLargeScanItemsJsonToFile(data, jsonFilePath);
|
|
208
|
+
await writeLargeScanItemsJsonToFile(data, jsonFilePath, itemsStore);
|
|
219
209
|
}
|
|
220
210
|
else {
|
|
221
211
|
await writeLargeJsonToFile(data, jsonFilePath);
|
|
@@ -234,10 +224,10 @@ const writeJsonFileAndCompressedJsonFile = async (data, storagePath, filename) =
|
|
|
234
224
|
throw error;
|
|
235
225
|
}
|
|
236
226
|
};
|
|
237
|
-
const writeJsonAndBase64Files = async (allIssues, storagePath) => {
|
|
227
|
+
const writeJsonAndBase64Files = async (allIssues, storagePath, itemsStore) => {
|
|
238
228
|
const { items, ...rest } = allIssues;
|
|
239
229
|
const { jsonFilePath: scanDataJsonFilePath, base64FilePath: scanDataBase64FilePath } = await writeJsonFileAndCompressedJsonFile(rest, storagePath, 'scanData');
|
|
240
|
-
const { jsonFilePath: scanItemsJsonFilePath, base64FilePath: scanItemsBase64FilePath } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...items }, storagePath, 'scanItems');
|
|
230
|
+
const { jsonFilePath: scanItemsJsonFilePath, base64FilePath: scanItemsBase64FilePath } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...items }, storagePath, 'scanItems', itemsStore);
|
|
241
231
|
['mustFix', 'goodToFix', 'needsReview', 'passed'].forEach(category => {
|
|
242
232
|
if (items[category].rules && Array.isArray(items[category].rules)) {
|
|
243
233
|
items[category].rules.forEach(rule => {
|
|
@@ -267,22 +257,22 @@ const writeJsonAndBase64Files = async (allIssues, storagePath) => {
|
|
|
267
257
|
const { jsonFilePath: scanIssuesSummaryJsonFilePath, base64FilePath: scanIssuesSummaryBase64FilePath, } = await writeJsonFileAndCompressedJsonFile({ oobeeAppVersion: allIssues.oobeeAppVersion, ...scanIssuesSummary }, storagePath, 'scanIssuesSummary');
|
|
268
258
|
items.mustFix.rules.forEach(rule => {
|
|
269
259
|
rule.pagesAffected.forEach(page => {
|
|
270
|
-
page.itemsCount = page.items.length;
|
|
260
|
+
page.itemsCount = page.itemsCount ?? (Array.isArray(page.items) ? page.items.length : 0);
|
|
271
261
|
});
|
|
272
262
|
});
|
|
273
263
|
items.goodToFix.rules.forEach(rule => {
|
|
274
264
|
rule.pagesAffected.forEach(page => {
|
|
275
|
-
page.itemsCount = page.items.length;
|
|
265
|
+
page.itemsCount = page.itemsCount ?? (Array.isArray(page.items) ? page.items.length : 0);
|
|
276
266
|
});
|
|
277
267
|
});
|
|
278
268
|
items.needsReview.rules.forEach(rule => {
|
|
279
269
|
rule.pagesAffected.forEach(page => {
|
|
280
|
-
page.itemsCount = page.items.length;
|
|
270
|
+
page.itemsCount = page.itemsCount ?? (Array.isArray(page.items) ? page.items.length : 0);
|
|
281
271
|
});
|
|
282
272
|
});
|
|
283
273
|
items.passed.rules.forEach(rule => {
|
|
284
274
|
rule.pagesAffected.forEach(page => {
|
|
285
|
-
page.itemsCount = page.items.length;
|
|
275
|
+
page.itemsCount = page.itemsCount ?? (Array.isArray(page.items) ? page.items.length : 0);
|
|
286
276
|
});
|
|
287
277
|
});
|
|
288
278
|
items.mustFix.totalRuleIssues = items.mustFix.rules.length;
|
|
@@ -15,8 +15,8 @@ export default function populateScanPagesDetail(allIssues) {
|
|
|
15
15
|
categoryData.rules.forEach(rule => {
|
|
16
16
|
const { rule: ruleId, conformance = [] } = rule;
|
|
17
17
|
rule.pagesAffected.forEach(p => {
|
|
18
|
-
const { url, pageTitle,
|
|
19
|
-
const itemsCount = items.length;
|
|
18
|
+
const { url, pageTitle, itemsCount: storedCount, items } = p;
|
|
19
|
+
const itemsCount = storedCount ?? (Array.isArray(items) ? items.length : 0);
|
|
20
20
|
if (!pagesMap[url]) {
|
|
21
21
|
pagesMap[url] = {
|
|
22
22
|
pageTitle,
|