Skip to content

Commit

Permalink
feat: Add toHaveValue matcher #82 (#90)
Browse files Browse the repository at this point in the history
  • Loading branch information
lukaszfiszer authored and gnapse committed Jun 7, 2019
1 parent 28f960f commit fd747bf
Show file tree
Hide file tree
Showing 8 changed files with 274 additions and 48 deletions.
9 changes: 9 additions & 0 deletions .all-contributorsrc
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,15 @@
"ideas",
"test"
]
},
{
"login": "lukaszfiszer",
"name": "Łukasz Fiszer",
"avatar_url": "https://avatars3.githubusercontent.com/u/1201711?v=4",
"profile": "https://github.com/lukaszfiszer",
"contributions": [
"code"
]
}
]
}
62 changes: 60 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
[![downloads][downloads-badge]][npmtrends]
[![MIT License][license-badge]][license]

[![All Contributors](https://img.shields.io/badge/all_contributors-25-orange.svg?style=flat-square)](#contributors)
[![All Contributors](https://img.shields.io/badge/all_contributors-26-orange.svg?style=flat-square)](#contributors)
[![PRs Welcome][prs-badge]][prs]
[![Code of Conduct][coc-badge]][coc]

Expand Down Expand Up @@ -61,6 +61,7 @@ to maintain.
- [`toHaveFormValues`](#tohaveformvalues)
- [`toHaveStyle`](#tohavestyle)
- [`toHaveTextContent`](#tohavetextcontent)
- [`toHaveValue`](#tohavevalue)
- [Deprecated matchers](#deprecated-matchers)
- [`toBeInTheDOM`](#tobeinthedom)
- [Inspiration](#inspiration)
Expand Down Expand Up @@ -839,6 +840,63 @@ expect(element).not.toHaveTextContent('content')
<hr />
### `toHaveValue`
```typescript
toHaveValue(value: string | string[] | number)
```
This allows you to check whether the given form element has the specified value.
It accepts `<input>`, `<select>` and `<textarea>` elements with the exception of
of `<input type="checkbox">` and `<input type="radio">`, which can be meaningfully
matched only using [`toHaveFormValue`](#tohaveformvalues).
For all other form elements, the value is matched using the same algorithm
as in [`toHaveFormValue`](#tohaveformvalues) does.
#### Examples
```html
<input type="text" value="text" data-testid="input-text" />
<input type="number" value="5" data-testid="input-number" />
<input type="text" data-testid="input-empty" />
<select data-testid="multiple" multiple data-testid="select-number">
<option value="first">First Value</option>
<option value="second" selected>Second Value</option>
<option value="third" selected>Third Value</option>
</select>
```
##### Using document.querySelector
```javascript
const textInput = document.querySelector('[data-testid="input-text"]')
const numberInput = document.querySelector('[data-testid="input-number"]')
const emptyInput = document.querySelector('[data-testid="input-empty"]')
const selectInput = document.querySelector('[data-testid="select-number"]')

expect(textInput).toHaveValue('text')
expect(numberInput).toHaveValue(5)
expect(emptyInput).not.toHaveValue()
expect(selectInput).not.toHaveValue(['second', 'third'])
```
##### Using dom-testing-library
```javascript
const {getByTestId} = render(/* Rendered HTML */)

const textInput = getByTestId('input-text')
const numberInput = getByTestId('input-number')
const emptyInput = getByTestId('input-empty')
const selectInput = getByTestId('select-number')

expect(textInput).toHaveValue('text')
expect(numberInput).toHaveValue(5)
expect(emptyInput).not.toHaveValue()
expect(selectInput).not.toHaveValue(['second', 'third'])
```
## Deprecated matchers
### `toBeInTheDOM`
Expand Down Expand Up @@ -912,7 +970,7 @@ Thanks goes to these people ([emoji key][emojis]):
| :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| [<img src="https://avatars1.githubusercontent.com/u/1241511?s=460&v=4" width="100px;" alt="Anto Aravinth"/><br /><sub><b>Anto Aravinth</b></sub>](https://github.com/antoaravinth)<br />[💻](https://github.com/testing-library/jest-dom/commits?author=antoaravinth "Code") [⚠️](https://github.com/testing-library/jest-dom/commits?author=antoaravinth "Tests") [📖](https://github.com/testing-library/jest-dom/commits?author=antoaravinth "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/3462296?v=4" width="100px;" alt="Jonah Moses"/><br /><sub><b>Jonah Moses</b></sub>](https://github.com/JonahMoses)<br />[📖](https://github.com/testing-library/jest-dom/commits?author=JonahMoses "Documentation") | [<img src="https://avatars1.githubusercontent.com/u/4002543?v=4" width="100px;" alt="Łukasz Gandecki"/><br /><sub><b>Łukasz Gandecki</b></sub>](http://team.thebrain.pro)<br />[💻](https://github.com/testing-library/jest-dom/commits?author=lgandecki "Code") [⚠️](https://github.com/testing-library/jest-dom/commits?author=lgandecki "Tests") [📖](https://github.com/testing-library/jest-dom/commits?author=lgandecki "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/498274?v=4" width="100px;" alt="Ivan Babak"/><br /><sub><b>Ivan Babak</b></sub>](https://sompylasar.github.io)<br />[🐛](https://github.com/testing-library/jest-dom/issues?q=author%3Asompylasar "Bug reports") [🤔](#ideas-sompylasar "Ideas, Planning, & Feedback") | [<img src="https://avatars3.githubusercontent.com/u/4439618?v=4" width="100px;" alt="Jesse Day"/><br /><sub><b>Jesse Day</b></sub>](https://github.com/jday3)<br />[💻](https://github.com/testing-library/jest-dom/commits?author=jday3 "Code") | [<img src="https://avatars0.githubusercontent.com/u/15199?v=4" width="100px;" alt="Ernesto García"/><br /><sub><b>Ernesto García</b></sub>](http://gnapse.github.io)<br />[💻](https://github.com/testing-library/jest-dom/commits?author=gnapse "Code") [📖](https://github.com/testing-library/jest-dom/commits?author=gnapse "Documentation") [⚠️](https://github.com/testing-library/jest-dom/commits?author=gnapse "Tests") | [<img src="https://avatars0.githubusercontent.com/u/79312?v=4" width="100px;" alt="Mark Volkmann"/><br /><sub><b>Mark Volkmann</b></sub>](http://ociweb.com/mark/)<br />[🐛](https://github.com/testing-library/jest-dom/issues?q=author%3Amvolkmann "Bug reports") [💻](https://github.com/testing-library/jest-dom/commits?author=mvolkmann "Code") |
| [<img src="https://avatars1.githubusercontent.com/u/1659099?v=4" width="100px;" alt="smacpherson64"/><br /><sub><b>smacpherson64</b></sub>](https://github.com/smacpherson64)<br />[💻](https://github.com/testing-library/jest-dom/commits?author=smacpherson64 "Code") [📖](https://github.com/testing-library/jest-dom/commits?author=smacpherson64 "Documentation") [⚠️](https://github.com/testing-library/jest-dom/commits?author=smacpherson64 "Tests") | [<img src="https://avatars2.githubusercontent.com/u/132233?v=4" width="100px;" alt="John Gozde"/><br /><sub><b>John Gozde</b></sub>](https://github.com/jgoz)<br />[🐛](https://github.com/testing-library/jest-dom/issues?q=author%3Ajgoz "Bug reports") [💻](https://github.com/testing-library/jest-dom/commits?author=jgoz "Code") | [<img src="https://avatars2.githubusercontent.com/u/7830590?v=4" width="100px;" alt="Iwona"/><br /><sub><b>Iwona</b></sub>](https://github.com/callada)<br />[💻](https://github.com/testing-library/jest-dom/commits?author=callada "Code") [📖](https://github.com/testing-library/jest-dom/commits?author=callada "Documentation") [⚠️](https://github.com/testing-library/jest-dom/commits?author=callada "Tests") | [<img src="https://avatars0.githubusercontent.com/u/840609?v=4" width="100px;" alt="Lewis"/><br /><sub><b>Lewis</b></sub>](https://github.com/6ewis)<br />[💻](https://github.com/testing-library/jest-dom/commits?author=6ewis "Code") | [<img src="https://avatars3.githubusercontent.com/u/2339362?v=4" width="100px;" alt="Leandro Lourenci"/><br /><sub><b>Leandro Lourenci</b></sub>](https://blog.lourenci.com/)<br />[🐛](https://github.com/testing-library/jest-dom/issues?q=author%3Alourenci "Bug reports") [📖](https://github.com/testing-library/jest-dom/commits?author=lourenci "Documentation") [💻](https://github.com/testing-library/jest-dom/commits?author=lourenci "Code") [⚠️](https://github.com/testing-library/jest-dom/commits?author=lourenci "Tests") | [<img src="https://avatars1.githubusercontent.com/u/626420?v=4" width="100px;" alt="Shukhrat Mukimov"/><br /><sub><b>Shukhrat Mukimov</b></sub>](https://github.com/mufasa71)<br />[🐛](https://github.com/testing-library/jest-dom/issues?q=author%3Amufasa71 "Bug reports") | [<img src="https://avatars3.githubusercontent.com/u/1481264?v=4" width="100px;" alt="Roman Usherenko"/><br /><sub><b>Roman Usherenko</b></sub>](https://github.com/dreyks)<br />[💻](https://github.com/testing-library/jest-dom/commits?author=dreyks "Code") [⚠️](https://github.com/testing-library/jest-dom/commits?author=dreyks "Tests") |
| [<img src="https://avatars1.githubusercontent.com/u/648?v=4" width="100px;" alt="Joe Hsu"/><br /><sub><b>Joe Hsu</b></sub>](http://josephhsu.com)<br />[📖](https://github.com/testing-library/jest-dom/commits?author=jhsu "Documentation") | [<img src="https://avatars3.githubusercontent.com/u/3068563?v=4" width="100px;" alt="Haz"/><br /><sub><b>Haz</b></sub>](https://twitter.com/diegohaz)<br />[🐛](https://github.com/testing-library/jest-dom/issues?q=author%3Adiegohaz "Bug reports") [💻](https://github.com/testing-library/jest-dom/commits?author=diegohaz "Code") | [<img src="https://avatars3.githubusercontent.com/u/463904?v=4" width="100px;" alt="Revath S Kumar"/><br /><sub><b>Revath S Kumar</b></sub>](https://blog.revathskumar.com)<br />[💻](https://github.com/testing-library/jest-dom/commits?author=revathskumar "Code") | [<img src="https://avatars0.githubusercontent.com/u/4989733?v=4" width="100px;" alt="hiwelo."/><br /><sub><b>hiwelo.</b></sub>](https://raccoon.studio)<br />[💻](https://github.com/testing-library/jest-dom/commits?author=hiwelo "Code") [🤔](#ideas-hiwelo "Ideas, Planning, & Feedback") [⚠️](https://github.com/testing-library/jest-dom/commits?author=hiwelo "Tests") |
| [<img src="https://avatars1.githubusercontent.com/u/648?v=4" width="100px;" alt="Joe Hsu"/><br /><sub><b>Joe Hsu</b></sub>](http://josephhsu.com)<br />[📖](https://github.com/testing-library/jest-dom/commits?author=jhsu "Documentation") | [<img src="https://avatars3.githubusercontent.com/u/3068563?v=4" width="100px;" alt="Haz"/><br /><sub><b>Haz</b></sub>](https://twitter.com/diegohaz)<br />[🐛](https://github.com/testing-library/jest-dom/issues?q=author%3Adiegohaz "Bug reports") [💻](https://github.com/testing-library/jest-dom/commits?author=diegohaz "Code") | [<img src="https://avatars3.githubusercontent.com/u/463904?v=4" width="100px;" alt="Revath S Kumar"/><br /><sub><b>Revath S Kumar</b></sub>](https://blog.revathskumar.com)<br />[💻](https://github.com/testing-library/jest-dom/commits?author=revathskumar "Code") | [<img src="https://avatars0.githubusercontent.com/u/4989733?v=4" width="100px;" alt="hiwelo."/><br /><sub><b>hiwelo.</b></sub>](https://raccoon.studio)<br />[💻](https://github.com/testing-library/jest-dom/commits?author=hiwelo "Code") [🤔](#ideas-hiwelo "Ideas, Planning, & Feedback") [⚠️](https://github.com/testing-library/jest-dom/commits?author=hiwelo "Tests") | [<img src="https://avatars3.githubusercontent.com/u/1201711?v=4" width="100px;" alt="Łukasz Fiszer"/><br /><sub><b>Łukasz Fiszer</b></sub>](https://github.com/lukaszfiszer)<br />[💻](https://github.com/gnapse/jest-dom/commits?author=lukaszfiszer "Code") | [<img src="https://avatars3.githubusercontent.com/u/1201711?v=4" width="100px;" alt="Łukasz Fiszer"/><br /><sub><b>Łukasz Fiszer</b></sub>](https://github.com/lukaszfiszer)<br />[💻](https://github.com/gnapse/jest-dom/commits?author=lukaszfiszer "Code") |
<!-- ALL-CONTRIBUTORS-LIST:END -->
Expand Down
1 change: 1 addition & 0 deletions extend-expect.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ declare namespace jest {
text: string | RegExp,
options?: {normalizeWhitespace: boolean},
): R
toHaveValue(value?: string | string[] | number): R
}
}
106 changes: 106 additions & 0 deletions src/__tests__/to-have-value.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import {render} from './helpers/test-utils'

describe('.toHaveValue', () => {
test('handles value of text input', () => {
const {queryByTestId} = render(`
<input type="text" value="foo" data-testid="value" />
<input type="text" value="" data-testid="empty" />
<input type="text" data-testid="without" />
`)

expect(queryByTestId('value')).toHaveValue('foo')
expect(queryByTestId('value')).toHaveValue()
expect(queryByTestId('value')).not.toHaveValue('bar')
expect(queryByTestId('value')).not.toHaveValue('')

expect(queryByTestId('empty')).toHaveValue('')
expect(queryByTestId('empty')).not.toHaveValue()
expect(queryByTestId('empty')).not.toHaveValue('foo')

expect(queryByTestId('without')).toHaveValue('')
expect(queryByTestId('without')).not.toHaveValue()
expect(queryByTestId('without')).not.toHaveValue('foo')
queryByTestId('without').value = 'bar'
expect(queryByTestId('without')).toHaveValue('bar')
})

test('handles value of number input', () => {
const {queryByTestId} = render(`
<input type="number" value="5" data-testid="number" />
<input type="number" value="" data-testid="empty" />
<input type="number" data-testid="without" />
`)

expect(queryByTestId('number')).toHaveValue(5)
expect(queryByTestId('number')).toHaveValue()
expect(queryByTestId('number')).not.toHaveValue(4)
expect(queryByTestId('number')).not.toHaveValue('5')

expect(queryByTestId('empty')).toHaveValue(null)
expect(queryByTestId('empty')).not.toHaveValue()
expect(queryByTestId('empty')).not.toHaveValue('5')

expect(queryByTestId('without')).toHaveValue(null)
expect(queryByTestId('without')).not.toHaveValue()
expect(queryByTestId('without')).not.toHaveValue('10')
queryByTestId('without').value = 10
expect(queryByTestId('without')).toHaveValue(10)
})

test('handles value of select element', () => {
const {queryByTestId} = render(`
<select data-testid="single">
<option value="first">First Value</option>
<option value="second" selected>Second Value</option>
<option value="third">Third Value</option>
</select>
<select data-testid="multiple" multiple>
<option value="first">First Value</option>
<option value="second" selected>Second Value</option>
<option value="third" selected>Third Value</option>
</select>
<select data-testid="not-selected" >
<option value="" disabled selected>- Select some value - </option>
<option value="first">First Value</option>
<option value="second">Second Value</option>
<option value="third">Third Value</option>
</select>
`)

expect(queryByTestId('single')).toHaveValue('second')
expect(queryByTestId('single')).toHaveValue()

expect(queryByTestId('multiple')).toHaveValue(['second', 'third'])
expect(queryByTestId('multiple')).toHaveValue()

expect(queryByTestId('not-selected')).not.toHaveValue()
expect(queryByTestId('not-selected')).toHaveValue('')

queryByTestId('single').children[0].setAttribute('selected', true)
expect(queryByTestId('single')).toHaveValue('first')
})

test('handles value of textarea element', () => {
const {queryByTestId} = render(`
<textarea data-testid="textarea">text value</textarea>
`)
expect(queryByTestId('textarea')).toHaveValue('text value')
})

test('throws when passed checkbox or radio', () => {
const {queryByTestId} = render(`
<input data-testid="checkbox" type="checkbox" name="checkbox" value="val" checked />
<input data-testid="radio" type="radio" name="radio" value="val" checked />
`)

expect(() => {
expect(queryByTestId('checkbox')).toHaveValue('')
}).toThrow()

expect(() => {
expect(queryByTestId('radio')).toHaveValue('')
}).toThrow()
})
})
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {toBeVisible} from './to-be-visible'
import {toBeDisabled, toBeEnabled} from './to-be-disabled'
import {toBeRequired} from './to-be-required'
import {toBeInvalid, toBeValid} from './to-be-invalid'
import {toHaveValue} from './to-have-value'

export {
toBeInTheDOM,
Expand All @@ -32,4 +33,5 @@ export {
toBeRequired,
toBeInvalid,
toBeValid,
toHaveValue,
}
51 changes: 5 additions & 46 deletions src/to-have-form-values.js
Original file line number Diff line number Diff line change
@@ -1,54 +1,13 @@
import {matcherHint} from 'jest-matcher-utils'
import jestDiff from 'jest-diff'
import isEqual from 'lodash/isEqual'
import isEqualWith from 'lodash/isEqualWith'
import uniq from 'lodash/uniq'
import {checkHtmlElement} from './utils'
import escape from 'css.escape'

function compareArraysAsSet(a, b) {
if (Array.isArray(a) && Array.isArray(b)) {
return isEqual(new Set(a), new Set(b))
}
return undefined
}

function getSelectValue({multiple, selectedOptions}) {
if (multiple) {
return [...selectedOptions].map(opt => opt.value)
}
/* istanbul ignore if */
if (selectedOptions.length === 0) {
return undefined // Couldn't make this happen, but just in case
}
return selectedOptions[0].value
}

function getInputValue(inputElement) {
switch (inputElement.type) {
case 'number':
return inputElement.value === '' ? null : Number(inputElement.value)
case 'checkbox':
return inputElement.checked
default:
return inputElement.value
}
}

function getSingleElementValue(element) {
/* istanbul ignore if */
if (!element) {
return undefined
}
switch (element.tagName.toLowerCase()) {
case 'input':
return getInputValue(element)
case 'select':
return getSelectValue(element)
default:
return element.value
}
}
import {
checkHtmlElement,
compareArraysAsSet,
getSingleElementValue,
} from './utils'

// Returns the combined value of several elements that have the same name
// e.g. radio buttons or groups of checkboxes
Expand Down
44 changes: 44 additions & 0 deletions src/to-have-value.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {matcherHint} from 'jest-matcher-utils'
import isEqualWith from 'lodash/isEqualWith'
import {
checkHtmlElement,
compareArraysAsSet,
getMessage,
getSingleElementValue,
} from './utils'

export function toHaveValue(htmlElement, expectedValue) {
checkHtmlElement(htmlElement, toHaveValue, this)

if (
htmlElement.tagName.toLowerCase() === 'input' &&
['checkbox', 'radio'].includes(htmlElement.type)
) {
throw new Error(
'input with type=checkbox or type=radio cannot be used with .toHaveValue(). Use .toHaveFormValues() instead',
)
}

const receivedValue = getSingleElementValue(htmlElement)
const expectsValue = expectedValue !== undefined
return {
pass: expectsValue
? isEqualWith(receivedValue, expectedValue, compareArraysAsSet)
: Boolean(receivedValue),
message: () => {
const to = this.isNot ? 'not to' : 'to'
const matcher = matcherHint(
`${this.isNot ? '.not' : ''}.toHaveValue`,
'element',
expectedValue,
)
return getMessage(
matcher,
`Expected the element ${to} have value`,
expectsValue ? expectedValue : '(any)',
'Received',
receivedValue,
)
},
}
}

0 comments on commit fd747bf

Please sign in to comment.