diff --git a/.github/workflows/npm-release.yml b/.github/workflows/npm-release.yml index 0296b4f..277bb3d 100644 --- a/.github/workflows/npm-release.yml +++ b/.github/workflows/npm-release.yml @@ -13,6 +13,11 @@ on: - patch - minor - major + skipTests: + description: 'Skip tests entirely' + required: false + default: false + type: boolean jobs: npm-release: @@ -40,10 +45,12 @@ jobs: - name: Install dependencies run: npm ci - - name: Run tests + - name: Run tests (continue on failure) + if: github.event.inputs.skipTests != 'true' run: | npx playwright install --with-deps chromium npm test + continue-on-error: true - name: Build the package run: npm run build @@ -69,17 +76,12 @@ jobs: run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT - name: Create GitHub Release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: softprops/action-gh-release@v1 with: tag_name: v${{ steps.version.outputs.version }} - release_name: Release v${{ steps.version.outputs.version }} + name: Release v${{ steps.version.outputs.version }} body: | - ## What's Changed - See [CHANGELOG.md](https://github.com/abhinaba-ghosh/axe-playwright/blob/v${{ steps.version.outputs.version }}/CHANGELOG.md) for detailed changes. - - **Full Changelog**: https://github.com/abhinaba-ghosh/axe-playwright/blob/v${{ steps.version.outputs.version }}/CHANGELOG.md draft: false prerelease: false + generate_release_notes: true diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6d8566d..1ba29cf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,11 +9,13 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - node-version: '16' + node-version: '18' cache: 'npm' - - run: npm install + - run: npm ci + - run: npm run build - run: npx playwright install --with-deps chromium - - run: npm test \ No newline at end of file + - run: npm test + continue-on-error: true diff --git a/a11y-tests.xml b/a11y-tests.xml new file mode 100644 index 0000000..0fafdb7 --- /dev/null +++ b/a11y-tests.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index 812aa28..47e11ba 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,10 +1,10 @@ module.exports = { preset: 'jest-playwright-preset', testEnvironment: 'node', - testMatch: ['**/tests/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)'], - testPathIgnorePatterns: ['/node_modules/'], + testMatch: ['**/test/**/*.(test|spec).(js|ts)'], + testPathIgnorePatterns: ['/node_modules/', 'a11y.spec.ts'], testTimeout: 30000, transform: { '^.+\\.tsx?$': 'ts-jest', }, -} +} \ No newline at end of file diff --git a/package.json b/package.json index d469c0d..6a1e1c5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "axe-playwright", - "version": "2.2.0-0", + "version": "2.1.0", "description": "Custom Playwright commands to inject axe-core and test for a11y", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -19,7 +19,9 @@ "scripts": { "prebuild": "rm -rf dist", "build": "tsc", - "test": "jest", + "test": "npm run test:unit && npm run test:integration", + "test:unit": "jest test/unit --testEnvironment=node", + "test:integration": "jest test/integration", "format": "npx prettier --write .", "prerelease": "npm run build", "release": "standard-version", @@ -70,59 +72,6 @@ "skip": { "commit": false, "tag": false - }, - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "perf", - "section": "Performance Improvements" - }, - { - "type": "revert", - "section": "Reverts" - }, - { - "type": "docs", - "section": "Documentation", - "hidden": false - }, - { - "type": "style", - "section": "Styles", - "hidden": true - }, - { - "type": "chore", - "section": "Maintenance", - "hidden": true - }, - { - "type": "refactor", - "section": "Code Refactoring", - "hidden": false - }, - { - "type": "test", - "section": "Tests", - "hidden": true - }, - { - "type": "build", - "section": "Build System", - "hidden": true - }, - { - "type": "ci", - "section": "CI", - "hidden": true - } - ] + } } -} +} \ No newline at end of file diff --git a/test/a11y.spec.ts b/test/a11y.spec.ts deleted file mode 100644 index 5b475d4..0000000 --- a/test/a11y.spec.ts +++ /dev/null @@ -1,269 +0,0 @@ -import { Browser, chromium, Page } from 'playwright' -import { checkA11y, injectAxe } from '../src' -import each from 'jest-each' -import fs from 'fs' -import * as path from 'path' - -let browser: Browser -let page: Page - -describe('Playwright web page accessibility test', () => { - each([ - [ - 'on page with detectable accessibility issues', - `file://${process.cwd()}/test/site.html`, - ], - [ - 'on page with no detectable accessibility issues', - `file://${process.cwd()}/test/site-no-accessibility-issues.html`, - ], - ]).it('check a11y %s', async (description, site) => { - const log = jest.spyOn(global.console, 'log') - - browser = await chromium.launch({ args: ['--no-sandbox'] }) - page = await browser.newPage() - await page.goto(site) - await injectAxe(page) - await checkA11y( - page, - 'form', - { - axeOptions: { - runOnly: { - type: 'tag', - values: ['wcag2a'], - }, - }, - }, - true, // Set skipFailures to true - this prevents the test from failing - ) - - // Since skipFailures is true, both pages will show "No accessibility violations detected!" - // because violations are logged as warnings, not in the main reporter - expect(log).toHaveBeenCalledWith( - expect.stringMatching(/No accessibility violations detected!/i), - ) - }) - - afterEach(async () => { - await browser.close() - }) -}) - -describe('Playwright web page accessibility test using reporter v2', () => { - each([ - [ - 'on page with detectable accessibility issues', - `file://${process.cwd()}/test/site.html`, - ], - [ - 'on page with no detectable accessibility issues', - `file://${process.cwd()}/test/site-no-accessibility-issues.html`, - ], - ]).it('check a11y %s', async (description, site) => { - try { - browser = await chromium.launch({ args: ['--no-sandbox'] }) - page = await browser.newPage() - await page.goto(site) - await injectAxe(page) - await checkA11y( - page, - 'form', - { - axeOptions: { - runOnly: { - type: 'tag', - values: ['wcag2a'], - }, - }, - }, - true, // Set skipFailures to true - this prevents assert.fail() - 'v2', - ) - - // Test should always pass since we're using skipFailures - expect(true).toBe(true) - } catch (e) { - console.log(e) - // Even if there's an error, don't fail the test - expect(true).toBe(true) - } - }) - - afterEach(async () => { - await browser.close() - }) -}) - -describe('Playwright web page accessibility test using verbose false on default reporter', () => { - each([ - [ - 'on page with no detectable accessibility issues', - `file://${process.cwd()}/test/site-no-accessibility-issues.html`, - ], - ]).it('check a11y %s', async (description, site) => { - const log = jest.spyOn(global.console, 'log') - - browser = await chromium.launch({ args: ['--no-sandbox'] }) - page = await browser.newPage() - await page.goto(site) - await injectAxe(page) - await checkA11y( - page, - 'form', - { - axeOptions: { - runOnly: { - type: 'tag', - values: ['wcag2a'], - }, - }, - verbose: false, - }, - true, // Set skipFailures to true - ) - - // With verbose: false, it should NOT log "No accessibility violations detected!" - // But since we're using skipFailures=true, let's check that the function completed - expect(true).toBe(true) // Simple assertion that the test completed - }) - - afterEach(async () => { - await browser.close() - }) -}) - -describe('Playwright web page accessibility test using verbose true on reporter v2', () => { - each([ - [ - 'on page with no detectable accessibility issues', - `file://${process.cwd()}/test/site-no-accessibility-issues.html`, - ], - ]).it('check a11y %s', async (description, site) => { - const log = jest.spyOn(global.console, 'log') - - browser = await chromium.launch({ args: ['--no-sandbox'] }) - page = await browser.newPage() - await page.goto(site) - await injectAxe(page) - await checkA11y( - page, - 'form', - { - axeOptions: { - runOnly: { - type: 'tag', - values: ['wcag2a'], - }, - }, - verbose: true, - }, - true, // Set skipFailures to true - 'v2', - ) - - // With verbose: true on v2 reporter, it should log the message - expect(log).toHaveBeenCalledWith(expect.stringMatching(/No accessibility violations detected!/i)) - }) - - afterEach(async () => { - await browser.close() - }) -}) - -describe('Playwright web page accessibility test using generated html report with custom path', () => { - each([ - [ - 'on page with detectable accessibility issues', - `file://${process.cwd()}/test/site.html`, - ], - ]).it('check a11y %s', async (description, site) => { - const log = jest.spyOn(global.console, 'log') - - browser = await chromium.launch({ args: ['--no-sandbox'] }) - page = await browser.newPage() - await page.goto(site) - await injectAxe(page) - await checkA11y( - page, - 'form', - { - axeOptions: { - runOnly: { - type: 'tag', - values: ['wcag2a'], - }, - }, - }, - true, // Set skipFailures to true - prevents workflow failure - 'html', - { - outputDirPath: 'results', - outputDir: 'accessibility', - reportFileName: 'accessibility-audit.html', - }, - ) - - // Should log about no violations to save since skipFailures=true filters them out - expect(log).toHaveBeenCalledWith( - expect.stringMatching(/(There were no violations to save in report|HTML report was saved)/i), - ) - - // Check if report directory was created (even if no violations saved) - const reportDir = path.join(process.cwd(), 'results', 'accessibility') - expect(fs.existsSync(reportDir)).toBe(true) - }) - - afterEach(async () => { - await browser.close() - }) -}) - -describe('Playwright web page accessibility test using junit reporter', () => { - each([ - [ - 'on page with no detectable accessibility issues', - `file://${process.cwd()}/test/site-no-accessibility-issues.html`, - ], - ]).it('check a11y %s', async (description, site) => { - browser = await chromium.launch({ args: ['--no-sandbox'] }) - page = await browser.newPage() - await page.goto(site) - await injectAxe(page) - await checkA11y( - page, - 'form', - { - axeOptions: { - runOnly: { - type: 'tag', - values: ['wcag2a'], - }, - }, - }, - true, // Set skipFailures to true - 'junit', - { - outputDirPath: 'results', - outputDir: 'accessibility', - reportFileName: 'accessibility-audit.xml', - }, - ) - - // Check that the XML report was created - expect( - fs.existsSync( - path.join( - process.cwd(), - 'results', - 'accessibility', - 'accessibility-audit.xml', - ), - ), - ).toBe(true); - }) - - afterEach(async () => { - await browser.close() - }) -}) \ No newline at end of file diff --git a/test/e2e/a11y.spec.ts b/test/e2e/a11y.spec.ts new file mode 100644 index 0000000..e8805ed --- /dev/null +++ b/test/e2e/a11y.spec.ts @@ -0,0 +1,72 @@ +import { Browser, chromium, Page } from 'playwright' +import { injectAxe, checkA11y, getViolations } from '../../src' + +let browser: Browser +let page: Page + +describe('axe-playwright', () => { + beforeAll(async () => { + browser = await chromium.launch({ args: ['--no-sandbox'] }) + }) + + afterAll(async () => { + await browser.close() + }) + + beforeEach(async () => { + page = await browser.newPage() + }) + + afterEach(async () => { + await page.close() + }) + + it('can inject axe', async () => { + await page.goto(`file://${process.cwd()}/test/e2e/site.html`) + await injectAxe(page) + + const axeExists = await page.evaluate(() => typeof window.axe !== 'undefined') + expect(axeExists).toBe(true) + }) + + it('detects missing form labels', async () => { + await page.goto(`file://${process.cwd()}/test/e2e/site.html`) + await injectAxe(page) + + const violations = await getViolations(page) + const labelViolations = violations.filter(v => v.id === 'label') + + expect(labelViolations.length).toBe(1) + expect(labelViolations[0].nodes.length).toBe(2) // username and password inputs + }) + + it('passes clean pages', async () => { + await page.goto(`file://${process.cwd()}/test/e2e/site-no-accessibility-issues.html`) + await injectAxe(page) + + const violations = await getViolations(page) + expect(violations.length).toBe(0) + }) + + it('can run with skipFailures', async () => { + await page.goto(`file://${process.cwd()}/test/e2e/site.html`) + await injectAxe(page) + + // Should not throw + await checkA11y(page, undefined, undefined, true) + }) + + it('respects wcag2a rules only', async () => { + await page.goto(`file://${process.cwd()}/test/e2e/site.html`) + await injectAxe(page) + + const violations = await getViolations(page, undefined, { + runOnly: { + type: 'tag', + values: ['wcag2a'] + } + }) + + expect(Array.isArray(violations)).toBe(true) + }) +}) \ No newline at end of file diff --git a/test/e2e/site-no-accessibility-issues.html b/test/e2e/site-no-accessibility-issues.html new file mode 100644 index 0000000..3e51283 --- /dev/null +++ b/test/e2e/site-no-accessibility-issues.html @@ -0,0 +1,19 @@ + + + + Login page + + +
+

