UNPKG

@skyux-sdk/testing

Version:

This library was generated with [Nx](https://nx.dev).

352 lines (335 loc) 12.5 kB
import axe from 'axe-core'; import { TestBed } from '@angular/core/testing'; import { SkyAppResourcesService, SkyLibResourcesService } from '@skyux/i18n'; import { firstValueFrom } from 'rxjs'; function parseMessage(violations) { let message = 'Expected element to pass accessibility checks.\n\n'; violations.forEach((violation) => { const wcagTags = violation.tags .filter((tag) => tag.match(/wcag\d{3}|^best*/gi)) .join(', '); const nodeResults = violation.nodes.filter(filterViolationNodeResults(violation)); const html = nodeResults.reduce((accumulator, node) => { const related = [...node.all, ...node.none, ...node.any] .map((checkResult) => { const relatedNodes = checkResult.relatedNodes || []; let relatedHtml = relatedNodes .map((relatedNode) => relatedNode.html.split(`\n`).join(`\n `)) .join(`\n\n `); if (relatedHtml) { relatedHtml = `\n Related Nodes:\n ${relatedHtml}`; } return ` - [${checkResult.id}] ${checkResult.message}${relatedHtml}`; }) .join(`\n`); const newInformation = [ node.failureSummary ? `[${node.impact?.toUpperCase()}] ${node.failureSummary .split(/\n */g) .join(`\n - `)}` : '', node.ancestry ? `Ancestry: ${node.ancestry.join(', ')}` : '', `Target: ${node.target.join(', ')}`, node.html ? `HTML: ${node.html}` : '', related, ].filter((info) => !!info); return `${accumulator}\n\n${newInformation.join(`\n`)}`; }, ''); const error = [ `aXe - [Rule: '${violation.id}'] ${violation.help} - WCAG: ${wcagTags}`, ` Get help at: ${violation.helpUrl}\n`, `${html}\n\n`, ].join('\n'); message += `${error}\n`; }); return message; } function filterViolationNodeResults(result) { if ([ 'aria-dialog-name', // AG Grid adds role="dialog" to its popup editor container, but doesn't set title, aria-label, or aria-labelledby 'aria-hidden-focus', // AG Grid uses aria-hidden on elements before they are ready 'aria-required-children', // AG Grid uses some aria-hidden elements that axe doesn't like 'aria-valid-attr', // AG Grid uses aria-description, which is still in draft 'scrollable-region-focusable', // AG Grid handles scrolling ].includes(result.id)) { return (node) => !node.html.includes('class="ag-'); } else if (result.id === 'aria-allowed-role') { const fieldsetRadiogroupRegex = new RegExp(/<fieldset[^>]+role="radiogroup"/); return (node) => !fieldsetRadiogroupRegex.test(node.html); } else { return () => true; } } /** * @internal */ class _SkyA11yAnalyzer { static { this.analyzer = axe; } static run(element, config) { if (element === undefined) { throw new Error('No element was specified for accessibility checking.'); } _SkyA11yAnalyzer.analyzer.reset(); const defaults = { rules: {}, }; // Disable autocomplete-valid // Chrome browsers ignore autocomplete="off", which forces us to use non-standard values // to disable the browser's native autofill. // https://bugs.chromium.org/p/chromium/issues/detail?id=468153#c164 defaults.rules['autocomplete-valid'] = { enabled: false }; return new Promise((resolve, reject) => { const callback = (error, results) => { if (error?.message) { reject(error); return; } const violations = results.violations.filter((violation) => violation.nodes.some(filterViolationNodeResults(violation))); if (violations.length > 0) { const message = parseMessage(violations); reject(new Error(message)); return; } resolve(); }; _SkyA11yAnalyzer.analyzer.run(element, { ...defaults, ...config }, callback); }); } } /** * @internal */ async function _skyTestingCheckAccessibility(el, options) { const target = el instanceof Document ? el.documentElement : el; if (!(target instanceof Element)) { throw new Error('toBeAccessible expects an Element or Document.'); } try { await _SkyA11yAnalyzer.run(target, options); return { pass: true, message: 'Expected accessibility violations, but none were found.', }; } catch (err) { return { pass: false, message: err.message, }; } } /** * @internal */ function _skyTestingCheckExistence(el) { const pass = !!el; return { pass, message: pass ? 'Expected element not to exist' : 'Expected element to exist', }; } async function getResourceString(resourceKey, resourceArgs = []) { const resourcesSvc = TestBed.inject(SkyAppResourcesService); return await firstValueFrom(resourcesSvc.getString(resourceKey, ...resourceArgs)); } async function getLibResourceString(resourceKey, resourceArgs = []) { const resourcesSvc = TestBed.inject(SkyLibResourcesService); return await firstValueFrom(resourcesSvc.getString(resourceKey, ...resourceArgs)); } function isTemplateMatch(sample, template) { let matches = true; const templateTokens = template.split(new RegExp('{\\d+}')).reverse(); let currentToken = templateTokens.pop(); let lastPosition = 0; while (currentToken !== undefined && matches) { const tokenPosition = sample.indexOf(currentToken, lastPosition); matches = tokenPosition >= lastPosition; lastPosition = tokenPosition + currentToken.length; currentToken = templateTokens.pop(); } return matches; } /** * @internal */ async function _skyTestingCheckLibResourceTemplate(el, resourceKey) { const actualText = el.textContent ?? ''; const expectedText = await getLibResourceString(resourceKey); const pass = isTemplateMatch(actualText, expectedText); return { pass, message: pass ? `Expected element's text "${actualText}" not to match "${expectedText}"` : `Expected element's text "${actualText}" to match "${expectedText}"`, }; } /** * @internal */ async function _skyTestingCheckLibResourceText(actualText, resourceKey, resourceArgs) { const expectedText = await getLibResourceString(resourceKey, resourceArgs); const pass = actualText === expectedText; return { pass, message: pass ? `Expected "${actualText}" not to equal "${expectedText}"` : `Expected "${actualText}" to equal "${expectedText}"`, }; } /** * @internal */ async function _skyTestingCheckResourceTemplate(el, resourceKey) { const actualText = el.textContent ?? ''; const expectedText = await getResourceString(resourceKey); const pass = isTemplateMatch(actualText, expectedText); return { pass, message: pass ? `Expected element's text "${actualText}" not to match "${expectedText}"` : `Expected element's text "${actualText}" to match "${expectedText}"`, }; } /** * @internal */ async function _skyTestingCheckResourceText(actualText, resourceKey, resourceArgs) { const expectedText = await getResourceString(resourceKey, resourceArgs); const pass = actualText === expectedText; return { pass, message: pass ? `Expected "${actualText}" not to equal "${expectedText}"` : `Expected "${actualText}" to equal "${expectedText}"`, }; } const DEFAULTS = { checkCssDisplay: true, checkCssVisibility: false, checkDimensions: false, checkExists: false, }; /** * @internal */ function _skyTestingCheckVisibility(el, options) { const settings = { ...DEFAULTS, ...options }; let pass = true; if (settings.checkExists) { pass = !!el; } if (pass) { const computedStyle = window.getComputedStyle(el); if (settings.checkCssDisplay) { pass = computedStyle.display !== 'none'; } if (settings.checkCssVisibility) { pass = computedStyle.visibility !== 'hidden'; } if (settings.checkDimensions) { const box = el.getBoundingClientRect(); pass = box.width > 0 && box.height > 0; } } return { pass, message: pass ? 'Expected element to not be visible' : 'Expected element to be visible', }; } function _skyTestingHasCssClass(el, expectedClassName) { if (expectedClassName.indexOf('.') === 0) { throw new Error('Please remove the leading dot from your class name.'); } const pass = el.classList.contains(expectedClassName); return { pass, message: pass ? `Expected element not to have CSS class "${expectedClassName}"` : `Expected element to have CSS class "${expectedClassName}"`, }; } /** * @internal */ async function _skyTestingHasLibResourceText(el, resourceKey, resourceArgs = [], trimWhitespace) { const expectedText = await getLibResourceString(resourceKey, resourceArgs); let actualText = el.textContent ?? ''; if (trimWhitespace) { actualText = actualText.trim(); } const pass = actualText === expectedText; return { pass, message: pass ? `Expected element's inner text "${actualText}" not to be "${expectedText}"` : `Expected element's inner text "${actualText}" to be "${expectedText}"`, }; } /** * @internal */ async function _skyTestingHasResourceText(el, resourceKey, resourceArgs = [], trimWhitespace) { const expectedText = await getResourceString(resourceKey, resourceArgs); let actualText = el.textContent ?? ''; if (trimWhitespace) { actualText = actualText.trim(); } const pass = actualText === expectedText; return { pass, message: pass ? `Expected element's inner text "${actualText}" not to be "${expectedText}"` : `Expected element's inner text "${actualText}" to be "${expectedText}"`, }; } /** * @internal */ function _skyTestingHasStyle(el, expectedStyles) { const messages = []; let hasFailure = false; const styles = window.getComputedStyle(el); for (const styleName of Object.keys(expectedStyles)) { const actualStyle = styles.getPropertyValue(styleName); const expectedStyle = expectedStyles[styleName]; if (actualStyle !== expectedStyle) { if (!hasFailure) { hasFailure = true; } messages.push(`Expected element to have CSS style "${styleName}: ${expectedStyle}"`); } else { messages.push(`Expected element not to have CSS style "${styleName}: ${expectedStyle}"`); } messages.push(`Actual styles are: "${styleName}: ${actualStyle}"`); } return { pass: !hasFailure, message: messages.join('\n'), }; } function _skyTestingHasText(el, expectedText, trimWhitespace) { let actualText = el.textContent ?? ''; if (trimWhitespace) { actualText = actualText.trim(); } const pass = actualText === expectedText; return { pass, message: pass ? `Expected element's inner text "${actualText}" not to be: "${expectedText}"` : `Expected element's inner text to be: "${expectedText}"\n` + `Actual element's inner text was: "${actualText}"`, }; } /** * Generated bundle index. Do not edit. */ export { _SkyA11yAnalyzer, _skyTestingCheckAccessibility, _skyTestingCheckExistence, _skyTestingCheckLibResourceTemplate, _skyTestingCheckLibResourceText, _skyTestingCheckResourceTemplate, _skyTestingCheckResourceText, _skyTestingCheckVisibility, _skyTestingHasCssClass, _skyTestingHasLibResourceText, _skyTestingHasResourceText, _skyTestingHasStyle, _skyTestingHasText }; //# sourceMappingURL=skyux-sdk-testing-private.mjs.map