Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ learn how: http://kcd.im/pull-request
Relevant code or config

```javascript

```

What you did:
Expand Down
9 changes: 2 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ clear to read and to maintain.
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->


- [Installation](#installation)
- [Usage](#usage)
- [With TypeScript](#with-typescript)
Expand Down Expand Up @@ -1203,12 +1202,8 @@ To perform a partial match, you can pass a `RegExp` or use
#### Examples

```html
<button aria-label="Close" aria-describedby="description-close">
X
</button>
<div id="description-close">
Closing will discard any changes
</div>
<button aria-label="Close" aria-describedby="description-close">X</button>
<div id="description-close">Closing will discard any changes</div>

<button>Delete</button>
```
Expand Down
4 changes: 2 additions & 2 deletions other/CODE_OF_CONDUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ representative at an online or offline event.

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
[email protected]. All complaints will be reviewed and investigated promptly
and fairly.
[email protected]. All complaints will be reviewed and investigated
promptly and fairly.

All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
Expand Down
8 changes: 6 additions & 2 deletions src/__tests__/to-be-empty-dom-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,15 @@ test('.toBeEmptyDOMElement', () => {

expect(() => expect(withComment).not.toBeEmptyDOMElement()).toThrowError()

expect(() => expect(withMultipleComments).not.toBeEmptyDOMElement()).toThrowError()
expect(() =>
expect(withMultipleComments).not.toBeEmptyDOMElement(),
).toThrowError()

expect(() => expect(withElement).toBeEmptyDOMElement()).toThrowError()

expect(() => expect(withElementAndComment).toBeEmptyDOMElement()).toThrowError()
expect(() =>
expect(withElementAndComment).toBeEmptyDOMElement(),
).toThrowError()

expect(() => expect(withWhitespace).toBeEmptyDOMElement()).toThrowError()

Expand Down
14 changes: 8 additions & 6 deletions src/__tests__/to-have-class.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,19 +102,21 @@ test('.toHaveClass with exact mode option', () => {
expect(queryByTestId('delete-button')).not.toHaveClass('btn extra', {
exact: true,
})
expect(
queryByTestId('delete-button'),
).not.toHaveClass('btn extra btn-danger foo', {exact: true})
expect(queryByTestId('delete-button')).not.toHaveClass(
'btn extra btn-danger foo',
{exact: true},
)

expect(queryByTestId('delete-button')).toHaveClass('btn extra btn-danger', {
exact: false,
})
expect(queryByTestId('delete-button')).toHaveClass('btn extra', {
exact: false,
})
expect(
queryByTestId('delete-button'),
).not.toHaveClass('btn extra btn-danger foo', {exact: false})
expect(queryByTestId('delete-button')).not.toHaveClass(
'btn extra btn-danger foo',
{exact: false},
)

expect(queryByTestId('delete-button')).toHaveClass(
'btn',
Expand Down
70 changes: 70 additions & 0 deletions src/__tests__/to-have-selection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {render} from './helpers/test-utils'

describe('.toHaveSelection', () => {
test.each(['text', 'password', 'textarea'])(
'handles selection within form elements',
testId => {
const {queryByTestId} = render(`
<input type="text" value="text selected text" data-testid="text" />
<input type="password" value="text selected text" data-testid="password" />
<textarea data-testid="textarea">text selected text</textarea>
`)

queryByTestId(testId).setSelectionRange(5, 13)
expect(queryByTestId(testId)).toHaveSelection('selected')

queryByTestId(testId).select()
expect(queryByTestId(testId)).toHaveSelection('text selected text')
},
)

test.each(['checkbox', 'radio'])(
'returns empty string for form elements without text',
testId => {
const {queryByTestId} = render(`
<input type="checkbox" value="checkbox" data-testid="checkbox" />
<input type="radio" value="radio" data-testid="radio" />
`)

queryByTestId(testId).select()
expect(queryByTestId(testId)).toHaveSelection('')
},
)

test('does not match subset string', () => {
const {queryByTestId} = render(`
<input type="text" value="text selected text" data-testid="text" />
`)

queryByTestId('text').setSelectionRange(5, 13)
expect(queryByTestId('text')).not.toHaveSelection('select')
expect(queryByTestId('text')).toHaveSelection('selected')
})

test('handles selection within text nodes', () => {
const {queryByTestId} = render(`
<div data-testid="prev">prev</div>
<div data-testid="parent">text <span data-testid="child">selected</span> text</div>
<div data-testid="next">next</div>
`)

const selection = queryByTestId('child').ownerDocument.getSelection()
const range = queryByTestId('child').ownerDocument.createRange()
selection.removeAllRanges()
selection.addRange(range)

range.selectNodeContents(queryByTestId('child'))

expect(queryByTestId('parent')).toHaveSelection('selected')

range.setStart(queryByTestId('prev'), 0)
range.setEnd(queryByTestId('child').childNodes[0], 3)

expect(queryByTestId('parent')).toHaveSelection('text sel')

range.setStart(queryByTestId('child').childNodes[0], 3)
range.setEnd(queryByTestId('next').childNodes[0], 4)

expect(queryByTestId('parent')).toHaveSelection('ected text')
})
})
4 changes: 2 additions & 2 deletions src/__tests__/to-have-style.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ describe('.toHaveStyle', () => {
<span data-testid="color-example" style="font-size: 12px">Hello World</span>
`)
expect(queryByTestId('color-example')).toHaveStyle({
fontSize: 12
fontSize: 12,
})
})

Expand All @@ -214,7 +214,7 @@ describe('.toHaveStyle', () => {
<span data-testid="color-example" style="font-size: 12rem">Hello World</span>
`)
expect(() => {
expect(queryByTestId('color-example')).toHaveStyle({ fontSize: '12px' })
expect(queryByTestId('color-example')).toHaveStyle({fontSize: '12px'})
}).toThrowError()
})

Expand Down
2 changes: 2 additions & 0 deletions src/matchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {toBeChecked} from './to-be-checked'
import {toBePartiallyChecked} from './to-be-partially-checked'
import {toHaveDescription} from './to-have-description'
import {toHaveErrorMessage} from './to-have-errormessage'
import {toHaveSelection} from './to-have-selection'

export {
toBeInTheDOM,
Expand Down Expand Up @@ -50,4 +51,5 @@ export {
toBePartiallyChecked,
toHaveDescription,
toHaveErrorMessage,
toHaveSelection,
}
10 changes: 6 additions & 4 deletions src/to-be-empty-dom-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ export function toBeEmptyDOMElement(element) {

/**
* Identifies if an element doesn't contain child nodes (excluding comments)
* ℹ Node.COMMENT_NODE can't be used because of the following issue
* ℹ Node.COMMENT_NODE can't be used because of the following issue
* https://github.com/jsdom/jsdom/issues/2220
*
* @param {*} element an HtmlElement or SVGElement
* @return {*} true if the element only contains comments or none
*/
function isEmptyElement(element){
const nonCommentChildNodes = [...element.childNodes].filter(node => node.nodeType !== 8);
return nonCommentChildNodes.length === 0;
function isEmptyElement(element) {
const nonCommentChildNodes = [...element.childNodes].filter(
node => node.nodeType !== 8,
)
return nonCommentChildNodes.length === 0
}
46 changes: 46 additions & 0 deletions src/to-have-selection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import isEqualWith from 'lodash/isEqualWith'
import {
checkHtmlElement,
compareArraysAsSet,
getMessage,
getSelection,
} from './utils'

export function toHaveSelection(htmlElement, expectedSelection) {
checkHtmlElement(htmlElement, toHaveSelection, this)

const receivedSelection = getSelection(htmlElement)
const expectsSelection = expectedSelection !== undefined

let expectedTypedSelection = expectedSelection
let receivedTypedSelection = receivedSelection
if (
expectedSelection == receivedSelection &&
expectedSelection !== receivedSelection
) {
expectedTypedSelection = `${expectedSelection} (${typeof expectedSelection})`
receivedTypedSelection = `${receivedSelection} (${typeof receivedSelection})`
}

return {
pass: expectsSelection
? isEqualWith(receivedSelection, expectedSelection, compareArraysAsSet)
: Boolean(receivedSelection),
message: () => {
const to = this.isNot ? 'not to' : 'to'
const matcher = this.utils.matcherHint(
`${this.isNot ? '.not' : ''}.toHaveSelection`,
'element',
expectedSelection,
)
return getMessage(
this,
matcher,
`Expected the element ${to} have selection`,
expectsSelection ? expectedTypedSelection : '(any)',
'Received',
receivedTypedSelection,
)
},
}
}
56 changes: 56 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,61 @@ function toSentence(
)
}

function getSelection(element) {
const selection = element.ownerDocument.getSelection()

if (['input', 'textarea'].includes(element.tagName.toLowerCase())) {
if (['radio', 'checkbox'].includes(element.type)) return ''
return element.value
.toString()
.substring(element.selectionStart, element.selectionEnd)
}

if (selection.anchorNode === null || selection.focusNode === null) {
// No selection
return ''
}

const originalRange = selection.getRangeAt(0)
const temporaryRange = element.ownerDocument.createRange()

if (selection.containsNode(element, false)) {
// Whole element is inside selection
temporaryRange.selectNodeContents(element)
selection.removeAllRanges()
selection.addRange(temporaryRange)
} else if (
element.contains(selection.anchorNode) &&
element.contains(selection.focusNode)
) {
// Element contains selection, nothing to do
} else if (selection.containsNode(element, true)) {
// Element is partially selected
const range = element.ownerDocument.getSelection().getRangeAt(0)
const selectionStartsWithinElement =
element === range.startContainer || element.contains(range.startContainer)
const selectionEndsWithinElement =
element === range.endContainer || element.contains(range.endContainer)

selection.removeAllRanges()
temporaryRange.selectNodeContents(element)

if (selectionStartsWithinElement) {
temporaryRange.setStart(range.startContainer, range.startOffset)
} else if (selectionEndsWithinElement) {
temporaryRange.setEnd(range.endContainer, range.endOffset)
}
selection.addRange(temporaryRange)
}

const result = selection.toString()

selection.removeAllRanges()
selection.addRange(originalRange)

return result
}

export {
HtmlElementTypeError,
NodeTypeError,
Expand All @@ -242,4 +297,5 @@ export {
getSingleElementValue,
compareArraysAsSet,
toSentence,
getSelection,
}