diff --git a/lib/commons/aria/is-accessible-ref.js b/lib/commons/aria/is-accessible-ref.js index 0278a1571e..1db12d27dd 100644 --- a/lib/commons/aria/is-accessible-ref.js +++ b/lib/commons/aria/is-accessible-ref.js @@ -4,7 +4,7 @@ const idRefsRegex = /^idrefs?$/; function cacheIdRefs(node, refAttrs) { if (node.hasAttribute) { if (node.nodeName.toUpperCase() === 'LABEL' && node.hasAttribute('for')) { - axe._cache.idRefs[node.getAttribute('for')] = true; + axe._cache.get('idRefs')[node.getAttribute('for')] = true; } refAttrs @@ -12,7 +12,7 @@ function cacheIdRefs(node, refAttrs) { .forEach(attr => { const attrValue = node.getAttribute(attr); axe.utils.tokenList(attrValue).forEach(id => { - axe._cache.idRefs[id] = true; + axe._cache.get('idRefs')[id] = true; }); }); } @@ -35,8 +35,8 @@ aria.isAccessibleRef = function isAccessibleRef(node) { // because axe.commons is not available in axe.utils, we can't do // this caching when we build up the virtual tree - if (!axe._cache.idRefs) { - axe._cache.idRefs = {}; + if (!axe._cache.get('idRefs')) { + axe._cache.set('idRefs', {}); // Get all idref(s) attributes on the lookup table const refAttrs = Object.keys(aria.lookupTable.attributes).filter(attr => { const { type } = aria.lookupTable.attributes[attr]; @@ -46,5 +46,5 @@ aria.isAccessibleRef = function isAccessibleRef(node) { cacheIdRefs(root, refAttrs); } - return axe._cache.idRefs[id] === true; + return axe._cache.get('idRefs')[id] === true; }; diff --git a/lib/commons/dom/is-skip-link.js b/lib/commons/dom/is-skip-link.js index e45525f5a6..0c99776493 100644 --- a/lib/commons/dom/is-skip-link.js +++ b/lib/commons/dom/is-skip-link.js @@ -17,8 +17,8 @@ dom.isSkipLink = function(element) { } let firstPageLink; - if (typeof axe._cache.firstPageLink !== 'undefined') { - firstPageLink = axe._cache.firstPageLink; + if (typeof axe._cache.get('firstPageLink') !== 'undefined') { + firstPageLink = axe._cache.get('firstPageLink'); } else { // define a skip link as any anchor element whose href starts with `#...` // and which precedes the first anchor element whose href doesn't start @@ -29,7 +29,7 @@ dom.isSkipLink = function(element) { )[0]; // null will signify no first page link - axe._cache.firstPageLink = firstPageLink || null; + axe._cache.set('firstPageLink', firstPageLink || null); } // if there are no page links then all all links will need to be diff --git a/lib/core/base/cache.js b/lib/core/base/cache.js new file mode 100644 index 0000000000..68bef2172f --- /dev/null +++ b/lib/core/base/cache.js @@ -0,0 +1,33 @@ +(function() { + 'use strict'; + let _cache = {}; + + const cache = { + /** + * Set an item in the cache. + * @param {String} key - Name of the key. + * @param {*} value - Value to store. + */ + set(key, value) { + _cache[key] = value; + }, + + /** + * Retrieve an item from the cache. + * @param {String} key - Name of the key the value was stored as. + * @returns {*} The item stored + */ + get(key) { + return _cache[key]; + }, + + /** + * Clear the cache. + */ + clear() { + _cache = {}; + } + }; + + axe._cache = cache; +})(); diff --git a/lib/core/public/run-rules.js b/lib/core/public/run-rules.js index d740531371..32be4c952b 100644 --- a/lib/core/public/run-rules.js +++ b/lib/core/public/run-rules.js @@ -3,7 +3,7 @@ // Clean up after resolve / reject function cleanup() { - axe._cache = undefined; + axe._cache.clear(); axe._tree = undefined; axe._selectorData = undefined; } @@ -19,10 +19,6 @@ function cleanup() { function runRules(context, options, resolve, reject) { 'use strict'; try { - axe._cache = { - nodeMap: new WeakMap() - }; - context = new Context(context); axe._tree = context.flatTree; axe._selectorData = axe.utils.getSelectorData(context.flatTree); diff --git a/lib/core/utils/flattened-tree.js b/lib/core/utils/flattened-tree.js index 5012acb2d3..89a23d5a60 100644 --- a/lib/core/utils/flattened-tree.js +++ b/lib/core/utils/flattened-tree.js @@ -48,7 +48,7 @@ function virtualDOMfromNode(node, shadowId) { return vNodeCache._tabbableElements; } }; - axe._cache.nodeMap.set(node, vNode); + axe._cache.get('nodeMap').set(node, vNode); return vNode; } @@ -78,11 +78,11 @@ function getSlotChildren(node) { * @param {String} shadowId, optional ID of the shadow DOM that is the closest shadow * ancestor of the node */ -axe.utils.getFlattenedTree = function(node, shadowId) { +function flattenTree(node, shadowId) { // using a closure here and therefore cannot easily refactor toreduce the statements var retVal, realArray, nodeName; function reduceShadowDOM(res, child) { - var replacements = axe.utils.getFlattenedTree(child, shadowId); + var replacements = flattenTree(child, shadowId); if (replacements) { res = res.concat(replacements); } @@ -144,6 +144,19 @@ axe.utils.getFlattenedTree = function(node, shadowId) { return undefined; } } +} + +/** + * Recursvely returns an array of the virtual DOM nodes at this level + * excluding comment nodes and the shadow DOM nodes and + * + * @param {Node} node the current node + * @param {String} shadowId, optional ID of the shadow DOM that is the closest shadow + * ancestor of the node + */ +axe.utils.getFlattenedTree = function(node, shadowId) { + axe._cache.set('nodeMap', new WeakMap()); + return flattenTree(node, shadowId); }; /** @@ -154,5 +167,5 @@ axe.utils.getFlattenedTree = function(node, shadowId) { */ axe.utils.getNodeFromTree = function(vNode, node) { const el = node || vNode; - return axe._cache.nodeMap.get(el); + return axe._cache.get('nodeMap') ? axe._cache.get('nodeMap').get(el) : null; }; diff --git a/test/core/base/cache.js b/test/core/base/cache.js new file mode 100644 index 0000000000..a2da9981c4 --- /dev/null +++ b/test/core/base/cache.js @@ -0,0 +1,32 @@ +describe('axe._cache', function() { + 'use strict'; + + it('should set items from the cache without error', function() { + function fn() { + axe._cache.set('foo', 'bar'); + } + assert.doesNotThrow(fn); + }); + + it('should not throw for non-string keys', function() { + function fn() { + axe._cache.set(1, 'bar'); + axe._cache.set({}, 'bar'); + axe._cache.set(null, 'bar'); + } + assert.doesNotThrow(fn); + }); + + it('should get an item from the cache', function() { + axe._cache.set('foo', 'bar'); + var value = axe._cache.get('foo'); + assert.equal(value, 'bar'); + }); + + it('should clear the cache', function() { + axe._cache.set('foo', 'bar'); + axe._cache.clear(); + var value = axe._cache.get('foo'); + assert.isUndefined(value); + }); +}); diff --git a/test/integration/full/plugin/plugin.html b/test/integration/full/plugin/plugin.html new file mode 100644 index 0000000000..7d8335a90f --- /dev/null +++ b/test/integration/full/plugin/plugin.html @@ -0,0 +1,27 @@ + + + + Plugin Test + + + + + + + +

Hello World

+
+ + + + diff --git a/test/integration/full/plugin/plugin.js b/test/integration/full/plugin/plugin.js new file mode 100644 index 0000000000..a0f7ac90fd --- /dev/null +++ b/test/integration/full/plugin/plugin.js @@ -0,0 +1,107 @@ +describe('plugin test', function() { + it('should register the plugin', function() { + axe.registerPlugin({ + id: 'doStuff', + run: function(id, action, options, callback) { + var frames; + var q = axe.utils.queue(); + var that = this; + frames = axe.utils.toArray(document.querySelectorAll('iframe, frame')); + + frames.forEach(function(frame) { + q.defer(function(done) { + axe.utils.sendCommandToFrame( + frame, + { + options: options, + command: 'run-doStuff', + parameter: id, + action: action + }, + function() { + done(); + } + ); + }); + }); + + if (!options.context.length) { + q.defer(function(done) { + that._registry[id][action].call( + that._registry[id], + document, + options, + done + ); + }); + } + q.then(callback); + }, + commands: [ + { + id: 'run-doStuff', + callback: function(data, callback) { + return axe.plugins.doStuff.run( + data.parameter, + data.action, + data.options, + callback + ); + } + } + ] + }); + + assert.isOk(axe.plugins.doStuff); + }); + + it('should add plugin instance', function() { + var highlight = { + id: 'highlight', + highlighter: function(node) { + node.style.backgroundColor = 'yellow'; + this._node = node; + }, + run: function(contextNode, options, done) { + var that = this; + Array.prototype.slice + .call(contextNode.querySelectorAll(options.selector)) + .forEach(function(node) { + that.highlighter(node, options); + }); + done(); + }, + cleanup: function(done) { + this._node.style.backgroundColor = ''; + done(); + } + }; + + axe.plugins.doStuff.add(highlight); + assert.equal(axe.plugins.doStuff._registry.highlight, highlight); + }); + + it('should run the plugin', function(done) { + var h1 = document.querySelector('.my-heading'); + + axe.plugins.doStuff.run( + 'highlight', + 'run', + { + selector: '.my-heading', + context: [] + }, + function() { + assert.equal(h1.style.backgroundColor, 'yellow'); + done(); + } + ); + }); + + it('should cleanup the plugin', function() { + var h1 = document.querySelector('.my-heading'); + + axe.cleanup(); + assert.equal(h1.style.backgroundColor, ''); + }); +}); diff --git a/test/testutils.js b/test/testutils.js index a2384834cf..12573c8635 100644 --- a/test/testutils.js +++ b/test/testutils.js @@ -308,8 +308,6 @@ testUtils.isIE11 = (function isIE11(navigator) { axe.testUtils = testUtils; -beforeEach(function() { - axe._cache = { - nodeMap: new WeakMap() - }; +afterEach(function() { + axe._cache.clear(); });