From 4a6eee8abb6b35c731da0230bc259696c7e05845 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Wed, 1 May 2019 15:45:39 -0600 Subject: [PATCH 1/4] fix(utils): make cache global instead of only setup in axe.run --- lib/commons/aria/is-accessible-ref.js | 10 ++++---- lib/commons/dom/is-skip-link.js | 6 ++--- lib/core/public/run-rules.js | 6 +---- lib/core/utils/cache.js | 33 +++++++++++++++++++++++++++ lib/core/utils/flattened-tree.js | 23 +++++++++++++++---- test/core/utils/cache.js | 32 ++++++++++++++++++++++++++ test/testutils.js | 6 ++--- 7 files changed, 95 insertions(+), 21 deletions(-) create mode 100644 lib/core/utils/cache.js create mode 100644 test/core/utils/cache.js diff --git a/lib/commons/aria/is-accessible-ref.js b/lib/commons/aria/is-accessible-ref.js index 0278a1571e..2bfb0a64eb 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.utils.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.utils.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.utils.cache.get('idRefs')) { + axe.utils.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.utils.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..9fce2d3067 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.utils.cache.get('firstPageLink') !== 'undefined') { + firstPageLink = axe.utils.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.utils.cache.set('firstPageLink', firstPageLink || null); } // if there are no page links then all all links will need to be diff --git a/lib/core/public/run-rules.js b/lib/core/public/run-rules.js index d740531371..8e0a03150f 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.utils.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/cache.js b/lib/core/utils/cache.js new file mode 100644 index 0000000000..b4f1959c33 --- /dev/null +++ b/lib/core/utils/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.utils.cache = cache; +})(); diff --git a/lib/core/utils/flattened-tree.js b/lib/core/utils/flattened-tree.js index 5012acb2d3..e96f2fdf22 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.utils.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.utils.cache.set('nodeMap', new WeakMap()); + return flattenTree(node, shadowId); }; /** @@ -154,5 +167,7 @@ axe.utils.getFlattenedTree = function(node, shadowId) { */ axe.utils.getNodeFromTree = function(vNode, node) { const el = node || vNode; - return axe._cache.nodeMap.get(el); + return axe.utils.cache.get('nodeMap') + ? axe.utils.cache.get('nodeMap').get(el) + : null; }; diff --git a/test/core/utils/cache.js b/test/core/utils/cache.js new file mode 100644 index 0000000000..309ea13d45 --- /dev/null +++ b/test/core/utils/cache.js @@ -0,0 +1,32 @@ +describe('axe.utils.cache', function() { + 'use strict'; + + it('should set items from the cache without error', function() { + function fn() { + axe.utils.cache.set('foo', 'bar'); + } + assert.doesNotThrow(fn); + }); + + it('should not throw for non-string keys', function() { + function fn() { + axe.utils.cache.set(1, 'bar'); + axe.utils.cache.set({}, 'bar'); + axe.utils.cache.set(null, 'bar'); + } + assert.doesNotThrow(fn); + }); + + it('should get an item from the cache', function() { + axe.utils.cache.set('foo', 'bar'); + var value = axe.utils.cache.get('foo'); + assert.equal(value, 'bar'); + }); + + it('should clear the cache', function() { + axe.utils.cache.set('foo', 'bar'); + axe.utils.cache.clear(); + var value = axe.utils.cache.get('foo'); + assert.isUndefined(value); + }); +}); diff --git a/test/testutils.js b/test/testutils.js index a2384834cf..92369a0f8b 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.utils.cache.clear(); }); From 2724e3605cf704287b41c854b3e36c9efd3fbb00 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Tue, 7 May 2019 14:42:15 -0600 Subject: [PATCH 2/4] make cache not public --- lib/commons/aria/is-accessible-ref.js | 10 +++++----- lib/commons/dom/is-skip-link.js | 6 +++--- lib/core/{utils => base}/cache.js | 2 +- lib/core/public/run-rules.js | 2 +- lib/core/utils/flattened-tree.js | 8 +++----- test/core/{utils => base}/cache.js | 20 ++++++++++---------- test/testutils.js | 2 +- 7 files changed, 24 insertions(+), 26 deletions(-) rename lib/core/{utils => base}/cache.js (95%) rename test/core/{utils => base}/cache.js (53%) diff --git a/lib/commons/aria/is-accessible-ref.js b/lib/commons/aria/is-accessible-ref.js index 2bfb0a64eb..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.utils.cache.get('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.utils.cache.get('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.utils.cache.get('idRefs')) { - axe.utils.cache.set('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.utils.cache.get('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 9fce2d3067..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.utils.cache.get('firstPageLink') !== 'undefined') { - firstPageLink = axe.utils.cache.get('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.utils.cache.set('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/utils/cache.js b/lib/core/base/cache.js similarity index 95% rename from lib/core/utils/cache.js rename to lib/core/base/cache.js index b4f1959c33..68bef2172f 100644 --- a/lib/core/utils/cache.js +++ b/lib/core/base/cache.js @@ -29,5 +29,5 @@ } }; - axe.utils.cache = cache; + axe._cache = cache; })(); diff --git a/lib/core/public/run-rules.js b/lib/core/public/run-rules.js index 8e0a03150f..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.utils.cache.clear(); + axe._cache.clear(); axe._tree = undefined; axe._selectorData = undefined; } diff --git a/lib/core/utils/flattened-tree.js b/lib/core/utils/flattened-tree.js index e96f2fdf22..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.utils.cache.get('nodeMap').set(node, vNode); + axe._cache.get('nodeMap').set(node, vNode); return vNode; } @@ -155,7 +155,7 @@ function flattenTree(node, shadowId) { * ancestor of the node */ axe.utils.getFlattenedTree = function(node, shadowId) { - axe.utils.cache.set('nodeMap', new WeakMap()); + axe._cache.set('nodeMap', new WeakMap()); return flattenTree(node, shadowId); }; @@ -167,7 +167,5 @@ axe.utils.getFlattenedTree = function(node, shadowId) { */ axe.utils.getNodeFromTree = function(vNode, node) { const el = node || vNode; - return axe.utils.cache.get('nodeMap') - ? axe.utils.cache.get('nodeMap').get(el) - : null; + return axe._cache.get('nodeMap') ? axe._cache.get('nodeMap').get(el) : null; }; diff --git a/test/core/utils/cache.js b/test/core/base/cache.js similarity index 53% rename from test/core/utils/cache.js rename to test/core/base/cache.js index 309ea13d45..a2da9981c4 100644 --- a/test/core/utils/cache.js +++ b/test/core/base/cache.js @@ -1,32 +1,32 @@ -describe('axe.utils.cache', function() { +describe('axe._cache', function() { 'use strict'; it('should set items from the cache without error', function() { function fn() { - axe.utils.cache.set('foo', 'bar'); + axe._cache.set('foo', 'bar'); } assert.doesNotThrow(fn); }); it('should not throw for non-string keys', function() { function fn() { - axe.utils.cache.set(1, 'bar'); - axe.utils.cache.set({}, 'bar'); - axe.utils.cache.set(null, 'bar'); + 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.utils.cache.set('foo', 'bar'); - var value = axe.utils.cache.get('foo'); + axe._cache.set('foo', 'bar'); + var value = axe._cache.get('foo'); assert.equal(value, 'bar'); }); it('should clear the cache', function() { - axe.utils.cache.set('foo', 'bar'); - axe.utils.cache.clear(); - var value = axe.utils.cache.get('foo'); + axe._cache.set('foo', 'bar'); + axe._cache.clear(); + var value = axe._cache.get('foo'); assert.isUndefined(value); }); }); diff --git a/test/testutils.js b/test/testutils.js index 92369a0f8b..12573c8635 100644 --- a/test/testutils.js +++ b/test/testutils.js @@ -309,5 +309,5 @@ testUtils.isIE11 = (function isIE11(navigator) { axe.testUtils = testUtils; afterEach(function() { - axe.utils.cache.clear(); + axe._cache.clear(); }); From 2b4df4bd146c273e03dbe271d60ec203fcdb8340 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Wed, 8 May 2019 09:06:16 -0600 Subject: [PATCH 3/4] add integration test for plugins --- test/integration/full/plugin/plugin.html | 27 ++++++ test/integration/full/plugin/plugin.js | 107 +++++++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 test/integration/full/plugin/plugin.html create mode 100644 test/integration/full/plugin/plugin.js 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..b05f55a039 --- /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.background = '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.background = ''; + 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.background, 'yellow'); + done(); + } + ); + }); + + it('should cleanup the plugin', function() { + var h1 = document.querySelector('.my-heading'); + + axe.cleanup(); + assert.equal(h1.style.background, ''); + }); +}); From 2e66ba3fe63136d676e08d58d89679db4fc02e08 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Wed, 8 May 2019 09:23:02 -0600 Subject: [PATCH 4/4] use backgroundcolor instead of background --- test/integration/full/plugin/plugin.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/integration/full/plugin/plugin.js b/test/integration/full/plugin/plugin.js index b05f55a039..a0f7ac90fd 100644 --- a/test/integration/full/plugin/plugin.js +++ b/test/integration/full/plugin/plugin.js @@ -59,7 +59,7 @@ describe('plugin test', function() { var highlight = { id: 'highlight', highlighter: function(node) { - node.style.background = 'yellow'; + node.style.backgroundColor = 'yellow'; this._node = node; }, run: function(contextNode, options, done) { @@ -72,7 +72,7 @@ describe('plugin test', function() { done(); }, cleanup: function(done) { - this._node.style.background = ''; + this._node.style.backgroundColor = ''; done(); } }; @@ -92,7 +92,7 @@ describe('plugin test', function() { context: [] }, function() { - assert.equal(h1.style.background, 'yellow'); + assert.equal(h1.style.backgroundColor, 'yellow'); done(); } ); @@ -102,6 +102,6 @@ describe('plugin test', function() { var h1 = document.querySelector('.my-heading'); axe.cleanup(); - assert.equal(h1.style.background, ''); + assert.equal(h1.style.backgroundColor, ''); }); });