Skip to content
This repository was archived by the owner on Oct 13, 2022. It is now read-only.

Commit 515edc0

Browse files
badeballbahmutov
authored andcommitted
feat: allow command chaining and cy.within() (#33)
* feat: add support for command chaining * feat: add support for using with cy.within() * chore: update readme with chaining + within support * fix: use `exist` assertions instead of `empty` The empty assertion doesn't do what I thought it did. A jquery-wrapped empty array isn't "empty" per said assertion, hence our tests failed in context of 5afe28d.
1 parent 5afe28d commit 515edc0

File tree

3 files changed

+119
-4
lines changed

3 files changed

+119
-4
lines changed

README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,57 @@ it('finds list items', () => {
2525
})
2626
```
2727

28+
You can also chain `xpath` off of another command.
29+
30+
```js
31+
it('finds list items', () => {
32+
cy.xpath('//ul[@class="todo-list"]')
33+
.xpath('./li')
34+
.should('have.length', 3)
35+
})
36+
```
37+
38+
As with other cy commands, it is scoped by `cy.within()`.
39+
40+
```js
41+
it('finds list items', () => {
42+
cy.xpath('//ul[@class="todo-list"]').within(() => {
43+
cy.xpath('./li')
44+
.should('have.length', 3)
45+
});
46+
})
47+
```
48+
2849
**note:** you can test XPath expressions from DevTools console using `$x(...)` function, for example `$x('//div')` to find all divs.
2950

3051
See [cypress/integration/spec.js](cypress/integration/spec.js)
3152

53+
## Beware the XPath // trap
54+
55+
In XPath the expression // means something very specific, and it might not be what you think. Contrary to common belief, // means "anywhere in the document" not "anywhere in the current context". As an example:
56+
57+
```js
58+
cy.xpath('//body')
59+
.xpath('//script')
60+
```
61+
62+
You might expect this to find all script tags in the body, but actually, it finds all script tags in the entire document, not only those in the body! What you're looking for is the .// expression which means "any descendant of the current node":
63+
64+
```js
65+
cy.xpath('//body')
66+
.xpath('.//script')
67+
```
68+
69+
The same thing goes for within:
70+
71+
```js
72+
cy.xpath('//body').within(() => {
73+
cy.xpath('.//script')
74+
})
75+
```
76+
77+
This explanation was shamelessly copied from [teamcapybara/capybara][capybara-xpath-trap].
78+
3279
## Roadmap
3380

3481
- [x] wrap returned DOM nodes in jQuery [#2](https://github.com/cypress-io/cypress-xpath/issues/2)
@@ -43,3 +90,4 @@ This project is licensed under the terms of the [MIT license](/LICENSE.md).
4390

4491
[renovate-badge]: https://img.shields.io/badge/renovate-app-blue.svg
4592
[renovate-app]: https://renovateapp.com/
93+
[capybara-xpath-trap]: https://github.com/teamcapybara/capybara/tree/3.18.0#beware-the-xpath--trap

cypress/integration/spec.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,57 @@ describe('cypress-xpath', () => {
4646
cy.xpath('string(//*[@id="inserted"])').should('equal', 'inserted text')
4747
})
4848

49+
describe('chaining', () => {
50+
it('finds h1 within main', () => {
51+
// first assert that h1 doesn't exist as a child of the implicit document subject
52+
cy.xpath('./h1').should('not.exist')
53+
54+
cy.xpath('//main').xpath('./h1').should('exist')
55+
})
56+
57+
it('finds body outside of main when succumbing to // trap', () => {
58+
// first assert that body doesn't actually exist within main
59+
cy.xpath('//main').xpath('.//body').should('not.exist')
60+
61+
cy.xpath('//main').xpath('//body').should('exist')
62+
})
63+
64+
it('finds h1 within document', () => {
65+
cy.document().xpath('//h1').should('exist')
66+
})
67+
68+
it('throws when subject is more than a single element', (done) => {
69+
cy.on('fail', (err) => {
70+
expect(err.message).to.eq('xpath() can only be called on a single element. Your subject contained 2 elements.')
71+
done()
72+
})
73+
74+
cy.get('main, div').xpath('foo')
75+
})
76+
})
77+
78+
describe('within()', () => {
79+
it('finds h1 within within-subject', () => {
80+
// first assert that h1 doesn't exist as a child of the implicit document subject
81+
cy.xpath('./h1').should('not.exist')
82+
83+
cy.xpath('//main').within(() => {
84+
cy.xpath('./h1').should('exist')
85+
})
86+
})
87+
88+
it('finds body outside of within-subject when succumbing to // trap', () => {
89+
// first assert that body doesn't actually exist within main
90+
cy.xpath('//main').within(() => {
91+
cy.xpath('.//body').should('not.exist')
92+
});
93+
94+
cy.xpath('//main').within(() => {
95+
cy.xpath('//body').should('exist')
96+
});
97+
})
98+
})
99+
49100
describe('primitives', () => {
50101
it('counts h1 elements', () => {
51102
cy.xpath('count(//h1)').should('equal', 1)

src/index.js

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
})
1414
```
1515
*/
16-
const xpath = (selector, options = {}) => {
16+
const xpath = (subject, selector, options = {}) => {
1717
/* global XPathResult */
1818
const isNumber = (xpathResult) => xpathResult.resultType === XPathResult.NUMBER_TYPE
1919
const numberResult = (xpathResult) => xpathResult.numberValue
@@ -33,10 +33,26 @@ const xpath = (selector, options = {}) => {
3333
message: selector,
3434
}
3535

36+
if (Cypress.dom.isElement(subject) && subject.length > 1) {
37+
throw new Error('xpath() can only be called on a single element. Your subject contained ' + subject.length + ' elements.')
38+
}
39+
3640
const getValue = () => {
3741
let nodes = []
38-
const document = cy.state('window').document
39-
let iterator = document.evaluate(selector, document)
42+
let contextNode
43+
let withinSubject = cy.state('withinSubject')
44+
45+
if (Cypress.dom.isElement(subject)) {
46+
contextNode = subject[0]
47+
} else if (Cypress.dom.isDocument(subject)) {
48+
contextNode = subject
49+
} else if (withinSubject) {
50+
contextNode = withinSubject[0]
51+
} else {
52+
contextNode = cy.state('window').document
53+
}
54+
55+
let iterator = document.evaluate(selector, contextNode)
4056

4157
if (isNumber(iterator)) {
4258
const result = numberResult(iterator)
@@ -116,4 +132,4 @@ const xpath = (selector, options = {}) => {
116132

117133
}
118134

119-
Cypress.Commands.add('xpath', xpath)
135+
Cypress.Commands.add('xpath', { prevSubject: ['optional', 'element', 'document'] }, xpath)

0 commit comments

Comments
 (0)