Skip to content

Commit cd4771b

Browse files
committed
add tests
- clean up formatting and linting
1 parent 1d2753a commit cd4771b

4 files changed

Lines changed: 155 additions & 27 deletions

File tree

.github/actions/find/src/findForUrl.ts

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,24 @@ export async function findForUrl(
3030
try {
3131
const scansContext = getScansContext()
3232

33-
let rawFindings = {} as axe.AxeResults
33+
let rawFindings: axe.AxeResults | undefined
3434
if (scansContext.shouldPerformAxeScan) {
3535
rawFindings = await new AxeBuilder({page}).analyze()
3636
}
3737

38-
const plugins = await loadPlugins()
39-
for (const plugin of plugins) {
40-
if (scansContext.scans.includes(plugin.name)) {
41-
console.log('Running plugin: ', plugin.name)
42-
await plugin.default({page, addFinding, url})
43-
} else {
44-
console.log(`Skipping plugin ${plugin.name} because it is not included in the 'scans' input`)
38+
// - this condition is not required, but makes it easier to make assertions
39+
// in unit tests on whether 'loadPlugins' was called or not
40+
// - alternatively, we can wrap the 'plugin.default(...)' call in another function
41+
// and make assertions on whether that function was called or not
42+
if (scansContext.shouldRunPlugins) {
43+
const plugins = await loadPlugins()
44+
for (const plugin of plugins) {
45+
if (scansContext.scansToPerform.includes(plugin.name)) {
46+
console.log('Running plugin: ', plugin.name)
47+
await plugin.default({page, addFinding, url})
48+
} else {
49+
console.log(`Skipping plugin ${plugin.name} because it is not included in the 'scans' input`)
50+
}
4551
}
4652
}
4753

@@ -51,17 +57,18 @@ export async function findForUrl(
5157
}
5258

5359
console.log('rawFindings: ', rawFindings)
54-
findings = rawFindings?.violations.map(violation => ({
55-
scannerType: 'axe',
56-
url,
57-
html: violation.nodes[0].html.replace(/'/g, '''),
58-
problemShort: violation.help.toLowerCase().replace(/'/g, '''),
59-
problemUrl: violation.helpUrl.replace(/'/g, '''),
60-
ruleId: violation.id,
61-
solutionShort: violation.description.toLowerCase().replace(/'/g, '''),
62-
solutionLong: violation.nodes[0].failureSummary?.replace(/'/g, '''),
63-
screenshotId,
64-
}))
60+
findings =
61+
rawFindings?.violations.map(violation => ({
62+
scannerType: 'axe',
63+
url,
64+
html: violation.nodes[0].html.replace(/'/g, '''),
65+
problemShort: violation.help.toLowerCase().replace(/'/g, '''),
66+
problemUrl: violation.helpUrl.replace(/'/g, '''),
67+
ruleId: violation.id,
68+
solutionShort: violation.description.toLowerCase().replace(/'/g, '''),
69+
solutionLong: violation.nodes[0].failureSummary?.replace(/'/g, '''),
70+
screenshotId,
71+
})) || []
6572
} catch (e) {
6673
console.error('Error during accessibility scan:', e)
6774
}

.github/actions/find/src/pluginManager.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@ import * as fs from 'fs'
22
import * as path from 'path'
33
import {fileURLToPath} from 'url'
44
import {dynamicImport} from './dynamicImport.js'
5+
import type {Finding} from './types.d.js'
6+
import playwright from 'playwright'
57

68
// Helper to get __dirname equivalent in ES Modules
79
const __filename = fileURLToPath(import.meta.url)
810
const __dirname = path.dirname(__filename)
911

10-
// - plugins are js files right now, so they dont have a type
11-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
12-
const plugins: any[] = []
12+
export type Plugin = {
13+
name: string
14+
default: (options: {page: playwright.Page; addFinding: (findingData: Finding) => void; url: string}) => Promise<void>
15+
}
16+
17+
const plugins: Plugin[] = []
1318
let pluginsLoaded = false
1419

1520
export async function loadPlugins() {
@@ -36,7 +41,6 @@ export const abortError = [
3641
].join('\n')
3742

3843
export function clearCache() {
39-
console.log('clearing plugin cache')
4044
pluginsLoaded = false
4145
plugins.length = 0
4246
}
Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,36 @@
11
import core from '@actions/core'
22

33
type ScansContext = {
4-
scans: Array<string>
4+
scansToPerform: Array<string>
55
shouldPerformAxeScan: boolean
6+
shouldRunPlugins: boolean
67
}
78
let scansContext: ScansContext | undefined
89

910
export function getScansContext() {
1011
if (!scansContext) {
1112
const scansJson = core.getInput('scans', {required: false})
12-
const scans = JSON.parse(scansJson || '{}')
13+
console.log('scans input: ', scansJson)
14+
const scansToPerform = JSON.parse(scansJson || '{}')
15+
// - if we dont have a scans input
16+
// or we do have a scans input, but it only has 1 item and its 'axe'
17+
// then we only want to run 'axe' and not the plugins
18+
const onlyAxeScan = scansToPerform.length === 0 || (scansToPerform.length === 1 && scansToPerform[0] === 'axe')
1319

1420
scansContext = {
15-
scans,
21+
scansToPerform,
1622
// - if no 'scans' input is provided, we default to the existing behavior
1723
// (only axe scan) for backwards compatability.
1824
// - we can enforce using the 'scans' input in a future major release and
1925
// mark it as required
20-
shouldPerformAxeScan: !scansJson || scans.includes('axe'),
26+
shouldPerformAxeScan: !scansJson || scansToPerform.includes('axe'),
27+
shouldRunPlugins: scansToPerform.length > 0 && !onlyAxeScan,
2128
}
2229
}
2330

2431
return scansContext
2532
}
33+
34+
export function clearCache() {
35+
scansContext = undefined
36+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import {describe, it, expect, vi} from 'vitest'
2+
import core from '@actions/core'
3+
import {findForUrl} from '../src/findForUrl.js'
4+
import AxeBuilder from '@axe-core/playwright'
5+
import axe from 'axe-core'
6+
import * as pluginManager from '../src/pluginManager.js'
7+
import {clearCache} from '../src/scansContextProvider.js'
8+
9+
vi.mock('playwright', () => ({
10+
default: {
11+
chromium: {
12+
launch: () => ({
13+
newContext: () => ({
14+
newPage: () => ({
15+
pageUrl: '',
16+
goto: () => {},
17+
url: () => {},
18+
}),
19+
close: () => {},
20+
}),
21+
close: () => {},
22+
}),
23+
},
24+
},
25+
}))
26+
27+
vi.mock('@axe-core/playwright', () => {
28+
const AxeBuilderMock = vi.fn()
29+
AxeBuilderMock.prototype.analyze = vi.fn(() => {
30+
console.log('calling mock analyze')
31+
return Promise.resolve({violations: []} as unknown as axe.AxeResults)
32+
})
33+
return {default: AxeBuilderMock}
34+
})
35+
36+
let actionInput: string = ''
37+
let loadedPlugins: pluginManager.Plugin[] = []
38+
39+
function clearAll() {
40+
clearCache()
41+
vi.clearAllMocks()
42+
}
43+
44+
describe('findForUrl', () => {
45+
vi.spyOn(core, 'getInput').mockImplementation(() => actionInput)
46+
vi.spyOn(pluginManager, 'loadPlugins').mockImplementation(() => Promise.resolve(loadedPlugins))
47+
48+
async function axeOnlyTest() {
49+
clearAll()
50+
51+
await findForUrl('test.com')
52+
expect(AxeBuilder.prototype.analyze).toHaveBeenCalledTimes(1)
53+
expect(pluginManager.loadPlugins).toHaveBeenCalledTimes(0)
54+
}
55+
56+
describe('when no scans list is provided', () => {
57+
it('defaults to running only axe scan', async () => {
58+
actionInput = ''
59+
await axeOnlyTest()
60+
})
61+
})
62+
63+
describe('when a scans list is provided', () => {
64+
describe('and the list _only_ includes axe', () => {
65+
it('runs only the axe scan', async () => {
66+
actionInput = JSON.stringify(['axe'])
67+
await axeOnlyTest()
68+
})
69+
})
70+
71+
describe('and the list includes axe and other scans', () => {
72+
it('runs axe and plugins', async () => {
73+
actionInput = JSON.stringify(['axe', 'custom-scan'])
74+
clearAll()
75+
76+
await findForUrl('test.com')
77+
expect(AxeBuilder.prototype.analyze).toHaveBeenCalledTimes(1)
78+
expect(pluginManager.loadPlugins).toHaveBeenCalledTimes(1)
79+
})
80+
})
81+
82+
describe('and the list does not include axe', () => {
83+
it('only runs plugins', async () => {
84+
actionInput = JSON.stringify(['custom-scan'])
85+
clearAll()
86+
87+
await findForUrl('test.com')
88+
expect(AxeBuilder.prototype.analyze).toHaveBeenCalledTimes(0)
89+
expect(pluginManager.loadPlugins).toHaveBeenCalledTimes(1)
90+
})
91+
})
92+
93+
it('should only run scans that are included in the list', async () => {
94+
loadedPlugins = [
95+
{name: 'custom-scan-1', default: vi.fn()},
96+
{name: 'custom-scan-2', default: vi.fn()},
97+
]
98+
actionInput = JSON.stringify(['custom-scan-1'])
99+
clearAll()
100+
101+
await findForUrl('test.com')
102+
expect(loadedPlugins[0].default).toHaveBeenCalledTimes(1)
103+
expect(loadedPlugins[1].default).toHaveBeenCalledTimes(0)
104+
})
105+
})
106+
})

0 commit comments

Comments
 (0)