From 5edd8b4c17a2ca755c23b029df682593165c976d Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:16:28 +0000 Subject: [PATCH 01/18] Adds reflow scan plugin --- .github/actions/file/src/generateIssueBody.ts | 2 +- .github/actions/file/src/openIssue.ts | 5 +++- .github/actions/file/src/types.d.ts | 4 ++-- .../file/src/updateFilingsWithNewFindings.ts | 2 +- .../file/tests/generateIssueBody.test.ts | 15 ++++++++++++ .github/actions/find/src/findForUrl.ts | 1 + .github/actions/find/src/pluginManager.ts | 5 ++-- .github/actions/find/src/types.d.ts | 2 +- .../{test-plugin => reflow-scan}/index.js | 23 +++++++------------ .../scanner-plugins/reflow-scan/package.json | 6 +++++ .../scanner-plugins/test-plugin/package.json | 6 ----- PLUGINS.md | 2 +- README.md | 3 ++- tests/site-with-errors.test.ts | 8 +++++++ tests/types.d.ts | 2 +- 15 files changed, 54 insertions(+), 32 deletions(-) rename .github/scanner-plugins/{test-plugin => reflow-scan}/index.js (58%) create mode 100644 .github/scanner-plugins/reflow-scan/package.json delete mode 100644 .github/scanner-plugins/test-plugin/package.json diff --git a/.github/actions/file/src/generateIssueBody.ts b/.github/actions/file/src/generateIssueBody.ts index a216cddf..941f597a 100644 --- a/.github/actions/file/src/generateIssueBody.ts +++ b/.github/actions/file/src/generateIssueBody.ts @@ -26,7 +26,7 @@ export function generateIssueBody(finding: Finding, screenshotRepo: string): str ` const body = `## What - An accessibility scan flagged the element \`${finding.html}\` on ${finding.url} because ${finding.problemShort}. Learn more about why this was flagged by visiting ${finding.problemUrl}. + An accessibility scan ${finding.html ? `flagged the element \`${finding.html}\`` : `found an issue on ${finding.url}`} because ${finding.problemShort}. Learn more about why this was flagged by visiting ${finding.problemUrl}. ${screenshotSection ?? ''} To fix this, ${finding.solutionShort}. diff --git a/.github/actions/file/src/openIssue.ts b/.github/actions/file/src/openIssue.ts index 2297daa4..d4ab452c 100644 --- a/.github/actions/file/src/openIssue.ts +++ b/.github/actions/file/src/openIssue.ts @@ -21,7 +21,10 @@ export async function openIssue(octokit: Octokit, repoWithOwner: string, finding const owner = repoWithOwner.split('/')[0] const repo = repoWithOwner.split('/')[1] - const labels = [`${finding.scannerType} rule: ${finding.ruleId}`, `${finding.scannerType}-scanning-issue`] + const labels = [ + `${finding.scannerType}${finding.ruleId ? ` rule: ${finding.ruleId}` : ''}`, + `${finding.scannerType}-scanning-issue`, + ] const title = truncateWithEllipsis( `Accessibility issue: ${finding.problemShort[0].toUpperCase() + finding.problemShort.slice(1)} on ${new URL(finding.url).pathname}`, GITHUB_ISSUE_TITLE_MAX_LENGTH, diff --git a/.github/actions/file/src/types.d.ts b/.github/actions/file/src/types.d.ts index 2c0c8ac7..ee91bc67 100644 --- a/.github/actions/file/src/types.d.ts +++ b/.github/actions/file/src/types.d.ts @@ -1,8 +1,8 @@ export type Finding = { scannerType: string - ruleId: string + ruleId?: string url: string - html: string + html?: string problemShort: string problemUrl: string solutionShort: string diff --git a/.github/actions/file/src/updateFilingsWithNewFindings.ts b/.github/actions/file/src/updateFilingsWithNewFindings.ts index a674e687..acbfdf9f 100644 --- a/.github/actions/file/src/updateFilingsWithNewFindings.ts +++ b/.github/actions/file/src/updateFilingsWithNewFindings.ts @@ -5,7 +5,7 @@ function getFilingKey(filing: ResolvedFiling | RepeatedFiling): string { } function getFindingKey(finding: Finding): string { - return `${finding.url};${finding.ruleId};${finding.html}` + return `${finding.url};${finding.ruleId ?? ''};${finding.html ?? ''}` } export function updateFilingsWithNewFindings( diff --git a/.github/actions/file/tests/generateIssueBody.test.ts b/.github/actions/file/tests/generateIssueBody.test.ts index 96023b60..52a7b6e5 100644 --- a/.github/actions/file/tests/generateIssueBody.test.ts +++ b/.github/actions/file/tests/generateIssueBody.test.ts @@ -11,6 +11,14 @@ const baseFinding = { solutionShort: 'ensure the contrast between foreground and background colors meets WCAG thresholds', } +const findingWithEmptyOptionalFields = { + scannerType: 'reflow', + url: 'https://example.com/page', + problemShort: 'elements must meet minimum color contrast ratio thresholds', + problemUrl: 'https://dequeuniversity.com/rules/axe/4.10/color-contrast?application=playwright', + solutionShort: 'ensure the contrast between foreground and background colors meets WCAG thresholds', +} + describe('generateIssueBody', () => { it('includes acceptance criteria and omits the Specifically section when solutionLong is missing', () => { const body = generateIssueBody(baseFinding, 'github/accessibility-scanner') @@ -61,4 +69,11 @@ describe('generateIssueBody', () => { expect(body).not.toContain('View screenshot') expect(body).not.toContain('.screenshots') }) + + it('uses url fallback when html is not present', () => { + const body = generateIssueBody(findingWithEmptyOptionalFields, 'github/accessibility-scanner') + + expect(body).toContain(`found an issue on ${findingWithEmptyOptionalFields.url}`) + expect(body).not.toContain('flagged the element') + }) }) diff --git a/.github/actions/find/src/findForUrl.ts b/.github/actions/find/src/findForUrl.ts index eded5d11..aed74d7b 100644 --- a/.github/actions/find/src/findForUrl.ts +++ b/.github/actions/find/src/findForUrl.ts @@ -48,6 +48,7 @@ export async function findForUrl( plugin, page, addFinding, + url, }) } else { core.info(`Skipping plugin ${plugin.name} because it is not included in the 'scans' input`) diff --git a/.github/actions/find/src/pluginManager.ts b/.github/actions/find/src/pluginManager.ts index d93c585c..8fb68bbe 100644 --- a/.github/actions/find/src/pluginManager.ts +++ b/.github/actions/find/src/pluginManager.ts @@ -13,6 +13,7 @@ const __dirname = path.dirname(__filename) type PluginDefaultParams = { page: playwright.Page addFinding: (findingData: Finding) => void + url: string } type Plugin = { @@ -102,6 +103,6 @@ export async function loadPluginsFromPath({pluginsPath}: {pluginsPath: string}) type InvokePluginParams = PluginDefaultParams & { plugin: Plugin } -export function invokePlugin({plugin, page, addFinding}: InvokePluginParams) { - return plugin.default({page, addFinding}) +export function invokePlugin({plugin, page, addFinding, url}: InvokePluginParams) { + return plugin.default({page, addFinding, url}) } diff --git a/.github/actions/find/src/types.d.ts b/.github/actions/find/src/types.d.ts index e23dff9c..f8fb7205 100644 --- a/.github/actions/find/src/types.d.ts +++ b/.github/actions/find/src/types.d.ts @@ -1,7 +1,7 @@ export type Finding = { scannerType: string url: string - html: string + html?: string problemShort: string problemUrl: string solutionShort: string diff --git a/.github/scanner-plugins/test-plugin/index.js b/.github/scanner-plugins/reflow-scan/index.js similarity index 58% rename from .github/scanner-plugins/test-plugin/index.js rename to .github/scanner-plugins/reflow-scan/index.js index 32ac8996..2490eb1e 100644 --- a/.github/scanner-plugins/test-plugin/index.js +++ b/.github/scanner-plugins/reflow-scan/index.js @@ -1,6 +1,6 @@ -export default async function test({ page, addFinding, url } = {}) { - console.log('test plugin'); - // Check for horizontal scrolling at 320x256 viewport +export default async function reflowScan({ page, addFinding, url } = {}) { + console.log('reflow plugin'); + // Check for horizontal scrolling at 320x256 viewport try { await page.setViewportSize({ width: 320, height: 256 }); const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); @@ -8,25 +8,18 @@ export default async function test({ page, addFinding, url } = {}) { // If horizontal scroll is required (with 1px tolerance for rounding) if (scrollWidth > clientWidth + 1) { - const htmlSnippet = await page.evaluate(() => { - return ``; - }); - - addFinding({ - scannerType: 'viewport', - ruleId: 'horizontal-scroll-320x256', + await addFinding({ + scannerType: 'reflow-scan', url, - html: htmlSnippet.replace(/'/g, "'"), - problemShort: 'page requires horizontal scrolling at 320x256 viewport', + problemShort: 'Page requires horizontal scrolling at 320x256 viewport', problemUrl: 'https://www.w3.org/WAI/WCAG21/Understanding/reflow.html', - solutionShort: 'ensure content is responsive and does not require horizontal scrolling at small viewport sizes', + solutionShort: 'Ensure content is responsive and does not require horizontal scrolling at small viewport sizes', solutionLong: `The page has a scroll width of ${scrollWidth}px but a client width of only ${clientWidth}px at 320x256 viewport, requiring horizontal scrolling. This violates WCAG 2.1 Level AA Success Criterion 1.4.10 (Reflow).` }); } } catch (e) { console.error('Error checking horizontal scroll:', e); } - } -export const name = 'test-plugin'; +export const name = 'reflow-scan'; diff --git a/.github/scanner-plugins/reflow-scan/package.json b/.github/scanner-plugins/reflow-scan/package.json new file mode 100644 index 00000000..27ca8369 --- /dev/null +++ b/.github/scanner-plugins/reflow-scan/package.json @@ -0,0 +1,6 @@ +{ + "name": "reflow-scan", + "version": "1.0.0", + "description": "Scans pages at a 320x256 viewport size to identify potential reflow issues, such as horizontal scrolling and content overflow.", + "type": "module" +} diff --git a/.github/scanner-plugins/test-plugin/package.json b/.github/scanner-plugins/test-plugin/package.json deleted file mode 100644 index 5bc2b0c1..00000000 --- a/.github/scanner-plugins/test-plugin/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "reflow-plugin-test", - "version": "1.0.0", - "description": "A test plugin for reflow testing", - "type": "module" -} diff --git a/PLUGINS.md b/PLUGINS.md index 8d32fead..545bde8f 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -1,6 +1,6 @@ # Plugins -The plugin system allows teams to create custom scans/tests to run on their pages. An example of this is Axe interaction tests. In some cases, it might be desirable to perform specific interactions on elements of a given page before doing an Axe scan. These interactions are usually unique to each page that is scanned, so it would require the owning team to write a custom plugin that can interact with the page and run the Axe scan when ready. See the example under [.github/scanner-plugins/test-plugin](https://github.com/github/accessibility-scanner/tree/main/.github/scanner-plugins/test-plugin) (this is not an Axe interaction test, but should give a general understanding of plugin structure). +The plugin system allows teams to create custom scans/tests to run on their pages. An example of this is Axe interaction tests. In some cases, it might be desirable to perform specific interactions on elements of a given page before doing an Axe scan. These interactions are usually unique to each page that is scanned, so it would require the owning team to write a custom plugin that can interact with the page and run the Axe scan when ready. See the existing plugins under [.github/scanner-plugins](https://github.com/github/accessibility-scanner/tree/main/.github/scanner-plugins) for examples of plugin structure. Some plugins come built-in with the scanner and can be enabled via [actions inputs](https://github.com/github/accessibility-scanner/tree/main/action.yml#L48-L50). diff --git a/README.md b/README.md index 5431a31e..c4d1e465 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ jobs: # open_grouped_issues: false # Optional: Set to true to open an issue grouping individual issues per violation # reduced_motion: no-preference # Optional: Playwright reduced motion configuration option # color_scheme: light # Optional: Playwright color scheme configuration option + # scans #Optional: An array of scans (or plugins) to be performed. If not provided, only Axe will be performed. ``` > 👉 Update all `REPLACE_THIS` placeholders with your actual values. See [Action Inputs](#action-inputs) for details. @@ -125,7 +126,7 @@ Trigger the workflow manually or automatically based on your configuration. The | `include_screenshots` | No | Whether to capture screenshots of scanned pages and include links to them in filed issues. Screenshots are stored on the `gh-cache` branch of the repository running the workflow. Default: `false` | `true` | | `reduced_motion` | No | Playwright `reducedMotion` setting for scan contexts. Allowed values: `reduce`, `no-preference` | `reduce` | | `color_scheme` | No | Playwright `colorScheme` setting for scan contexts. Allowed values: `light`, `dark`, `no-preference` | `dark` | -| `scans` | No | An array of scans (or plugins) to be performed. If not provided, only Axe will be performed. | `['axe', ...other plugins]` | +| `scans` | No | An array of scans (or plugins) to be performed. If not provided, only Axe will be performed. | `['axe', 'reflow-scan', ...other plugins]` | --- diff --git a/tests/site-with-errors.test.ts b/tests/site-with-errors.test.ts index 24d1129a..54a6917c 100644 --- a/tests/site-with-errors.test.ts +++ b/tests/site-with-errors.test.ts @@ -107,6 +107,13 @@ describe('site-with-errors', () => { ruleId: 'empty-heading', solutionShort: 'ensure headings have discernible text', }, + { + scannerType: 'reflow', + url: 'http://127.0.0.1:4000/404.html', + problemShort: 'Page requires horizontal scrolling at 320x256 viewport', + solutionShort: 'Ensure content is responsive and does not require horizontal scrolling at small viewport sizes', + problemUrl: 'https://www.w3.org/WAI/WCAG21/Understanding/reflow.html', + }, ] // Check that: // - every expected object exists (no more and no fewer), and @@ -153,6 +160,7 @@ describe('site-with-errors', () => { 'Accessibility issue: Headings should not be empty on /404.html', 'Accessibility issue: Elements must meet minimum color contrast ratio thresholds on /about/', 'Accessibility issue: Elements must meet minimum color contrast ratio thresholds on /jekyll/update/2025/07/30/welcome-to-jekyll.html', + 'Accessibility issue: Page requires horizontal scrolling at 320x256 viewport on /404.html', ] expect(actualTitles).toHaveLength(expectedTitles.length) expect(actualTitles).toEqual(expect.arrayContaining(expectedTitles)) diff --git a/tests/types.d.ts b/tests/types.d.ts index cc2c15e0..8fdf46ab 100644 --- a/tests/types.d.ts +++ b/tests/types.d.ts @@ -2,7 +2,7 @@ export type Finding = { scannerType: string ruleId: string url: string - html: string + html?: string problemShort: string problemUrl: string solutionShort: string From 6ac9324825d7f0e20c0867971449625a4fade49c Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:34:16 +0000 Subject: [PATCH 02/18] Updates README to include quotes around scans input --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c4d1e465..1f614dba 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ Trigger the workflow manually or automatically based on your configuration. The | `include_screenshots` | No | Whether to capture screenshots of scanned pages and include links to them in filed issues. Screenshots are stored on the `gh-cache` branch of the repository running the workflow. Default: `false` | `true` | | `reduced_motion` | No | Playwright `reducedMotion` setting for scan contexts. Allowed values: `reduce`, `no-preference` | `reduce` | | `color_scheme` | No | Playwright `colorScheme` setting for scan contexts. Allowed values: `light`, `dark`, `no-preference` | `dark` | -| `scans` | No | An array of scans (or plugins) to be performed. If not provided, only Axe will be performed. | `['axe', 'reflow-scan', ...other plugins]` | +| `scans` | No | An array of scans (or plugins) to be performed. If not provided, only Axe will be performed. | `'["axe", "reflow-scan", ...other plugins]'` | --- From 1a50d8132c51d836dbf039905841a0f4f3fe5504 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:57:11 +0000 Subject: [PATCH 03/18] Copilot updates + minor issue body changes --- .github/actions/file/src/generateIssueBody.ts | 4 ++-- .../actions/file/src/updateFilingsWithNewFindings.ts | 2 +- .github/actions/find/src/pluginManager.ts | 2 +- .github/scanner-plugins/reflow-scan/index.js | 8 ++++---- .github/workflows/test.yml | 1 + PLUGINS.md | 4 ++++ README.md | 2 +- sites/site-with-errors/404.html | 6 ++++++ tests/site-with-errors.test.ts | 10 +++++----- 9 files changed, 25 insertions(+), 14 deletions(-) diff --git a/.github/actions/file/src/generateIssueBody.ts b/.github/actions/file/src/generateIssueBody.ts index 941f597a..a3cb0308 100644 --- a/.github/actions/file/src/generateIssueBody.ts +++ b/.github/actions/file/src/generateIssueBody.ts @@ -19,9 +19,9 @@ export function generateIssueBody(finding: Finding, screenshotRepo: string): str } const acceptanceCriteria = `## Acceptance Criteria - - [ ] The specific axe violation reported in this issue is no longer reproducible. + - [ ] The specific violation reported in this issue is no longer reproducible. - [ ] The fix MUST meet WCAG 2.1 guidelines OR the accessibility standards specified by the repository or organization. - - [ ] A test SHOULD be added to ensure this specific axe violation does not regress. + - [ ] A test SHOULD be added to ensure this specific violation does not regress. - [ ] This PR MUST NOT introduce any new accessibility issues or regressions. ` diff --git a/.github/actions/file/src/updateFilingsWithNewFindings.ts b/.github/actions/file/src/updateFilingsWithNewFindings.ts index acbfdf9f..5842c606 100644 --- a/.github/actions/file/src/updateFilingsWithNewFindings.ts +++ b/.github/actions/file/src/updateFilingsWithNewFindings.ts @@ -5,7 +5,7 @@ function getFilingKey(filing: ResolvedFiling | RepeatedFiling): string { } function getFindingKey(finding: Finding): string { - return `${finding.url};${finding.ruleId ?? ''};${finding.html ?? ''}` + return `${finding.url};${finding.scannerType};${finding.problemUrl}` } export function updateFilingsWithNewFindings( diff --git a/.github/actions/find/src/pluginManager.ts b/.github/actions/find/src/pluginManager.ts index 8fb68bbe..0a7d9b14 100644 --- a/.github/actions/find/src/pluginManager.ts +++ b/.github/actions/find/src/pluginManager.ts @@ -12,7 +12,7 @@ const __dirname = path.dirname(__filename) type PluginDefaultParams = { page: playwright.Page - addFinding: (findingData: Finding) => void + addFinding: (findingData: Finding) => Promise url: string } diff --git a/.github/scanner-plugins/reflow-scan/index.js b/.github/scanner-plugins/reflow-scan/index.js index 2490eb1e..28578703 100644 --- a/.github/scanner-plugins/reflow-scan/index.js +++ b/.github/scanner-plugins/reflow-scan/index.js @@ -11,11 +11,11 @@ export default async function reflowScan({ page, addFinding, url } = {}) { await addFinding({ scannerType: 'reflow-scan', url, - problemShort: 'Page requires horizontal scrolling at 320x256 viewport', + problemShort: 'page requires horizontal scrolling at 320x256 viewport', problemUrl: 'https://www.w3.org/WAI/WCAG21/Understanding/reflow.html', - solutionShort: 'Ensure content is responsive and does not require horizontal scrolling at small viewport sizes', - solutionLong: `The page has a scroll width of ${scrollWidth}px but a client width of only ${clientWidth}px at 320x256 viewport, requiring horizontal scrolling. This violates WCAG 2.1 Level AA Success Criterion 1.4.10 (Reflow).` - }); + solutionShort: 'ensure content is responsive and does not require horizontal scrolling at small viewport sizes', + solutionLong: `The page has a scroll width of ${scrollWidth}px but a client width of only ${clientWidth}px at 320x256 viewport, requiring horizontal scrolling. This violates WCAG 2.1 Level AA Success Criterion 1.4.10 (Reflow).`, + }) } } catch (e) { console.error('Error checking horizontal scroll:', e); diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c3a29d17..68e88e2f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -80,6 +80,7 @@ jobs: repository: ${{ env.TESTING_REPOSITORY }} token: ${{ secrets.GH_TOKEN }} cache_key: ${{ steps.cache_key.outputs.cache_key }} + scans: "['axe', 'reflow-scan']" - name: Retrieve cached results uses: ./.github/actions/gh-cache/restore diff --git a/PLUGINS.md b/PLUGINS.md index 545bde8f..b57a34b3 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -22,6 +22,10 @@ A async function (you must use `await` or `.then` when invoking this function) t - An object that should match the [`Finding` type](https://github.com/github/accessibility-scanner/blob/main/.github/actions/find/src/types.d.ts#L1-L9). +#### `url` + +Passes in the URL of the page being scanned to be used when a finding is added. + ## How to create plugins As mentioned above, plugins need to exist under `./.github/scanner-plugins`. For a plugin to work, it needs to meet the following criteria: diff --git a/README.md b/README.md index 1f614dba..abaa94c5 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ jobs: # open_grouped_issues: false # Optional: Set to true to open an issue grouping individual issues per violation # reduced_motion: no-preference # Optional: Playwright reduced motion configuration option # color_scheme: light # Optional: Playwright color scheme configuration option - # scans #Optional: An array of scans (or plugins) to be performed. If not provided, only Axe will be performed. + # scans: '["axe","reflow-scan"]' #Optional: An array of scans (or plugins) to be performed. If not provided, only Axe will be performed. ``` > 👉 Update all `REPLACE_THIS` placeholders with your actual values. See [Action Inputs](#action-inputs) for details. diff --git a/sites/site-with-errors/404.html b/sites/site-with-errors/404.html index 3a16ab53..887dd886 100644 --- a/sites/site-with-errors/404.html +++ b/sites/site-with-errors/404.html @@ -15,6 +15,11 @@ line-height: 1; letter-spacing: -1px; } + .wide-element { + width: 500px; + background: #eee; + padding: 10px; + }
@@ -22,4 +27,5 @@