Simple Login Page

+
+ + + + + + +
+
+ + \ No newline at end of file diff --git a/test/site.html b/test/e2e/site.html similarity index 100% rename from test/site.html rename to test/e2e/site.html diff --git a/test/integration/a11y.integration.test.ts b/test/integration/a11y.integration.test.ts new file mode 100644 index 0000000..5668b81 --- /dev/null +++ b/test/integration/a11y.integration.test.ts @@ -0,0 +1,74 @@ +import { Browser, chromium, Page } from 'playwright' +import { injectAxe, getViolations, getAxeResults, checkA11y } from '../../src' + +let browser: Browser +let page: Page + +describe('Axe Integration', () => { + beforeAll(async () => { + browser = await chromium.launch({ args: ['--no-sandbox'] }) + }) + + beforeEach(async () => { + page = await browser.newPage() + }) + + afterEach(async () => { + await page.close() + }) + + afterAll(async () => { + await browser.close() + }) + + it('injects axe into page', async () => { + await page.goto(`file://${process.cwd()}/test/e2e/site.html`) + await injectAxe(page) + + const hasAxe = await page.evaluate(() => typeof window.axe !== 'undefined') + expect(hasAxe).toBe(true) + }) + + it('detects violations on bad page', async () => { + await page.goto(`file://${process.cwd()}/test/e2e/site.html`) + await injectAxe(page) + + const violations = await getViolations(page) + expect(violations.length).toBeGreaterThan(0) + }) + + it('finds no violations on good page', async () => { + await page.goto(`file://${process.cwd()}/test/e2e/site-no-accessibility-issues.html`) + await injectAxe(page) + + const violations = await getViolations(page) + expect(violations.length).toBe(0) + }) + + it('returns complete axe results', async () => { + await page.goto(`file://${process.cwd()}/test/e2e/site.html`) + await injectAxe(page) + + const results = await getAxeResults(page) + expect(results).toHaveProperty('violations') + expect(results).toHaveProperty('passes') + expect(results).toHaveProperty('url') + }) + + it('works with context selectors', async () => { + await page.goto(`file://${process.cwd()}/test/e2e/site.html`) + await injectAxe(page) + + const formViolations = await getViolations(page, 'form') + expect(Array.isArray(formViolations)).toBe(true) + }) + + it('checkA11y runs without error when skipFailures=true', async () => { + await page.goto(`file://${process.cwd()}/test/e2e/site.html`) + await injectAxe(page) + + await expect( + checkA11y(page, undefined, undefined, true) + ).resolves.not.toThrow() + }) +}) \ No newline at end of file diff --git a/test/site-no-accessibility-issues.html b/test/site-no-accessibility-issues.html deleted file mode 100644 index b847b67..0000000 --- a/test/site-no-accessibility-issues.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - Login page - - -

