diff --git a/lib/core/base/check.js b/lib/core/base/check.js index 6646d9eac2..593c9b521e 100644 --- a/lib/core/base/check.js +++ b/lib/core/base/check.js @@ -108,7 +108,7 @@ Check.prototype.run = function run(node, options, context, resolve, reject) { // possible reference error. if (node && node.actualNode) { // Save a reference to the node we errored on for futher debugging. - e.errorNode = new DqElement(node.actualNode).toJSON(); + e.errorNode = new DqElement(node).toJSON(); } reject(e); return; @@ -162,7 +162,7 @@ Check.prototype.runSync = function runSync(node, options, context) { // possible reference error. if (node && node.actualNode) { // Save a reference to the node we errored on for futher debugging. - e.errorNode = new DqElement(node.actualNode).toJSON(); + e.errorNode = new DqElement(node).toJSON(); } throw e; } diff --git a/lib/core/base/rule.js b/lib/core/base/rule.js index ad6bcdc79b..652e10375c 100644 --- a/lib/core/base/rule.js +++ b/lib/core/base/rule.js @@ -252,7 +252,7 @@ Rule.prototype.run = function run(context, options = {}, resolve, reject) { .then(results => { const result = getResult(results); if (result) { - result.node = new DqElement(node.actualNode, options); + result.node = new DqElement(node, options); ruleResult.nodes.push(result); // mark rule as incomplete rather than failure for rules with reviewOnFail @@ -321,9 +321,7 @@ Rule.prototype.runSync = function runSync(context, options = {}) { const result = getResult(results); if (result) { - result.node = node.actualNode - ? new DqElement(node.actualNode, options) - : null; + result.node = node.actualNode ? new DqElement(node, options) : null; ruleResult.nodes.push(result); // mark rule as incomplete rather than failure for rules with reviewOnFail diff --git a/lib/core/base/virtual-node/virtual-node.js b/lib/core/base/virtual-node/virtual-node.js index db3ecb1fdb..1228b68ee2 100644 --- a/lib/core/base/virtual-node/virtual-node.js +++ b/lib/core/base/virtual-node/virtual-node.js @@ -4,6 +4,7 @@ import { isFocusable, getTabbableElements } from '../../../commons/dom'; import cache from '../cache'; let isXHTMLGlobal; +let nodeIndex = 0; class VirtualNode extends AbstractVirtualNode { /** @@ -19,6 +20,11 @@ class VirtualNode extends AbstractVirtualNode { this.actualNode = node; this.parent = parent; + if (!parent) { + nodeIndex = 0; + } + this.nodeIndex = nodeIndex++; + this._isHidden = null; // will be populated by axe.utils.isHidden this._cache = {}; diff --git a/lib/core/utils/dq-element.js b/lib/core/utils/dq-element.js index 8953798a9f..bf676da980 100644 --- a/lib/core/utils/dq-element.js +++ b/lib/core/utils/dq-element.js @@ -1,6 +1,8 @@ import getSelector from './get-selector'; import getAncestry from './get-ancestry'; import getXpath from './get-xpath'; +import getNodeFromTree from './get-node-from-tree'; +import AbstractVirtualNode from '../base/virtual-node/abstract-virtual-node'; function truncate(str, maxLength) { maxLength = maxLength || 300; @@ -14,6 +16,9 @@ function truncate(str, maxLength) { } function getSource(element) { + if (!element?.outerHTML) { + return ''; + } var source = element.outerHTML; if (!source && typeof XMLSerializer === 'function') { source = new XMLSerializer().serializeToString(element); @@ -27,33 +32,46 @@ function getSource(element) { * @param {HTMLElement} element The element to serialize * @param {Object} spec Properties to use in place of the element when instantiated on Elements from other frames */ -function DqElement(element, options, spec) { - this._fromFrame = !!spec; +function DqElement(elm, options = {}, spec = {}) { + this.spec = spec; + if (elm instanceof AbstractVirtualNode) { + this._virtualNode = elm; + this._element = elm.actualNode; + } else { + this._element = elm; + this._virtualNode = getNodeFromTree(elm); + } - this.spec = spec || {}; - if (options && options.absolutePaths) { + /** + * Whether DqElement was created from an iframe + * @type {boolean} + */ + this.fromFrame = this.spec.selector?.length > 1; + + if (options.absolutePaths) { this._options = { toRoot: true }; } /** - * The generated HTML source code of the element - * @type {String} + * Number by which nodes in the flat tree can be sorted + * @type {Number} */ - // TODO: es-modules_audit - if (axe._audit.noHtml) { - this.source = null; - } else if (this.spec.source !== undefined) { - this.source = this.spec.source; - } else { - this.source = getSource(element); + this.nodeIndexes = []; + if (Array.isArray(this.spec.nodeIndexes)) { + this.nodeIndexes = this.spec.nodeIndexes; + } else if (typeof this._virtualNode?.nodeIndex === 'number') { + this.nodeIndexes = [this._virtualNode.nodeIndex]; } /** - * The element which this object is based off or the containing frame, used for sorting. - * Excluded in toJSON method. - * @type {HTMLElement} + * The generated HTML source code of the element + * @type {String|null} */ - this._element = element; + this.source = null; + // TODO: es-modules_audit + if (!axe._audit.noHtml) { + this.source = this.spec.source ?? getSource(this._element); + } } DqElement.prototype = { @@ -88,16 +106,13 @@ DqElement.prototype = { return this._element; }, - get fromFrame() { - return this._fromFrame; - }, - toJSON() { return { selector: this.selector, source: this.source, xpath: this.xpath, - ancestry: this.ancestry + ancestry: this.ancestry, + nodeIndexes: this.nodeIndexes }; } }; @@ -107,7 +122,8 @@ DqElement.fromFrame = function fromFrame(node, options, frame) { ...node, selector: [...frame.selector, ...node.selector], ancestry: [...frame.ancestry, ...node.ancestry], - xpath: [...frame.xpath, ...node.xpath] + xpath: [...frame.xpath, ...node.xpath], + nodeIndexes: [...frame.nodeIndexes, ...node.nodeIndexes] }; return new DqElement(frame.element, options, spec); }; diff --git a/lib/core/utils/get-standards.js b/lib/core/utils/get-standards.js index d902832f87..545b0d08fa 100644 --- a/lib/core/utils/get-standards.js +++ b/lib/core/utils/get-standards.js @@ -1,5 +1,5 @@ -import standards from '../../standards' -import clone from './clone' +import standards from '../../standards'; +import clone from './clone'; export default function getStandards() { return clone(standards); diff --git a/lib/core/utils/merge-results.js b/lib/core/utils/merge-results.js index 739ed8421d..6a0b004b62 100644 --- a/lib/core/utils/merge-results.js +++ b/lib/core/utils/merge-results.js @@ -1,6 +1,5 @@ import DqElement from './dq-element'; import getAllChecks from './get-all-checks'; -import nodeSorter from './node-sorter'; import findBy from './find-by'; /** @@ -32,25 +31,22 @@ function pushFrame(resultSet, dqFrame, options) { */ function spliceNodes(target, to) { const firstFromFrame = to[0].node; - for (let i = 0; i < target.length; i++) { const node = target[i].node; - const sorterResult = nodeSorter( - { actualNode: node.element }, - { actualNode: firstFromFrame.element } + const resultSort = nodeIndexSort( + node.nodeIndexes, + firstFromFrame.nodeIndexes ); - if ( - sorterResult > 0 || - (sorterResult === 0 && + resultSort > 0 || + (resultSort === 0 && firstFromFrame.selector.length < node.selector.length) ) { - target.splice.apply(target, [i, 0].concat(to)); + target.splice(i, 0, ...to); return; } } - - target.push.apply(target, to); + target.push(...to); } function normalizeResult(result) { @@ -105,28 +101,34 @@ function mergeResults(frameResults, options) { }); }); - // Only sort results if we have the ability to run - // document position (such as serial node context) and if - // we have more than 1 result - if (frameResults.length > 1 && window && window.Node) { - mergedResult.forEach(result => { - if (result.nodes) { - result.nodes.sort((a, b) => { - const aNode = a.node.element; - const bNode = b.node.element; - - // only sort if the nodes are from different frames - if (aNode !== bNode && (a.node._fromFrame || b.node._fromFrame)) { - return nodeSorter(aNode, bNode); - } + // Sort results in DOM order + mergedResult.forEach(result => { + if (result.nodes) { + result.nodes.sort((nodeA, nodeB) => { + return nodeIndexSort(nodeA.node.nodeIndexes, nodeB.node.nodeIndexes); + }); + } + }); + return mergedResult; +} - return 0; - }); - } - }); +function nodeIndexSort(nodeIndexesA = [], nodeIndexesB = []) { + const length = Math.max(nodeIndexesA?.length, nodeIndexesB?.length); + for (let i = 0; i < length; i++) { + const indexA = nodeIndexesA?.[i]; + const indexB = nodeIndexesB?.[i]; + if (typeof indexA !== 'number' || isNaN(indexA)) { + // Empty arrays go at the end, otherwise shortest array first + return i === 0 ? 1 : -1; + } + if (typeof indexB !== 'number' || isNaN(indexB)) { + return i === 0 ? -1 : 1; + } + if (indexA !== indexB) { + return indexA - indexB; + } } - - return mergedResult; + return 0; } export default mergeResults; diff --git a/test/core/base/virtual-node/virtual-node.js b/test/core/base/virtual-node/virtual-node.js index 84d8701e42..dbd5459ae0 100644 --- a/test/core/base/virtual-node/virtual-node.js +++ b/test/core/base/virtual-node/virtual-node.js @@ -152,6 +152,32 @@ describe('VirtualNode', function() { }); }); + describe('nodeIndex', function() { + it('increments nodeIndex when a parent is passed', function() { + var vHtml = new VirtualNode({ nodeName: 'html' }); + var vHead = new VirtualNode({ nodeName: 'head' }, vHtml); + var vTitle = new VirtualNode({ nodeName: 'title' }, vHead); + var vBody = new VirtualNode({ nodeName: 'body' }, vHtml); + + assert.equal(vHtml.nodeIndex, 0); + assert.equal(vHead.nodeIndex, 1); + assert.equal(vTitle.nodeIndex, 2); + assert.equal(vBody.nodeIndex, 3); + }); + + it('resets nodeIndex when no parent is passed', function() { + var vHtml = new VirtualNode({ nodeName: 'html' }); + var vHead = new VirtualNode({ nodeName: 'head' }, vHtml); + assert.equal(vHtml.nodeIndex, 0); + assert.equal(vHead.nodeIndex, 1); + + vHtml = new VirtualNode({ nodeName: 'html' }); + vHead = new VirtualNode({ nodeName: 'head' }, vHtml); + assert.equal(vHtml.nodeIndex, 0); + assert.equal(vHead.nodeIndex, 1); + }); + }); + describe.skip('isFocusable', function() { var commons; diff --git a/test/core/public/run-rules.js b/test/core/public/run-rules.js index ebbd3574ff..9f98aac843 100644 --- a/test/core/public/run-rules.js +++ b/test/core/public/run-rules.js @@ -134,14 +134,17 @@ describe('runRules', function() { var nodes = r[0].passes.map(function(detail) { return detail.node.selector; }); - - assert.deepEqual(nodes, [ - ['#level0'], - ['#level0', '#level1'], - ['#level0', '#level1', '#level2a'], - ['#level0', '#level1', '#level2b'] - ]); - done(); + try { + assert.deepEqual(nodes, [ + ['#level0'], + ['#level0', '#level1'], + ['#level0', '#level1', '#level2a'], + ['#level0', '#level1', '#level2b'] + ]); + done(); + } catch (e) { + done(e); + } }, isNotCalled ); @@ -229,7 +232,8 @@ describe('runRules', function() { "/iframe[@id='context-test']", "/div[@id='target']" ], - source: '
' + source: '
', + nodeIndexes: [12, 14] }, any: [ { @@ -271,7 +275,8 @@ describe('runRules', function() { "/div[@id='foo']" ], source: - '
\n
\n
' + '
\n
\n
', + nodeIndexes: [12, 9] }, any: [ { @@ -289,7 +294,8 @@ describe('runRules', function() { "/div[@id='foo']" ], source: - '
\n
\n
' + '
\n
\n
', + nodeIndexes: [12, 9] } ] } @@ -536,7 +542,8 @@ describe('runRules', function() { 'html > body > div:nth-child(1) > div:nth-child(1)' ], xpath: ["/div[@id='target']"], - source: '
Target!
' + source: '
Target!
', + nodeIndexes: [12] }, impact: 'moderate', any: [ @@ -579,7 +586,8 @@ describe('runRules', function() { ancestry: [ 'html > body > div:nth-child(1) > div:nth-child(1)' ], - source: '
Target!
' + source: '
Target!
', + nodeIndexes: [12] }, any: [ { @@ -595,7 +603,8 @@ describe('runRules', function() { 'html > body > div:nth-child(1) > div:nth-child(1)' ], xpath: ["/div[@id='target']"], - source: '
Target!
' + source: '
Target!
', + nodeIndexes: [12] } ] } diff --git a/test/core/utils/dq-element.js b/test/core/utils/dq-element.js index fee058d451..5e998b6950 100644 --- a/test/core/utils/dq-element.js +++ b/test/core/utils/dq-element.js @@ -4,227 +4,258 @@ describe('DqElement', function() { var DqElement = axe.utils.DqElement; var fixture = document.getElementById('fixture'); var fixtureSetup = axe.testUtils.fixtureSetup; + var queryFixture = axe.testUtils.queryFixture; afterEach(function() { axe.reset(); }); - it('should be a function', function() { - assert.isFunction(DqElement); - }); - it('should be exposed to utils', function() { assert.equal(axe.utils.DqElement, DqElement); }); - it('should take a node as a parameter and return an object', function() { - var node = document.createElement('div'); - var result = new DqElement(node); + it('should take a virtual node as a parameter and return an object', function() { + var vNode = queryFixture('
'); + var result = new DqElement(vNode); + assert.equal(result.element, vNode.actualNode); + }); - assert.isObject(result); + it('should take an actual node as a parameter and return an object', function() { + var vNode = queryFixture('
'); + var result = new DqElement(vNode.actualNode); + assert.equal(result.element, vNode.actualNode); }); + describe('element', function() { it('should store reference to the element', function() { - var div = document.createElement('div'); - var dqEl = new DqElement(div); - assert.equal(dqEl.element, div); + var vNode = queryFixture('
'); + var dqEl = new DqElement(vNode); + assert.equal(dqEl.element, vNode.actualNode); }); it('should not be present in stringified version', function() { - var div = document.createElement('div'); - fixtureSetup(); - - var dqEl = new DqElement(div); - + var vNode = queryFixture('
'); + var dqEl = new DqElement(vNode); assert.isUndefined(JSON.parse(JSON.stringify(dqEl)).element); }); }); describe('source', function() { it('should include the outerHTML of the element', function() { - fixture.innerHTML = '
Hello!
'; - - var result = new DqElement(fixture.firstChild); - assert.equal(result.source, fixture.firstChild.outerHTML); + var vNode = queryFixture('
Hello!
'); + var outerHTML = vNode.actualNode.outerHTML; + var result = new DqElement(vNode); + assert.equal(result.source, outerHTML); }); it('should work with SVG elements', function() { - fixture.innerHTML = ''; + var vNode = queryFixture(''); + var result = new DqElement(vNode); - var result = new DqElement(fixture.firstChild); - assert.isString(result.source); + if (axe.testUtils.isIE11) { + assert.isString(result.source); + } else { + assert.equal(result.source, vNode.actualNode.outerHTML); + } }); + it('should work with MathML', function() { - fixture.innerHTML = - 'x2'; + var vNode = queryFixture( + '' + + 'x2' + + '' + ); - var result = new DqElement(fixture.firstChild); - assert.isString(result.source); + var result = new DqElement(vNode); + if (axe.testUtils.isIE11) { + assert.isString(result.source); + } else { + assert.equal(result.source, vNode.actualNode.outerHTML); + } }); it('should truncate large elements', function() { - var div = '
'; + var div = '
'; for (var i = 0; i < 300; i++) { div += i; } div += '
'; - fixture.innerHTML = div; - - var result = new DqElement(fixture.firstChild); - assert.equal(result.source.length, '
'.length); + var vNode = queryFixture(div); + var result = new DqElement(vNode); + assert.equal(result.source, '
'); }); it('should use spec object over passed element', function() { - fixture.innerHTML = '
Hello!
'; - var result = new DqElement( - fixture.firstChild, - {}, - { - source: 'woot' - } - ); + var vNode = queryFixture('
Hello!
'); + var spec = { source: 'woot' }; + var result = new DqElement(vNode, {}, spec); assert.equal(result.source, 'woot'); }); it('should return null if audit.noHtml is set', function() { axe.configure({ noHtml: true }); - fixture.innerHTML = '
Hello!
'; - var result = new DqElement(fixture.firstChild); + var vNode = queryFixture('
Hello!
'); + var result = new DqElement(vNode); assert.isNull(result.source); }); it('should not use spec object over passed element if audit.noHtml is set', function() { axe.configure({ noHtml: true }); - fixture.innerHTML = '
Hello!
'; - var result = new DqElement( - fixture.firstChild, - {}, - { - source: 'woot' - } - ); + var vNode = queryFixture('
Hello!
'); + var spec = { source: 'woot' }; + var result = new DqElement(vNode, {}, spec); assert.isNull(result.source); }); }); describe('selector', function() { it('should prefer selector from spec object', function() { - fixture.innerHTML = '
Hello!
'; - var result = new DqElement( - fixture.firstChild, - {}, - { - selector: 'woot' - } - ); + var vNode = queryFixture('
Hello!
'); + var spec = { selector: 'woot' }; + var result = new DqElement(vNode, {}, spec); assert.equal(result.selector, 'woot'); }); }); describe('ancestry', function() { it('should prefer selector from spec object', function() { - fixture.innerHTML = '
Hello!
'; - var result = new DqElement( - fixture.firstChild, - {}, - { - ancestry: 'woot' - } - ); + var vNode = queryFixture('
Hello!
'); + var spec = { ancestry: 'woot' }; + var result = new DqElement(vNode, {}, spec); assert.equal(result.ancestry, 'woot'); }); }); describe('xpath', function() { it('should prefer selector from spec object', function() { - fixture.innerHTML = '
Hello!
'; - var result = new DqElement( - fixture.firstChild, - {}, - { - xpath: 'woot' - } - ); + var vNode = queryFixture('
Hello!
'); + var spec = { xpath: 'woot' }; + var result = new DqElement(vNode, {}, spec); assert.equal(result.xpath, 'woot'); }); }); describe('absolutePaths', function() { it('creates a path all the way to root', function() { - fixtureSetup('
Hello!
'); - - var result = new DqElement(fixture.firstChild, { + var vNode = queryFixture('
Hello!
'); + var result = new DqElement(vNode, { absolutePaths: true }); assert.include(result.selector[0], 'html > '); assert.include(result.selector[0], '#fixture > '); - assert.include(result.selector[0], '#foo'); + assert.include(result.selector[0], '#target'); + }); + }); + + describe('nodeIndexes', function() { + it('is taken from virtualNode', function() { + fixtureSetup(''); + assert.deepEqual(new DqElement(fixture.children[0]).nodeIndexes, [1]); + assert.deepEqual(new DqElement(fixture.children[1]).nodeIndexes, [2]); + assert.deepEqual(new DqElement(fixture.children[2]).nodeIndexes, [3]); + }); + + it('is taken from spec, over virtualNode', function() { + var vNode = queryFixture('
'); + var spec = { nodeIndexes: [123, 456] }; + var dqElm = new DqElement(vNode, {}, spec); + assert.deepEqual(dqElm.nodeIndexes, [123, 456]); + }); + + it('is [] when the element is not in the virtual tree.', function() { + var div = document.createElement('div'); + var dqElm = new DqElement(div); + assert.deepEqual(dqElm.nodeIndexes, []); }); }); describe('toJSON', function() { it('should only stringify selector and source', function() { - var expected = { + var spec = { selector: 'foo > bar > joe', source: '', xpath: '/foo/bar/joe', - ancestry: 'foo > bar > joe' + ancestry: 'foo > bar > joe', + nodeIndexes: [123, 456] }; - var result = new DqElement('joe', {}, expected); - assert.deepEqual(JSON.stringify(result), JSON.stringify(expected)); + var div = document.createElement('div'); + var result = new DqElement(div, {}, spec); + assert.deepEqual(result.toJSON(), spec); }); }); describe('fromFrame', function() { var dqMain, dqIframe; beforeEach(function() { - var main = document.createElement('main'); - main.id = 'main'; - dqMain = new DqElement( - main, - {}, - { - selector: ['#main'], - ancestry: ['html > body > main'], - xpath: ['/main'] - } + var tree = fixtureSetup( + '
' ); + var main = axe.utils.querySelectorAll(tree, 'main')[0]; + var mainSpec = { + selector: ['#main'], + ancestry: ['html > body > main'], + xpath: ['/main'] + }; + dqMain = new DqElement(main, {}, mainSpec); - var iframe = document.createElement('iframe'); - iframe.id = 'iframe'; - dqIframe = new DqElement( - iframe, - {}, - { - selector: ['#iframe'], - ancestry: ['html > body > iframe'], - xpath: ['/iframe'] - } - ); + var iframe = axe.utils.querySelectorAll(tree, 'iframe')[0]; + var iframeSpec = { + selector: ['#iframe'], + ancestry: ['html > body > iframe'], + xpath: ['/iframe'] + }; + dqIframe = new DqElement(iframe, {}, iframeSpec); }); - it('returns a new DqElement', function() { - assert.instanceOf(DqElement.fromFrame(dqMain, {}, dqIframe), DqElement); - }); + describe('DqElement.fromFrame', function() { + it('returns a new DqElement', function() { + assert.instanceOf(DqElement.fromFrame(dqMain, {}, dqIframe), DqElement); + }); + + it('sets options for DqElement', function() { + var options = { absolutePaths: true }; + var dqElm = DqElement.fromFrame(dqMain, options, dqIframe); + assert.isTrue(dqElm._options.toRoot); + }); - it('sets options for DqElement', function() { - var options = { absolutePaths: true }; - var dqElm = DqElement.fromFrame(dqMain, options, dqIframe); - assert.isTrue(dqElm._options.toRoot); + it('merges node and frame selectors', function() { + var dqElm = DqElement.fromFrame(dqMain, {}, dqIframe); + assert.deepEqual(dqElm.selector, [ + dqIframe.selector[0], + dqMain.selector[0] + ]); + assert.deepEqual(dqElm.ancestry, [ + dqIframe.ancestry[0], + dqMain.ancestry[0] + ]); + assert.deepEqual(dqElm.xpath, [dqIframe.xpath[0], dqMain.xpath[0]]); + }); + + it('merges nodeIndexes', function() { + var dqElm = DqElement.fromFrame(dqMain, {}, dqIframe); + assert.deepEqual(dqElm.nodeIndexes, [ + dqIframe.nodeIndexes[0], + dqMain.nodeIndexes[0] + ]); + }); }); - it('merges node and frame selectors', function() { - var dqElm = DqElement.fromFrame(dqMain, {}, dqIframe); - assert.deepEqual(dqElm.selector, [ - dqIframe.selector[0], - dqMain.selector[0] - ]); - assert.deepEqual(dqElm.ancestry, [ - dqIframe.ancestry[0], - dqMain.ancestry[0] - ]); - assert.deepEqual(dqElm.xpath, [dqIframe.xpath[0], dqMain.xpath[0]]); + describe('DqElement.prototype.fromFrame', function() { + it('is false when created without a spec', function() { + assert.isFalse(dqMain.fromFrame); + }); + + it('is false when spec is not from a frame', function() { + var specMain = dqMain.toJSON(); + var dqElm = new DqElement(dqMain, {}, specMain); + assert.isFalse(dqElm.fromFrame); + }); + + it('is true when created with a spec', function() { + var dqElm = DqElement.fromFrame(dqMain, {}, dqIframe); + assert.isTrue(dqElm.fromFrame); + }); }); }); }); diff --git a/test/core/utils/flattened-tree.js b/test/core/utils/flattened-tree.js index 71ea346dd0..9c458339de 100644 --- a/test/core/utils/flattened-tree.js +++ b/test/core/utils/flattened-tree.js @@ -3,7 +3,6 @@ var shadowSupport = axe.testUtils.shadowSupport; describe('axe.utils.getFlattenedTree', function() { 'use strict'; - function createStyle(box) { var style = document.createElement('style'); style.textContent = @@ -75,6 +74,10 @@ describe('axe.utils.getFlattenedTree', function() { ); } + afterEach(function() { + fixture.innerHTML = ''; + }); + it('should default to document', function() { fixture.innerHTML = ''; var tree = axe.utils.getFlattenedTree(); @@ -86,11 +89,26 @@ describe('axe.utils.getFlattenedTree', function() { assert(tree[0].parent === null); }); + it('creates virtual nodes in the correct order', function() { + fixture.innerHTML = '

'; + + var vNode = axe.utils.getFlattenedTree(fixture)[0]; + assert.equal(vNode.nodeIndex, 0); + assert.equal(vNode.props.nodeName, 'div'); + assert.equal(vNode.children[0].nodeIndex, 1); + assert.equal(vNode.children[0].props.nodeName, 'p'); + assert.equal(vNode.children[0].children[0].nodeIndex, 2); + assert.equal(vNode.children[0].children[0].props.nodeName, 'b'); + assert.equal(vNode.children[0].children[0].children[0].nodeIndex, 3); + assert.equal(vNode.children[0].children[0].children[0].props.nodeName, 'i'); + assert.equal(vNode.children[1].nodeIndex, 4); + assert.equal(vNode.children[1].props.nodeName, 'u'); + assert.equal(vNode.children[1].children[0].nodeIndex, 5); + assert.equal(vNode.children[1].children[0].props.nodeName, 's'); + }); + if (shadowSupport.v0) { describe('shadow DOM v0', function() { - afterEach(function() { - fixture.innerHTML = ''; - }); beforeEach(function() { function createStoryGroup(className, contentSelector) { var group = document.createElement('div'); @@ -139,9 +157,6 @@ describe('axe.utils.getFlattenedTree', function() { if (shadowSupport.v1) { describe('shadow DOM v1', function() { - afterEach(function() { - fixture.innerHTML = ''; - }); beforeEach(function() { function createStoryGroup(className, slotName) { var group = document.createElement('div'); diff --git a/test/core/utils/merge-results.js b/test/core/utils/merge-results.js index 2e1f38311f..df7f40722d 100644 --- a/test/core/utils/merge-results.js +++ b/test/core/utils/merge-results.js @@ -1,5 +1,7 @@ describe('axe.utils.mergeResults', function() { 'use strict'; + var queryFixture = axe.testUtils.queryFixture; + it('should normalize empty results', function() { var result = axe.utils.mergeResults([ { results: [] }, @@ -14,16 +16,16 @@ describe('axe.utils.mergeResults', function() { }); it('merges frame content, including all selector types', function() { - var iframe = document.createElement('iframe'); - iframe.id = 'myframe'; + var iframe = queryFixture('').actualNode; var node = { selector: ['#foo'], xpath: ['html/#foo'], - ancestry: ['html > div'] + ancestry: ['html > div'], + nodeIndexes: [123] }; var result = axe.utils.mergeResults([ { - frame: '#myframe', + frame: '#target', frameElement: iframe, results: [ { @@ -39,25 +41,16 @@ describe('axe.utils.mergeResults', function() { assert.lengthOf(result[0].nodes, 1); var node = result[0].nodes[0].node; - assert.deepEqual(node.selector, ['#myframe', '#foo']); - assert.deepEqual(node.xpath, ['/iframe', 'html/#foo']); - assert.deepEqual(node.ancestry, ['iframe', 'html > div']); + assert.deepEqual(node.selector, ['#target', '#foo']); + assert.deepEqual(node.xpath, ["/iframe[@id='target']", 'html/#foo']); + assert.deepEqual(node.ancestry, [ + 'html > body > div:nth-child(1) > iframe', + 'html > div' + ]); + assert.deepEqual(node.nodeIndexes, [1, 123]); }); it('sorts results from iframes into their correct DOM position', function() { - var iframe1 = document.createElement('iframe'); - iframe1.id = 'iframe1'; - var iframe2 = document.createElement('iframe'); - iframe2.id = 'iframe2'; - var h1 = document.createElement('h1'); - var h4 = document.createElement('h4'); - var fixture = document.querySelector('#fixture'); - - fixture.appendChild(h1); - fixture.appendChild(iframe1); - fixture.appendChild(iframe2); - fixture.appendChild(h4); - var result = axe.utils.mergeResults([ { results: [ @@ -68,7 +61,7 @@ describe('axe.utils.mergeResults', function() { { node: { selector: ['h1'], - element: h1 + nodeIndexes: [1] } } ] @@ -80,7 +73,7 @@ describe('axe.utils.mergeResults', function() { { node: { selector: ['h4'], - element: h4 + nodeIndexes: [4] } } ] @@ -91,9 +84,9 @@ describe('axe.utils.mergeResults', function() { nodes: [ { node: { - selector: ['iframe1'], - element: iframe1, - _fromFrame: true + selector: ['iframe1', 'h2'], + nodeIndexes: [2, 1], + fromFrame: true } } ] @@ -104,9 +97,275 @@ describe('axe.utils.mergeResults', function() { nodes: [ { node: { - selector: ['iframe2'], - element: iframe2, - _fromFrame: true + selector: ['iframe1', 'h3'], + nodeIndexes: [2, 2], + fromFrame: true + } + } + ] + } + ] + } + ]); + + var ids = result[0].nodes.map(function(el) { + return el.node.selector.join(' >> '); + }); + assert.deepEqual(ids, ['h1', 'iframe1 >> h2', 'iframe1 >> h3', 'h4']); + }); + + it('sorts nested iframes', function() { + var result = axe.utils.mergeResults([ + { + results: [ + { + id: 'heading-order', + result: true, + nodes: [ + { + node: { + selector: ['h1'], + nodeIndexes: [1] + } + }, + { + node: { + selector: ['h5'], + nodeIndexes: [3] + } + } + ] + }, + { + id: 'heading-order', + result: true, + nodes: [ + { + node: { + selector: ['iframe1', 'h2'], + nodeIndexes: [2, 1], + fromFrame: true + } + }, + { + node: { + selector: ['iframe1', 'h4'], + nodeIndexes: [2, 3], + fromFrame: true + } + } + ] + }, + { + id: 'heading-order', + result: true, + nodes: [ + { + node: { + selector: ['iframe1', 'iframe2', 'h3'], + nodeIndexes: [2, 2, 1], + fromFrame: true + } + } + ] + } + ] + } + ]); + + var ids = result[0].nodes.map(function(el) { + return el.node.selector.join(' >> '); + }); + assert.deepEqual(ids, [ + 'h1', + 'iframe1 >> h2', + 'iframe1 >> iframe2 >> h3', + 'iframe1 >> h4', + 'h5' + ]); + }); + + it('sorts results even if nodeIndexes are empty', function() { + var result = axe.utils.mergeResults([ + { + results: [ + { + id: 'heading-order', + result: true, + nodes: [ + { + node: { + selector: ['h1'], + nodeIndexes: [1] + } + }, + { + node: { + selector: ['nill'], + nodeIndexes: [] + } + }, + { + node: { + selector: ['h3'], + nodeIndexes: [3] + } + } + ] + }, + { + id: 'heading-order', + result: true, + nodes: [ + { + node: { + selector: ['nill'], + nodeIndexes: [] + } + } + ] + }, + { + id: 'heading-order', + result: true, + nodes: [ + { + node: { + selector: ['iframe1', 'h2'], + nodeIndexes: [2, 1], + fromFrame: true + } + }, + { + node: { + selector: ['nill'], + nodeIndexes: [] + } + } + ] + } + ] + } + ]); + + var ids = result[0].nodes.map(function(el) { + return el.node.selector.join(' >> '); + }); + // Order of "nill" varies in IE + assert.deepEqual(ids, [ + 'h1', + 'iframe1 >> h2', + 'h3', + 'nill', + 'nill', + 'nill' + ]); + }); + + it('sorts results even if nodeIndexes are undefined', function() { + var result = axe.utils.mergeResults([ + { + results: [ + { + id: 'heading-order', + result: true, + nodes: [ + { + node: { + selector: ['h1'], + nodeIndexes: [1] + } + }, + { + node: { + selector: ['nill'] + } + }, + { + node: { + selector: ['h3'], + nodeIndexes: [3] + } + } + ] + }, + { + id: 'heading-order', + result: true, + nodes: [ + { + node: { + selector: ['nill'] + } + } + ] + }, + { + id: 'heading-order', + result: true, + nodes: [ + { + node: { + selector: ['iframe1', 'h2'], + nodeIndexes: [2, 1], + fromFrame: true + } + }, + { + node: { + selector: ['nill'] + } + } + ] + } + ] + } + ]); + + var ids = result[0].nodes.map(function(el) { + return el.node.selector.join(' >> '); + }); + // Order of "nill" varies in IE + assert.deepEqual(ids, [ + 'h1', + 'iframe1 >> h2', + 'h3', + 'nill', + 'nill', + 'nill' + ]); + }); + + it('sorts nodes all placed on the same result', function() { + var result = axe.utils.mergeResults([ + { + results: [ + { + id: 'iframe', + result: 'inapplicable', + nodes: [ + { + node: { + selector: ['#level0', '#level1', '#level2a'], + nodeIndexes: [12, 14, 14] + } + }, + { + node: { + selector: ['#level0', '#level1', '#level2b'], + nodeIndexes: [12, 14, 16] + } + }, + { + node: { + selector: ['#level0', '#level1'], + nodeIndexes: [12, 14] + } + }, + { + node: { + selector: ['#level0'], + nodeIndexes: [12] } } ] @@ -116,9 +375,14 @@ describe('axe.utils.mergeResults', function() { ]); var ids = result[0].nodes.map(function(el) { - return el.node.selector; + return el.node.selector.join(' >> '); }); - assert.deepEqual(ids, [['h1'], ['iframe1'], ['iframe2'], ['h4']]); + assert.deepEqual(ids, [ + '#level0', + '#level0 >> #level1', + '#level0 >> #level1 >> #level2a', + '#level0 >> #level1 >> #level2b' + ]); }); });