Skip to content

Add plugins#133

Open
abdulahmad307 wants to merge 65 commits intomainfrom
abdul/test-plugins
Open

Add plugins#133
abdulahmad307 wants to merge 65 commits intomainfrom
abdul/test-plugins

Conversation

@abdulahmad307
Copy link

@abdulahmad307 abdulahmad307 commented Feb 18, 2026

This is a very minimal proof of concept around the idea of using plugins to make additional scans and tests modular. Looking for feedback on general approach, pros/cons, etc...

this currently works with the plugins defined in the scanner repo. I'm still working on making this work with plugins defined in the workflow repo (the repo that uses the action). There might be some issues with using absolute paths with the dynamic import() feature - but i'm still testing.

I've taken @lindseywild’s reflow test and added it as a plugin for testing. the idea is each plugin lives under a new folder under the scanner-plugins folder (which lives under .github like actions). and each plugin has a primary index.js file, which we will use to interface with the plugin.

all of this is just how I've built it as a proposal - we can tweak naming/folder-structure, etc... if we feel something else makes more sense

@@ -0,0 +1,32 @@
export default async function test({ page, addFinding, url } = {}) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if this file should be included 🤔 - is this the final/valid way to test for reflow?

// - alternatively, we can wrap the 'plugin.default(...)' call in another function
// and make assertions on whether that function was called or not
// - the other option is to wrap each plugin in a class instance
// and make assertions on something like 'plugin.run' being called or not
Copy link
Author

@abdulahmad307 abdulahmad307 Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm open to suggestions here: we can leave it as is if everyone is ok with it, or we can implement another solution as outlined in the comment (or if someone has a better approach)

@@ -0,0 +1,9 @@
// - this exists because I'm not sure how to mock
// the dynamic import function, so mocking this instead
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, i'm worried that mocking it will break imports across the board. and i couldn't find anything useful on google

@@ -1,27 +1,30 @@
name: "Find"
description: "Finds potential accessibility gaps."
name: 'Find'
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all these quote changes were automatically done by prettier

"name": "reflow-plugin-test",
"version": "1.0.0",
"description": "A test plugin for reflow testing",
"type": "module"
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was mainly added so we can tell node that this is a module to avoid the dynamic conversion at run-time

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a proof-of-concept plugin system to make additional accessibility scans and tests modular. The implementation allows the scanner to load and execute custom test plugins alongside the existing axe-core scanner, with built-in and custom plugin support.

Changes:

  • Added plugin architecture with dynamic loading from .github/scanner-plugins/ directories
  • Introduced configurable scans input to control which scanners/plugins to execute
  • Implemented a reflow-test plugin as a proof-of-concept to check for horizontal scrolling at 320x256 viewport

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
action.yml Adds shell script to copy scanner-plugins directory to action workspace
.gitignore Adds .vscode to ignored files
.github/scanner-plugins/reflow-test/package.json Package metadata for the reflow test plugin
.github/scanner-plugins/reflow-test/index.js Implementation of the reflow test plugin that checks viewport scrolling
.github/actions/find/tests/pluginManager.test.ts Unit tests for plugin loading and caching functionality
.github/actions/find/tests/findForUrl.test.ts Unit tests for the updated findForUrl with plugin support
.github/actions/find/src/scansContextProvider.ts Provides context for determining which scans to perform based on input
.github/actions/find/src/pluginManager.ts Core plugin loading logic for built-in and custom plugins
.github/actions/find/src/findForUrl.ts Updated to support plugin execution alongside axe scans
.github/actions/find/src/dynamicImport.ts Wrapper for dynamic imports to enable mocking in tests
.github/actions/find/action.yml Adds new scans input parameter and formatting changes
.github/actions/find/README.md Documents the new scans input parameter
Comments suppressed due to low confidence (4)

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

  • Using string concatenation with process.cwd() + '/.github/scanner-plugins/' is less robust than using path.join(). The current approach could cause issues with path separators on different operating systems. Consider using path.join(process.cwd(), '.github', 'scanner-plugins') for consistency and cross-platform compatibility, similar to how line 54 handles built-in plugins.
  const pluginsPath = process.cwd() + '/.github/scanner-plugins/'

