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:
'',
@@ -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:
'',
@@ -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
});
});