-
Notifications
You must be signed in to change notification settings - Fork 22
Add plugins #133
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
abdulahmad307
wants to merge
76
commits into
main
Choose a base branch
from
abdul/test-plugins
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Add plugins #133
Changes from 66 commits
Commits
Show all changes
76 commits
Select commit
Hold shift + click to select a range
af39fad
testing
abdulahmad307 56272ed
testing
abdulahmad307 6e30cf0
testing
abdulahmad307 99f629a
test
abdulahmad307 28c2f32
test
abdulahmad307 66d0e7b
test
abdulahmad307 c18fa60
test
abdulahmad307 c1a88ee
try hardcoded string
abdulahmad307 4bcbc50
test import
abdulahmad307 177b498
try require
abdulahmad307 8568551
testing local index file
abdulahmad307 26f7450
testing plugin usage
abdulahmad307 920fd85
try new plugin folder
abdulahmad307 0dbb21d
update local path lookup
abdulahmad307 0df2385
update local path lookup
abdulahmad307 276016c
update local path lookup
abdulahmad307 cf05219
update local path lookup
abdulahmad307 6580781
update local path lookup
abdulahmad307 93609d4
update local path lookup
abdulahmad307 e3aad3e
update local path lookup
abdulahmad307 05a12ad
update local path lookup
abdulahmad307 c61c2ca
update local path lookup
abdulahmad307 b0d19ec
update local path lookup
abdulahmad307 c6929eb
add all new content
abdulahmad307 500fba2
log dirs
abdulahmad307 aa20c65
log dirs
abdulahmad307 b0a666c
log dirs
abdulahmad307 b87a5d8
log dirs
abdulahmad307 b46b2e8
log dirs
abdulahmad307 bc41a57
test import
abdulahmad307 0742390
test import
abdulahmad307 712291e
test import
abdulahmad307 774622a
test import
abdulahmad307 286bb84
test reflow plugin
abdulahmad307 97ca241
move findings back into function
abdulahmad307 babf756
remove unused test folder
abdulahmad307 626723a
update code structure
abdulahmad307 0dcb9d6
update code structure
abdulahmad307 72a9ea8
test reading files for custom plugins
abdulahmad307 426734d
test reading files for custom plugins
abdulahmad307 dd97ecb
test reading files for custom plugins
abdulahmad307 538185b
test with custom plugins
abdulahmad307 b89f21c
test with custom plugins
abdulahmad307 640b572
test with custom plugins
abdulahmad307 991fff4
test with custom plugins
abdulahmad307 beffd51
testing after refactor
abdulahmad307 3668d95
testing after refactor
abdulahmad307 0529838
testing after refactor
abdulahmad307 98b2037
move pluginManager to dedicated file to not bloat the 'find' file
abdulahmad307 c2de456
update messaging on plugin abort
abdulahmad307 ac003a1
merge main
abdulahmad307 321c623
add plugin manager test
abdulahmad307 0b87aaf
eslint cleanup
abdulahmad307 9a27f85
fix formatting
abdulahmad307 1d2753a
read scans input
abdulahmad307 cd4771b
add tests
abdulahmad307 407bc84
update comments
abdulahmad307 2e09eb0
remove console.log
abdulahmad307 a8ec788
remove .vscode and add to gitignore
abdulahmad307 600d158
clean up a line to make it easier to read
abdulahmad307 e86870e
update action and readme
abdulahmad307 4e43b7f
fix casing for 'scans' in readme
abdulahmad307 8402e61
copilot PR feedback
abdulahmad307 a1d4095
update string literal
abdulahmad307 64c996c
update code comments with more context
abdulahmad307 74f131f
update comment
abdulahmad307 2ce029f
PR feedback
abdulahmad307 fbc7513
change reflow test to 'test-plugin'
abdulahmad307 8f9b250
merge main
abdulahmad307 6ffffc7
fix prettier stuff
abdulahmad307 765d845
add plugin docs
abdulahmad307 7a17cc6
update docs with minor detail
abdulahmad307 0359d54
move plugin docs to standalone file
abdulahmad307 d75e16e
Merge branch 'main' into abdul/test-plugins
abdulahmad307 9f9bb3d
update docs more
abdulahmad307 59da986
update pulgins docs
abdulahmad307 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,27 +1,30 @@ | ||
| name: "Find" | ||
| description: "Finds potential accessibility gaps." | ||
| name: 'Find' | ||
abdulahmad307 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| description: 'Finds potential accessibility gaps.' | ||
|
|
||
| inputs: | ||
| urls: | ||
| description: "Newline-delimited list of URLs to check for accessibility issues" | ||
| description: 'Newline-delimited list of URLs to check for accessibility issues' | ||
| required: true | ||
| multiline: true | ||
| auth_context: | ||
| description: "Stringified JSON object containing 'username', 'password', 'cookies', and/or 'localStorage' from an authenticated session" | ||
| required: false | ||
| include_screenshots: | ||
| description: "Whether to capture screenshots of scanned pages and include links to them in the issue" | ||
| description: 'Whether to capture screenshots of scanned pages and include links to them in the issue' | ||
| required: false | ||
| default: 'false' | ||
| scans: | ||
| description: 'Stringified JSON array of scans to perform. If not provided, only axe will be performed' | ||
| required: false | ||
| default: "false" | ||
|
|
||
| outputs: | ||
| findings: | ||
| description: "List of potential accessibility gaps, as stringified JSON" | ||
| description: 'List of potential accessibility gaps, as stringified JSON' | ||
|
|
||
| runs: | ||
| using: "node24" | ||
| main: "bootstrap.js" | ||
| using: 'node24' | ||
| main: 'bootstrap.js' | ||
|
|
||
| branding: | ||
| icon: "compass" | ||
| color: "blue" | ||
| icon: 'compass' | ||
| color: 'blue' | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| // - this exists because it looks like there's no straight-forward | ||
| // way to mock the dynamic import function, so mocking this instead | ||
| // (also, if it _is_ possible to mock the dynamic import, | ||
| // there's the risk of altering/breaking the behavior of imports | ||
| // across the board - including non-dynamic imports) | ||
| // | ||
| // - also, vitest has a limitation on mocking: | ||
| // https://vitest.dev/guide/mocking/modules.html#mocking-modules-pitfalls | ||
| // | ||
| // - basically if a function is called by another function in the same file | ||
| // it can't be mocked. So this was extracted into a separate file | ||
| // | ||
| // - one thing to note is vitest does the same thing here: | ||
| // https://github.com/vitest-dev/vitest/blob/main/test/core/src/dynamic-import.ts | ||
| // - and uses that with tests here: | ||
| // https://github.com/vitest-dev/vitest/blob/main/test/core/test/mock-internals.test.ts#L27 | ||
| // | ||
| // - so this looks like a reasonable approach | ||
| export async function dynamicImport(path: string) { | ||
| return import(path) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| import * as fs from 'fs' | ||
| import * as path from 'path' | ||
| import {fileURLToPath} from 'url' | ||
| import {dynamicImport} from './dynamicImport.js' | ||
| import type {Finding} from './types.d.js' | ||
| import playwright from 'playwright' | ||
|
|
||
| // Helper to get __dirname equivalent in ES Modules | ||
| const __filename = fileURLToPath(import.meta.url) | ||
| const __dirname = path.dirname(__filename) | ||
|
|
||
| export type Plugin = { | ||
| name: string | ||
| default: (options: {page: playwright.Page; addFinding: (findingData: Finding) => void; url: string}) => Promise<void> | ||
| } | ||
|
|
||
| const plugins: Plugin[] = [] | ||
| let pluginsLoaded = false | ||
abdulahmad307 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| export async function loadPlugins() { | ||
| console.log('loading plugins') | ||
|
|
||
| try { | ||
| if (!pluginsLoaded) { | ||
| await loadBuiltInPlugins() | ||
| await loadCustomPlugins() | ||
| } | ||
| } catch { | ||
| plugins.length = 0 | ||
| console.log(abortError) | ||
abdulahmad307 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } finally { | ||
| pluginsLoaded = true | ||
| return plugins | ||
| } | ||
| } | ||
|
|
||
| export const abortError = ` | ||
| There was an error while loading plugins. | ||
| Clearing all plugins and aborting custom plugin scans. | ||
| Please check the logs for hints as to what may have gone wrong. | ||
| ` | ||
|
|
||
| export function clearCache() { | ||
| pluginsLoaded = false | ||
| plugins.length = 0 | ||
| } | ||
|
|
||
| // exported for mocking/testing. not for actual use | ||
| export async function loadBuiltInPlugins() { | ||
| console.log('Loading built-in plugins') | ||
|
|
||
| const pluginsPath = '../../../scanner-plugins/' | ||
| await loadPluginsFromPath({ | ||
| readPath: path.join(__dirname, pluginsPath), | ||
| importPath: pluginsPath, | ||
| }) | ||
| } | ||
|
|
||
| // exported for mocking/testing. not for actual use | ||
| export async function loadCustomPlugins() { | ||
| console.log('Loading custom plugins') | ||
|
|
||
| const pluginsPath = process.cwd() + '/.github/scanner-plugins/' | ||
| await loadPluginsFromPath({ | ||
| readPath: pluginsPath, | ||
| importPath: pluginsPath, | ||
| }) | ||
| } | ||
|
|
||
| // exported for mocking/testing. not for actual use | ||
| export async function loadPluginsFromPath({readPath, importPath}: {readPath: string; importPath: string}) { | ||
| try { | ||
| const res = fs.readdirSync(readPath) | ||
| for (const pluginFolder of res) { | ||
abdulahmad307 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const pluginFolderPath = path.join(importPath, pluginFolder) | ||
| if (fs.lstatSync(pluginFolderPath).isDirectory()) { | ||
| console.log('Found plugin: ', pluginFolder) | ||
| plugins.push(await dynamicImport(path.join(importPath, pluginFolder, '/index.js'))) | ||
| } | ||
| } | ||
| } catch (e) { | ||
| // - log errors here for granular info | ||
| console.log('error: ') | ||
| console.log(e) | ||
| // - throw error to handle aborting the plugin scans | ||
| throw e | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import core from '@actions/core' | ||
|
|
||
| type ScansContext = { | ||
| scansToPerform: Array<string> | ||
| shouldPerformAxeScan: boolean | ||
| shouldRunPlugins: boolean | ||
| } | ||
| let scansContext: ScansContext | undefined | ||
|
|
||
| export function getScansContext() { | ||
| if (!scansContext) { | ||
| const scansJson = core.getInput('scans', {required: false}) | ||
abdulahmad307 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| const scansToPerform = JSON.parse(scansJson || '[]') | ||
| // - if we don't have a scans input | ||
| // or we do have a scans input, but it only has 1 item and its 'axe' | ||
| // then we only want to run 'axe' and not the plugins | ||
| // - keep in mind, 'onlyAxeScan' is not the same as 'shouldPerformAxeScan' | ||
| const onlyAxeScan = scansToPerform.length === 0 || (scansToPerform.length === 1 && scansToPerform[0] === 'axe') | ||
|
|
||
| scansContext = { | ||
| scansToPerform, | ||
| // - if no 'scans' input is provided, we default to the existing behavior | ||
| // (only axe scan) for backwards compatability. | ||
| // - we can enforce using the 'scans' input in a future major release and | ||
| // mark it as required | ||
| shouldPerformAxeScan: !scansJson || scansToPerform.includes('axe'), | ||
| shouldRunPlugins: scansToPerform.length > 0 && !onlyAxeScan, | ||
| } | ||
| } | ||
|
|
||
| return scansContext | ||
| } | ||
|
|
||
| export function clearCache() { | ||
| scansContext = undefined | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| import {describe, it, expect, vi} from 'vitest' | ||
| import core from '@actions/core' | ||
| import {findForUrl} from '../src/findForUrl.js' | ||
| import AxeBuilder from '@axe-core/playwright' | ||
| import axe from 'axe-core' | ||
| import * as pluginManager from '../src/pluginManager.js' | ||
| import {clearCache} from '../src/scansContextProvider.js' | ||
|
|
||
| vi.mock('playwright', () => ({ | ||
| default: { | ||
| chromium: { | ||
| launch: () => ({ | ||
| newContext: () => ({ | ||
| newPage: () => ({ | ||
| pageUrl: '', | ||
| goto: () => {}, | ||
| url: () => {}, | ||
| }), | ||
| close: () => {}, | ||
| }), | ||
| close: () => {}, | ||
| }), | ||
| }, | ||
| }, | ||
| })) | ||
|
|
||
| vi.mock('@axe-core/playwright', () => { | ||
| const AxeBuilderMock = vi.fn() | ||
| const rawFinding = {violations: []} as unknown as axe.AxeResults | ||
| AxeBuilderMock.prototype.analyze = vi.fn(() => Promise.resolve(rawFinding)) | ||
| return {default: AxeBuilderMock} | ||
| }) | ||
|
|
||
| let actionInput: string = '' | ||
| let loadedPlugins: pluginManager.Plugin[] = [] | ||
|
|
||
| function clearAll() { | ||
| clearCache() | ||
| vi.clearAllMocks() | ||
| } | ||
|
|
||
| describe('findForUrl', () => { | ||
| vi.spyOn(core, 'getInput').mockImplementation(() => actionInput) | ||
| vi.spyOn(pluginManager, 'loadPlugins').mockImplementation(() => Promise.resolve(loadedPlugins)) | ||
|
|
||
| async function axeOnlyTest() { | ||
| clearAll() | ||
|
|
||
| await findForUrl('test.com') | ||
| expect(AxeBuilder.prototype.analyze).toHaveBeenCalledTimes(1) | ||
| expect(pluginManager.loadPlugins).toHaveBeenCalledTimes(0) | ||
| } | ||
|
|
||
| describe('when no scans list is provided', () => { | ||
| it('defaults to running only axe scan', async () => { | ||
| actionInput = '' | ||
| await axeOnlyTest() | ||
| }) | ||
| }) | ||
|
|
||
| describe('when a scans list is provided', () => { | ||
| describe('and the list _only_ includes axe', () => { | ||
| it('runs only the axe scan', async () => { | ||
| actionInput = JSON.stringify(['axe']) | ||
| await axeOnlyTest() | ||
| }) | ||
| }) | ||
|
|
||
| describe('and the list includes axe and other scans', () => { | ||
| it('runs axe and plugins', async () => { | ||
| actionInput = JSON.stringify(['axe', 'custom-scan']) | ||
| clearAll() | ||
|
|
||
| await findForUrl('test.com') | ||
| expect(AxeBuilder.prototype.analyze).toHaveBeenCalledTimes(1) | ||
| expect(pluginManager.loadPlugins).toHaveBeenCalledTimes(1) | ||
| }) | ||
| }) | ||
|
|
||
| describe('and the list does not include axe', () => { | ||
| it('only runs plugins', async () => { | ||
| actionInput = JSON.stringify(['custom-scan']) | ||
| clearAll() | ||
|
|
||
| await findForUrl('test.com') | ||
| expect(AxeBuilder.prototype.analyze).toHaveBeenCalledTimes(0) | ||
| expect(pluginManager.loadPlugins).toHaveBeenCalledTimes(1) | ||
| }) | ||
| }) | ||
|
|
||
| it('should only run scans that are included in the list', async () => { | ||
| loadedPlugins = [ | ||
| {name: 'custom-scan-1', default: vi.fn()}, | ||
| {name: 'custom-scan-2', default: vi.fn()}, | ||
| ] | ||
| actionInput = JSON.stringify(['custom-scan-1']) | ||
| clearAll() | ||
|
|
||
| await findForUrl('test.com') | ||
| expect(loadedPlugins[0].default).toHaveBeenCalledTimes(1) | ||
| expect(loadedPlugins[1].default).toHaveBeenCalledTimes(0) | ||
| }) | ||
| }) | ||
| }) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.