diff --git a/packages/terra-functional-testing/src/commands/axe/run.js b/packages/terra-functional-testing/src/commands/axe/run.js index c7175e76d..4f546e65d 100644 --- a/packages/terra-functional-testing/src/commands/axe/run.js +++ b/packages/terra-functional-testing/src/commands/axe/run.js @@ -1,41 +1,30 @@ -/* global browser, axe */ +/* global browser, axe, Terra */ const injectAxe = require('./inject'); /** * Executes axe on the browser. - * @param {Object} overrides - The axe options. - * @param {Array} overrides.rules - The rule overrides. + * @param {Object} options - The axe options. + * @param {Array} options.rules - The rule overrides. */ -const runAxe = (overrides = {}) => { - // Extract the axe options for the Terra service from the global browser object. - const [, options = {}] = browser.options.services.find(([service]) => ( - typeof service === 'function' && service.name === 'TerraService' - )); - - const { axe: axeOptions } = options; +const runAxe = (options = {}) => { const isAxeUnavailable = browser.execute(() => window.axe === undefined); // Inject axe-core onto the page if it has not already been initialized. if (isAxeUnavailable) { - injectAxe(axeOptions); - } + /** + * Converts the global rule overrides into an array. + * The axe.configure API requires the rules to be an array of objects. The axe.run API requires + * the rules to be an object keyed by the rule ID. + */ + const globalRuleArray = Object.keys(Terra.axe.rules).map((rule) => ( + { ...Terra.axe.rules[rule], id: rule } + )); - /** - * This rule was introduced in axe-core v3.3 and causes failures in many Terra components. - * The solution to address this failure vary by component. It is being disabled until a solution is identified in the future. - * - * Reference: https://github.com/cerner/terra-framework/issues/991 - */ - const ruleOverrides = { - 'scrollable-region-focusable': { enabled: false }, - }; + injectAxe({ rules: globalRuleArray }); + } - // Merge the global rules and overrides together. - const rules = { - ...ruleOverrides, - ...axeOptions && axeOptions.rules, - ...overrides.rules, - }; + // Merge the global rules and option overrides together. + const rules = { ...Terra.axe.rules, ...options.rules }; // eslint-disable-next-line prefer-arrow-callback, func-names return browser.executeAsync(function (opts, done) { @@ -43,7 +32,7 @@ const runAxe = (overrides = {}) => { axe.run(document, opts, function (error, result) { done({ error, result }); }); - }, { rules, restoreScroll: true, runOnly: ['wcag2a', 'wcag2aa', 'wcag21aa', 'section508'] }); + }, { rules, runOnly: ['wcag2a', 'wcag2aa', 'wcag21aa', 'section508'] }); }; module.exports = runAxe; diff --git a/packages/terra-functional-testing/src/config/wdio.conf.js b/packages/terra-functional-testing/src/config/wdio.conf.js index e626a2bb3..90b100308 100644 --- a/packages/terra-functional-testing/src/config/wdio.conf.js +++ b/packages/terra-functional-testing/src/config/wdio.conf.js @@ -133,6 +133,7 @@ exports.config = { [TerraService, { /* Use to change the form factor (test viewport) used in the wdio run. */ ...FORM_FACTOR && { formFactor: FORM_FACTOR }, + ...THEME && { theme: THEME }, }], [AssetServerService, { ...SITE && { site: SITE }, diff --git a/packages/terra-functional-testing/src/services/wdio-terra-service.js b/packages/terra-functional-testing/src/services/wdio-terra-service.js index d40352ffa..fa76c5131 100644 --- a/packages/terra-functional-testing/src/services/wdio-terra-service.js +++ b/packages/terra-functional-testing/src/services/wdio-terra-service.js @@ -8,7 +8,10 @@ const hideInputCaret = require('../commands/hide-input-caret'); class TerraService { constructor(options = {}) { - this.formFactor = options.formFactor; + const { formFactor, theme } = options; + + this.formFactor = formFactor; + this.theme = theme || 'terra-default-theme'; } /** @@ -16,7 +19,7 @@ class TerraService { * Initializes the Terra Service's custom commands. */ before(capabilities) { - // Add the Jest expect module the use the Jest matchers. + // Set Jest's expect module as the global assertion framework. global.expect = expect; global.expect.extend({ toBeAccessible }); @@ -24,14 +27,35 @@ class TerraService { global.Terra = { validates: { accessibility }, - // viewports provides access Terra's list of test viewports. + // Provides access to Terra's list of supported testing viewports. viewports: getViewports, - // describeViewports provides a custom describe block for looping test viewports. + // Provides a custom describe block for looping test viewports. describeViewports, - // hideInputCaret hides the blinking input caret that appears in inputs or editable text areas. + // Hides the blinking input caret that appears in inputs or editable text areas. hideInputCaret, + + axe: { + /** + * Global rule overrides. + * Rules modified here will be applied globally for all tests. + */ + rules: { + /** + * This rule was introduced in axe-core v3.3 and causes failures in many Terra components. + * The solution to address this failure vary by component. It is being disabled until a solution is identified in the future. + * + * Reference: https://github.com/cerner/terra-framework/issues/991 + */ + 'scrollable-region-focusable': { enabled: false }, + /** + * The lowlight theme adheres to a non-default color contrast ratio and fails the default ratio check. + * The color-contrast ratio check is disabled for lowlight theme testing. + */ + 'color-contrast': { enabled: this.theme !== 'clinical-lowlight-theme' }, + }, + }, }; // IE driver takes longer to be ready for browser interactions. @@ -46,10 +70,10 @@ class TerraService { setViewport(this.formFactor); } - afterCommand(commandName, args, result, error) { + afterCommand(commandName, _args, _result, error) { if ((commandName === 'refresh' || commandName === 'url') && !error) { try { - // This is only meant as a convenience so failure is not particularly concerning + // This is only meant as a convenience so failure is not particularly concerning. global.Terra.hideInputCaret('body'); if (global.browser.$('[data-terra-dev-site-loading]').isExisting()) { @@ -59,8 +83,7 @@ class TerraService { }); } } catch (err) { - // Intentionally blank - // If this fails we don't want to warn because the user can't fix the issue. + // Intentionally blank. If this fails we don't want to warn because the user can't fix the issue. } } } diff --git a/packages/terra-functional-testing/src/terra-dev-site/terra-functional-testing/wdio-services/TerraService.tool.mdx b/packages/terra-functional-testing/src/terra-dev-site/terra-functional-testing/wdio-services/TerraService.tool.mdx index 372f881b5..f73a0e7a4 100644 --- a/packages/terra-functional-testing/src/terra-dev-site/terra-functional-testing/wdio-services/TerraService.tool.mdx +++ b/packages/terra-functional-testing/src/terra-dev-site/terra-functional-testing/wdio-services/TerraService.tool.mdx @@ -31,17 +31,21 @@ export.config = { ## Options -### axe +### formFactor -The axe configuration options for adding or customizing the rules used during testing. See the [axe API](https://github.com/dequelabs/axe-core/blob/develop/doc/API.md) for more information on configuring rules. +Tests can be executed in a specific form factor by setting the `formFactor` configuration option or the `FORM_FACTOR` environment variable . The form factors can be any of the following supported viewports: `tiny`; `small`; `medium`; `large`; `huge`; `enormous`. In order for the tests to run in this form factor, it must be one that is already specified in the `Terra.describeViewport` block. -Type: `Object` +Type: `String` Required: `false` -Default: `{}` +Default: `undefined` -Example: +Examples: + +```js +"FORM_FACTOR=huge npm run test:wdio" +``` ```js // wdio.conf.js @@ -51,32 +55,24 @@ export.config = { // ... services: [ [TerraService, { - axe: { - rules: { - 'color-contrast': { enabled: true }, - } - }, + formFactor: 'huge', }] ], // ... }; ``` -### formFactor +### theme -Tests can be executed in a specific form factor by setting the `formFactor` configuration option or the `FORM_FACTOR` environment variable . The form factors can be any of the following supported viewports: `tiny`; `small`; `medium`; `large`; `huge`; `enormous`. In order for the tests to run in this form factor, it must be one that is already specified in the `Terra.describeViewport` block. +An optional theme name that will be used to configure the testing environment. This option will flex the axe-core rules used during testing to account for the current theme. -Type: `String` +Type: `string` Required: `false` -Default: `undefined` - -Examples: +Default: `terra-default-theme` -```js -"FORM_FACTOR=huge npm run test:wdio" -``` +Example: ```js // wdio.conf.js @@ -86,11 +82,9 @@ export.config = { // ... services: [ [TerraService, { - formFactor: 'huge', + theme: 'terra-theme-name', }] ], // ... }; ``` - - diff --git a/packages/terra-functional-testing/tests/jest/commands/axe/run.test.js b/packages/terra-functional-testing/tests/jest/commands/axe/run.test.js index 4c7ec27e4..b2348c746 100644 --- a/packages/terra-functional-testing/tests/jest/commands/axe/run.test.js +++ b/packages/terra-functional-testing/tests/jest/commands/axe/run.test.js @@ -1,7 +1,37 @@ -jest.mock('../../../../src/commands/axe/inject'); +const injectAxe = require('../../../../src/commands/axe/inject'); const runAxe = require('../../../../src/commands/axe/run'); +jest.mock('../../../../src/commands/axe/inject'); + describe('Run Axe', () => { + it('should inject axe if not already available', () => { + const mockAxeRun = jest.fn().mockImplementation((_document, _opts, func) => { + func(jest.fn(), jest.fn()); + }); + + const mockExecuteAsync = jest.fn().mockImplementation((func, opts) => { + func(opts, jest.fn()); + return {}; + }); + + global.browser = { + execute: () => true, + executeAsync: mockExecuteAsync, + }; + + global.axe = { + run: mockAxeRun, + }; + + global.Terra = { axe: { rules: { 'scrollable-region-focusable': { enabled: false } } } }; + + runAxe(); + + const expectedRules = { rules: [{ enabled: false, id: 'scrollable-region-focusable' }] }; + + expect(injectAxe).toHaveBeenCalledWith(expectedRules); + }); + it('should run axe on the document', () => { const mockAxeRun = jest.fn().mockImplementation((_document, opts, func) => { func(jest.fn(), jest.fn()); @@ -11,16 +41,13 @@ describe('Run Axe', () => { return {}; }); - const TerraService = () => { }; - global.browser = { execute: () => true, executeAsync: mockExecuteAsync, - options: { - services: [[TerraService]], - }, }; + global.Terra = { axe: { rules: {} } }; + global.axe = { run: mockAxeRun, }; @@ -31,36 +58,26 @@ describe('Run Axe', () => { }); it('should run axe with the service options', () => { - const TerraService = () => { }; - const serviceOptions = { axe: { rules: { 'mock-rule': { enabled: true } } } }; - const mockExecuteAsync = jest.fn().mockImplementation((func, opts) => { const { rules } = opts; - const expectedRules = { - 'mock-rule': { enabled: true }, - 'scrollable-region-focusable': { enabled: false }, - }; - - expect(rules).toEqual(expectedRules); + expect(rules).toEqual({ 'mock-rule': { enabled: true } }); return {}; }); global.browser = { execute: jest.fn(), executeAsync: mockExecuteAsync, - options: { - services: [[TerraService, serviceOptions]], - }, }; + global.Terra = { axe: { rules: { 'mock-rule': { enabled: true } } } }; + runAxe(); expect.assertions(1); }); it('should run axe with the options provided', () => { - const TerraService = () => { }; const mockExecuteAsync = jest.fn().mockImplementation((func, opts) => { const { rules } = opts; @@ -76,27 +93,22 @@ describe('Run Axe', () => { global.browser = { execute: jest.fn(), executeAsync: mockExecuteAsync, - options: { - services: [[TerraService]], - }, }; + global.Terra = { axe: { rules: { 'scrollable-region-focusable': { enabled: false } } } }; + runAxe({ rules: { 'mock-rule': { enabled: true } } }); expect.assertions(1); }); it('should run axe with merged rules from the service and from options provided', () => { - const TerraService = () => { }; - const serviceOptions = { axe: { rules: { 'mock-rule-1': { enabled: true } } } }; - const mockExecuteAsync = jest.fn().mockImplementation((func, opts) => { const { rules } = opts; const expectedRules = { 'mock-rule-1': { enabled: true }, 'mock-rule-2': { enabled: true }, - 'scrollable-region-focusable': { enabled: false }, }; expect(rules).toEqual(expectedRules); @@ -106,11 +118,10 @@ describe('Run Axe', () => { global.browser = { execute: jest.fn(), executeAsync: mockExecuteAsync, - options: { - services: [[TerraService, serviceOptions]], - }, }; + global.Terra = { axe: { rules: { 'mock-rule-1': { enabled: true } } } }; + runAxe({ rules: { 'mock-rule-2': { enabled: true } } }); expect.assertions(1); diff --git a/packages/terra-functional-testing/tests/jest/services/wdio-terra-service.test.js b/packages/terra-functional-testing/tests/jest/services/wdio-terra-service.test.js index 49b9f70fb..84dc0f3db 100644 --- a/packages/terra-functional-testing/tests/jest/services/wdio-terra-service.test.js +++ b/packages/terra-functional-testing/tests/jest/services/wdio-terra-service.test.js @@ -32,6 +32,32 @@ describe('WDIO Terra Service', () => { expect(global.Terra.validates.accessibility).toBeDefined(); }); + it('should setup the global terra axe configuration', () => { + const service = new WdioTerraService(); + + service.before({ browserName: 'chrome' }); + + const rules = { + 'scrollable-region-focusable': { enabled: false }, + 'color-contrast': { enabled: true }, + }; + + expect(global.Terra.axe).toEqual({ rules }); + }); + + it('should disable the axe color contrast rule for lowlight theme', () => { + const service = new WdioTerraService({ theme: 'clinical-lowlight-theme' }); + + service.before({ browserName: 'chrome' }); + + const rules = { + 'scrollable-region-focusable': { enabled: false }, + 'color-contrast': { enabled: false }, + }; + + expect(global.Terra.axe).toEqual({ rules }); + }); + it('should set the expect command as a global api', () => { const service = new WdioTerraService();