Skip to content

Commit 1035f9e

Browse files
strakerdylanb
andauthored
feat(get-xpath): return proper relative selector for id (#4846)
Closes: #4845 --------- Co-authored-by: Dylan Barrell <[email protected]>
1 parent 5351231 commit 1035f9e

File tree

5 files changed

+96
-60
lines changed

5 files changed

+96
-60
lines changed

lib/core/utils/get-xpath.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ function getXPathArray(node, path) {
6969
function xpathToString(xpathArray) {
7070
return xpathArray.reduce((str, elm) => {
7171
if (elm.id) {
72-
return `/${elm.str}[@id='${elm.id}']`;
72+
return `//${elm.str}[@id='${elm.id}']`;
7373
} else {
7474
return str + `/${elm.str}` + (elm.count > 0 ? `[${elm.count}]` : '');
7575
}

test/core/public/run-rules.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -221,8 +221,8 @@ describe('runRules', function () {
221221
'html > body > div:nth-child(2)'
222222
],
223223
xpath: [
224-
"/iframe[@id='context-test']",
225-
"/div[@id='target']"
224+
"//iframe[@id='context-test']",
225+
"//div[@id='target']"
226226
],
227227
source: '<div id="target"></div>',
228228
nodeIndexes: [12, 14],
@@ -264,8 +264,8 @@ describe('runRules', function () {
264264
'html > body > div:nth-child(1)'
265265
],
266266
xpath: [
267-
"/iframe[@id='context-test']",
268-
"/div[@id='foo']"
267+
"//iframe[@id='context-test']",
268+
"//div[@id='foo']"
269269
],
270270
source:
271271
'<div id="foo">\n <div id="bar"></div>\n </div>',
@@ -284,8 +284,8 @@ describe('runRules', function () {
284284
'html > body > div:nth-child(1)'
285285
],
286286
xpath: [
287-
"/iframe[@id='context-test']",
288-
"/div[@id='foo']"
287+
"//iframe[@id='context-test']",
288+
"//div[@id='foo']"
289289
],
290290
source:
291291
'<div id="foo">\n <div id="bar"></div>\n </div>',
@@ -536,7 +536,7 @@ describe('runRules', function () {
536536
ancestry: [
537537
'html > body > div:nth-child(1) > div:nth-child(1)'
538538
],
539-
xpath: ["/div[@id='target']"],
539+
xpath: ["//div[@id='target']"],
540540
source: '<div id="target">Target!</div>',
541541
nodeIndexes: [12],
542542
fromFrame: false
@@ -578,7 +578,7 @@ describe('runRules', function () {
578578
impact: null,
579579
node: {
580580
selector: ['#target'],
581-
xpath: ["/div[@id='target']"],
581+
xpath: ["//div[@id='target']"],
582582
ancestry: [
583583
'html > body > div:nth-child(1) > div:nth-child(1)'
584584
],
@@ -599,7 +599,7 @@ describe('runRules', function () {
599599
ancestry: [
600600
'html > body > div:nth-child(1) > div:nth-child(1)'
601601
],
602-
xpath: ["/div[@id='target']"],
602+
xpath: ["//div[@id='target']"],
603603
source: '<div id="target">Target!</div>',
604604
nodeIndexes: [12],
605605
fromFrame: false

test/core/public/run.js

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ describe('axe.run', function () {
44
var fixture = document.getElementById('fixture');
55
var noop = function () {};
66
var origRunRules = axe._runRules;
7+
var captureError = axe.testUtils.captureError;
78

89
beforeEach(function () {
910
axe._load({
@@ -347,12 +348,12 @@ describe('axe.run', function () {
347348
{
348349
xpath: true
349350
},
350-
function (err, result) {
351+
captureError(function (err, result) {
351352
assert.deepEqual(result.violations[0].nodes[0].xpath, [
352-
"/div[@id='fixture']"
353+
"//div[@id='fixture']"
353354
]);
354355
done();
355-
}
356+
}, done)
356357
);
357358
});
358359

@@ -362,13 +363,13 @@ describe('axe.run', function () {
362363
{
363364
xpath: true
364365
},
365-
function (err, result) {
366+
captureError(function (err, result) {
366367
assert.deepEqual(
367368
result.violations[0].nodes[0].none[0].relatedNodes[0].xpath,
368-
["/div[@id='fixture']"]
369+
["//div[@id='fixture']"]
369370
);
370371
done();
371-
}
372+
}, done)
372373
);
373374
});
374375

@@ -379,12 +380,12 @@ describe('axe.run', function () {
379380
xpath: true,
380381
reporter: 'no-passes'
381382
},
382-
function (err, result) {
383+
captureError(function (err, result) {
383384
assert.deepEqual(result.violations[0].nodes[0].xpath, [
384-
"/div[@id='fixture']"
385+
"//div[@id='fixture']"
385386
]);
386387
done();
387-
}
388+
}, done)
388389
);
389390
});
390391
});
@@ -396,10 +397,10 @@ describe('axe.run', function () {
396397
{
397398
absolutePaths: 0
398399
},
399-
function (err, result) {
400+
captureError(function (err, result) {
400401
assert.deepEqual(result.violations[0].nodes[0].target, ['#fixture']);
401402
done();
402-
}
403+
}, done)
403404
);
404405
});
405406

@@ -409,12 +410,12 @@ describe('axe.run', function () {
409410
{
410411
absolutePaths: 'yes please'
411412
},
412-
function (err, result) {
413+
captureError(function (err, result) {
413414
assert.deepEqual(result.violations[0].nodes[0].target, [
414415
'html > body > #fixture'
415416
]);
416417
done();
417-
}
418+
}, done)
418419
);
419420
});
420421

@@ -424,13 +425,13 @@ describe('axe.run', function () {
424425
{
425426
absolutePaths: true
426427
},
427-
function (err, result) {
428+
captureError(function (err, result) {
428429
assert.deepEqual(
429430
result.violations[0].nodes[0].none[0].relatedNodes[0].target,
430431
['html > body > #fixture']
431432
);
432433
done();
433-
}
434+
}, done)
434435
);
435436
});
436437
});

test/core/utils/get-xpath.js

Lines changed: 68 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,91 @@
1-
describe('axe.utils.getXpath', function () {
1+
describe('axe.utils.getXpath', () => {
22
'use strict';
33

4-
var fixture = document.getElementById('fixture');
4+
const fixture = document.getElementById('fixture');
55

6-
afterEach(function () {
7-
fixture.innerHTML = '';
8-
});
6+
// @see https://stackoverflow.com/a/14284815/2124254
7+
function getElementByXPath(path) {
8+
return document.evaluate(
9+
path,
10+
document,
11+
() => 'http://www.w3.org/1998/Math/MathML',
12+
XPathResult.FIRST_ORDERED_NODE_TYPE,
13+
null
14+
).singleNodeValue;
15+
}
916

10-
it('should be a function', function () {
17+
it('should be a function', () => {
1118
assert.isFunction(axe.utils.getXpath);
1219
});
1320

14-
it('should generate an XPath selector', function () {
15-
var node = document.createElement('div');
21+
it('should generate an XPath selector', () => {
22+
const node = document.createElement('div');
1623
fixture.appendChild(node);
1724

18-
var sel = axe.utils.getXpath(node);
25+
const sel = axe.utils.getXpath(node);
1926

20-
assert.equal(sel, "/div[@id='fixture']/div");
27+
assert.equal(sel, "//div[@id='fixture']/div");
28+
assert.equal(node, getElementByXPath(sel));
2129
});
2230

23-
it('should handle special characters', function () {
24-
var node = document.createElement('div');
31+
it('should handle special characters', () => {
32+
const node = document.createElement('div');
2533
node.id = 'monkeys#are.animals\\ok';
2634
fixture.appendChild(node);
27-
assert.equal(
28-
axe.utils.getXpath(node),
29-
"/div[@id='monkeys#are.animals\\ok']"
30-
);
35+
36+
const sel = axe.utils.getXpath(node);
37+
38+
assert.equal(sel, "//div[@id='monkeys#are.animals\\ok']");
39+
40+
assert.equal(node, getElementByXPath(sel));
3141
});
3242

33-
it('should stop on unique ID', function () {
34-
var node = document.createElement('div');
43+
it('should stop on unique ID', () => {
44+
const node = document.createElement('div');
3545
node.id = 'monkeys';
3646
fixture.appendChild(node);
3747

38-
var sel = axe.utils.getXpath(node);
39-
assert.equal(sel, "/div[@id='monkeys']");
48+
const sel = axe.utils.getXpath(node);
49+
assert.equal(sel, "//div[@id='monkeys']");
50+
assert.equal(node, getElementByXPath(sel));
51+
});
52+
53+
it('should use the nearest unique ID', () => {
54+
fixture.innerHTML = `
55+
<div id="dogs">
56+
<div>
57+
<div>
58+
<div id="monkeys">
59+
<div></div>
60+
</div>
61+
</div>
62+
</div>
63+
</div>
64+
`;
65+
const node = fixture.querySelector('#monkeys > div');
66+
67+
const sel = axe.utils.getXpath(node);
68+
assert.equal(sel, "//div[@id='monkeys']/div");
69+
assert.equal(node, getElementByXPath(sel));
4070
});
4171

42-
it('should not use ids if they are not unique', function () {
43-
var node = document.createElement('div');
72+
it('should not use ids if they are not unique', () => {
73+
let node = document.createElement('div');
4474
node.id = 'monkeys';
4575
fixture.appendChild(node);
4676

4777
node = document.createElement('div');
4878
node.id = 'monkeys';
4979
fixture.appendChild(node);
5080

51-
var sel = axe.utils.getXpath(node);
81+
const sel = axe.utils.getXpath(node);
5282

53-
assert.equal(sel, "/div[@id='fixture']/div[2]");
83+
assert.equal(sel, "//div[@id='fixture']/div[2]");
84+
assert.equal(node, getElementByXPath(sel));
5485
});
5586

56-
it('should properly calculate number when siblings are of different type', function () {
57-
var node, target;
87+
it('should properly calculate number when siblings are of different type', () => {
88+
let node, target;
5889
node = document.createElement('span');
5990
fixture.appendChild(node);
6091

@@ -74,26 +105,30 @@ describe('axe.utils.getXpath', function () {
74105
node = document.createElement('span');
75106
fixture.appendChild(node);
76107

77-
var sel = axe.utils.getXpath(target);
108+
const sel = axe.utils.getXpath(target);
78109

79-
assert.equal(sel, "/div[@id='fixture']/div[2]");
110+
assert.equal(sel, "//div[@id='fixture']/div[2]");
111+
assert.equal(target, getElementByXPath(sel));
80112
});
81113

82-
it('should work on the documentElement', function () {
83-
var sel = axe.utils.getXpath(document.documentElement);
114+
it('should work on the documentElement', () => {
115+
const sel = axe.utils.getXpath(document.documentElement);
84116
assert.equal(sel, '/html');
117+
assert.equal(document.documentElement, getElementByXPath(sel));
85118
});
86119

87-
it('should work on the body', function () {
88-
var sel = axe.utils.getXpath(document.body);
120+
it('should work on the body', () => {
121+
const sel = axe.utils.getXpath(document.body);
89122
assert.equal(sel, '/html/body');
123+
assert.equal(document.body, getElementByXPath(sel));
90124
});
91125

92126
it('should work on namespaced elements', function () {
93127
fixture.innerHTML = '<hx:include>Hello</hx:include>';
94128
var node = fixture.firstChild;
95129
var sel = axe.utils.getXpath(node);
96130

97-
assert.equal(sel, "/div[@id='fixture']/hx:include");
131+
assert.equal(sel, "//div[@id='fixture']/hx:include");
132+
// couldn't figure out how to use document.evaluate to select an element with namespace
98133
});
99134
});

test/core/utils/merge-results.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ describe('axe.utils.mergeResults', function () {
4141

4242
var node = result[0].nodes[0].node;
4343
assert.deepEqual(node.selector, ['#target', '#foo']);
44-
assert.deepEqual(node.xpath, ["/iframe[@id='target']", 'html/#foo']);
44+
assert.deepEqual(node.xpath, ["//iframe[@id='target']", 'html/#foo']);
4545
assert.deepEqual(node.ancestry, [
4646
'html > body > div:nth-child(1) > iframe',
4747
'html > div'
@@ -76,7 +76,7 @@ describe('axe.utils.mergeResults', function () {
7676

7777
var node = result[0].nodes[0].node;
7878
assert.deepEqual(node.selector, ['#target', '#foo']);
79-
assert.deepEqual(node.xpath, ["/iframe[@id='target']", 'html/#foo']);
79+
assert.deepEqual(node.xpath, ["//iframe[@id='target']", 'html/#foo']);
8080
assert.deepEqual(node.ancestry, [
8181
'html > body > div:nth-child(1) > iframe',
8282
'html > div'

0 commit comments

Comments
 (0)