From d98d92506c2a2b53d4d29b79332bc175e75d024e Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Fri, 4 Jun 2021 14:42:15 +0200 Subject: [PATCH] chore: add nodeIndexes to DqElement --- lib/core/base/check.js | 4 +- lib/core/base/rule.js | 4 +- lib/core/utils/dq-element.js | 49 ++++---- test/core/public/run-rules.js | 18 ++- test/core/utils/dq-element.js | 204 +++++++++++++++++----------------- 5 files changed, 148 insertions(+), 131 deletions(-) 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..12788d4c1d 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 @@ -322,7 +322,7 @@ Rule.prototype.runSync = function runSync(context, options = {}) { const result = getResult(results); if (result) { result.node = node.actualNode - ? new DqElement(node.actualNode, options) + ? new DqElement(node, options) : null; ruleResult.nodes.push(result); diff --git a/lib/core/utils/dq-element.js b/lib/core/utils/dq-element.js index 8953798a9f..898df34126 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; @@ -27,33 +29,40 @@ 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 = {}) { + 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) { + this.spec = spec; + 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(spec.nodeIndexes)) { + this.nodeIndexes = spec.nodeIndexes + } else if (this._virtualNode?.nodeIndex) { + 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 = { @@ -97,7 +106,8 @@ DqElement.prototype = { selector: this.selector, source: this.source, xpath: this.xpath, - ancestry: this.ancestry + ancestry: this.ancestry, + nodeIndexes: this.nodeIndexes }; } }; @@ -107,7 +117,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/test/core/public/run-rules.js b/test/core/public/run-rules.js index ebbd3574ff..be7700f769 100644 --- a/test/core/public/run-rules.js +++ b/test/core/public/run-rules.js @@ -229,7 +229,8 @@ describe('runRules', function() { "/iframe[@id='context-test']", "/div[@id='target']" ], - source: '
' + source: '
', + nodeIndexes: [12, 14] }, any: [ { @@ -271,7 +272,8 @@ describe('runRules', function() { "/div[@id='foo']" ], source: - '
\n
\n
' + '
\n
\n
', + nodeIndexes: [12, 9] }, any: [ { @@ -289,7 +291,8 @@ describe('runRules', function() { "/div[@id='foo']" ], source: - '
\n
\n
' + '
\n
\n
', + nodeIndexes: [12, 9] } ] } @@ -536,7 +539,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 +583,8 @@ describe('runRules', function() { ancestry: [ 'html > body > div:nth-child(1) > div:nth-child(1)' ], - source: '
Target!
' + source: '
Target!
', + nodeIndexes: [12] }, any: [ { @@ -595,7 +600,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..3899c633ee 100644 --- a/test/core/utils/dq-element.js +++ b/test/core/utils/dq-element.js @@ -4,6 +4,7 @@ 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(); @@ -18,190 +19,181 @@ describe('DqElement', function() { }); it('should take a node as a parameter and return an object', function() { - var node = document.createElement('div'); - var result = new DqElement(node); - + var vNode = queryFixture('
') + var result = new DqElement(vNode); assert.isObject(result); }); + 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 result = new DqElement(fixture.firstChild); - assert.isString(result.source); + var vNode = queryFixture(''); + var outerHTML = vNode.actualNode.outerHTML; + var result = new DqElement(vNode); + assert.equal(result.source, outerHTML); }); - it('should work with MathML', function() { - fixture.innerHTML = - 'x2'; - var result = new DqElement(fixture.firstChild); - assert.isString(result.source); + it('should work with MathML', function() { + var vNode = queryFixture( + '' + + 'x2' + + '' + ); + var outerHTML = vNode.actualNode.outerHTML; + var result = new DqElement(vNode); + assert.equal(result.source, 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 unknown.', 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() { @@ -226,5 +218,13 @@ describe('DqElement', function() { ]); 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] + ]); + }) }); });