[Enterprise Search] Set up cypress-axe tests (#108465) (#109920)

* Set up cypress-axe

@see https://github.com/component-driven/cypress-axe

* DRY out Kibana axe rules into constants that Cypress can use

* Create shared & configured checkA11y command

+ fix string union type error
+ remove unnecessary tsconfig exclude

* Add Overview plugin a11y tests

* Add AS & WS placeholder a11y checks

- Mostly just re-exporting the shared command and checking for failures, I only ran this after the shared axe config settings and found no failures

* Configure our axe settings further to catch best practices

- notably heading level issues (thanks Byron for catching this!)

- however I now also need to set an ignore on a duplicate landmark violation caused by the global header (not sure why it's showing up - shouldn't it be out of context? bah)

- remove option to pass args into checkA11y - I figure it's not super likely we'll need to override axe settings per-page (vs not running it), but we can pass it custom configs or args later if needed

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Constance <constancecchen@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2021-08-24 17:28:49 -04:00 committed by GitHub
parent 3006381216
commit 9af96a8009
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 113 additions and 55 deletions

View file

@ -687,6 +687,7 @@
"css-loader": "^3.4.2",
"cssnano": "^4.1.11",
"cypress": "^6.8.0",
"cypress-axe": "^0.13.0",
"cypress-cucumber-preprocessor": "^2.5.2",
"cypress-multi-reporters": "^1.4.0",
"cypress-pipe": "^2.0.0",

View file

@ -10,6 +10,7 @@ import chalk from 'chalk';
import testSubjectToCss from '@kbn/test-subj-selector';
import { FtrService } from '../../ftr_provider_context';
import { AXE_CONFIG, AXE_OPTIONS } from './constants';
import { AxeReport, printResult } from './axe_report';
// @ts-ignore JS that is run in browser as is
import { analyzeWithAxe, analyzeWithAxeWithClient } from './analyze_with_axe';
@ -77,26 +78,13 @@ export class AccessibilityService extends FtrService {
}
private async captureAxeReport(context: AxeContext): Promise<AxeReport> {
const axeOptions = {
reporter: 'v2',
runOnly: ['wcag2a', 'wcag2aa'],
rules: {
'color-contrast': {
enabled: false, // disabled because we have too many failures
},
bypass: {
enabled: false, // disabled because it's too flaky
},
},
};
await this.Wd.driver.manage().setTimeouts({
...(await this.Wd.driver.manage().getTimeouts()),
script: 600000,
});
const report = normalizeResult(
await this.browser.executeAsync(analyzeWithAxe, context, axeOptions)
await this.browser.executeAsync(analyzeWithAxe, context, AXE_CONFIG, AXE_OPTIONS)
);
if (report !== false) {
@ -104,7 +92,7 @@ export class AccessibilityService extends FtrService {
}
const withClientReport = normalizeResult(
await this.browser.executeAsync(analyzeWithAxeWithClient, context, axeOptions)
await this.browser.executeAsync(analyzeWithAxeWithClient, context, AXE_CONFIG, AXE_OPTIONS)
);
if (withClientReport === false) {

View file

@ -8,45 +8,11 @@
import { readFileSync } from 'fs';
export function analyzeWithAxe(context, options, callback) {
export function analyzeWithAxe(context, config, options, callback) {
Promise.resolve()
.then(() => {
if (window.axe) {
window.axe.configure({
rules: [
{
id: 'scrollable-region-focusable',
selector: '[data-skip-axe="scrollable-region-focusable"]',
},
{
id: 'aria-required-children',
selector: '[data-skip-axe="aria-required-children"] > *',
},
{
id: 'label',
selector: '[data-test-subj="comboBoxSearchInput"] *',
},
{
id: 'aria-roles',
selector: '[data-test-subj="comboBoxSearchInput"] *',
},
{
// EUI bug: https://github.com/elastic/eui/issues/4474
id: 'aria-required-parent',
selector: '[class=*"euiDataGridRowCell"][role="gridcell"]',
},
{
// 3rd-party library; button has aria-describedby
id: 'button-name',
selector: '[data-rbd-drag-handle-draggable-id]',
},
{
// EUI bug: https://github.com/elastic/eui/issues/4536
id: 'duplicate-id',
selector: '.euiSuperDatePicker *',
},
],
});
window.axe.configure(config);
return window.axe.run(context, options);
}

View file

@ -0,0 +1,58 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ReporterVersion } from 'axe-core';
export const AXE_CONFIG = {
rules: [
{
id: 'scrollable-region-focusable',
selector: '[data-skip-axe="scrollable-region-focusable"]',
},
{
id: 'aria-required-children',
selector: '[data-skip-axe="aria-required-children"] > *',
},
{
id: 'label',
selector: '[data-test-subj="comboBoxSearchInput"] *',
},
{
id: 'aria-roles',
selector: '[data-test-subj="comboBoxSearchInput"] *',
},
{
// EUI bug: https://github.com/elastic/eui/issues/4474
id: 'aria-required-parent',
selector: '[class=*"euiDataGridRowCell"][role="gridcell"]',
},
{
// 3rd-party library; button has aria-describedby
id: 'button-name',
selector: '[data-rbd-drag-handle-draggable-id]',
},
{
// EUI bug: https://github.com/elastic/eui/issues/4536
id: 'duplicate-id',
selector: '.euiSuperDatePicker *',
},
],
};
export const AXE_OPTIONS = {
reporter: 'v2' as ReporterVersion,
runOnly: ['wcag2a', 'wcag2aa'],
rules: {
'color-contrast': {
enabled: false, // disabled because we have too many failures
},
bypass: {
enabled: false, // disabled because it's too flaky
},
},
};

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { login } from '../support/commands';
import { login, checkA11y } from '../support/commands';
context('Engines', () => {
beforeEach(() => {
@ -14,5 +14,6 @@ context('Engines', () => {
it('renders', () => {
cy.contains('Engines');
checkA11y();
});
});

View file

@ -5,6 +5,7 @@
* 2.0.
*/
export { checkA11y } from '../../../shared/cypress/commands';
import { login as baseLogin } from '../../../shared/cypress/commands';
import { appSearchPath } from '../../../shared/cypress/routes';

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { login } from '../../../shared/cypress/commands';
import { login, checkA11y } from '../../../shared/cypress/commands';
import { overviewPath } from '../../../shared/cypress/routes';
context('Enterprise Search Overview', () => {
@ -26,6 +26,8 @@ context('Enterprise Search Overview', () => {
.contains('Open Workplace Search')
.should('have.attr', 'href')
.and('match', /workplace_search/);
checkA11y();
});
it('should have a setup guide', () => {
@ -38,5 +40,7 @@ context('Enterprise Search Overview', () => {
cy.visit(`${overviewPath}/setup_guide`);
cy.contains('Setup Guide');
cy.contains('Add your Enterprise Search host URL to your Kibana configuration');
checkA11y();
});
});

View file

@ -33,3 +33,34 @@ export const login = ({
},
});
};
/*
* Cypress setup/helpers
*/
// eslint-disable-next-line import/no-extraneous-dependencies
import 'cypress-axe'; // eslint complains this should be in `dependencies` and not `devDependencies`, but these tests should only run on dev
import { AXE_CONFIG, AXE_OPTIONS } from 'test/accessibility/services/a11y/constants';
const axeConfig = {
...AXE_CONFIG,
rules: [
...AXE_CONFIG.rules,
{
id: 'landmark-no-duplicate-banner',
selector: '[data-test-subj="headerGlobalNav"]',
},
],
};
const axeOptions = {
...AXE_OPTIONS,
runOnly: [...AXE_OPTIONS.runOnly, 'best-practice'],
};
// @see https://github.com/component-driven/cypress-axe#cychecka11y for params
export const checkA11y = () => {
cy.injectAxe();
cy.configureAxe(axeConfig);
const context = '.kbnAppWrapper'; // Scopes a11y checks to only our app
cy.checkA11y(context, axeOptions);
};

View file

@ -1,8 +1,9 @@
{
"extends": "../../../../../../../tsconfig.base.json",
"references": [{ "path": "../../../../../../../test/tsconfig.json" }],
"include": ["./**/*"],
"compilerOptions": {
"outDir": "../../../../target/cypress/types/shared",
"types": ["cypress", "node"]
"types": ["cypress", "cypress-axe", "node"]
}
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { login } from '../support/commands';
import { login, checkA11y } from '../support/commands';
context('Overview', () => {
beforeEach(() => {
@ -14,5 +14,6 @@ context('Overview', () => {
it('renders', () => {
cy.contains('Workplace Search');
checkA11y();
});
});

View file

@ -5,6 +5,7 @@
* 2.0.
*/
export { checkA11y } from '../../../shared/cypress/commands';
import { login as baseLogin } from '../../../shared/cypress/commands';
import { workplaceSearchPath } from '../../../shared/cypress/routes';

View file

@ -10939,6 +10939,11 @@ cyclist@~0.2.2:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=
cypress-axe@^0.13.0:
version "0.13.0"
resolved "https://registry.yarnpkg.com/cypress-axe/-/cypress-axe-0.13.0.tgz#3234e1a79a27701f2451fcf2f333eb74204c7966"
integrity sha512-fCIy7RiDCm7t30U3C99gGwQrUO307EYE1QqXNaf9ToK4DVqW8y5on+0a/kUHMrHdlls2rENF6TN9ZPpPpwLrnw==
cypress-cucumber-preprocessor@^2.5.2:
version "2.5.5"
resolved "https://registry.yarnpkg.com/cypress-cucumber-preprocessor/-/cypress-cucumber-preprocessor-2.5.5.tgz#af20aa40d3dd6dc67b28f6819411831bb0bea925"