@govtechsg/oobee 0.10.92 → 0.10.94
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/AGENTS.md +34 -0
- package/README.md +19 -0
- package/dist/cli.js +3 -2
- package/dist/combine.js +4 -4
- package/dist/constants/common.js +136 -49
- package/dist/crawlers/commonCrawlerFunc.js +54 -2
- package/dist/crawlers/crawlDomain.js +9 -2
- package/dist/crawlers/crawlIntelligentSitemap.js +9 -4
- package/dist/crawlers/crawlSitemap.js +14 -2
- package/dist/crawlers/custom/utils.js +22 -9
- package/dist/crawlers/guards/urlGuard.js +19 -1
- package/dist/crawlers/runCustom.js +8 -2
- package/dist/generateOobeeClientScanner.js +1 -1
- package/dist/mergeAxeResults/itemsStore.js +32 -3
- package/dist/static/ejs/partials/components/allIssues/CategoryBadges.ejs +3 -0
- package/dist/static/ejs/partials/components/allIssues/IssuesTable.ejs +3 -3
- package/dist/static/ejs/partials/components/header/aboutScanModal/AboutScanModal.ejs +1 -1
- package/dist/static/ejs/partials/components/header/aboutScanModal/ScanConfiguration.ejs +3 -3
- package/dist/static/ejs/partials/components/header/aboutScanModal/ScanDetails.ejs +34 -27
- package/dist/static/ejs/partials/components/ruleModal/ruleOffcanvas.ejs +1 -0
- package/dist/static/ejs/partials/components/scannedPagesSegmentedTabs.ejs +7 -0
- package/dist/static/ejs/partials/components/wcagCoverageDetails.ejs +5 -5
- package/dist/static/ejs/partials/scripts/header/aboutScanModal/AboutScanModal.ejs +3 -3
- package/dist/static/ejs/partials/scripts/prioritiseIssues/PrioritiseIssues.ejs +21 -19
- package/dist/static/ejs/partials/scripts/ruleModal/pageAccordionBuilder.ejs +39 -8
- package/dist/static/ejs/partials/scripts/scannedPagesSegmentedTabs.ejs +11 -5
- package/dist/static/ejs/partials/scripts/screenshotLightbox.ejs +49 -31
- package/dist/static/ejs/partials/styles/header/SiteInfo.ejs +1 -1
- package/dist/static/ejs/partials/styles/header/aboutScanModal/ScanDetails.ejs +36 -16
- package/dist/static/ejs/partials/styles/prioritiseIssues/PrioritiseIssues.ejs +22 -1
- package/dist/static/ejs/partials/styles/styles.ejs +1 -1
- package/dist/static/ejs/partials/styles/wcagCompliance/WcagGaugeBar.ejs +6 -0
- package/dist/static/ejs/partials/styles/wcagCompliance.ejs +5 -4
- package/dist/static/ejs/partials/styles/wcagCoverageDetails.ejs +6 -1
- package/oobee-client-scanner.js +4 -4
- package/package.json +2 -2
- package/src/cli.ts +3 -2
- package/src/combine.ts +4 -2
- package/src/constants/common.ts +131 -35
- package/src/crawlers/commonCrawlerFunc.ts +56 -2
- package/src/crawlers/crawlDomain.ts +11 -1
- package/src/crawlers/crawlIntelligentSitemap.ts +10 -4
- package/src/crawlers/crawlSitemap.ts +19 -2
- package/src/crawlers/custom/utils.ts +26 -13
- package/src/crawlers/guards/urlGuard.ts +18 -1
- package/src/crawlers/runCustom.ts +10 -1
- package/src/generateOobeeClientScanner.ts +1 -1
- package/src/mergeAxeResults/itemsStore.ts +37 -3
- package/src/static/ejs/partials/components/allIssues/CategoryBadges.ejs +3 -0
- package/src/static/ejs/partials/components/allIssues/IssuesTable.ejs +3 -3
- package/src/static/ejs/partials/components/header/aboutScanModal/AboutScanModal.ejs +1 -1
- package/src/static/ejs/partials/components/header/aboutScanModal/ScanConfiguration.ejs +3 -3
- package/src/static/ejs/partials/components/header/aboutScanModal/ScanDetails.ejs +34 -27
- package/src/static/ejs/partials/components/ruleModal/ruleOffcanvas.ejs +1 -0
- package/src/static/ejs/partials/components/scannedPagesSegmentedTabs.ejs +7 -0
- package/src/static/ejs/partials/components/wcagCoverageDetails.ejs +5 -5
- package/src/static/ejs/partials/scripts/header/aboutScanModal/AboutScanModal.ejs +3 -3
- package/src/static/ejs/partials/scripts/prioritiseIssues/PrioritiseIssues.ejs +21 -19
- package/src/static/ejs/partials/scripts/ruleModal/pageAccordionBuilder.ejs +39 -8
- package/src/static/ejs/partials/scripts/scannedPagesSegmentedTabs.ejs +11 -5
- package/src/static/ejs/partials/scripts/screenshotLightbox.ejs +49 -31
- package/src/static/ejs/partials/styles/header/SiteInfo.ejs +1 -1
- package/src/static/ejs/partials/styles/header/aboutScanModal/ScanDetails.ejs +36 -16
- package/src/static/ejs/partials/styles/prioritiseIssues/PrioritiseIssues.ejs +22 -1
- package/src/static/ejs/partials/styles/styles.ejs +1 -1
- package/src/static/ejs/partials/styles/wcagCompliance/WcagGaugeBar.ejs +6 -0
- package/src/static/ejs/partials/styles/wcagCompliance.ejs +5 -4
- package/src/static/ejs/partials/styles/wcagCoverageDetails.ejs +6 -1
- package/testStaticJSScanner.html +1 -1
- /package/{d5e2f6a7-0279-41a3-8763-844970cdf0ba.txt → 67e8137b-1939-4253-8f11-a82bc833cfcb.txt} +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import readline from 'readline';
|
|
4
|
+
import { consoleLogger } from '../logs.js';
|
|
4
5
|
import type { ItemsInfo } from './types.js';
|
|
5
6
|
|
|
6
7
|
export interface ItemsStoreEntry {
|
|
@@ -16,6 +17,7 @@ export interface ItemsStoreEntry {
|
|
|
16
17
|
export class ItemsStore {
|
|
17
18
|
private basePath: string;
|
|
18
19
|
private ensuredDirs = new Set<string>();
|
|
20
|
+
private fileWriteQueues = new Map<string, Promise<void>>();
|
|
19
21
|
|
|
20
22
|
constructor(storagePath: string) {
|
|
21
23
|
this.basePath = path.join(storagePath, 'tmp-items');
|
|
@@ -40,8 +42,29 @@ export class ItemsStore {
|
|
|
40
42
|
async appendPageItems(category: string, ruleId: string, entry: ItemsStoreEntry): Promise<void> {
|
|
41
43
|
await this.ensureDir(category);
|
|
42
44
|
const filePath = this.getRuleFilePath(category, ruleId);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
let line = JSON.stringify(entry);
|
|
46
|
+
|
|
47
|
+
// JSON.stringify should never produce literal newlines inside strings, but HTML content
|
|
48
|
+
// from page evaluation may contain edge-case characters (e.g. unescaped control chars in
|
|
49
|
+
// non-spec-compliant innerHTML). Strip any embedded \r or \n that would break JSONL format readline parsing.
|
|
50
|
+
line = line.replace(/[\n\r]/g, (match) => {
|
|
51
|
+
if (match === '\n') return '\\n';
|
|
52
|
+
if (match === '\r') return '\\r';
|
|
53
|
+
return match;
|
|
54
|
+
});
|
|
55
|
+
line += '\n';
|
|
56
|
+
|
|
57
|
+
// Serialize writes per rule file to avoid concurrent append interleaving/truncation.
|
|
58
|
+
const previous = this.fileWriteQueues.get(filePath) ?? Promise.resolve();
|
|
59
|
+
const next = previous.then(() => fs.appendFile(filePath, line, 'utf8'));
|
|
60
|
+
this.fileWriteQueues.set(
|
|
61
|
+
filePath,
|
|
62
|
+
next.catch(() => {
|
|
63
|
+
// Keep queue alive for subsequent writes.
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
await next;
|
|
45
68
|
}
|
|
46
69
|
|
|
47
70
|
async *readRuleItems(category: string, ruleId: string): AsyncGenerator<ItemsStoreEntry> {
|
|
@@ -51,9 +74,19 @@ export class ItemsStore {
|
|
|
51
74
|
const fileStream = fs.createReadStream(filePath, { encoding: 'utf8' });
|
|
52
75
|
const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
53
76
|
|
|
77
|
+
let lineNumber = 0;
|
|
54
78
|
for await (const line of rl) {
|
|
55
|
-
|
|
79
|
+
lineNumber += 1;
|
|
80
|
+
if (!line.trim()) continue;
|
|
81
|
+
|
|
82
|
+
try {
|
|
56
83
|
yield JSON.parse(line) as ItemsStoreEntry;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
// Tolerate malformed/truncated JSONL lines (e.g. interrupted append) so report generation can continue.
|
|
86
|
+
const preview = line.slice(0, 200);
|
|
87
|
+
consoleLogger.warn(
|
|
88
|
+
`Skipping malformed itemsStore JSONL line ${lineNumber} in ${filePath}: ${(error as Error).message}. Content preview: ${preview}`,
|
|
89
|
+
);
|
|
57
90
|
}
|
|
58
91
|
}
|
|
59
92
|
}
|
|
@@ -68,6 +101,7 @@ export class ItemsStore {
|
|
|
68
101
|
}
|
|
69
102
|
|
|
70
103
|
async cleanup(): Promise<void> {
|
|
104
|
+
await Promise.all(this.fileWriteQueues.values());
|
|
71
105
|
await fs.rm(this.basePath, { recursive: true, force: true });
|
|
72
106
|
}
|
|
73
107
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
<button
|
|
8
8
|
type="button"
|
|
9
9
|
class="category-tooltip-icon"
|
|
10
|
+
aria-label="About Must Fix category"
|
|
10
11
|
aria-describedby="mustFixTooltip"
|
|
11
12
|
>
|
|
12
13
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"
|
|
@@ -34,6 +35,7 @@
|
|
|
34
35
|
<button
|
|
35
36
|
type="button"
|
|
36
37
|
class="category-tooltip-icon"
|
|
38
|
+
aria-label="About Good to Fix category"
|
|
37
39
|
aria-describedby="goodToFixTooltip"
|
|
38
40
|
>
|
|
39
41
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"
|
|
@@ -61,6 +63,7 @@
|
|
|
61
63
|
<button
|
|
62
64
|
type="button"
|
|
63
65
|
class="category-tooltip-icon"
|
|
66
|
+
aria-label="About Manual Test category"
|
|
64
67
|
aria-describedby="manualTestTooltip"
|
|
65
68
|
>
|
|
66
69
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"
|
|
@@ -2,21 +2,21 @@
|
|
|
2
2
|
<table class="issues-table" id="issuesTable">
|
|
3
3
|
<thead>
|
|
4
4
|
<tr>
|
|
5
|
-
<th class="sortable"
|
|
5
|
+
<th class="sortable" tabindex="0" aria-sort="none" style="width: 15%;">
|
|
6
6
|
<span>Severity</span>
|
|
7
7
|
<svg class="sort-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
8
8
|
<path d="M7 9L12 4L17 9H7Z" fill="currentColor" opacity="1" />
|
|
9
9
|
<path d="M7 15L12 20L17 15H7Z" fill="currentColor" opacity="0.3" />
|
|
10
10
|
</svg>
|
|
11
11
|
</th>
|
|
12
|
-
<th class="sortable"
|
|
12
|
+
<th class="sortable" tabindex="0" aria-sort="none">
|
|
13
13
|
<span>Issue Name</span>
|
|
14
14
|
<svg class="sort-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
15
15
|
<path d="M7 9L12 4L17 9H7Z" fill="currentColor" opacity="0.3" />
|
|
16
16
|
<path d="M7 15L12 20L17 15H7Z" fill="currentColor" opacity="1" />
|
|
17
17
|
</svg>
|
|
18
18
|
</th>
|
|
19
|
-
<th class="sortable"
|
|
19
|
+
<th class="sortable" tabindex="0" aria-sort="descending" style="width: 15%;">
|
|
20
20
|
<span>Occurrence</span>
|
|
21
21
|
<svg class="sort-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
22
22
|
<path d="M7 9L12 4L17 9H7Z" fill="currentColor" opacity="0.3" />
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<div id="aboutScanModal" class="modal fade" tabindex="-1" aria-
|
|
1
|
+
<div id="aboutScanModal" class="modal fade" tabindex="-1" aria-label="About this scan" aria-hidden="true">
|
|
2
2
|
<div class="modal-dialog modal-dialog-centered">
|
|
3
3
|
<div class="modal-content">
|
|
4
4
|
<div class="modal-header">
|
|
@@ -5,17 +5,17 @@
|
|
|
5
5
|
</h2>
|
|
6
6
|
<%- include('../../scannedPagesSegmentedTabs') %>
|
|
7
7
|
<div class="seg-panels">
|
|
8
|
-
<div id="pages-scanned" role="tabpanel">
|
|
8
|
+
<div id="pages-scanned" role="tabpanel" aria-labelledby="seg-scanned">
|
|
9
9
|
<ul id="pagesScannedList" class="unbulleted-list">
|
|
10
10
|
<!-- dynamically populated -->
|
|
11
11
|
</ul>
|
|
12
12
|
</div>
|
|
13
|
-
<div id="pages-not-scanned" role="tabpanel" hidden>
|
|
13
|
+
<div id="pages-not-scanned" role="tabpanel" aria-labelledby="seg-not-scanned" hidden>
|
|
14
14
|
<ul id="pagesNotScannedList" class="unbulleted-list">
|
|
15
15
|
<!-- dynamically populated -->
|
|
16
16
|
</ul>
|
|
17
17
|
</div>
|
|
18
|
-
<div id="pages-unsupported" role="tabpanel" hidden>
|
|
18
|
+
<div id="pages-unsupported" role="tabpanel" aria-labelledby="seg-unsupported" hidden>
|
|
19
19
|
<ul id="unsupportedDocsList" class="unbulleted-list">
|
|
20
20
|
<!-- dynamically populated -->
|
|
21
21
|
</ul>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<aside id="scan-about" class="about-scan-details-left">
|
|
2
|
-
<h1>About this scan</h1>
|
|
2
|
+
<h1 id="aboutScanModalLabel">About this scan</h1>
|
|
3
3
|
<ul>
|
|
4
4
|
<li>
|
|
5
5
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
@@ -33,7 +33,9 @@
|
|
|
33
33
|
</span>
|
|
34
34
|
</li>
|
|
35
35
|
|
|
36
|
-
<
|
|
36
|
+
<li class="about-scan-divider-item" aria-hidden="true">
|
|
37
|
+
<hr class="about-scan-divider" />
|
|
38
|
+
</li>
|
|
37
39
|
|
|
38
40
|
<% if (viewport !== null) { %>
|
|
39
41
|
<li>
|
|
@@ -64,7 +66,7 @@
|
|
|
64
66
|
type="button"
|
|
65
67
|
class="js-view-btn about-scan-toggle"
|
|
66
68
|
aria-controls="view-crawl"
|
|
67
|
-
aria-
|
|
69
|
+
aria-expanded="true"
|
|
68
70
|
>
|
|
69
71
|
<div class="d-flex">
|
|
70
72
|
<div class="about-scan-link-row">
|
|
@@ -82,30 +84,33 @@
|
|
|
82
84
|
<path d="M13.2346 8.78333C13.2668 8.53333 13.2828 8.275 13.2828 8C13.2828 7.73333 13.2668 7.46667 13.2266 7.21667L14.857 5.9C15.0016 5.78333 15.0418 5.55833 14.9534 5.39167L13.4113 2.625C13.315 2.44167 13.1142 2.38333 12.9375 2.44167L11.0179 3.24167C10.6163 2.925 10.1906 2.65833 9.71675 2.45833L9.42761 0.341667C9.39548 0.141667 9.23485 0 9.04209 0H5.95791C5.76515 0 5.61255 0.141667 5.58042 0.341667L5.29128 2.45833C4.81741 2.65833 4.3837 2.93333 3.99015 3.24167L2.07057 2.44167C1.89387 2.375 1.69308 2.44167 1.5967 2.625L0.0626475 5.39167C-0.0337329 5.56667 -0.00160615 5.78333 0.159028 5.9L1.78946 7.21667C1.7493 7.46667 1.71718 7.74167 1.71718 8C1.71718 8.25833 1.73324 8.53333 1.7734 8.78333L0.142964 10.1C-0.00160614 10.2167 -0.0417645 10.4417 0.0465841 10.6083L1.58867 13.375C1.68505 13.5583 1.88584 13.6167 2.06254 13.5583L3.98212 12.7583C4.3837 13.075 4.80938 13.3417 5.28325 13.5417L5.57239 15.6583C5.61255 15.8583 5.76515 16 5.95791 16H9.04209C9.23485 16 9.39548 15.8583 9.41958 15.6583L9.70872 13.5417C10.1826 13.3417 10.6163 13.075 11.0099 12.7583L12.9294 13.5583C13.1061 13.625 13.3069 13.5583 13.4033 13.375L14.9454 10.6083C15.0418 10.425 15.0016 10.2167 14.849 10.1L13.2346 8.78333ZM7.5 11C5.90972 11 4.60859 9.65 4.60859 8C4.60859 6.35 5.90972 5 7.5 5C9.09028 5 10.3914 6.35 10.3914 8C10.3914 9.65 9.09028 11 7.5 11Z" fill="#686868"/>
|
|
83
85
|
</svg>
|
|
84
86
|
|
|
85
|
-
<div>
|
|
86
|
-
|
|
87
|
+
<div class="advanced-group-content">
|
|
88
|
+
<div>
|
|
89
|
+
Advanced scan options enabled
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<ul class="advanced-sublist">
|
|
93
|
+
<% if (advancedScanOptionsSummaryItems.showIncludeScreenshots) { %>
|
|
94
|
+
<li class="advanced-sublist-li">Include screenshots</li>
|
|
95
|
+
<% } %>
|
|
96
|
+
<% if (advancedScanOptionsSummaryItems.showAllowSubdomains) { %>
|
|
97
|
+
<li class="advanced-sublist-li">Allow subdomains for scans</li>
|
|
98
|
+
<% } %>
|
|
99
|
+
<% if (advancedScanOptionsSummaryItems.showEnableCustomChecks) { %>
|
|
100
|
+
<li class="advanced-sublist-li">Enable custom checks</li>
|
|
101
|
+
<% } %>
|
|
102
|
+
<% if (advancedScanOptionsSummaryItems.showEnableWcagAaa) { %>
|
|
103
|
+
<li class="advanced-sublist-li">Enable WCAG AAA checks</li>
|
|
104
|
+
<% } %>
|
|
105
|
+
<% if (advancedScanOptionsSummaryItems.showSlowScanMode) { %>
|
|
106
|
+
<li class="advanced-sublist-li">Slow scan mode</li>
|
|
107
|
+
<% } %>
|
|
108
|
+
<% if (advancedScanOptionsSummaryItems.showAdhereRobots) { %>
|
|
109
|
+
<li class="advanced-sublist-li">Adhere to robots.txt</li>
|
|
110
|
+
<% } %>
|
|
111
|
+
</ul>
|
|
87
112
|
</div>
|
|
88
113
|
</li>
|
|
89
|
-
<ul class="advanced-sublist">
|
|
90
|
-
<% if (advancedScanOptionsSummaryItems.showIncludeScreenshots) { %>
|
|
91
|
-
<li class="advanced-sublist-li">Include screenshots</li>
|
|
92
|
-
<% } %>
|
|
93
|
-
<% if (advancedScanOptionsSummaryItems.showAllowSubdomains) { %>
|
|
94
|
-
<li class="advanced-sublist-li">Allow subdomains for scans</li>
|
|
95
|
-
<% } %>
|
|
96
|
-
<% if (advancedScanOptionsSummaryItems.showEnableCustomChecks) { %>
|
|
97
|
-
<li class="advanced-sublist-li">Enable custom checks</li>
|
|
98
|
-
<% } %>
|
|
99
|
-
<% if (advancedScanOptionsSummaryItems.showEnableWcagAaa) { %>
|
|
100
|
-
<li class="advanced-sublist-li">Enable WCAG AAA checks</li>
|
|
101
|
-
<% } %>
|
|
102
|
-
<% if (advancedScanOptionsSummaryItems.showSlowScanMode) { %>
|
|
103
|
-
<li class="advanced-sublist-li">Slow scan mode</li>
|
|
104
|
-
<% } %>
|
|
105
|
-
<% if (advancedScanOptionsSummaryItems.showAdhereRobots) { %>
|
|
106
|
-
<li class="advanced-sublist-li">Adhere to robots.txt</li>
|
|
107
|
-
<% } %>
|
|
108
|
-
</ul>
|
|
109
114
|
|
|
110
115
|
<li>
|
|
111
116
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
@@ -115,7 +120,7 @@
|
|
|
115
120
|
type="button"
|
|
116
121
|
class="js-view-btn about-scan-toggle"
|
|
117
122
|
aria-controls="view-wcag"
|
|
118
|
-
aria-
|
|
123
|
+
aria-expanded="false"
|
|
119
124
|
>
|
|
120
125
|
<span class="wcag-criteria-label">
|
|
121
126
|
WCAG Automated Testing
|
|
@@ -131,7 +136,9 @@
|
|
|
131
136
|
</button>
|
|
132
137
|
</li>
|
|
133
138
|
|
|
134
|
-
<
|
|
139
|
+
<li class="about-scan-divider-item" aria-hidden="true">
|
|
140
|
+
<hr class="about-scan-divider" />
|
|
141
|
+
</li>
|
|
135
142
|
|
|
136
143
|
<li>
|
|
137
144
|
<span id="oobeeAppVersion" class="oobee-version-text">N/A</span>
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
<div class="segmented-tabs" role="tablist" aria-label="Crawl views">
|
|
2
2
|
<button
|
|
3
3
|
type="button"
|
|
4
|
+
id="seg-scanned"
|
|
4
5
|
class="seg-pill"
|
|
6
|
+
role="tab"
|
|
5
7
|
aria-controls="pages-scanned"
|
|
6
8
|
aria-selected="true"
|
|
9
|
+
tabindex="0"
|
|
7
10
|
data-tab-target="#pages-scanned"
|
|
8
11
|
>
|
|
9
12
|
<span id="totalPagesScannedLabel">
|
|
@@ -18,8 +21,10 @@
|
|
|
18
21
|
type="button"
|
|
19
22
|
id="seg-not-scanned"
|
|
20
23
|
class="seg-pill"
|
|
24
|
+
role="tab"
|
|
21
25
|
aria-controls="pages-not-scanned"
|
|
22
26
|
aria-selected="false"
|
|
27
|
+
tabindex="-1"
|
|
23
28
|
data-tab-target="#pages-not-scanned"
|
|
24
29
|
>
|
|
25
30
|
<span id="totalPagesNotScannedLabel">
|
|
@@ -34,8 +39,10 @@
|
|
|
34
39
|
type="button"
|
|
35
40
|
id="seg-unsupported"
|
|
36
41
|
class="seg-pill"
|
|
42
|
+
role="tab"
|
|
37
43
|
aria-controls="pages-unsupported"
|
|
38
44
|
aria-selected="false"
|
|
45
|
+
tabindex="-1"
|
|
39
46
|
data-tab-target="#pages-unsupported"
|
|
40
47
|
>
|
|
41
48
|
<span id="totalUnsupportedDocsLabel">
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
<div id="wcagCoverage" class="my-3">
|
|
2
|
-
<
|
|
3
|
-
<span id="wcagAALabelCount">20</span> (A &
|
|
4
|
-
</
|
|
2
|
+
<h3 class="wcag-criteria-heading fw-semibold mb-2">
|
|
3
|
+
<span id="wcagAALabelCount">20</span> (A & AA) WCAG Success Criteria
|
|
4
|
+
</h3>
|
|
5
5
|
<div class="wcag-box">
|
|
6
6
|
<ul id="wcagLinksListAA" class="wcag-grid list-unstyled m-0">
|
|
7
7
|
<!-- dynamically populated -->
|
|
8
8
|
</ul>
|
|
9
9
|
</div>
|
|
10
|
-
<
|
|
10
|
+
<h3 class="wcag-criteria-heading fw-semibold mt-4 mb-2">
|
|
11
11
|
<span id="wcagAAALabelCount">6</span> (AAA) WCAG Success Criteria
|
|
12
|
-
</
|
|
12
|
+
</h3>
|
|
13
13
|
<div class="wcag-box">
|
|
14
14
|
<ul id="wcagLinksListAAA" class="wcag-grid list-unstyled m-0">
|
|
15
15
|
<!-- dynamically populated -->
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
function showPane(targetId) {
|
|
7
7
|
// hide all panes
|
|
8
8
|
panes.forEach(p => p.hidden = true);
|
|
9
|
-
//
|
|
10
|
-
btns.forEach(b => b.setAttribute('aria-
|
|
9
|
+
// collapse all buttons
|
|
10
|
+
btns.forEach(b => b.setAttribute('aria-expanded', 'false'));
|
|
11
11
|
|
|
12
12
|
// show the requested pane
|
|
13
13
|
const pane = document.getElementById(targetId);
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
// mark the controlling button active
|
|
18
18
|
const btn = btns.find(b => b.getAttribute('aria-controls') === targetId);
|
|
19
|
-
if (btn) btn.setAttribute('aria-
|
|
19
|
+
if (btn) btn.setAttribute('aria-expanded', 'true');
|
|
20
20
|
|
|
21
21
|
// move focus to the pane heading for screen readers/keyboard users
|
|
22
22
|
const h = pane.querySelector('h4, h3, h2, [role="heading"]');
|
|
@@ -84,11 +84,13 @@
|
|
|
84
84
|
: '';
|
|
85
85
|
|
|
86
86
|
return `
|
|
87
|
-
<li class="priority-issue-item
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
<li class="priority-issue-item" data-rule-id="${issue.ruleId}">
|
|
88
|
+
<button type="button" class="priority-issue-action d-flex g-one" aria-pressed="false">
|
|
89
|
+
<div class="d-flex justify-content-between align-items-center w-90">
|
|
90
|
+
<div class="priority-issue-title">${issue.description}</div>
|
|
91
|
+
${disabilityBadges}
|
|
92
|
+
</div>
|
|
93
|
+
</button>
|
|
92
94
|
</li>
|
|
93
95
|
`;
|
|
94
96
|
})
|
|
@@ -122,7 +124,7 @@
|
|
|
122
124
|
data-bs-parent="#prioritiseIssuesAccordion"
|
|
123
125
|
>
|
|
124
126
|
<div class="accordion-body">
|
|
125
|
-
<ul class="priority-issues-list">
|
|
127
|
+
<ul class="priority-issues-list" aria-label="Priority issues">
|
|
126
128
|
${issuesListHTML}
|
|
127
129
|
</ul>
|
|
128
130
|
</div>
|
|
@@ -135,19 +137,25 @@
|
|
|
135
137
|
accordionContainer.innerHTML = accordionHTML;
|
|
136
138
|
|
|
137
139
|
document.querySelectorAll('.priority-issue-item').forEach(item => {
|
|
140
|
+
const actionButton = item.querySelector('.priority-issue-action');
|
|
141
|
+
if (!actionButton) return;
|
|
142
|
+
|
|
138
143
|
const selectIssue = function () {
|
|
139
144
|
const ruleId = item.getAttribute('data-rule-id');
|
|
140
145
|
|
|
141
146
|
document.querySelectorAll('.priority-issue-item').forEach(el => {
|
|
142
147
|
el.classList.remove('active');
|
|
143
|
-
el.
|
|
148
|
+
const button = el.querySelector('.priority-issue-action');
|
|
149
|
+
if (button) {
|
|
150
|
+
button.setAttribute('aria-pressed', 'false');
|
|
151
|
+
}
|
|
144
152
|
});
|
|
145
153
|
document.querySelectorAll('.priority-issue-title').forEach(el => {
|
|
146
154
|
el.classList.remove('active');
|
|
147
155
|
});
|
|
148
156
|
|
|
149
157
|
item.classList.add('active');
|
|
150
|
-
|
|
158
|
+
actionButton.setAttribute('aria-pressed', 'true');
|
|
151
159
|
const title = item.querySelector('.priority-issue-title');
|
|
152
160
|
if (title) {
|
|
153
161
|
title.classList.add('active');
|
|
@@ -167,16 +175,7 @@
|
|
|
167
175
|
}
|
|
168
176
|
};
|
|
169
177
|
|
|
170
|
-
|
|
171
|
-
item.addEventListener('click', selectIssue);
|
|
172
|
-
|
|
173
|
-
// Keyboard handler
|
|
174
|
-
item.addEventListener('keydown', function (event) {
|
|
175
|
-
if (event.key === 'Enter' || event.key === ' ') {
|
|
176
|
-
event.preventDefault();
|
|
177
|
-
selectIssue();
|
|
178
|
-
}
|
|
179
|
-
});
|
|
178
|
+
actionButton.addEventListener('click', selectIssue);
|
|
180
179
|
});
|
|
181
180
|
|
|
182
181
|
// Automatically open first accordion and select first issue
|
|
@@ -197,7 +196,10 @@
|
|
|
197
196
|
const firstIssueItem = document.querySelector('.priority-issue-item');
|
|
198
197
|
if (firstIssueItem) {
|
|
199
198
|
firstIssueItem.classList.add('active');
|
|
200
|
-
firstIssueItem.
|
|
199
|
+
const firstIssueButton = firstIssueItem.querySelector('.priority-issue-action');
|
|
200
|
+
if (firstIssueButton) {
|
|
201
|
+
firstIssueButton.setAttribute('aria-pressed', 'true');
|
|
202
|
+
}
|
|
201
203
|
const firstIssueTitle = firstIssueItem.querySelector('.priority-issue-title');
|
|
202
204
|
if (firstIssueTitle) {
|
|
203
205
|
firstIssueTitle.classList.add('active');
|
|
@@ -451,7 +451,7 @@
|
|
|
451
451
|
<div id="${accordionId}-content" class="accordion-collapse collapse" aria-labelledby="${accordionId}-title">
|
|
452
452
|
<div class="accordion-body p-3">
|
|
453
453
|
${isCustomFlow
|
|
454
|
-
? createCustomFlowContent(page)
|
|
454
|
+
? createCustomFlowContent(page, ruleInCategory)
|
|
455
455
|
:
|
|
456
456
|
`<div class="d-flex align-items-center gap-1 accordion-link" style=" word-wrap: break-word; word-break: break-word;">
|
|
457
457
|
<a href="${page.url}" target="_blank">${page.url}
|
|
@@ -486,16 +486,46 @@
|
|
|
486
486
|
return accordion;
|
|
487
487
|
}
|
|
488
488
|
|
|
489
|
-
function
|
|
489
|
+
function getCustomFlowPageScreenshot(page, ruleInCategory) {
|
|
490
|
+
if (page && page.pageImagePath) return page.pageImagePath;
|
|
491
|
+
|
|
492
|
+
// Use same source-of-truth as scanAboutModal.
|
|
493
|
+
const scannedPages =
|
|
494
|
+
typeof scanData !== 'undefined' &&
|
|
495
|
+
scanData &&
|
|
496
|
+
Array.isArray(scanData.pagesScanned)
|
|
497
|
+
? scanData.pagesScanned
|
|
498
|
+
: [];
|
|
499
|
+
|
|
500
|
+
if (page) {
|
|
501
|
+
const matchByUrl = scannedPages.find((p) => p && p.url === page.url && p.pageImagePath);
|
|
502
|
+
if (matchByUrl) return matchByUrl.pageImagePath;
|
|
503
|
+
|
|
504
|
+
const matchByTitle = scannedPages.find(
|
|
505
|
+
(p) => p && p.pageTitle === page.pageTitle && p.pageImagePath,
|
|
506
|
+
);
|
|
507
|
+
if (matchByTitle) return matchByTitle.pageImagePath;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return '';
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function createCustomFlowContent(page, ruleInCategory) {
|
|
514
|
+
const pageScreenshot = getCustomFlowPageScreenshot(page, ruleInCategory);
|
|
515
|
+
|
|
490
516
|
return `
|
|
491
517
|
<div class="custom-flow-screenshot-container">
|
|
492
518
|
<div class="custom-flow-thumb">
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
519
|
+
${
|
|
520
|
+
pageScreenshot
|
|
521
|
+
? `<img
|
|
522
|
+
src="${pageScreenshot}"
|
|
523
|
+
alt="Screenshot of ${page.url}"
|
|
524
|
+
class="custom-flow-screenshot"
|
|
525
|
+
onerror="this.onerror = null; this.remove();"
|
|
526
|
+
>`
|
|
527
|
+
: ''
|
|
528
|
+
}
|
|
499
529
|
</div>
|
|
500
530
|
<div class="display-url-container">
|
|
501
531
|
<div><a href="${page.url}" target="_blank">${page.url}</a></div>
|
|
@@ -538,6 +568,7 @@
|
|
|
538
568
|
|
|
539
569
|
function setupCustomFlowScreenshot(accordion, page) {
|
|
540
570
|
const customScreenshotElem = accordion.getElementsByClassName('custom-flow-screenshot')[0];
|
|
571
|
+
if (!customScreenshotElem) return;
|
|
541
572
|
|
|
542
573
|
customScreenshotElem.onerror = function(event) {
|
|
543
574
|
this.onerror = null;
|
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
document.addEventListener('click', function (e) {
|
|
3
|
-
const btn = e.target.closest('.seg-pill');
|
|
3
|
+
const btn = e.target.closest('.seg-pill[role="tab"]');
|
|
4
4
|
if (!btn) return;
|
|
5
5
|
|
|
6
|
-
const container = btn.closest('.segmented-tabs');
|
|
6
|
+
const container = btn.closest('.segmented-tabs[role="tablist"]');
|
|
7
|
+
if (!container) return;
|
|
8
|
+
|
|
7
9
|
const targetSel = btn.getAttribute('data-tab-target');
|
|
8
10
|
const panel = document.querySelector(targetSel);
|
|
9
11
|
if (!panel) return;
|
|
10
12
|
|
|
11
|
-
container.querySelectorAll('.seg-pill').forEach((p) =>
|
|
12
|
-
p.setAttribute('aria-selected', 'false')
|
|
13
|
-
|
|
13
|
+
container.querySelectorAll('.seg-pill[role="tab"]').forEach((p) => {
|
|
14
|
+
p.setAttribute('aria-selected', 'false');
|
|
15
|
+
p.setAttribute('tabindex', '-1');
|
|
16
|
+
});
|
|
17
|
+
|
|
14
18
|
btn.setAttribute('aria-selected', 'true');
|
|
19
|
+
btn.setAttribute('tabindex', '0');
|
|
15
20
|
|
|
16
21
|
document
|
|
17
22
|
.querySelectorAll('.seg-panels > [role="tabpanel"]')
|
|
18
23
|
.forEach((p) => (p.hidden = true));
|
|
24
|
+
|
|
19
25
|
panel.hidden = false;
|
|
20
26
|
});
|
|
21
27
|
|
|
@@ -6,18 +6,32 @@
|
|
|
6
6
|
const lightboxContent = document.getElementsByClassName('lightbox-content')[0];
|
|
7
7
|
const lightboxImg = document.getElementById('lightbox-image');
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
9
|
+
// Handle dynamically inserted custom-flow screenshots (about scan modal content is rendered later)
|
|
10
|
+
document.addEventListener(
|
|
11
|
+
'error',
|
|
12
|
+
event => {
|
|
13
|
+
const target = event.target;
|
|
14
|
+
if (target && target.classList && target.classList.contains('custom-flow-screenshot')) {
|
|
15
|
+
target.onerror = null;
|
|
16
|
+
target.remove();
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
true,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
document.addEventListener('click', event => {
|
|
23
|
+
const screenshot = event.target.closest('.custom-flow-screenshot');
|
|
24
|
+
if (!screenshot) return;
|
|
25
|
+
|
|
26
|
+
event.preventDefault();
|
|
27
|
+
|
|
28
|
+
const container = screenshot.closest('.custom-flow-screenshot-container');
|
|
29
|
+
const link = container ? container.querySelector('.display-url-container a') : null;
|
|
30
|
+
|
|
31
|
+
const pageTitle = link?.textContent?.trim() || screenshot.getAttribute('alt') || 'Screenshot';
|
|
32
|
+
const pageUrl = link?.href || '';
|
|
33
|
+
|
|
34
|
+
openLightbox(screenshot.src, pageTitle, pageUrl);
|
|
21
35
|
});
|
|
22
36
|
|
|
23
37
|
lightbox.addEventListener('click', event => {
|
|
@@ -31,8 +45,9 @@
|
|
|
31
45
|
});
|
|
32
46
|
|
|
33
47
|
const offcanvasElem = document.getElementsByClassName('offcanvas')[0];
|
|
48
|
+
let offcanvasItem = null;
|
|
34
49
|
if (offcanvasElem) {
|
|
35
|
-
|
|
50
|
+
offcanvasItem = new bootstrap.Offcanvas(offcanvasElem);
|
|
36
51
|
offcanvasItem._config.keyboard = false; // Disable default keyboard handling
|
|
37
52
|
}
|
|
38
53
|
|
|
@@ -43,27 +58,30 @@
|
|
|
43
58
|
pagesScannedModalItem._config.keyboard = false;
|
|
44
59
|
}
|
|
45
60
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
61
|
+
// Use capture phase so lightbox handles Escape before Bootstrap modal/offcanvas handlers.
|
|
62
|
+
document.addEventListener(
|
|
63
|
+
'keydown',
|
|
64
|
+
event => {
|
|
65
|
+
if (event.key !== 'Escape') return;
|
|
66
|
+
|
|
67
|
+
if (lightbox.style.display === 'block') {
|
|
68
|
+
event.preventDefault();
|
|
69
|
+
event.stopPropagation();
|
|
70
|
+
event.stopImmediatePropagation();
|
|
71
|
+
closeLightbox();
|
|
72
|
+
return;
|
|
55
73
|
}
|
|
56
74
|
|
|
57
|
-
if (
|
|
58
|
-
|
|
59
|
-
event.preventDefault(); // Prevent default bootstrap behaviour
|
|
60
|
-
closeLightbox();
|
|
61
|
-
} else {
|
|
62
|
-
pagesScannedModalItem.hide();
|
|
63
|
-
}
|
|
75
|
+
if (offcanvasItem && offcanvasItem._isShown) {
|
|
76
|
+
offcanvasItem.hide();
|
|
64
77
|
}
|
|
65
|
-
|
|
66
|
-
|
|
78
|
+
|
|
79
|
+
if (pagesScannedModalItem && pagesScannedModalItem._isShown) {
|
|
80
|
+
pagesScannedModalItem.hide();
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
true,
|
|
84
|
+
);
|
|
67
85
|
|
|
68
86
|
function openLightbox(imgSrc, pageTitle, pageUrl) {
|
|
69
87
|
lightbox.style.display = 'block';
|