.github/actions/find/src/scansContextProvider.ts:23

  • Spelling error: "compatability" should be "compatibility".
      // - if no 'scans' input is provided, we default to the existing behavior
      //   (only axe scan) for backwards compatability.

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

  • There's a potential race condition in the plugin loading logic. If loadPlugins() is called concurrently from multiple places before the first call completes, both calls will see pluginsLoaded as false and both will attempt to load plugins simultaneously, potentially causing duplicate plugins to be added to the array. While this may not be an issue in the current single-threaded JavaScript execution model, consider adding a loading state or using a promise-based lock to prevent concurrent loading attempts, especially if this code might be used in parallel contexts in the future.
export async function loadPlugins() {
  console.log('loading plugins')

  try {
    if (!pluginsLoaded) {
      await loadBuiltInPlugins()
      await loadCustomPlugins()
    }
  } catch {
    plugins.length = 0
    console.log(abortError)
  } finally {
    pluginsLoaded = true
    return plugins
  }

.github/actions/find/src/scansContextProvider.ts:15

  • Grammar issue: "its" should be "it's" (contraction of "it is").
    //   or we do have a scans input, but it only has 1 item and its 'axe'

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

fi
mkdir -p "${ACTION_DIR}/.github/scanner-plugins"
if [ "$(realpath ".github/scanner-plugins")" != "$(realpath "${ACTION_DIR}/.github/scanner-plugins")" ]; then
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shell script doesn't check if the .github/scanner-plugins directory exists in the source before attempting to copy. If the directory doesn't exist (e.g., in a workflow repo that doesn't define custom plugins), the cp -a command will fail. Consider adding an existence check before the copy operation, similar to how it's done for the .github/actions directory on line 54. For example: if [ -d ".github/scanner-plugins" ] && [ "$(realpath ".github/scanner-plugins")" != "$(realpath "${ACTION_DIR}/.github/scanner-plugins")" ]; then

Suggested change
if [ "$(realpath ".github/scanner-plugins")" != "$(realpath "${ACTION_DIR}/.github/scanner-plugins")" ]; then
if [ -d ".github/scanner-plugins" ] && [ "$(realpath ".github/scanner-plugins")" != "$(realpath "${ACTION_DIR}/.github/scanner-plugins")" ]; then

Copilot uses AI. Check for mistakes.
- update plugin manager tests to account for new isDirectory check
}

const plugins: Plugin[] = []
let pluginsLoaded = false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may be missing something, but is this state needed vs. checking for plugins.length > 0?

Copy link
Author

@abdulahmad307 abdulahmad307 Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea so, if plugin loading fails for some reason, plugins.length will still be 0 - notice these lines:

} catch {
  plugins.length = 0
  console.log(abortError)Expand commentComment on lines R28 to R30Resolved
} finally {
  pluginsLoaded = true
  return plugins
}

however, even if we didn't explicitly reset plugins.length back to 0 on error, if we run into an error before any plugin is loaded, the length will still be 0.

In this case (as I'm sure you're already aware) the code will re-try on every page it reaches if we check against plugins.length > 0 (because findForUrl runs for each url, and will likely fail every time). To avoid this, I used a boolean (which is set to true regardless of failure). And the only reason I care about this running many times is because it could be considered a 'heavy' operation (reading the file system and iterating on the directories).

another way we can do this is to move the loadPlugins call to the start of the action so it only runs once, and then we can have another function to getPlugins that we call in findForUrl. This way we can omit the bool and the if condition altogether. The thing to consider here is if someone accidentally calls 'loadPlugins' somewhere else in the future, we end up running that logic more than once (potentially many times if it gets moved to findForUrl by accident). I'm ok with either way but I think keeping it as-is is safer.

On a personal level, I prefer the current approach because calling loadPlugins at the top of the action is a bit out of context - someone reading the code for the first time (or without knowing what plugins are or how they work) may not understand why we're calling that there and what it does in the grand-scheme of things. Not until they reach findForUrl will they understand why that exists. Whereas calling it in the place where we do need to work directly with the returned plugins gives it more context.

The other benefit (which is not critical in this case) is the current approach is lazy-loaded on demand. I don't think we care too much about this micro-optimization in an action run. It would be more significant in a user-facing program to delay loading of things all at once at the beginning of the program.

but either approach will get the job done. lmk what you think

@abdulahmad307 abdulahmad307 changed the title Add plugins - POC Add plugins Feb 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants