Skip to content

Commit

Permalink
feat: added functionality for jest custom ruleset (#630)
Browse files Browse the repository at this point in the history
* feat: added functionality for jest custom ruleset

* fix: changed coverage

* fix: review added

* fix: reading custom rules file async

---------

Co-authored-by: Navateja Alagam <navateja215@gmail.com>
  • Loading branch information
jaig-0911 and navateja-alagam committed Jan 22, 2024
1 parent c5a9571 commit 3e96398
Show file tree
Hide file tree
Showing 12 changed files with 80 additions and 9 deletions.
3 changes: 2 additions & 1 deletion cSpell.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"wcag",
"wdio",
"webdriverio",
"webm"
"webm",
"Vidyard"
],
"flagWords": ["master-slave", "slave", "blacklist", "whitelist"],
"allowCompoundWords": true
Expand Down
21 changes: 20 additions & 1 deletion packages/common/__tests__/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import { useFilesToBeExempted, log } from '../src/helpers';
import { useFilesToBeExempted, log, useCustomRules } from '../src/helpers';

describe('Your Module', () => {
afterEach(() => {
Expand Down Expand Up @@ -66,4 +66,23 @@ describe('Your Module', () => {

delete process.env.SA11Y_AUTO_FILTER_LIST_PACKAGE_NAME;
});

it('should return empty array if SA11Y_CUSTOM_RULES env variable is not set', async () => {
process.env.SA11Y_CUSTOM_RULES = '';
const result = await useCustomRules();
expect(result).toEqual([]);
});

it('should return empty array if reading file encounters an error', async () => {
process.env.SA11Y_CUSTOM_RULES = 'packages/common/testMocks/WrongFile.json';
const result = await useCustomRules();
expect(result).toEqual([]);
});

it('should return rules array if file is read successfully', async () => {
process.env.SA11Y_CUSTOM_RULES = 'packages/common/testMocks/sa11y-custom-rules.json';
const mockRules = ['rule1', 'rule2'];
const result = await useCustomRules();
expect(result).toEqual(mockRules);
});
});
17 changes: 17 additions & 0 deletions packages/common/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* Logging is enabled only when environment variable `SA11Y_DEBUG` is set.
*/

import * as fs from 'fs/promises';
export function log(...args: unknown[]): void {
// Probably not worth it to mock and test console logging for this helper util
/* istanbul ignore next */
Expand All @@ -32,3 +33,19 @@ export function useFilesToBeExempted(): string[] {
}
return [];
}

export async function useCustomRules(): Promise<string[]> {
const filePath = process.env.SA11Y_CUSTOM_RULES ?? '';
if (filePath !== '') {
try {
// Read the file asynchronously
const data = await fs.readFile(filePath, 'utf-8');
const { rules } = JSON.parse(data) as { rules: string[] };
// Access the rules array
return rules;
} catch (err) {
console.error('Error reading the custom ruleset file:', err);
}
}
return [];
}
2 changes: 1 addition & 1 deletion packages/common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
export { A11yConfig, AxeResults, axeRuntimeExceptionMsgPrefix, axeVersion, getAxeRules, getViolations } from './axe';
export { WdioAssertFunction, WdioOptions } from './wdio';
export { errMsgHeader, ExceptionList } from './format';
export { log, useFilesToBeExempted } from './helpers';
export { log, useFilesToBeExempted, useCustomRules } from './helpers';
3 changes: 3 additions & 0 deletions packages/common/testMocks/sa11y-custom-rules.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"rules": ["rule1", "rule2"]
}
8 changes: 8 additions & 0 deletions packages/jest/__tests__/automatic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
domWithNoA11yIssues,
domWithNoA11yIssuesChildCount,
domWithDescendancyA11yIssues,
customRulesFilePath,
} from '@sa11y/test-utils';
import * as Sa11yCommon from '@sa11y/common';
import { expect, jest } from '@jest/globals';
Expand Down Expand Up @@ -203,4 +204,11 @@ describe('automatic checks call', () => {
document.body.innerHTML = domWithA11yIssues;
await expect(automaticCheck({ filesFilter: nonExistentFilePaths })).rejects.toThrow();
});

it('should take only custom rules if specified', async () => {
document.body.innerHTML = domWithA11yIssues;
process.env.SA11Y_CUSTOM_RULES = customRulesFilePath;
await expect(automaticCheck({ cleanupAfterEach: true })).rejects.toThrow('1 Accessibility');
delete process.env.SA11Y_CUSTOM_RULES;
});
});
11 changes: 10 additions & 1 deletion packages/jest/__tests__/setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import { adaptA11yConfig, registerSa11yMatcher } from '../src/setup';
import { adaptA11yConfig, adaptA11yConfigCustomRules, registerSa11yMatcher } from '../src/setup';
import { base, extended } from '@sa11y/preset-rules';
import { expect, jest } from '@jest/globals';

