diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index 2b8da214..3e533d37 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -18,6 +18,7 @@ learn how: http://kcd.im/pull-request
Relevant code or config
```javascript
+
```
What you did:
diff --git a/README.md b/README.md
index 5d1791ad..0aa1393e 100644
--- a/README.md
+++ b/README.md
@@ -49,7 +49,6 @@ clear to read and to maintain.
-
- [Installation](#installation)
- [Usage](#usage)
- [With TypeScript](#with-typescript)
@@ -1203,12 +1202,8 @@ To perform a partial match, you can pass a `RegExp` or use
#### Examples
```html
-
-
- Closing will discard any changes
-
+
+Closing will discard any changes
```
diff --git a/other/CODE_OF_CONDUCT.md b/other/CODE_OF_CONDUCT.md
index cfe82c06..8649c632 100644
--- a/other/CODE_OF_CONDUCT.md
+++ b/other/CODE_OF_CONDUCT.md
@@ -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
-TestingLibraryOSS@gmail.com. All complaints will be reviewed and investigated promptly
-and fairly.
+TestingLibraryOSS@gmail.com. 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.
diff --git a/src/__tests__/to-be-empty-dom-element.js b/src/__tests__/to-be-empty-dom-element.js
index 0d16bff9..055ae88e 100644
--- a/src/__tests__/to-be-empty-dom-element.js
+++ b/src/__tests__/to-be-empty-dom-element.js
@@ -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()
diff --git a/src/__tests__/to-have-class.js b/src/__tests__/to-have-class.js
index 85bf8538..da79ac8b 100644
--- a/src/__tests__/to-have-class.js
+++ b/src/__tests__/to-have-class.js
@@ -102,9 +102,10 @@ 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,
@@ -112,9 +113,10 @@ test('.toHaveClass with exact mode option', () => {
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',
diff --git a/src/__tests__/to-have-selection.js b/src/__tests__/to-have-selection.js
new file mode 100644
index 00000000..7534f962
--- /dev/null
+++ b/src/__tests__/to-have-selection.js
@@ -0,0 +1,198 @@
+import {render} from './helpers/test-utils'
+
+describe('.toHaveSelection', () => {
+ test.each(['text', 'password', 'textarea'])(
+ 'handles selection within form elements',
+ testId => {
+ const {queryByTestId} = render(`
+
+
+
+ `)
+
+ 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(`
+
+
+ `)
+
+ queryByTestId(testId).select()
+ expect(queryByTestId(testId)).toHaveSelection('')
+ },
+ )
+
+ test('does not match subset string', () => {
+ const {queryByTestId} = render(`
+
+ `)
+
+ queryByTestId('text').setSelectionRange(5, 13)
+ expect(queryByTestId('text')).not.toHaveSelection('select')
+ expect(queryByTestId('text')).toHaveSelection('selected')
+ })
+
+ test('accepts any selection when expected selection is missing', () => {
+ const {queryByTestId} = render(`
+
+ `)
+
+ expect(queryByTestId('text')).not.toHaveSelection()
+
+ queryByTestId('text').setSelectionRange(5, 13)
+
+ expect(queryByTestId('text')).toHaveSelection()
+ })
+
+ test('throws when form element is not selected', () => {
+ const {queryByTestId} = render(`
+
+ `)
+
+ let errorMessage
+ try {
+ expect(queryByTestId('text')).toHaveSelection()
+ } catch (error) {
+ errorMessage = error.message
+ }
+
+ expect(errorMessage).toMatchInlineSnapshot(`
+ expect(>element>).toHaveSelection(>expected>)>
+
+ Expected the element to have selection:
+ (any)>
+ Received:
+
+ `)
+ })
+
+ test('throws when form element is selected', () => {
+ const {queryByTestId} = render(`
+
+ `)
+ queryByTestId('text').setSelectionRange(5, 13)
+
+ let errorMessage
+ try {
+ expect(queryByTestId('text')).not.toHaveSelection()
+ } catch (error) {
+ errorMessage = error.message
+ }
+
+ expect(errorMessage).toMatchInlineSnapshot(`
+ expect(>element>).not.toHaveSelection(>expected>)>
+
+ Expected the element not to have selection:
+ (any)>
+ Received:
+ selected>
+ `)
+ })
+
+ test('throws when element is not selected', () => {
+ const {queryByTestId} = render(`
+ text
+ `)
+
+ let errorMessage
+ try {
+ expect(queryByTestId('text')).toHaveSelection()
+ } catch (error) {
+ errorMessage = error.message
+ }
+
+ expect(errorMessage).toMatchInlineSnapshot(`
+ expect(>element>).toHaveSelection(>expected>)>
+
+ Expected the element to have selection:
+ (any)>
+ Received:
+
+ `)
+ })
+
+ test('throws when element selection does not match', () => {
+ const {queryByTestId} = render(`
+
+ `)
+ queryByTestId('text').setSelectionRange(0, 4)
+
+ let errorMessage
+ try {
+ expect(queryByTestId('text')).toHaveSelection('no match')
+ } catch (error) {
+ errorMessage = error.message
+ }
+
+ expect(errorMessage).toMatchInlineSnapshot(`
+ expect(>element>).toHaveSelection(>no match>)>
+
+ Expected the element to have selection:
+ no match>
+ Received:
+ text>
+ `)
+ })
+
+ test('handles selection within text nodes', () => {
+ const {queryByTestId} = render(`
+ prev
+ text selected text
+ next
+ `)
+
+ const selection = queryByTestId('child').ownerDocument.getSelection()
+ const range = queryByTestId('child').ownerDocument.createRange()
+ selection.removeAllRanges()
+ selection.addRange(range)
+
+ range.selectNodeContents(queryByTestId('child'))
+
+ expect(queryByTestId('child')).toHaveSelection('selected')
+ expect(queryByTestId('parent')).toHaveSelection('selected')
+
+ range.selectNodeContents(queryByTestId('parent'))
+
+ expect(queryByTestId('child')).toHaveSelection('selected')
+ expect(queryByTestId('parent')).toHaveSelection('text selected text')
+
+ range.setStart(queryByTestId('prev'), 0)
+ range.setEnd(queryByTestId('child').childNodes[0], 3)
+
+ expect(queryByTestId('child')).toHaveSelection('sel')
+ expect(queryByTestId('parent')).toHaveSelection('text sel')
+
+ range.setStart(queryByTestId('child').childNodes[0], 3)
+ range.setEnd(queryByTestId('next').childNodes[0], 4)
+
+ expect(queryByTestId('child')).toHaveSelection('ected')
+ expect(queryByTestId('parent')).toHaveSelection('ected text')
+ })
+
+ test('throws with information when the expected selection is not string', () => {
+ const {container} = render(`1
`)
+ const element = container.firstChild
+ const range = element.ownerDocument.createRange()
+ range.selectNodeContents(element)
+ element.ownerDocument.getSelection().addRange(range)
+ let errorMessage
+ try {
+ expect(element).toHaveSelection(1)
+ } catch (error) {
+ errorMessage = error.message
+ }
+
+ expect(errorMessage).toMatchInlineSnapshot(
+ `expected selection must be a string or undefined`,
+ )
+ })
+})
diff --git a/src/__tests__/to-have-style.js b/src/__tests__/to-have-style.js
index 0d94efaa..07396c52 100644
--- a/src/__tests__/to-have-style.js
+++ b/src/__tests__/to-have-style.js
@@ -205,7 +205,7 @@ describe('.toHaveStyle', () => {
Hello World
`)
expect(queryByTestId('color-example')).toHaveStyle({
- fontSize: 12
+ fontSize: 12,
})
})
@@ -214,7 +214,7 @@ describe('.toHaveStyle', () => {
Hello World
`)
expect(() => {
- expect(queryByTestId('color-example')).toHaveStyle({ fontSize: '12px' })
+ expect(queryByTestId('color-example')).toHaveStyle({fontSize: '12px'})
}).toThrowError()
})
diff --git a/src/matchers.js b/src/matchers.js
index c90945d5..5be731f5 100644
--- a/src/matchers.js
+++ b/src/matchers.js
@@ -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,
@@ -50,4 +51,5 @@ export {
toBePartiallyChecked,
toHaveDescription,
toHaveErrorMessage,
+ toHaveSelection,
}
diff --git a/src/to-be-empty-dom-element.js b/src/to-be-empty-dom-element.js
index 652f5299..63a9044a 100644
--- a/src/to-be-empty-dom-element.js
+++ b/src/to-be-empty-dom-element.js
@@ -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
}
diff --git a/src/to-have-selection.js b/src/to-have-selection.js
new file mode 100644
index 00000000..06baac00
--- /dev/null
+++ b/src/to-have-selection.js
@@ -0,0 +1,96 @@
+import isEqualWith from 'lodash/isEqualWith'
+import {checkHtmlElement, compareArraysAsSet, getMessage} from './utils'
+
+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 selectionStartsWithinElement =
+ element === originalRange.startContainer ||
+ element.contains(originalRange.startContainer)
+ const selectionEndsWithinElement =
+ element === originalRange.endContainer ||
+ element.contains(originalRange.endContainer)
+
+ temporaryRange.selectNodeContents(element)
+
+ if (selectionStartsWithinElement) {
+ temporaryRange.setStart(
+ originalRange.startContainer,
+ originalRange.startOffset,
+ )
+ } else if (selectionEndsWithinElement) {
+ temporaryRange.setEnd(originalRange.endContainer, originalRange.endOffset)
+ }
+
+ selection.removeAllRanges()
+ selection.addRange(temporaryRange)
+ }
+
+ const result = selection.toString()
+
+ selection.removeAllRanges()
+ selection.addRange(originalRange)
+
+ return result
+}
+
+export function toHaveSelection(htmlElement, expectedSelection) {
+ checkHtmlElement(htmlElement, toHaveSelection, this)
+
+ const expectsSelection = expectedSelection !== undefined
+
+ if (expectsSelection && typeof expectedSelection !== 'string') {
+ throw new Error(`expected selection must be a string or undefined`)
+ }
+
+ const receivedSelection = getSelection(htmlElement)
+
+ 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 ? expectedSelection : '(any)',
+ 'Received',
+ receivedSelection,
+ )
+ },
+ }
+}