Skip to content

Commit

Permalink
Fixes to react$ and react$$ scripts (#3862)
Browse files Browse the repository at this point in the history
* first implementation of react$ command (#3794)

* implement react60245 command

* add note for react support

* add unit tests

* don't throw error when no element is found

* add better docs for react29872

* minor wording

* remove workaround

* webdriverio: cleanup resq scripts

* webdriverio: add tests for resq script

* webdriverio: cleanup resq script test

* webdriverio: add module test. adjust error message for react32456

* webdriverio: element > elem in resq script

* webdriverio: return empty array if no components foudn in react32456

* webdriverio: adjust react script when selecting multiple fragments

* webdriverio: doc update for react scripts

* docs: finalize docs for react$ and react58211 commands

* webdriverio: adjust resq version. add more tests to resq script
  • Loading branch information
baruchvlz authored and christian-bromann committed Apr 23, 2019
1 parent 78ca355 commit d006047
Show file tree
Hide file tree
Showing 16 changed files with 615 additions and 192 deletions.
115 changes: 115 additions & 0 deletions docs/Selectors.md
Expand Up @@ -270,3 +270,118 @@ With selector chaining it gets way easier as you can narrow down the desired ele
```js
$('.row .entry:nth-child(2)').$('button*=Add').click();
```

## React Selectors

WebdriverIO provides a way to select React components based on the component name. To do this you have an option of two commands, `react$` and `react$$`. These commands allow you to select components off the [React VirtualDOM](https://reactjs.org/docs/faq-internals.html) and return either a single WebdriverIO Element or an array of elements depending on which function is being used.

**Note**: The commands `react$` and `react$$` are similar in fuctionality, except that `react$$` will return all the instances that match the selector as an array of WebdriverIO elements and `react$` will return the first found instance.

#### Basic example

```jsx
// index.jsx
import React from 'react'
import ReactDOM from 'react-dom'

function MyComponent() {
return (
<div>
MyComponent
</div>
)
}

function App() {
return (<MyComponent />)
}

ReactDOM.render(<App />, document.querySelector('#root'))
```

Given the above code, we have a simple `MyComponent` instance inside the application which React is rendering inside a HTML element with `id="root"`. With the `browser.react$` command we can select our instance of `MyComponent`:

```js
const myCmp = browser.react$('MyComponent')
```

Now that we have the WebdriverIO element stored in our `myCmp` variable, we can execute element commands against it.

#### Filtering components

The library that WebdriverIO uses internally allows to filter your selection by props and/or state of the component. To do so you need to pass a second argument for props and/or a third argument for state to the browser command.

```jsx
// index.jsx
import React from 'react'
import ReactDOM from 'react-dom'

function MyComponent(props) {
return (
<div>
Hello { props.name || 'World' }!
</div>
)
}

function App() {
return (
<div>
<MyComponent name="WebdriverIO" />
<MyComponent />
</div>
)
}

ReactDOM.render(<App />, document.querySelector('#root'))
```

If we want to select the instance of `MyComponent` that has a prop `name` as `WebdriverIO` we would execute the command like so:

```js
const myCmp = browser.react$('MyComponent', { name: 'WebdriverIO' })
```

If we wanted to filter our selection by state, the browser command would looks something like so:

```js
const myCmp = browser.react$('MyComponent', undefined, { myState: 'some value' })
```

#### Dealing with `React.Fragment`

When using the `react$` command to select React [fragments](https://reactjs.org/docs/fragments.html), WebdriverIO will return the first child of that component as the component's node. If you use `react$$` you will receive an array containing all the HTML nodes inside the fragments that match the selector.

```jsx
// index.jsx
import React from 'react'
import ReactDOM from 'react-dom'

function MyComponent() {
return (
<React.Fragment>
<div>
MyComponent
</div>
<div>
MyComponent
</div>
</React.Fragment>
)
}

function App() {
return (<MyComponent />)
}

ReactDOM.render(<App />, document.querySelector('#root'))
```

Given the above example, this is how the commands would work

```js
browser.react$('MyComponent') // returns the WebdriverIO Element for the first <div />
browser.react$$('MyComponent') // returns the WebdriverIO Elements for the array [<div />, <div />]
```

It is important to note that if you have multiple instances of `MyComponent` and you use `react$$` to select these fragment components, you will be returned an one-dimensional array of all the nodes. In other words, if you have 3 `<MyComponent />` instances, you will be returned an array with six WebdriverIO elements.
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -77,7 +77,7 @@
},
"husky": {
"hooks": {
"pre-commit": "npm run test:eslint",
"pre-commit": "git diff-index --name-only --diff-filter=d HEAD | grep -E \"(.*)\\.js$\" | xargs node_modules/eslint/bin/eslint.js -c .eslintrc.js",
"pre-push": "npm run test:eslint"
}
},
Expand Down
1 change: 1 addition & 0 deletions packages/webdriverio/package.json
Expand Up @@ -64,6 +64,7 @@
"lodash.isplainobject": "^4.0.6",
"lodash.merge": "^4.6.1",
"lodash.zip": "^4.2.0",
"resq": "^1.4.0-rc.1",
"rgb2hex": "^0.1.0",
"serialize-error": "^3.0.0",
"webdriver": "^5.7.15"
Expand Down
52 changes: 3 additions & 49 deletions packages/webdriverio/src/commands/browser/$$.js
Expand Up @@ -40,56 +40,10 @@
* @type utility
*
*/
import { webdriverMonad } from 'webdriver'
import { wrapCommand, runFnInFiberContext } from '@wdio/config'
import merge from 'lodash.merge'

import { findElements, getPrototype as getWDIOPrototype, getElementFromResponse } from '../../utils'
import { elementErrorHandler } from '../../middlewares'
import { ELEMENT_KEY } from '../../constants'
import { findElements } from '../../utils'
import { getElements } from '../../utils/getElementObject'

export default async function $$ (selector) {
const res = await findElements.call(this, selector)
const prototype = merge({}, this.__propertiesObject__, getWDIOPrototype('element'), { scope: 'element' })

const elements = res.map((res, i) => {
const element = webdriverMonad(this.options, (client) => {
const elementId = getElementFromResponse(res)

if (elementId) {
/**
* set elementId for easy access
*/
client.elementId = elementId

/**
* set element id with proper key so element can be passed into execute commands
*/
if (this.isW3C) {
client[ELEMENT_KEY] = elementId
} else {
client.ELEMENT = elementId
}
} else {
client.error = res
}

client.selector = selector
client.parent = this
client.index = i
client.emit = ::this.emit
return client
}, prototype)

const elementInstance = element(this.sessionId, elementErrorHandler(wrapCommand))

const origAddCommand = ::elementInstance.addCommand
elementInstance.addCommand = (name, fn) => {
this.__propertiesObject__[name] = { value: fn }
origAddCommand(name, runFnInFiberContext(fn))
}
return elementInstance
})

return elements
return getElements.call(this, selector, res)
}
47 changes: 3 additions & 44 deletions packages/webdriverio/src/commands/browser/$.js
Expand Up @@ -40,51 +40,10 @@
* @type utility
*
*/
import { webdriverMonad } from 'webdriver'
import { wrapCommand, runFnInFiberContext } from '@wdio/config'
import merge from 'lodash.merge'

import { findElement, getPrototype as getWDIOPrototype, getElementFromResponse } from '../../utils'
import { elementErrorHandler } from '../../middlewares'
import { ELEMENT_KEY } from '../../constants'
import { findElement } from '../../utils'
import { getElement } from '../../utils/getElementObject'

export default async function $ (selector) {
const res = await findElement.call(this, selector)
const prototype = merge({}, this.__propertiesObject__, getWDIOPrototype('element'), { scope: 'element' })

const element = webdriverMonad(this.options, (client) => {
const elementId = getElementFromResponse(res)

if (elementId) {
/**
* set elementId for easy access
*/
client.elementId = elementId

/**
* set element id with proper key so element can be passed into execute commands
*/
if (this.isW3C) {
client[ELEMENT_KEY] = elementId
} else {
client.ELEMENT = elementId
}
} else {
client.error = res
}

client.selector = selector
client.parent = this
client.emit = ::this.emit
return client
}, prototype)

const elementInstance = element(this.sessionId, elementErrorHandler(wrapCommand))

const origAddCommand = ::elementInstance.addCommand
elementInstance.addCommand = (name, fn) => {
this.__propertiesObject__[name] = { value: fn }
origAddCommand(name, runFnInFiberContext(fn))
}
return elementInstance
return getElement.call(this, selector, res)
}
35 changes: 35 additions & 0 deletions packages/webdriverio/src/commands/browser/react$$.js
@@ -0,0 +1,35 @@
/**
*
* The `react$$` command is a useful command to query multiple React Components
* by their actual name and filter them by props and state.
*
* **NOTE:** the command only works with applications using React v16.x
*
* <example>
:pause.js
it('should calculate 7 * 6', () => {
browser.url('https://ahfarmer.github.io/calculator/');
const orangeButtons = browser.react$$('t', { orange: true })
console.log(orangeButtons.map((btn) => btn.getText())); // prints "[ '÷', 'x', '-', '+', '=' ]"
});
* </example>
*
* @alias browser.react$
* @param {Object=} props React props the element should contain
* @param {Object=} state React state the element should be in
* @return {Element}
*
*/
import fs from 'fs'
import { getElements } from '../../utils/getElementObject'
import { waitToLoadReact, react$$ as react$$Script } from '../../scripts/resq'

const resqScript = fs.readFileSync(require.resolve('resq'))

export default async function react$$ (selector, props = {}, state = {}) {
await this.executeScript(resqScript.toString(), [])
await this.execute(waitToLoadReact)
const res = await this.execute(react$$Script, selector, props, state)
return getElements.call(this, selector, res)
}
39 changes: 39 additions & 0 deletions packages/webdriverio/src/commands/browser/react$.js
@@ -0,0 +1,39 @@
/**
*
* The `react$` command is a useful command to query React Components by their
* actual name and filter them by props and state.
*
* **NOTE:** the command only works with applications using React v16.x
*
* <example>
:pause.js
it('should calculate 7 * 6', () => {
browser.url('https://ahfarmer.github.io/calculator/');
browser.react$('t', { name: '7' }).click()
browser.react$('t', { name: 'x' }).click()
browser.react$('t', { name: '6' }).click()
browser.react$('t', { name: '=' }).click()
console.log($('.component-display').getText()); // prints "42"
});
* </example>
*
* @alias browser.react$
* @param {Object=} props React props the element should contain
* @param {Object=} state React state the element should be in
* @return {Element}
*
*/
import fs from 'fs'
import { getElement } from '../../utils/getElementObject'
import { waitToLoadReact, react$ as react$Script } from '../../scripts/resq'

const resqScript = fs.readFileSync(require.resolve('resq'))

export default async function react$ (selector, props = {}, state = {}) {
await this.executeScript(resqScript.toString(), [])
await this.execute(waitToLoadReact)
const res = await this.execute(react$Script, selector, props, state)
return getElement.call(this, selector, res)
}
53 changes: 3 additions & 50 deletions packages/webdriverio/src/commands/element/$$.js
Expand Up @@ -37,57 +37,10 @@
* @type utility
*
*/
import { webdriverMonad } from 'webdriver'
import { wrapCommand, runFnInFiberContext } from '@wdio/config'
import merge from 'lodash.merge'

import { findElements, getBrowserObject, getPrototype as getWDIOPrototype, getElementFromResponse } from '../../utils'
import { elementErrorHandler } from '../../middlewares'
import { ELEMENT_KEY } from '../../constants'
import { findElements } from '../../utils'
import { getElements } from '../../utils/getElementObject'

export default async function $$ (selector) {
const res = await findElements.call(this, selector)
const browser = getBrowserObject(this)
const prototype = merge({}, browser.__propertiesObject__, getWDIOPrototype('element'), { scope: 'element' })

const elements = res.map((res, i) => {
const element = webdriverMonad(this.options, (client) => {
const elementId = getElementFromResponse(res)

if (elementId) {
/**
* set elementId for easy access
*/
client.elementId = elementId

/**
* set element id with proper key so element can be passed into execute commands
*/
if (this.isW3C) {
client[ELEMENT_KEY] = elementId
} else {
client.ELEMENT = elementId
}
} else {
client.error = res
}

client.selector = selector
client.parent = this
client.index = i
client.emit = ::this.emit
return client
}, prototype)

const elementInstance = element(this.sessionId, elementErrorHandler(wrapCommand))

const origAddCommand = ::elementInstance.addCommand
elementInstance.addCommand = (name, fn) => {
browser.__propertiesObject__[name] = { value: fn }
origAddCommand(name, runFnInFiberContext(fn))
}
return elementInstance
})

return elements
return getElements.call(this, selector, res)
}

0 comments on commit d006047

Please sign in to comment.