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

Commit

Permalink
feat: allow command chaining and cy.within() (#33)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
badeball authored and bahmutov committed May 2, 2019
1 parent 5afe28d commit 515edc0
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 4 deletions.
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,57 @@ it('finds list items', () => {
})
```

You can also chain `xpath` off of another command.

```js
it('finds list items', () => {
cy.xpath('//ul[@class="todo-list"]')
.xpath('./li')
.should('have.length', 3)
})
```

As with other cy commands, it is scoped by `cy.within()`.

```js
it('finds list items', () => {
cy.xpath('//ul[@class="todo-list"]').within(() => {
cy.xpath('./li')
.should('have.length', 3)
});
})
```

**note:** you can test XPath expressions from DevTools console using `$x(...)` function, for example `$x('//div')` to find all divs.

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

## Beware the XPath // trap

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:

```js
cy.xpath('//body')
.xpath('//script')
```

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":

```js
cy.xpath('//body')
.xpath('.//script')
```

The same thing goes for within:

```js
cy.xpath('//body').within(() => {
cy.xpath('.//script')
})
```

This explanation was shamelessly copied from [teamcapybara/capybara][capybara-xpath-trap].

## Roadmap

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

[renovate-badge]: https://img.shields.io/badge/renovate-app-blue.svg
[renovate-app]: https://renovateapp.com/
[capybara-xpath-trap]: https://github.com/teamcapybara/capybara/tree/3.18.0#beware-the-xpath--trap
51 changes: 51 additions & 0 deletions cypress/integration/spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,57 @@ describe('cypress-xpath', () => {
cy.xpath('string(//*[@id="inserted"])').should('equal', 'inserted text')
})

describe('chaining', () => {
it('finds h1 within main', () => {
// first assert that h1 doesn't exist as a child of the implicit document subject
cy.xpath('./h1').should('not.exist')

cy.xpath('//main').xpath('./h1').should('exist')
})

it('finds body outside of main when succumbing to // trap', () => {
// first assert that body doesn't actually exist within main
cy.xpath('//main').xpath('.//body').should('not.exist')

cy.xpath('//main').xpath('//body').should('exist')
})

it('finds h1 within document', () => {
cy.document().xpath('//h1').should('exist')
})

it('throws when subject is more than a single element', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.eq('xpath() can only be called on a single element. Your subject contained 2 elements.')
done()
})

cy.get('main, div').xpath('foo')
})
})

describe('within()', () => {
it('finds h1 within within-subject', () => {
// first assert that h1 doesn't exist as a child of the implicit document subject
cy.xpath('./h1').should('not.exist')

cy.xpath('//main').within(() => {
cy.xpath('./h1').should('exist')
})
})

it('finds body outside of within-subject when succumbing to // trap', () => {
// first assert that body doesn't actually exist within main
cy.xpath('//main').within(() => {
cy.xpath('.//body').should('not.exist')
});

cy.xpath('//main').within(() => {
cy.xpath('//body').should('exist')
});
})
})

describe('primitives', () => {
it('counts h1 elements', () => {
cy.xpath('count(//h1)').should('equal', 1)
Expand Down
24 changes: 20 additions & 4 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
})
```
*/
const xpath = (selector, options = {}) => {
const xpath = (subject, selector, options = {}) => {
/* global XPathResult */
const isNumber = (xpathResult) => xpathResult.resultType === XPathResult.NUMBER_TYPE
const numberResult = (xpathResult) => xpathResult.numberValue
Expand All @@ -33,10 +33,26 @@ const xpath = (selector, options = {}) => {
message: selector,
}

if (Cypress.dom.isElement(subject) && subject.length > 1) {
throw new Error('xpath() can only be called on a single element. Your subject contained ' + subject.length + ' elements.')
}

const getValue = () => {
let nodes = []
const document = cy.state('window').document
let iterator = document.evaluate(selector, document)
let contextNode
let withinSubject = cy.state('withinSubject')

if (Cypress.dom.isElement(subject)) {
contextNode = subject[0]
} else if (Cypress.dom.isDocument(subject)) {
contextNode = subject
} else if (withinSubject) {
contextNode = withinSubject[0]
} else {
contextNode = cy.state('window').document
}

let iterator = document.evaluate(selector, contextNode)

if (isNumber(iterator)) {
const result = numberResult(iterator)
Expand Down Expand Up @@ -116,4 +132,4 @@ const xpath = (selector, options = {}) => {

}

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

0 comments on commit 515edc0

Please sign in to comment.