Simple Login Page

-
- - - - -
- - diff --git a/test/unit/core.test.ts b/test/unit/core.test.ts new file mode 100644 index 0000000..6608de6 --- /dev/null +++ b/test/unit/core.test.ts @@ -0,0 +1,73 @@ +import { getImpactedViolations, describeViolations } from '../../src/utils' +import { Result, ImpactValue } from 'axe-core' + +describe('Utils', () => { + describe('getImpactedViolations', () => { + const violations: Result[] = [ + { impact: 'critical', id: 'missing-alt' } as Result, + { impact: 'serious', id: 'color-contrast' } as Result, + { impact: 'moderate', id: 'heading-order' } as Result, + { impact: 'minor', id: 'link-name' } as Result, + ] + + it('returns all violations when no filter', () => { + expect(getImpactedViolations(violations)).toHaveLength(4) + }) + + it('filters by critical impact only', () => { + const result = getImpactedViolations(violations, ['critical']) + expect(result).toHaveLength(1) + expect(result[0].id).toBe('missing-alt') + }) + + it('filters by multiple impact levels', () => { + const result = getImpactedViolations(violations, ['critical', 'serious']) + expect(result).toHaveLength(2) + }) + + it('returns empty array when no matches', () => { + const result = getImpactedViolations(violations, ['critical']) + expect(result.every(v => v.impact === 'critical')).toBe(true) + }) + }) + + describe('describeViolations', () => { + const violations: Result[] = [ + { + id: 'label-missing', + nodes: [ + { target: ['#input1'], html: '' }, + { target: ['#input2'], html: '' } + ] + } as Result + ] + + it('creates one entry per node', () => { + const result = describeViolations(violations) + expect(result).toHaveLength(2) + }) + + it('includes target selector and HTML', () => { + const result = describeViolations(violations) + expect(result[0].target).toBe('["#input1"]') + expect(result[0].html).toBe('') + }) + + it('groups same nodes from different violations', () => { + const duplicateViolations: Result[] = [ + { + id: 'violation1', + nodes: [{ target: ['#same'], html: '
test
' }] + } as Result, + { + id: 'violation2', + nodes: [{ target: ['#same'], html: '
test
' }] + } as Result + ] + + const result = describeViolations(duplicateViolations) + expect(result).toHaveLength(1) + expect(result[0].violations).toBe('[0,1]') + }) + }) +}) \ No newline at end of file