diff --git a/build/tasks/esbuild.js b/build/tasks/esbuild.js index 6a5479dff9..30dbeb4ef6 100644 --- a/build/tasks/esbuild.js +++ b/build/tasks/esbuild.js @@ -26,7 +26,10 @@ module.exports = function (grunt) { bundle: true }) .then(done) - .catch(done); + .catch(e => { + grunt.fail.fatal(e); + done(); + }); }); }); } diff --git a/lib/checks/navigation/region-evaluate.js b/lib/checks/navigation/region-evaluate.js index 0fed9ea4b3..0e14b8a168 100644 --- a/lib/checks/navigation/region-evaluate.js +++ b/lib/checks/navigation/region-evaluate.js @@ -4,7 +4,6 @@ import * as standards from '../../commons/standards'; import matches from '../../commons/matches'; import cache from '../../core/base/cache'; -const landmarkRoles = standards.getAriaRolesByType('landmark'); const implicitAriaLiveRoles = ['alert', 'log', 'status']; export default function regionEvaluate(node, options, virtualNode) { @@ -92,6 +91,7 @@ function isRegion(virtualNode, options) { const node = virtualNode.actualNode; const role = getRole(virtualNode); const ariaLive = (node.getAttribute('aria-live') || '').toLowerCase().trim(); + const landmarkRoles = standards.getAriaRolesByType('landmark'); // Ignore content inside of aria-live if ( diff --git a/lib/commons/color/incomplete-data.js b/lib/commons/color/incomplete-data.js index 0ece568087..c9b7194f32 100644 --- a/lib/commons/color/incomplete-data.js +++ b/lib/commons/color/incomplete-data.js @@ -1,9 +1,12 @@ +import cache from '../../core/base/cache'; + +const cacheKey = 'color.incompleteData'; + /** * API for handling incomplete color data * @namespace axe.commons.color.incompleteData * @inner */ -let data = {}; const incompleteData = { /** * Store incomplete data by key with a string value @@ -17,6 +20,7 @@ const incompleteData = { if (typeof key !== 'string') { throw new Error('Incomplete data: key must be a string'); } + const data = cache.get(cacheKey, () => ({})); if (reason) { data[key] = reason; } @@ -31,7 +35,8 @@ const incompleteData = { * @return {String} String for reason we couldn't tell */ get: function (key) { - return data[key]; + const data = cache.get(cacheKey); + return data?.[key]; }, /** * Clear incomplete data on demand @@ -40,7 +45,7 @@ const incompleteData = { * @instance */ clear: function () { - data = {}; + cache.set(cacheKey, {}); } }; diff --git a/lib/commons/standards/implicit-html-roles.js b/lib/commons/standards/implicit-html-roles.js index 0a7b2da142..cc3c8edb74 100644 --- a/lib/commons/standards/implicit-html-roles.js +++ b/lib/commons/standards/implicit-html-roles.js @@ -10,13 +10,19 @@ import isRowHeader from '../table/is-row-header'; import sanitize from '../text/sanitize'; import isFocusable from '../dom/is-focusable'; import { closest } from '../../core/utils'; +import cache from '../../core/base/cache'; import getExplicitRole from '../aria/get-explicit-role'; -const sectioningElementSelector = - getElementsByContentType('sectioning') - .map(nodeName => `${nodeName}:not([role])`) - .join(', ') + - ' , main:not([role]), [role=article], [role=complementary], [role=main], [role=navigation], [role=region]'; +const getSectioningElementSelector = () => { + return cache.get('sectioningElementSelector', () => { + return ( + getElementsByContentType('sectioning') + .map(nodeName => `${nodeName}:not([role])`) + .join(', ') + + ' , main:not([role]), [role=article], [role=complementary], [role=main], [role=navigation], [role=region]' + ); + }); +}; // sectioning elements only have an accessible name if the // aria-label, aria-labelledby, or title attribute has valid @@ -64,7 +70,7 @@ const implicitHtmlRoles = { fieldset: 'group', figure: 'figure', footer: vNode => { - const sectioningElement = closest(vNode, sectioningElementSelector); + const sectioningElement = closest(vNode, getSectioningElementSelector()); return !sectioningElement ? 'contentinfo' : null; }, @@ -78,7 +84,7 @@ const implicitHtmlRoles = { h5: 'heading', h6: 'heading', header: vNode => { - const sectioningElement = closest(vNode, sectioningElementSelector); + const sectioningElement = closest(vNode, getSectioningElementSelector()); return !sectioningElement ? 'banner' : null; }, diff --git a/lib/core/base/virtual-node/virtual-node.js b/lib/core/base/virtual-node/virtual-node.js index e752e07040..589b61237f 100644 --- a/lib/core/base/virtual-node/virtual-node.js +++ b/lib/core/base/virtual-node/virtual-node.js @@ -3,7 +3,6 @@ import { isXHTML, validInputTypes } from '../../utils'; import { isFocusable, getTabbableElements } from '../../../commons/dom'; import cache from '../cache'; -let isXHTMLGlobal; let nodeIndex = 0; class VirtualNode extends AbstractVirtualNode { @@ -27,11 +26,7 @@ class VirtualNode extends AbstractVirtualNode { this._isHidden = null; // will be populated by axe.utils.isHidden this._cache = {}; - - if (typeof isXHTMLGlobal === 'undefined') { - isXHTMLGlobal = isXHTML(node.ownerDocument); - } - this._isXHTML = isXHTMLGlobal; + this._isXHTML = isXHTML(node.ownerDocument); // we will normalize the type prop for inputs by looking strictly // at the attribute and not what the browser resolves the type diff --git a/lib/core/utils/frame-messenger/message-id.js b/lib/core/utils/frame-messenger/message-id.js index 0766e1b36d..d01533ca7e 100644 --- a/lib/core/utils/frame-messenger/message-id.js +++ b/lib/core/utils/frame-messenger/message-id.js @@ -1,5 +1,6 @@ import { v4 as createUuid } from '../uuid'; +// No cache, so that this can persist across axe.run calls const messageIds = []; export function createMessageId() { diff --git a/lib/core/utils/frame-messenger/post-message.js b/lib/core/utils/frame-messenger/post-message.js index 5f90b76ca3..a682bee5d1 100644 --- a/lib/core/utils/frame-messenger/post-message.js +++ b/lib/core/utils/frame-messenger/post-message.js @@ -15,10 +15,6 @@ import { createMessageId } from './message-id'; * @return {Boolean} true if the message was sent */ export function postMessage(win, data, sendToParent, replyHandler) { - if (typeof replyHandler === 'function') { - storeReplyHandler(data.channelId, replyHandler, sendToParent); - } - // Prevent messaging to an inappropriate window sendToParent ? assertIsParentWindow(win) : assertIsFrameWindow(win); if (data.message instanceof Error && !sendToParent) { @@ -37,6 +33,9 @@ export function postMessage(win, data, sendToParent, replyHandler) { return false; } + if (typeof replyHandler === 'function') { + storeReplyHandler(data.channelId, replyHandler, sendToParent); + } // There is no way to know the origin of `win`, so we'll try them all. allowedOrigins.forEach(origin => { try { diff --git a/lib/core/utils/get-selector.js b/lib/core/utils/get-selector.js index 02c192f8ec..28af5bf6af 100644 --- a/lib/core/utils/get-selector.js +++ b/lib/core/utils/get-selector.js @@ -5,7 +5,6 @@ import matchesSelector from './element-matches'; import isXHTML from './is-xhtml'; import getShadowSelector from './get-shadow-selector'; -let xhtml; const ignoredAttributes = [ 'class', 'style', @@ -238,9 +237,7 @@ function getElmId(elm) { * @return {String|Array} Base CSS selector for the node */ function getBaseSelector(elm) { - if (typeof xhtml === 'undefined') { - xhtml = isXHTML(document); - } + const xhtml = isXHTML(document); return escapeSelector(xhtml ? elm.localName : elm.nodeName.toLowerCase()); } diff --git a/lib/core/utils/is-xhtml.js b/lib/core/utils/is-xhtml.js index 09851a2153..3ed4c51776 100644 --- a/lib/core/utils/is-xhtml.js +++ b/lib/core/utils/is-xhtml.js @@ -1,3 +1,5 @@ +import memoize from './memoize'; + /** * Determines if a document node is XHTML * @method isXHTML @@ -5,11 +7,11 @@ * @param {Node} doc a document node * @return {Boolean} */ -function isXHTML(doc) { - if (!doc.createElement) { +const isXHTML = memoize(doc => { + if (!doc?.createElement) { return false; } return doc.createElement('A').localName === 'A'; -} +}); export default isXHTML; diff --git a/lib/core/utils/query-selector-all-filter.js b/lib/core/utils/query-selector-all-filter.js index 770e3651c1..d1d92461b3 100644 --- a/lib/core/utils/query-selector-all-filter.js +++ b/lib/core/utils/query-selector-all-filter.js @@ -1,3 +1,4 @@ +import cache from '../base/cache'; import { matchesExpression, convertSelector } from './matches'; import { getNodesMatchingExpression } from './selector-cache'; @@ -19,17 +20,20 @@ function createLocalVariables( return retVal; } -/** - * Allocating new objects in createLocalVariables is quite expensive given - * that matchExpressions is in the hot path. - * - * Keep track of previously allocated objects to avoid useless allocations - * and garbage collection. This is intentionally shared between calls of - * matchExpressions. - */ -const recycledLocalVariables = []; - function matchExpressions(domTree, expressions, filter) { + /** + * Allocating new objects in createLocalVariables is quite expensive given + * that matchExpressions is in the hot path. + * + * Keep track of previously allocated objects to avoid useless allocations + * and garbage collection. This is intentionally shared between calls of + * matchExpressions. + */ + const recycledLocalVariables = cache.get( + 'qsa.recycledLocalVariables', + () => [] + ); + const stack = []; const vNodes = Array.isArray(domTree) ? domTree : [domTree]; let currentLevel = createLocalVariables( diff --git a/test/checks/navigation/region.js b/test/checks/navigation/region.js index 2c23ddc084..fb4d13b3e9 100644 --- a/test/checks/navigation/region.js +++ b/test/checks/navigation/region.js @@ -18,6 +18,7 @@ describe('region', function () { afterEach(function () { fixture.innerHTML = ''; checkContext.reset(); + axe.reset(); }); it('should return true when content is inside the region', function () { @@ -28,6 +29,23 @@ describe('region', function () { assert.isTrue(checkEvaluate.apply(checkContext, checkArgs)); }); + it('should return true when a region role is added to standards', () => { + axe.configure({ + standards: { + ariaRoles: { + feed: { + type: 'landmark' + } + } + } + }); + var checkArgs = checkSetup( + '
This is random content.
' + + '

Introduction

' + ); + assert.isTrue(checkEvaluate.apply(checkContext, checkArgs)); + }); + it('should return false when img content is outside the region', function () { var checkArgs = checkSetup( '

Introduction

'