From a94a5ec072b6c316d8a8c9bcbb627537a548cc51 Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Thu, 31 Jul 2025 12:02:55 -0600 Subject: [PATCH 1/2] fix(get-xpath): return proper relative selector for id --- lib/core/utils/get-xpath.js | 2 +- test/core/public/run-rules.js | 18 +++---- test/core/public/run.js | 31 ++++++------ test/core/utils/get-xpath.js | 82 +++++++++++++++++++------------- test/core/utils/merge-results.js | 4 +- 5 files changed, 76 insertions(+), 61 deletions(-) diff --git a/lib/core/utils/get-xpath.js b/lib/core/utils/get-xpath.js index 14298daab0..d79b0f830f 100644 --- a/lib/core/utils/get-xpath.js +++ b/lib/core/utils/get-xpath.js @@ -69,7 +69,7 @@ function getXPathArray(node, path) { function xpathToString(xpathArray) { return xpathArray.reduce((str, elm) => { if (elm.id) { - return `/${elm.str}[@id='${elm.id}']`; + return `//${elm.str}[@id='${elm.id}']`; } else { return str + `/${elm.str}` + (elm.count > 0 ? `[${elm.count}]` : ''); } diff --git a/test/core/public/run-rules.js b/test/core/public/run-rules.js index d04b853a80..394093ee73 100644 --- a/test/core/public/run-rules.js +++ b/test/core/public/run-rules.js @@ -221,8 +221,8 @@ describe('runRules', function () { 'html > body > div:nth-child(2)' ], xpath: [ - "/iframe[@id='context-test']", - "/div[@id='target']" + "//iframe[@id='context-test']", + "//div[@id='target']" ], source: '
', nodeIndexes: [12, 14], @@ -264,8 +264,8 @@ describe('runRules', function () { 'html > body > div:nth-child(1)' ], xpath: [ - "/iframe[@id='context-test']", - "/div[@id='foo']" + "//iframe[@id='context-test']", + "//div[@id='foo']" ], source: '
\n
\n
', @@ -284,8 +284,8 @@ describe('runRules', function () { 'html > body > div:nth-child(1)' ], xpath: [ - "/iframe[@id='context-test']", - "/div[@id='foo']" + "//iframe[@id='context-test']", + "//div[@id='foo']" ], source: '
\n
\n
', @@ -536,7 +536,7 @@ describe('runRules', function () { ancestry: [ 'html > body > div:nth-child(1) > div:nth-child(1)' ], - xpath: ["/div[@id='target']"], + xpath: ["//div[@id='target']"], source: '
Target!
', nodeIndexes: [12], fromFrame: false @@ -578,7 +578,7 @@ describe('runRules', function () { impact: null, node: { selector: ['#target'], - xpath: ["/div[@id='target']"], + xpath: ["//div[@id='target']"], ancestry: [ 'html > body > div:nth-child(1) > div:nth-child(1)' ], @@ -599,7 +599,7 @@ describe('runRules', function () { ancestry: [ 'html > body > div:nth-child(1) > div:nth-child(1)' ], - xpath: ["/div[@id='target']"], + xpath: ["//div[@id='target']"], source: '
Target!
', nodeIndexes: [12], fromFrame: false diff --git a/test/core/public/run.js b/test/core/public/run.js index f3e376045e..19e0b91c23 100644 --- a/test/core/public/run.js +++ b/test/core/public/run.js @@ -4,6 +4,7 @@ describe('axe.run', function () { var fixture = document.getElementById('fixture'); var noop = function () {}; var origRunRules = axe._runRules; + var captureError = axe.testUtils.captureError; beforeEach(function () { axe._load({ @@ -347,12 +348,12 @@ describe('axe.run', function () { { xpath: true }, - function (err, result) { + captureError(function (err, result) { assert.deepEqual(result.violations[0].nodes[0].xpath, [ - "/div[@id='fixture']" + "//div[@id='fixture']" ]); done(); - } + }, done) ); }); @@ -362,13 +363,13 @@ describe('axe.run', function () { { xpath: true }, - function (err, result) { + captureError(function (err, result) { assert.deepEqual( result.violations[0].nodes[0].none[0].relatedNodes[0].xpath, - ["/div[@id='fixture']"] + ["//div[@id='fixture']"] ); done(); - } + }, done) ); }); @@ -379,12 +380,12 @@ describe('axe.run', function () { xpath: true, reporter: 'no-passes' }, - function (err, result) { + captureError(function (err, result) { assert.deepEqual(result.violations[0].nodes[0].xpath, [ - "/div[@id='fixture']" + "//div[@id='fixture']" ]); done(); - } + }, done) ); }); }); @@ -396,10 +397,10 @@ describe('axe.run', function () { { absolutePaths: 0 }, - function (err, result) { + captureError(function (err, result) { assert.deepEqual(result.violations[0].nodes[0].target, ['#fixture']); done(); - } + }, done) ); }); @@ -409,12 +410,12 @@ describe('axe.run', function () { { absolutePaths: 'yes please' }, - function (err, result) { + captureError(function (err, result) { assert.deepEqual(result.violations[0].nodes[0].target, [ 'html > body > #fixture' ]); done(); - } + }, done) ); }); @@ -424,13 +425,13 @@ describe('axe.run', function () { { absolutePaths: true }, - function (err, result) { + captureError(function (err, result) { assert.deepEqual( result.violations[0].nodes[0].none[0].relatedNodes[0].target, ['html > body > #fixture'] ); done(); - } + }, done) ); }); }); diff --git a/test/core/utils/get-xpath.js b/test/core/utils/get-xpath.js index 78f4ec174d..28f9a5ad92 100644 --- a/test/core/utils/get-xpath.js +++ b/test/core/utils/get-xpath.js @@ -1,46 +1,60 @@ -describe('axe.utils.getXpath', function () { +describe('axe.utils.getXpath', () => { 'use strict'; - var fixture = document.getElementById('fixture'); + const fixture = document.getElementById('fixture'); - afterEach(function () { - fixture.innerHTML = ''; - }); - - it('should be a function', function () { + it('should be a function', () => { assert.isFunction(axe.utils.getXpath); }); - it('should generate an XPath selector', function () { - var node = document.createElement('div'); + it('should generate an XPath selector', () => { + const node = document.createElement('div'); fixture.appendChild(node); - var sel = axe.utils.getXpath(node); + const sel = axe.utils.getXpath(node); - assert.equal(sel, "/div[@id='fixture']/div"); + assert.equal(sel, "//div[@id='fixture']/div"); }); - it('should handle special characters', function () { - var node = document.createElement('div'); + it('should handle special characters', () => { + const node = document.createElement('div'); node.id = 'monkeys#are.animals\\ok'; fixture.appendChild(node); assert.equal( axe.utils.getXpath(node), - "/div[@id='monkeys#are.animals\\ok']" + "//div[@id='monkeys#are.animals\\ok']" ); }); - it('should stop on unique ID', function () { - var node = document.createElement('div'); + it('should stop on unique ID', () => { + const node = document.createElement('div'); node.id = 'monkeys'; fixture.appendChild(node); - var sel = axe.utils.getXpath(node); - assert.equal(sel, "/div[@id='monkeys']"); + const sel = axe.utils.getXpath(node); + assert.equal(sel, "//div[@id='monkeys']"); + }); + + it('should use the nearest unique ID', () => { + fixture.innerHTML = ` +
+
+
+
+
+
+
+
+
+ `; + const node = fixture.querySelector('#monkeys > div'); + + const sel = axe.utils.getXpath(node); + assert.equal(sel, "//div[@id='monkeys']/div"); }); - it('should not use ids if they are not unique', function () { - var node = document.createElement('div'); + it('should not use ids if they are not unique', () => { + let node = document.createElement('div'); node.id = 'monkeys'; fixture.appendChild(node); @@ -48,13 +62,13 @@ describe('axe.utils.getXpath', function () { node.id = 'monkeys'; fixture.appendChild(node); - var sel = axe.utils.getXpath(node); + const sel = axe.utils.getXpath(node); - assert.equal(sel, "/div[@id='fixture']/div[2]"); + assert.equal(sel, "//div[@id='fixture']/div[2]"); }); - it('should properly calculate number when siblings are of different type', function () { - var node, target; + it('should properly calculate number when siblings are of different type', () => { + let node, target; node = document.createElement('span'); fixture.appendChild(node); @@ -74,26 +88,26 @@ describe('axe.utils.getXpath', function () { node = document.createElement('span'); fixture.appendChild(node); - var sel = axe.utils.getXpath(target); + const sel = axe.utils.getXpath(target); - assert.equal(sel, "/div[@id='fixture']/div[2]"); + assert.equal(sel, "//div[@id='fixture']/div[2]"); }); - it('should work on the documentElement', function () { - var sel = axe.utils.getXpath(document.documentElement); + it('should work on the documentElement', () => { + const sel = axe.utils.getXpath(document.documentElement); assert.equal(sel, '/html'); }); - it('should work on the body', function () { - var sel = axe.utils.getXpath(document.body); + it('should work on the body', () => { + const sel = axe.utils.getXpath(document.body); assert.equal(sel, '/html/body'); }); - it('should work on namespaced elements', function () { + it('should work on namespaced elements', () => { fixture.innerHTML = 'Hello'; - var node = fixture.firstChild; - var sel = axe.utils.getXpath(node); + const node = fixture.firstChild; + const sel = axe.utils.getXpath(node); - assert.equal(sel, "/div[@id='fixture']/hx:include"); + assert.equal(sel, "//div[@id='fixture']/hx:include"); }); }); diff --git a/test/core/utils/merge-results.js b/test/core/utils/merge-results.js index 85c15760fc..f7c758c099 100644 --- a/test/core/utils/merge-results.js +++ b/test/core/utils/merge-results.js @@ -41,7 +41,7 @@ describe('axe.utils.mergeResults', function () { var node = result[0].nodes[0].node; assert.deepEqual(node.selector, ['#target', '#foo']); - assert.deepEqual(node.xpath, ["/iframe[@id='target']", 'html/#foo']); + assert.deepEqual(node.xpath, ["//iframe[@id='target']", 'html/#foo']); assert.deepEqual(node.ancestry, [ 'html > body > div:nth-child(1) > iframe', 'html > div' @@ -76,7 +76,7 @@ describe('axe.utils.mergeResults', function () { var node = result[0].nodes[0].node; assert.deepEqual(node.selector, ['#target', '#foo']); - assert.deepEqual(node.xpath, ["/iframe[@id='target']", 'html/#foo']); + assert.deepEqual(node.xpath, ["//iframe[@id='target']", 'html/#foo']); assert.deepEqual(node.ancestry, [ 'html > body > div:nth-child(1) > iframe', 'html > div' From 0fa8c3f2e9ac7dde0610e37014ff5ee93fc30a27 Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Thu, 7 Aug 2025 13:11:11 -0600 Subject: [PATCH 2/2] add tests to select node --- test/core/utils/get-xpath.js | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/test/core/utils/get-xpath.js b/test/core/utils/get-xpath.js index 28f9a5ad92..94586b6d66 100644 --- a/test/core/utils/get-xpath.js +++ b/test/core/utils/get-xpath.js @@ -3,6 +3,17 @@ describe('axe.utils.getXpath', () => { const fixture = document.getElementById('fixture'); + // @see https://stackoverflow.com/a/14284815/2124254 + function getElementByXPath(path) { + return document.evaluate( + path, + document, + () => 'http://www.w3.org/1998/Math/MathML', + XPathResult.FIRST_ORDERED_NODE_TYPE, + null + ).singleNodeValue; + } + it('should be a function', () => { assert.isFunction(axe.utils.getXpath); }); @@ -14,16 +25,19 @@ describe('axe.utils.getXpath', () => { const sel = axe.utils.getXpath(node); assert.equal(sel, "//div[@id='fixture']/div"); + assert.equal(node, getElementByXPath(sel)); }); it('should handle special characters', () => { const node = document.createElement('div'); node.id = 'monkeys#are.animals\\ok'; fixture.appendChild(node); - assert.equal( - axe.utils.getXpath(node), - "//div[@id='monkeys#are.animals\\ok']" - ); + + const sel = axe.utils.getXpath(node); + + assert.equal(sel, "//div[@id='monkeys#are.animals\\ok']"); + + assert.equal(node, getElementByXPath(sel)); }); it('should stop on unique ID', () => { @@ -33,6 +47,7 @@ describe('axe.utils.getXpath', () => { const sel = axe.utils.getXpath(node); assert.equal(sel, "//div[@id='monkeys']"); + assert.equal(node, getElementByXPath(sel)); }); it('should use the nearest unique ID', () => { @@ -51,6 +66,7 @@ describe('axe.utils.getXpath', () => { const sel = axe.utils.getXpath(node); assert.equal(sel, "//div[@id='monkeys']/div"); + assert.equal(node, getElementByXPath(sel)); }); it('should not use ids if they are not unique', () => { @@ -65,6 +81,7 @@ describe('axe.utils.getXpath', () => { const sel = axe.utils.getXpath(node); assert.equal(sel, "//div[@id='fixture']/div[2]"); + assert.equal(node, getElementByXPath(sel)); }); it('should properly calculate number when siblings are of different type', () => { @@ -91,23 +108,27 @@ describe('axe.utils.getXpath', () => { const sel = axe.utils.getXpath(target); assert.equal(sel, "//div[@id='fixture']/div[2]"); + assert.equal(target, getElementByXPath(sel)); }); it('should work on the documentElement', () => { const sel = axe.utils.getXpath(document.documentElement); assert.equal(sel, '/html'); + assert.equal(document.documentElement, getElementByXPath(sel)); }); it('should work on the body', () => { const sel = axe.utils.getXpath(document.body); assert.equal(sel, '/html/body'); + assert.equal(document.body, getElementByXPath(sel)); }); - it('should work on namespaced elements', () => { + it('should work on namespaced elements', function () { fixture.innerHTML = 'Hello'; - const node = fixture.firstChild; - const sel = axe.utils.getXpath(node); + var node = fixture.firstChild; + var sel = axe.utils.getXpath(node); assert.equal(sel, "//div[@id='fixture']/hx:include"); + // couldn't figure out how to use document.evaluate to select an element with namespace }); });