Expand All @@ -21,6 +21,15 @@ describe('jest setup', () => {
// original ruleset is not modified
expect(config.runOnly.values).toContain('color-contrast');
});
it('should customize preset-rule as expected for custom rules', () => {
expect(base.runOnly.values).toContain('color-contrast');
const rules = adaptA11yConfigCustomRules(base, ['rule1', 'rule2']).runOnly.values;
expect(rules).toContain('rule1');
expect(rules).toContain('rule2');

// original ruleset is not modified
expect(base.runOnly.values).toContain('color-contrast');
});

/* Skipped: Difficult to mock the global "expect" when we are `import {expect} from '@jest/globals'` */
it.skip('should throw error when global expect is undefined', () => {

Check warning on line 35 in packages/jest/__tests__/setup.test.ts

View workflow job for this annotation

GitHub Actions / Lint, Build, Test - Node 16.x on ubuntu-latest

Disabled test

Check warning on line 35 in packages/jest/__tests__/setup.test.ts

View workflow job for this annotation

GitHub Actions / Lint, Build, Test - Node 18.x on ubuntu-latest

Disabled test
Expand Down
12 changes: 9 additions & 3 deletions packages/jest/src/automatic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import { AxeResults, log } from '@sa11y/common';
import { AxeResults, log, useCustomRules } from '@sa11y/common';
import { getViolationsJSDOM } from '@sa11y/assert';
import { A11yError } from '@sa11y/format';
import { isTestUsingFakeTimer } from './matcher';
import { expect } from '@jest/globals';
import { adaptA11yConfig } from './setup';
import { adaptA11yConfig, adaptA11yConfigCustomRules } from './setup';
import { defaultRuleset } from '@sa11y/preset-rules';

/**
Expand Down Expand Up @@ -82,6 +82,7 @@ export async function automaticCheck(opts: AutoCheckOpts = defaultAutoCheckOpts)
// Create a DOM walker filtering only elements (skipping text, comment nodes etc)
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
let currNode = walker.firstChild();
const customRules = await useCustomRules();
try {
while (currNode !== null) {
// TODO (spike): Use a logger lib with log levels selectable at runtime
Expand All @@ -91,7 +92,12 @@ export async function automaticCheck(opts: AutoCheckOpts = defaultAutoCheckOpts)
// : ${testPath}`
// );
// W-10004832 - Exclude descendancy based rules from automatic checks
violations.push(...(await getViolationsJSDOM(currNode, adaptA11yConfig(defaultRuleset))));
if (customRules.length === 0)
violations.push(...(await getViolationsJSDOM(currNode, adaptA11yConfig(defaultRuleset))));
else
violations.push(
...(await getViolationsJSDOM(currNode, adaptA11yConfigCustomRules(defaultRuleset, customRules)))
);
currNode = walker.nextSibling();
}
} finally {
Expand Down
7 changes: 5 additions & 2 deletions packages/jest/src/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ export function setup(opts: Sa11yOpts = defaultSa11yOpts): void {
]);
registerSa11yAutomaticChecks(autoCheckOpts);
}

/**
* Register accessibility helpers toBeAccessible as jest matchers
*/
Expand All @@ -141,10 +140,14 @@ export function registerSa11yMatcher(): void {
);
}
}

/**
* Customize sa11y preset rules specific to JSDOM
*/
export function adaptA11yConfigCustomRules(config: A11yConfig, customRules: string[]): A11yConfig {
const adaptedConfig = JSON.parse(JSON.stringify(config)) as A11yConfig;
adaptedConfig.runOnly.values = customRules;
return adaptedConfig;
}
export function adaptA11yConfig(config: A11yConfig, filterRules = disabledRules): A11yConfig {
// TODO (refactor): Move into preset-rules pkg as a generic rules filter util
const adaptedConfig = JSON.parse(JSON.stringify(config)) as A11yConfig;
Expand Down
3 changes: 3 additions & 0 deletions packages/test-utils/__data__/sa11y-custom-rules.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"rules": ["link-name"]
}
1 change: 1 addition & 0 deletions packages/test-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ export {
a11yIssuesCountFiltered,
shadowDomID,
videoURL,
customRulesFilePath,
} from './test-data';
export { beforeEachSetup, cartesianProduct } from './utils';
1 change: 1 addition & 0 deletions packages/test-utils/src/test-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const dataDir = path.resolve(__dirname, '../__data__/');
// DOM with a11y issues
export const domWithA11yIssuesBodyID = 'dom-with-issues';
const fileWithA11yIssues = path.resolve(dataDir, 'a11yIssues.html');
export const customRulesFilePath = path.resolve(dataDir, 'sa11y-custom-rules.json');
const fileWithDescendancyA11yIssues = path.resolve(dataDir, 'descendancyA11yIssues.html');
export const htmlFileWithA11yIssues = 'file:///' + fileWithA11yIssues;
export const domWithA11yIssues = fs.readFileSync(fileWithA11yIssues).toString();
Expand Down

0 comments on commit 3e96398

Please sign in to comment.