ember-a11y-testing
Version:
Accessibility testing for Ember applications
187 lines (159 loc) • 4.92 kB
text/typescript
import QUnit from 'qunit';
import {
currentRouteName,
currentURL,
getContext,
getTestMetadata,
} from '@ember/test-helpers';
import type { AxeResults, Result } from 'axe-core';
import { setCustomReporter } from './reporter';
import { DEBUG } from '@glimmer/env';
import { setEnableA11yAudit } from './should-force-audit';
export interface TestMetadata {
testName?: string;
setupTypes: string[];
usedHelpers: string[];
[key: string]: any;
readonly isRendering: boolean;
readonly isApplication: boolean;
}
interface AxeTestResult {
moduleName: string;
testName: string;
testMetaData: TestMetadata;
urls: string[] | Set<string>;
routes: string[] | Set<string>;
helpers: string[];
stack: string;
violations: Result[];
}
export const TEST_SUITE_RESULTS: AxeTestResult[] = [];
let currentTestResult: AxeTestResult | undefined = undefined;
let currentViolationsMap: Map<string, Result> | undefined = undefined;
let currentUrls: Set<string> | undefined;
let currentRouteNames: Set<string> | undefined;
/**
* Utility to retrieve the route name corresponding to the current test. Absorbs the emitted
* assertion error if route name is `null`, resulting in an empty string return value.
*
* @param getFn Function to use to derive the route name.
* @returns Route name or empty string.
*/
export function _getCurrentRouteName(getFn = currentRouteName): string {
let routeName = '';
try {
routeName = getFn();
} catch (error: unknown) {
if (
error instanceof Error &&
!/currentRouteName (\w|\s)+ string/.test(error.message)
) {
throw error;
}
}
return routeName;
}
/**
* A custom reporter that is invoked once per failed a11yAudit call. This can be called
* multiple times per test, and the results are accumulated until testDone.
*
* @param axeResults The axe results for each a11yAudit.
* @returns Early returns if no violations are found.
*/
export async function middlewareReporter(axeResults: AxeResults) {
if (axeResults.violations.length === 0) {
return;
}
const context = getContext();
if (!currentTestResult) {
let { module, testName } = QUnit.config.current;
if (!context) {
throw new Error(
'You tried to run ember-a11y-testing without calling one of the `setupTest` helpers from `@ember/test-helpers`. Please make sure your test setup calls `setupTest()`, `setupRenderingTest()`, or `setupApplicationTest()`!',
);
}
let testMetaData = getTestMetadata(context);
let stack = (!DEBUG && new Error().stack) || '';
currentTestResult = {
moduleName: module.name,
testName,
testMetaData,
urls: [],
routes: [],
helpers: [],
stack,
violations: [],
};
currentViolationsMap = new Map();
currentUrls = new Set();
currentRouteNames = new Set();
}
if (context) {
currentUrls!.add(currentURL());
currentRouteNames!.add(_getCurrentRouteName());
}
axeResults.violations.forEach((violation) => {
let rule = currentViolationsMap!.get(violation.id);
if (rule === undefined) {
currentViolationsMap!.set(violation.id, violation);
} else {
rule.nodes.push(...violation.nodes);
}
});
}
/**
* Invoked once per test. Accumulates the results into a set of results used for
* reporting via the middleware reporter.
*/
export function pushTestResult() {
if (typeof currentTestResult !== 'undefined') {
currentTestResult.violations = [...currentViolationsMap!.values()];
currentTestResult.urls = [...currentUrls!.values()];
currentTestResult.routes = [...currentRouteNames!.values()];
currentTestResult.helpers = currentTestResult.testMetaData.usedHelpers;
TEST_SUITE_RESULTS.push(currentTestResult);
currentTestResult = undefined;
currentViolationsMap = undefined;
currentUrls = undefined;
currentRouteNames = undefined;
}
}
type TestemCallback = (config: any, data: any, callback: () => void) => void;
declare global {
interface Window {
Testem?: {
afterTests: (callback: TestemCallback) => void;
};
}
}
/**
* Sets up the middleware reporter, which reports results when the test suite is done.
*/
export function setupMiddlewareReporter() {
setCustomReporter(middlewareReporter);
setEnableA11yAudit(true);
QUnit.testDone(pushTestResult);
if (window.Testem) {
window.Testem.afterTests(async function (_config, _data, callback) {
try {
await sendViolationsToServer();
} finally {
callback();
}
});
} else {
QUnit.done(async function () {
await sendViolationsToServer();
});
}
}
async function sendViolationsToServer() {
let response = await fetch('/report-violations', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(TEST_SUITE_RESULTS),
});
return response.json();
}