404

Page not found :(

The requested page could not be found.

+
This element is too wide for small viewports.
diff --git a/tests/site-with-errors.test.ts b/tests/site-with-errors.test.ts index 54a6917c..332438a4 100644 --- a/tests/site-with-errors.test.ts +++ b/tests/site-with-errors.test.ts @@ -38,15 +38,15 @@ describe('site-with-errors', () => { }) it('cache has expected results', () => { - const actual = results.map(({issue: {url: issueUrl}, findings}) => { + const actual = results.map(;({issue: {url: issueUrl}, findings}) => { const {problemUrl, solutionLong, screenshotId, ...finding} = findings[0] // Check volatile fields for existence only expect(issueUrl).toBeDefined() expect(problemUrl).toBeDefined() - expect(solutionLong).toBeDefined() - // Check `problemUrl`, ignoring axe version - expect(problemUrl.startsWith('https://dequeuniversity.com/rules/axe/')).toBe(true) - expect(problemUrl.endsWith(`/${finding.ruleId}?application=playwright`)).toBe(true) + // solutionLong is optional for non-axe scans + if (finding.scannerType === 'axe') { + expect(solutionLong).toBeDefined() + } // screenshotId is only present when include_screenshots is enabled if (screenshotId !== undefined) { expect(screenshotId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/) From b80cbb9e579d0e3387f3c94e0fa38fd643662489 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:01:24 +0000 Subject: [PATCH 04/18] Udpates adding a ruleId label --- .github/actions/file/src/openIssue.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/file/src/openIssue.ts b/.github/actions/file/src/openIssue.ts index d4ab452c..f8c7db8f 100644 --- a/.github/actions/file/src/openIssue.ts +++ b/.github/actions/file/src/openIssue.ts @@ -22,8 +22,8 @@ export async function openIssue(octokit: Octokit, repoWithOwner: string, finding const repo = repoWithOwner.split('/')[1] const labels = [ - `${finding.scannerType}${finding.ruleId ? ` rule: ${finding.ruleId}` : ''}`, `${finding.scannerType}-scanning-issue`, + ...(finding.ruleId ? [`${finding.scannerType} rule: ${finding.ruleId}`] : []), ] const title = truncateWithEllipsis( `Accessibility issue: ${finding.problemShort[0].toUpperCase() + finding.problemShort.slice(1)} on ${new URL(finding.url).pathname}`, From d2505608fd7879a8ee0aa341bbc31a6d3c1709c2 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:07:53 +0000 Subject: [PATCH 05/18] Fixes test --- .github/actions/file/src/openIssue.ts | 1 + .github/actions/file/tests/generateIssueBody.test.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/actions/file/src/openIssue.ts b/.github/actions/file/src/openIssue.ts index f8c7db8f..055a596f 100644 --- a/.github/actions/file/src/openIssue.ts +++ b/.github/actions/file/src/openIssue.ts @@ -21,6 +21,7 @@ export async function openIssue(octokit: Octokit, repoWithOwner: string, finding const owner = repoWithOwner.split('/')[0] const repo = repoWithOwner.split('/')[1] + // Only include a ruleId label when it's defined const labels = [ `${finding.scannerType}-scanning-issue`, ...(finding.ruleId ? [`${finding.scannerType} rule: ${finding.ruleId}`] : []), diff --git a/.github/actions/file/tests/generateIssueBody.test.ts b/.github/actions/file/tests/generateIssueBody.test.ts index 52a7b6e5..7e8a6d86 100644 --- a/.github/actions/file/tests/generateIssueBody.test.ts +++ b/.github/actions/file/tests/generateIssueBody.test.ts @@ -25,7 +25,7 @@ describe('generateIssueBody', () => { expect(body).toContain('## What') expect(body).toContain('## Acceptance Criteria') - expect(body).toContain('The specific axe violation reported in this issue is no longer reproducible.') + expect(body).toContain('The specific violation reported in this issue is no longer reproducible.') expect(body).not.toContain('Specifically:') }) From 79d3cfa94a73b0fd87d1a44db62095a963cf30d6 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:10:42 +0000 Subject: [PATCH 06/18] Updates site with errors test --- tests/site-with-errors.test.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/site-with-errors.test.ts b/tests/site-with-errors.test.ts index 332438a4..06ba78da 100644 --- a/tests/site-with-errors.test.ts +++ b/tests/site-with-errors.test.ts @@ -38,14 +38,16 @@ describe('site-with-errors', () => { }) it('cache has expected results', () => { - const actual = results.map(;({issue: {url: issueUrl}, findings}) => { + const actual = results.map(({issue: {url: issueUrl}, findings}) => { const {problemUrl, solutionLong, screenshotId, ...finding} = findings[0] // Check volatile fields for existence only expect(issueUrl).toBeDefined() expect(problemUrl).toBeDefined() - // solutionLong is optional for non-axe scans + // Axe-specific assertions if (finding.scannerType === 'axe') { expect(solutionLong).toBeDefined() + expect(problemUrl.startsWith('https://dequeuniversity.com/rules/axe/')).toBe(true) + expect(problemUrl.endsWith(`/${finding.ruleId}?application=playwright`)).toBe(true) } // screenshotId is only present when include_screenshots is enabled if (screenshotId !== undefined) { @@ -108,11 +110,10 @@ describe('site-with-errors', () => { solutionShort: 'ensure headings have discernible text', }, { - scannerType: 'reflow', + scannerType: 'reflow-scan', url: 'http://127.0.0.1:4000/404.html', - problemShort: 'Page requires horizontal scrolling at 320x256 viewport', - solutionShort: 'Ensure content is responsive and does not require horizontal scrolling at small viewport sizes', - problemUrl: 'https://www.w3.org/WAI/WCAG21/Understanding/reflow.html', + problemShort: 'page requires horizontal scrolling at 320x256 viewport', + solutionShort: 'ensure content is responsive and does not require horizontal scrolling at small viewport sizes', }, ] // Check that: From c050d259607974ad64fa660b44d34acc7d426820 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:13:34 +0000 Subject: [PATCH 07/18] Resets viewport to original size after mobile scan runs --- .github/scanner-plugins/reflow-scan/index.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/scanner-plugins/reflow-scan/index.js b/.github/scanner-plugins/reflow-scan/index.js index 28578703..8ccb6703 100644 --- a/.github/scanner-plugins/reflow-scan/index.js +++ b/.github/scanner-plugins/reflow-scan/index.js @@ -1,10 +1,11 @@ export default async function reflowScan({ page, addFinding, url } = {}) { console.log('reflow plugin'); + const originalViewport = page.viewportSize() // Check for horizontal scrolling at 320x256 viewport try { - await page.setViewportSize({ width: 320, height: 256 }); - const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); - const clientWidth = await page.evaluate(() => document.documentElement.clientWidth); + await page.setViewportSize({width: 320, height: 256}) + const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth) + const clientWidth = await page.evaluate(() => document.documentElement.clientWidth) // If horizontal scroll is required (with 1px tolerance for rounding) if (scrollWidth > clientWidth + 1) { @@ -18,7 +19,12 @@ export default async function reflowScan({ page, addFinding, url } = {}) { }) } } catch (e) { - console.error('Error checking horizontal scroll:', e); + console.error('Error checking horizontal scroll:', e) + } finally { + // Restore original viewport so subsequent scans (e.g. Axe) aren't affected + if (originalViewport) { + await page.setViewportSize(originalViewport) + } } } From 3cf1970fd235dc87268bd5981014f15155eb65aa Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:32:48 +0000 Subject: [PATCH 08/18] Fixes open issue test --- .github/actions/file/tests/openIssue.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/file/tests/openIssue.test.ts b/.github/actions/file/tests/openIssue.test.ts index 17573818..77a184c3 100644 --- a/.github/actions/file/tests/openIssue.test.ts +++ b/.github/actions/file/tests/openIssue.test.ts @@ -60,7 +60,7 @@ describe('openIssue', () => { expect(octokit.request).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ - labels: ['axe rule: color-contrast', 'axe-scanning-issue'], + labels: ['axe-scanning-issue', 'axe rule: color-contrast'], }), ) }) From daed7e9285f84be629569a7afdcc7053baaa70a5 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:41:22 +0000 Subject: [PATCH 09/18] Updates syntax --- .github/workflows/test.yml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 68e88e2f..c846efc3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -80,7 +80,7 @@ jobs: repository: ${{ env.TESTING_REPOSITORY }} token: ${{ secrets.GH_TOKEN }} cache_key: ${{ steps.cache_key.outputs.cache_key }} - scans: "['axe', 'reflow-scan']" + scans: '["axe", "reflow-scan"]' - name: Retrieve cached results uses: ./.github/actions/gh-cache/restore diff --git a/README.md b/README.md index abaa94c5..7ab43882 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ jobs: # open_grouped_issues: false # Optional: Set to true to open an issue grouping individual issues per violation # reduced_motion: no-preference # Optional: Playwright reduced motion configuration option # color_scheme: light # Optional: Playwright color scheme configuration option - # scans: '["axe","reflow-scan"]' #Optional: An array of scans (or plugins) to be performed. If not provided, only Axe will be performed. + # scans: '["axe","reflow-scan"]' # Optional: An array of scans (or plugins) to be performed. If not provided, only Axe will be performed. ``` > 👉 Update all `REPLACE_THIS` placeholders with your actual values. See [Action Inputs](#action-inputs) for details. From 738ba9c7cfc831ec897c640524fdb61c924be729 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:53:58 +0000 Subject: [PATCH 10/18] Fixes issue body formatting --- .github/actions/file/src/generateIssueBody.ts | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/.github/actions/file/src/generateIssueBody.ts b/.github/actions/file/src/generateIssueBody.ts index a3cb0308..18e25d31 100644 --- a/.github/actions/file/src/generateIssueBody.ts +++ b/.github/actions/file/src/generateIssueBody.ts @@ -19,21 +19,20 @@ export function generateIssueBody(finding: Finding, screenshotRepo: string): str } const acceptanceCriteria = `## Acceptance Criteria - - [ ] The specific violation reported in this issue is no longer reproducible. - - [ ] The fix MUST meet WCAG 2.1 guidelines OR the accessibility standards specified by the repository or organization. - - [ ] A test SHOULD be added to ensure this specific violation does not regress. - - [ ] This PR MUST NOT introduce any new accessibility issues or regressions. - ` +- [ ] The specific violation reported in this issue is no longer reproducible. +- [ ] The fix MUST meet WCAG 2.1 guidelines OR the accessibility standards specified by the repository or organization. +- [ ] A test SHOULD be added to ensure this specific violation does not regress. +- [ ] This PR MUST NOT introduce any new accessibility issues or regressions.` const body = `## What - An accessibility scan ${finding.html ? `flagged the element \`${finding.html}\`` : `found an issue on ${finding.url}`} because ${finding.problemShort}. Learn more about why this was flagged by visiting ${finding.problemUrl}. +An accessibility scan ${finding.html ? `flagged the element \`${finding.html}\`` : `found an issue on ${finding.url}`} because ${finding.problemShort}. Learn more about why this was flagged by visiting ${finding.problemUrl}. - ${screenshotSection ?? ''} - To fix this, ${finding.solutionShort}. - ${solutionLong ? `\nSpecifically:\n\n${solutionLong}` : ''} +${screenshotSection ?? ''} +To fix this, ${finding.solutionShort}. +${solutionLong ? `\nSpecifically:\n\n${solutionLong}` : ''} - ${acceptanceCriteria} - ` +${acceptanceCriteria} +` return body } From 9e6acc476a3e4c04219521074061f5f42fa0e0c2 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Mon, 23 Mar 2026 16:08:26 +0000 Subject: [PATCH 11/18] Update 404 page --- sites/site-with-errors/404.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/sites/site-with-errors/404.html b/sites/site-with-errors/404.html index 887dd886..ef8db586 100644 --- a/sites/site-with-errors/404.html +++ b/sites/site-with-errors/404.html @@ -17,8 +17,6 @@ } .wide-element { width: 500px; - background: #eee; - padding: 10px; } From 953a7b597b071b93b82fa8afb0e30178eaaeecdb Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Mon, 23 Mar 2026 16:17:28 +0000 Subject: [PATCH 12/18] Updates plugin manager to de-dupe within it's own repo --- .github/actions/find/src/pluginManager.ts | 10 ++++-- .../actions/find/tests/pluginManager.test.ts | 33 +++++++++++++++++-- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/.github/actions/find/src/pluginManager.ts b/.github/actions/find/src/pluginManager.ts index 0a7d9b14..719be3b7 100644 --- a/.github/actions/find/src/pluginManager.ts +++ b/.github/actions/find/src/pluginManager.ts @@ -66,7 +66,7 @@ export async function loadCustomPlugins() { // - currently, the plugin manager will abort loading // all plugins if there's an error - // - the problem with this is that if a scanner user doesnt + // - the problem with this is that if a scanner user doesn't // have custom plugins, they won't have a 'scanner-plugins' folder // which will cause an error and abort loading all plugins, including built-in ones // - so for custom plugins, if the path doesn't exist, we can return early @@ -88,7 +88,13 @@ export async function loadPluginsFromPath({pluginsPath}: {pluginsPath: string}) if (fs.existsSync(pluginFolderPath) && fs.lstatSync(pluginFolderPath).isDirectory()) { core.info(`Found plugin: ${pluginFolder}`) - plugins.push(await dynamicImport(path.join(pluginsPath, pluginFolder, 'index.js'))) + const plugin = await dynamicImport(path.join(pluginsPath, pluginFolder, 'index.js')) + // Prevents a plugin from running twice + if (plugins.some(p => p.name === plugin.name)) { + core.info(`Skipping duplicate plugin: ${plugin.name}`) + continue + } + plugins.push(plugin) } } } catch (e) { diff --git a/.github/actions/find/tests/pluginManager.test.ts b/.github/actions/find/tests/pluginManager.test.ts index ec9b6cda..d05b2849 100644 --- a/.github/actions/find/tests/pluginManager.test.ts +++ b/.github/actions/find/tests/pluginManager.test.ts @@ -12,12 +12,17 @@ vi.mock('../src/pluginManager.js', {spy: true}) vi.mock('@actions/core', {spy: true}) describe('loadPlugins', () => { - vi.spyOn(dynamicImportModule, 'dynamicImport').mockImplementation(path => Promise.resolve(path)) + let dynamicImportCallCount = 0 + vi.spyOn(dynamicImportModule, 'dynamicImport').mockImplementation(() => { + dynamicImportCallCount++ + return Promise.resolve({name: `plugin-${dynamicImportCallCount}`, default: vi.fn()}) + }) beforeEach(() => { + dynamicImportCallCount = 0 // @ts-expect-error - we don't need the full fs readdirsync // method signature here - vi.spyOn(fs, 'readdirSync').mockImplementation(readPath => { - return [readPath + '/plugin-1', readPath + '/plugin-2'] + vi.spyOn(fs, 'readdirSync').mockImplementation(() => { + return ['folder-a', 'folder-b'] }) vi.spyOn(fs, 'lstatSync').mockImplementation(() => { return { @@ -61,4 +66,26 @@ describe('loadPlugins', () => { expect(logSpy).toHaveBeenCalledWith(pluginManager.abortError) }) }) + + describe('when built-in and custom plugins share the same name', () => { + beforeEach(() => { + // @ts-expect-error - we don't need the full fs readdirsync + // method signature here + vi.spyOn(fs, 'readdirSync').mockImplementation(() => { + return ['reflow-scan'] + }) + vi.spyOn(dynamicImportModule, 'dynamicImport').mockImplementation(() => { + return Promise.resolve({name: 'reflow-scan', default: vi.fn()}) + }) + }) + + it('skips the duplicate and only loads the plugin once', async () => { + pluginManager.clearCache() + const infoSpy = vi.spyOn(core, 'info').mockImplementation(() => {}) + const plugins = await pluginManager.loadPlugins() + expect(plugins.length).toBe(1) + expect(plugins[0].name).toBe('reflow-scan') + expect(infoSpy).toHaveBeenCalledWith('Skipping duplicate plugin: reflow-scan') + }) + }) }) From 381f66f4a3dff1ebdd07401e26b20d69916ab8de Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Mon, 23 Mar 2026 16:24:39 +0000 Subject: [PATCH 13/18] Minor updates for consistency --- .github/actions/file/tests/generateIssueBody.test.ts | 8 ++++---- .github/scanner-plugins/reflow-scan/index.js | 7 +++---- tests/types.d.ts | 2 +- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/actions/file/tests/generateIssueBody.test.ts b/.github/actions/file/tests/generateIssueBody.test.ts index 7e8a6d86..167ee5f8 100644 --- a/.github/actions/file/tests/generateIssueBody.test.ts +++ b/.github/actions/file/tests/generateIssueBody.test.ts @@ -13,10 +13,10 @@ const baseFinding = { const findingWithEmptyOptionalFields = { scannerType: 'reflow', - url: 'https://example.com/page', - problemShort: 'elements must meet minimum color contrast ratio thresholds', - problemUrl: 'https://dequeuniversity.com/rules/axe/4.10/color-contrast?application=playwright', - solutionShort: 'ensure the contrast between foreground and background colors meets WCAG thresholds', + url: 'https://example.com/404', + problemShort: 'page requires horizontal scrolling at 320x256 viewport', + problemUrl: 'https://www.w3.org/WAI/WCAG21/Understanding/reflow.html', + solutionShort: 'ensure content is responsive and does not require horizontal scrolling at small viewport sizes', } describe('generateIssueBody', () => { diff --git a/.github/scanner-plugins/reflow-scan/index.js b/.github/scanner-plugins/reflow-scan/index.js index 8ccb6703..d5f69845 100644 --- a/.github/scanner-plugins/reflow-scan/index.js +++ b/.github/scanner-plugins/reflow-scan/index.js @@ -1,5 +1,4 @@ -export default async function reflowScan({ page, addFinding, url } = {}) { - console.log('reflow plugin'); +export default async function reflowScan({page, addFinding, url} = {}) { const originalViewport = page.viewportSize() // Check for horizontal scrolling at 320x256 viewport try { @@ -15,7 +14,7 @@ export default async function reflowScan({ page, addFinding, url } = {}) { problemShort: 'page requires horizontal scrolling at 320x256 viewport', problemUrl: 'https://www.w3.org/WAI/WCAG21/Understanding/reflow.html', solutionShort: 'ensure content is responsive and does not require horizontal scrolling at small viewport sizes', - solutionLong: `The page has a scroll width of ${scrollWidth}px but a client width of only ${clientWidth}px at 320x256 viewport, requiring horizontal scrolling. This violates WCAG 2.1 Level AA Success Criterion 1.4.10 (Reflow).`, + solutionLong: `The page has a scroll width of ${scrollWidth}px but a client width of only ${clientWidth}px at a 320x256 viewport, requiring horizontal scrolling. This violates WCAG 2.1 Level AA Success Criterion 1.4.10 (Reflow).`, }) } } catch (e) { @@ -28,4 +27,4 @@ export default async function reflowScan({ page, addFinding, url } = {}) { } } -export const name = 'reflow-scan'; +export const name = 'reflow-scan' diff --git a/tests/types.d.ts b/tests/types.d.ts index 8fdf46ab..b12077ae 100644 --- a/tests/types.d.ts +++ b/tests/types.d.ts @@ -1,6 +1,6 @@ export type Finding = { scannerType: string - ruleId: string + ruleId?: string url: string html?: string problemShort: string From 4eab30a8e5406db60e227bd0e48921e2042d4db8 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Mon, 23 Mar 2026 19:34:25 +0000 Subject: [PATCH 14/18] Removes passing in url, updates cache key --- .github/actions/file/src/openIssue.ts | 9 +++++---- .github/actions/file/src/updateFilingsWithNewFindings.ts | 3 +++ .github/actions/find/src/findForUrl.ts | 1 - .github/actions/find/src/pluginManager.ts | 5 ++--- .github/scanner-plugins/reflow-scan/index.js | 3 ++- PLUGINS.md | 4 ---- 6 files changed, 12 insertions(+), 13 deletions(-) diff --git a/.github/actions/file/src/openIssue.ts b/.github/actions/file/src/openIssue.ts index 055a596f..937f06cf 100644 --- a/.github/actions/file/src/openIssue.ts +++ b/.github/actions/file/src/openIssue.ts @@ -21,11 +21,12 @@ export async function openIssue(octokit: Octokit, repoWithOwner: string, finding const owner = repoWithOwner.split('/')[0] const repo = repoWithOwner.split('/')[1] + const labels = [`${finding.scannerType}-scanning-issue`] // Only include a ruleId label when it's defined - const labels = [ - `${finding.scannerType}-scanning-issue`, - ...(finding.ruleId ? [`${finding.scannerType} rule: ${finding.ruleId}`] : []), - ] + if (finding.ruleId) { + labels.push(`${finding.scannerType} rule: ${finding.ruleId}`) + } + const title = truncateWithEllipsis( `Accessibility issue: ${finding.problemShort[0].toUpperCase() + finding.problemShort.slice(1)} on ${new URL(finding.url).pathname}`, GITHUB_ISSUE_TITLE_MAX_LENGTH, diff --git a/.github/actions/file/src/updateFilingsWithNewFindings.ts b/.github/actions/file/src/updateFilingsWithNewFindings.ts index 5842c606..eee1c6ae 100644 --- a/.github/actions/file/src/updateFilingsWithNewFindings.ts +++ b/.github/actions/file/src/updateFilingsWithNewFindings.ts @@ -5,6 +5,9 @@ function getFilingKey(filing: ResolvedFiling | RepeatedFiling): string { } function getFindingKey(finding: Finding): string { + if (finding.ruleId && finding.html) { + return `${finding.url};${finding.ruleId};${finding.html}` + } return `${finding.url};${finding.scannerType};${finding.problemUrl}` } diff --git a/.github/actions/find/src/findForUrl.ts b/.github/actions/find/src/findForUrl.ts index aed74d7b..eded5d11 100644 --- a/.github/actions/find/src/findForUrl.ts +++ b/.github/actions/find/src/findForUrl.ts @@ -48,7 +48,6 @@ export async function findForUrl( plugin, page, addFinding, - url, }) } else { core.info(`Skipping plugin ${plugin.name} because it is not included in the 'scans' input`) diff --git a/.github/actions/find/src/pluginManager.ts b/.github/actions/find/src/pluginManager.ts index 719be3b7..0c9d3a12 100644 --- a/.github/actions/find/src/pluginManager.ts +++ b/.github/actions/find/src/pluginManager.ts @@ -13,7 +13,6 @@ const __dirname = path.dirname(__filename) type PluginDefaultParams = { page: playwright.Page addFinding: (findingData: Finding) => Promise - url: string } type Plugin = { @@ -109,6 +108,6 @@ export async function loadPluginsFromPath({pluginsPath}: {pluginsPath: string}) type InvokePluginParams = PluginDefaultParams & { plugin: Plugin } -export function invokePlugin({plugin, page, addFinding, url}: InvokePluginParams) { - return plugin.default({page, addFinding, url}) +export function invokePlugin({plugin, page, addFinding}: InvokePluginParams) { + return plugin.default({page, addFinding}) } diff --git a/.github/scanner-plugins/reflow-scan/index.js b/.github/scanner-plugins/reflow-scan/index.js index d5f69845..077835fe 100644 --- a/.github/scanner-plugins/reflow-scan/index.js +++ b/.github/scanner-plugins/reflow-scan/index.js @@ -1,5 +1,6 @@ -export default async function reflowScan({page, addFinding, url} = {}) { +export default async function reflowScan({page, addFinding} = {}) { const originalViewport = page.viewportSize() + const url = page.url() // Check for horizontal scrolling at 320x256 viewport try { await page.setViewportSize({width: 320, height: 256}) diff --git a/PLUGINS.md b/PLUGINS.md index b57a34b3..545bde8f 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -22,10 +22,6 @@ A async function (you must use `await` or `.then` when invoking this function) t - An object that should match the [`Finding` type](https://github.com/github/accessibility-scanner/blob/main/.github/actions/find/src/types.d.ts#L1-L9). -#### `url` - -Passes in the URL of the page being scanned to be used when a finding is added. - ## How to create plugins As mentioned above, plugins need to exist under `./.github/scanner-plugins`. For a plugin to work, it needs to meet the following criteria: From effdb308fae621fd5e983ec04efc066adad4cafd Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:05:34 +0000 Subject: [PATCH 15/18] Uses hard-coded list of scans --- .github/actions/find/src/pluginManager.ts | 24 ++++++++++++------- .../actions/find/tests/pluginManager.test.ts | 7 +++--- README.md | 2 +- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/.github/actions/find/src/pluginManager.ts b/.github/actions/find/src/pluginManager.ts index 0c9d3a12..2db5c6de 100644 --- a/.github/actions/find/src/pluginManager.ts +++ b/.github/actions/find/src/pluginManager.ts @@ -20,6 +20,10 @@ type Plugin = { default: (options: PluginDefaultParams) => Promise } +// Built-in plugin names shipped with the scanner. +// Used to skip duplicates when loading custom plugins. +const BUILT_IN_PLUGINS = ['reflow-scan'] + const plugins: Plugin[] = [] let pluginsLoaded = false @@ -75,25 +79,29 @@ export async function loadCustomPlugins() { return } - await loadPluginsFromPath({pluginsPath}) + await loadPluginsFromPath({pluginsPath, skipBuiltInPlugins: BUILT_IN_PLUGINS}) } // exported for mocking/testing. not for actual use -export async function loadPluginsFromPath({pluginsPath}: {pluginsPath: string}) { +export async function loadPluginsFromPath({ + pluginsPath, + skipBuiltInPlugins, +}: { + pluginsPath: string + skipBuiltInPlugins?: string[] +}) { try { const res = fs.readdirSync(pluginsPath) for (const pluginFolder of res) { const pluginFolderPath = path.join(pluginsPath, pluginFolder) if (fs.existsSync(pluginFolderPath) && fs.lstatSync(pluginFolderPath).isDirectory()) { - core.info(`Found plugin: ${pluginFolder}`) - const plugin = await dynamicImport(path.join(pluginsPath, pluginFolder, 'index.js')) - // Prevents a plugin from running twice - if (plugins.some(p => p.name === plugin.name)) { - core.info(`Skipping duplicate plugin: ${plugin.name}`) + if (skipBuiltInPlugins?.includes(pluginFolder)) { + core.info(`Skipping built-in plugin: ${pluginFolder}`) continue } - plugins.push(plugin) + core.info(`Found plugin: ${pluginFolder}`) + plugins.push(await dynamicImport(path.join(pluginsPath, pluginFolder, 'index.js'))) } } } catch (e) { diff --git a/.github/actions/find/tests/pluginManager.test.ts b/.github/actions/find/tests/pluginManager.test.ts index d05b2849..3aced15d 100644 --- a/.github/actions/find/tests/pluginManager.test.ts +++ b/.github/actions/find/tests/pluginManager.test.ts @@ -67,7 +67,7 @@ describe('loadPlugins', () => { }) }) - describe('when built-in and custom plugins share the same name', () => { + describe('when a custom plugin folder matches a built-in plugin name', () => { beforeEach(() => { // @ts-expect-error - we don't need the full fs readdirsync // method signature here @@ -79,13 +79,14 @@ describe('loadPlugins', () => { }) }) - it('skips the duplicate and only loads the plugin once', async () => { + it('skips the built-in name in custom plugins and only loads it once', async () => { pluginManager.clearCache() const infoSpy = vi.spyOn(core, 'info').mockImplementation(() => {}) const plugins = await pluginManager.loadPlugins() + // Built-in loads it, custom skips the folder by name expect(plugins.length).toBe(1) expect(plugins[0].name).toBe('reflow-scan') - expect(infoSpy).toHaveBeenCalledWith('Skipping duplicate plugin: reflow-scan') + expect(infoSpy).toHaveBeenCalledWith('Skipping built-in plugin: reflow-scan') }) }) }) diff --git a/README.md b/README.md index 7ab43882..b3dafe1d 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ Trigger the workflow manually or automatically based on your configuration. The | `include_screenshots` | No | Whether to capture screenshots of scanned pages and include links to them in filed issues. Screenshots are stored on the `gh-cache` branch of the repository running the workflow. Default: `false` | `true` | | `reduced_motion` | No | Playwright `reducedMotion` setting for scan contexts. Allowed values: `reduce`, `no-preference` | `reduce` | | `color_scheme` | No | Playwright `colorScheme` setting for scan contexts. Allowed values: `light`, `dark`, `no-preference` | `dark` | -| `scans` | No | An array of scans (or plugins) to be performed. If not provided, only Axe will be performed. | `'["axe", "reflow-scan", ...other plugins]'` | +| `scans` | No | An array of scans (or plugins) to be performed. If not provided, only Axe will be performed. | `'["axe", "reflow-scan"]'` | --- From 8985ccc98a3a312686bf7bcd7c3fdafa161c6578 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:07:16 +0000 Subject: [PATCH 16/18] Updates README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b3dafe1d..00252e99 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ Trigger the workflow manually or automatically based on your configuration. The | `include_screenshots` | No | Whether to capture screenshots of scanned pages and include links to them in filed issues. Screenshots are stored on the `gh-cache` branch of the repository running the workflow. Default: `false` | `true` | | `reduced_motion` | No | Playwright `reducedMotion` setting for scan contexts. Allowed values: `reduce`, `no-preference` | `reduce` | | `color_scheme` | No | Playwright `colorScheme` setting for scan contexts. Allowed values: `light`, `dark`, `no-preference` | `dark` | -| `scans` | No | An array of scans (or plugins) to be performed. If not provided, only Axe will be performed. | `'["axe", "reflow-scan"]'` | +| `scans` | No | An array of scans (or plugins) to be performed. If not provided, only Axe will be performed. | `'["axe", "reflow-scan", ...other plugins]'` | --- From ff435ffa199c4365fdadfeb29c2c9c7b6d46ba47 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:09:48 +0000 Subject: [PATCH 17/18] chore(deps): Bump ruby/setup-ruby Bumps the github-actions group with 1 update in the / directory: [ruby/setup-ruby](https://github.com/ruby/setup-ruby). Updates `ruby/setup-ruby` from 1.293.0 to 1.295.0 - [Release notes](https://github.com/ruby/setup-ruby/releases) - [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb) - [Commits](https://github.com/ruby/setup-ruby/compare/dffb23f65a78bba8db45d387d5ea1bbd6be3ef18...319994f95fa847cf3fb3cd3dbe89f6dcde9f178f) --- updated-dependencies: - dependency-name: ruby/setup-ruby dependency-version: 1.295.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c3a29d17..3ad4faa4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,7 +34,7 @@ jobs: uses: actions/checkout@v6 - name: Setup Ruby - uses: ruby/setup-ruby@dffb23f65a78bba8db45d387d5ea1bbd6be3ef18 + uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f with: ruby-version: "3.4" bundler-cache: true From d3971348a8119893d8ee51951b37aa9d3ca1a158 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:39:10 +0000 Subject: [PATCH 18/18] Updates to check against plugin name, not folder name --- .github/actions/find/src/pluginManager.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/actions/find/src/pluginManager.ts b/.github/actions/find/src/pluginManager.ts index 2db5c6de..bec7ddfe 100644 --- a/.github/actions/find/src/pluginManager.ts +++ b/.github/actions/find/src/pluginManager.ts @@ -96,12 +96,15 @@ export async function loadPluginsFromPath({ const pluginFolderPath = path.join(pluginsPath, pluginFolder) if (fs.existsSync(pluginFolderPath) && fs.lstatSync(pluginFolderPath).isDirectory()) { - if (skipBuiltInPlugins?.includes(pluginFolder)) { - core.info(`Skipping built-in plugin: ${pluginFolder}`) + const plugin = await dynamicImport(path.join(pluginsPath, pluginFolder, 'index.js')) + + if (skipBuiltInPlugins?.includes(plugin.name)) { + core.info(`Skipping built-in plugin: ${plugin.name}`) continue } - core.info(`Found plugin: ${pluginFolder}`) - plugins.push(await dynamicImport(path.join(pluginsPath, pluginFolder, 'index.js'))) + + core.info(`Found plugin: ${plugin.name}`) + plugins.push(plugin) } } } catch (e) {