@@ -5,21 +5,30 @@ import path from 'path';
5
5
import fs from 'fs' ;
6
6
import { processSitemap } from './utils/process-sitemap' ;
7
7
8
+ const viewports = [
9
+ { name : 'mobile' , width : 375 , height : 667 } ,
10
+ { name : 'tablet' , width : 768 , height : 1024 } ,
11
+ { name : 'desktop' , width : 1280 , height : 720 } ,
12
+ ] ;
13
+
8
14
test . describe ( 'Accessibility tests' , ( ) => {
9
15
let urls : string [ ] = [ ] ;
10
- let totalPages = 0 ;
16
+ let sitemapCounts : Record < string , number > = { } ;
11
17
const combinedResults : Array < {
12
18
url : string ;
13
19
violations : any [ ] ;
14
20
screenshot ?: string ;
21
+ viewport : string ;
22
+ state ?: string ;
15
23
} > = [ ] ;
16
24
17
25
test . beforeAll ( async ( { baseURL } ) => {
18
- const sitemapUrl = `${ baseURL } /sitemap_index.xml` ;
19
- // const sitemapUrl = `${baseURL}/page-sitemap.xml`;
20
- // const sitemapUrl = `${baseURL}/post-sitemap.xml`;
21
- urls = await processSitemap ( sitemapUrl ) ;
22
- totalPages = urls . length ;
26
+ // Get sitemap index
27
+ const { urls : allUrls , counts } = await processSitemap (
28
+ `${ baseURL } /sitemap_index.xml`
29
+ ) ;
30
+ urls = allUrls ;
31
+ sitemapCounts = counts ;
23
32
24
33
const screenshotsDir = path . resolve ( './test-results/screenshots' ) ;
25
34
if ( ! fs . existsSync ( screenshotsDir ) ) {
@@ -28,12 +37,6 @@ test.describe('Accessibility tests', () => {
28
37
} ) ;
29
38
30
39
test ( 'Should not have accessibility issues' , async ( { page } ) => {
31
- const viewports = [
32
- { name : 'mobile' , width : 375 , height : 667 } ,
33
- { name : 'tablet' , width : 768 , height : 1024 } ,
34
- { name : 'desktop' , width : 1280 , height : 720 } ,
35
- ] ;
36
-
37
40
for ( const viewport of viewports ) {
38
41
await test . step ( `Viewport: ${ viewport . name } ` , async ( ) => {
39
42
await page . setViewportSize ( { width : viewport . width , height : viewport . height } ) ;
@@ -53,22 +56,122 @@ test.describe('Accessibility tests', () => {
53
56
console . log ( 'URL is: ' + url ) ;
54
57
55
58
if ( results . violations . length > 0 ) {
59
+ // Add ada-violation class to highlight each offending node
60
+ for ( const violation of results . violations ) {
61
+ for ( const node of violation . nodes ) {
62
+ const selector = node . target . join ( ' ' ) ;
63
+ await page . $eval ( selector , el => el . classList . add ( 'ada-violation' ) ) . catch ( ( ) => { } ) ;
64
+ }
65
+ }
66
+
56
67
// Capture screenshot
57
68
const screenshotPath = path . resolve (
58
69
'./test-results/screenshots' ,
59
70
`axe-violation-screenshot-${ Date . now ( ) } .png`
60
71
) ;
61
-
62
72
await page . screenshot ( { path : screenshotPath , fullPage : true } ) ;
63
73
74
+ // Remove ada-violation highlight
75
+ for ( const violation of results . violations ) {
76
+ for ( const node of violation . nodes ) {
77
+ const selector = node . target . join ( ' ' ) ;
78
+ await page . $eval ( selector , el => el . classList . remove ( 'ada-violation' ) ) . catch ( ( ) => { } ) ;
79
+ }
80
+ }
81
+
64
82
combinedResults . push ( {
65
83
url,
66
84
violations : results . violations ,
67
85
screenshot : screenshotPath ,
86
+ viewport : viewport . name ,
87
+ state : 'initial' ,
68
88
} ) ;
69
89
} else {
70
90
console . log ( 'Nothing found.' ) ;
71
91
}
92
+
93
+ // Hover tests, desktop only
94
+ if ( viewport . name === 'desktop' ) {
95
+ // Find hoverable elements
96
+ const hoverSelectors = await page . $$eval (
97
+ 'button, a, [role="button"], [role="link"], [tabindex]' ,
98
+ elements =>
99
+ elements
100
+ . filter ( el => el . offsetParent !== null && ! el . disabled )
101
+ . map ( el => {
102
+ if ( el . id ) return `#${ CSS . escape ( el . id ) } ` ;
103
+ if ( el . className ) {
104
+ return (
105
+ '.' +
106
+ el . className
107
+ . trim ( )
108
+ . split ( / \s + / )
109
+ . map ( cls => CSS . escape ( cls ) )
110
+ . join ( '.' )
111
+ ) ;
112
+ }
113
+ return el . tagName . toLowerCase ( ) ;
114
+ } )
115
+ ) ;
116
+
117
+ console . log ( `Found ${ hoverSelectors . length } hoverable elements.` ) ;
118
+ let nextId = combinedResults . length + 1 ;
119
+
120
+ // Hover each and test
121
+ for ( const selector of hoverSelectors ) {
122
+ await test . step ( `Hover: ${ selector } ` , async ( ) => {
123
+ console . log ( `Hovering: ${ selector } ` ) ;
124
+
125
+ const el = await page . $ ( selector ) ;
126
+ if ( ! el ) {
127
+ console . log ( `Element ${ selector } not found — skipping.` ) ;
128
+ return ;
129
+ }
130
+
131
+ await el . hover ( ) ;
132
+ await page . waitForTimeout ( 300 ) ;
133
+
134
+ const hoverResults = await new AxeBuilder ( { page } )
135
+ . include ( selector )
136
+ . withTags ( [ 'wcag2a' , 'wcag2aa' , 'wcag21a' , 'wcag21aa' ] )
137
+ . analyze ( ) ;
138
+
139
+ console . log (
140
+ `Violations for hover ${ selector } : ${ hoverResults . violations . length } `
141
+ ) ;
142
+
143
+ if ( hoverResults . violations . length > 0 ) {
144
+ // Add ada-violation highlight class to element
145
+ await el . evaluate ( node => node . classList . add ( 'ada-violation' ) ) ;
146
+
147
+ const screenshotPath = path . resolve (
148
+ './test-results/screenshots' ,
149
+ `axe-hover-${ selector . replace ( / [ ^ a - z 0 - 9 ] / gi, '_' ) } -${ Date . now ( ) } .png`
150
+ ) ;
151
+ await page . screenshot ( { path : screenshotPath , fullPage : true } ) ;
152
+
153
+ // Remove ada-violation highlight class
154
+ await el . evaluate ( node => node . classList . remove ( 'ada-violation' ) ) ;
155
+
156
+ hoverResults . violations . forEach ( v => {
157
+ v . reportId = nextId ++ ;
158
+ v . state = 'hover' ;
159
+ v . viewport = viewport . name ;
160
+ } ) ;
161
+
162
+ combinedResults . push ( {
163
+ url,
164
+ violations : hoverResults . violations ,
165
+ screenshot : screenshotPath ,
166
+ viewport : viewport . name ,
167
+ state : 'hover' ,
168
+ } ) ;
169
+
170
+ console . log ( `Added hover result for ${ selector } ` ) ;
171
+ }
172
+ } ) ;
173
+ }
174
+ }
72
175
} ) ;
73
176
}
74
177
} ) ;
@@ -94,26 +197,13 @@ test.describe('Accessibility tests', () => {
94
197
95
198
let screenshotSummary = '' ;
96
199
97
- // Add total pages scanned
98
- screenshotSummary += `<p>Scanned <strong>${ totalPages } </strong> URLs</p>\n` ;
99
-
100
- // Open <ul>
101
- screenshotSummary += '<ul>\n' ;
102
-
103
- for ( let i = 0 ; i < combinedResults . length ; i ++ ) {
104
- const result = combinedResults [ i ] ;
105
- if ( result . screenshot ) {
106
- const relPath = path . relative ( reportDirPath , result . screenshot ) ;
107
- const name = path . basename ( result . screenshot ) ;
108
-
109
- // Add li
110
- screenshotSummary += `<li><a href="${ relPath } ">${ i + 1 } . ${ name } </a></li>\n` ;
200
+ // Get sitemap + viewport info
201
+ for ( const viewport of viewports ) {
202
+ for ( const sitemapUrl of Object . keys ( sitemapCounts ) ) {
203
+ screenshotSummary += `<p>${ sitemapCounts [ sitemapUrl ] } items scanned from ${ sitemapUrl } at ${ viewport . name } </p>\n` ;
111
204
}
112
205
}
113
206
114
- // Close </ul>
115
- screenshotSummary += '</ul>\n' ;
116
-
117
207
// Generate the combined HTML report
118
208
createHtmlReport ( {
119
209
results : allResults ,
@@ -127,6 +217,47 @@ test.describe('Accessibility tests', () => {
127
217
console . log (
128
218
`Accessibility report saved at ${ path . join ( reportDir , 'combined-accessibility-report.html' ) } `
129
219
) ;
220
+
221
+ // Inline injection of screenshots, viewport and url
222
+ const reportPath = path . join ( reportDirPath , 'combined-accessibility-report.html' ) ;
223
+ let reportHtml = fs . readFileSync ( reportPath , 'utf8' ) ;
224
+
225
+ combinedResults . forEach ( ( result , resultIndex ) => {
226
+ if ( ! result . screenshot ) return ;
227
+
228
+ result . violations . forEach ( ( violation ) => {
229
+ // Try to find the matching violation by Axe rule ID
230
+ const ruleId = violation . id ;
231
+ const idNumber = resultIndex + 1 ;
232
+
233
+ console . log ( `Injecting screenshot for violation ${ idNumber } (${ ruleId } )` ) ;
234
+
235
+ // Regex to match the <a id="X"> anchor and its violation card
236
+ const regex = new RegExp ( `(<a id=\"${ idNumber } \">[\\s\\S]*?<\\/div>)` , 'g' ) ;
237
+
238
+ const imgTag = `
239
+ <p>
240
+ <strong>Breakpoint:</strong> ${ result . viewport } <br>
241
+ <strong>URL:</strong> <a href="${ result . url } " target="_blank" rel="noopener noreferrer">${ result . url } </a><br>
242
+ <strong>State:</strong> ${ result . state }
243
+ </p>
244
+
245
+ <img src=\"${ path . relative ( reportDirPath , result . screenshot ) } \"
246
+ alt=\"Screenshot for violation ${ idNumber } \"
247
+ style=\"max-width: 100%; margin:1rem 0;\"
248
+ />` ;
249
+
250
+ // Replace and inject
251
+ reportHtml = reportHtml . replace ( regex , ( match ) => {
252
+ console . log ( `Found match for ID ${ idNumber } , inserting details.` ) ;
253
+ return match + imgTag ;
254
+ } ) ;
255
+ } ) ;
256
+ } ) ;
257
+
258
+ // Save the updated HTML
259
+ fs . writeFileSync ( reportPath , reportHtml , 'utf8' ) ;
260
+ console . log ( `Final report at: ${ reportPath } ` ) ;
130
261
} else {
131
262
console . log ( 'No accessibility violations found across all tested URLs.' ) ;
132
263
}
0 commit comments