diff --git a/README.md b/README.md
index 84ed86eb..d34a2d91 100644
--- a/README.md
+++ b/README.md
@@ -81,6 +81,7 @@ clear to read and to maintain.
- [`toBePartiallyChecked`](#tobepartiallychecked)
- [`toHaveRole`](#tohaverole)
- [`toHaveErrorMessage`](#tohaveerrormessage)
+ - [`toHaveSelection`](#tohaveselection)
- [Deprecated matchers](#deprecated-matchers)
- [`toBeEmpty`](#tobeempty)
- [`toBeInTheDOM`](#tobeinthedom)
@@ -162,7 +163,8 @@ import '@testing-library/jest-dom/vitest'
setupFiles: ['./vitest-setup.js']
```
-Also, depending on your local setup, you may need to update your `tsconfig.json`:
+Also, depending on your local setup, you may need to update your
+`tsconfig.json`:
```json
// In tsconfig.json
@@ -1420,6 +1422,71 @@ expect(deleteButton).not.toHaveDescription()
expect(deleteButton).toHaveDescription('') // Missing or empty description always becomes a blank string
```
+
+
+### `toHaveSelection`
+
+This allows to assert that an element has a
+[text selection](https://developer.mozilla.org/en-US/docs/Web/API/Selection).
+
+This is useful to check if text or part of the text is selected within an
+element. The element can be either an input of type text, a textarea, or any
+other element that contains text, such as a paragraph, span, div etc.
+
+NOTE: the expected selection is a string, it does not allow to check for
+selection range indeces.
+
+```typescript
+toHaveSelection(expectedSelection?: string)
+```
+
+```html
+
+
+
+
prev
+
+ text selected text
+
+
next
+
+```
+
+```javascript
+getByTestId('text').setSelectionRange(5, 13)
+expect(getByTestId('text')).toHaveSelection('selected')
+
+getByTestId('textarea').setSelectionRange(0, 5)
+expect('textarea').toHaveSelection('text ')
+
+const selection = document.getSelection()
+const range = document.createRange()
+selection.removeAllRanges()
+selection.empty()
+selection.addRange(range)
+
+// selection of child applies to the parent as well
+range.selectNodeContents(getByTestId('child'))
+expect(getByTestId('child')).toHaveSelection('selected')
+expect(getByTestId('parent')).toHaveSelection('selected')
+
+// selection that applies from prev all, parent text before child, and part child.
+range.setStart(getByTestId('prev'), 0)
+range.setEnd(getByTestId('child').childNodes[0], 3)
+expect(queryByTestId('prev')).toHaveSelection('prev')
+expect(queryByTestId('child')).toHaveSelection('sel')
+expect(queryByTestId('parent')).toHaveSelection('text sel')
+expect(queryByTestId('next')).not.toHaveSelection()
+
+// selection that applies from part child, parent text after child and part next.
+range.setStart(getByTestId('child').childNodes[0], 3)
+range.setEnd(getByTestId('next').childNodes[0], 2)
+expect(queryByTestId('child')).toHaveSelection('ected')
+expect(queryByTestId('parent')).toHaveSelection('ected text')
+expect(queryByTestId('prev')).not.toHaveSelection()
+expect(queryByTestId('next')).toHaveSelection('ne')
+```
+
## Inspiration
This whole library was extracted out of Kent C. Dodds' [DOM Testing
diff --git a/src/__tests__/to-have-selection.js b/src/__tests__/to-have-selection.js
new file mode 100644
index 00000000..9ddcc2c8
--- /dev/null
+++ b/src/__tests__/to-have-selection.js
@@ -0,0 +1,189 @@
+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(`
+
+ `)
+
+ expect(() =>
+ expect(queryByTestId('text')).toHaveSelection(),
+ ).toThrowErrorMatchingInlineSnapshot(
+ `
+ 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)
+
+ expect(() =>
+ expect(queryByTestId('text')).not.toHaveSelection(),
+ ).toThrowErrorMatchingInlineSnapshot(
+ `
+ 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
+ `)
+
+ expect(() =>
+ expect(queryByTestId('text')).toHaveSelection(),
+ ).toThrowErrorMatchingInlineSnapshot(
+ `
+ 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)
+
+ expect(() =>
+ expect(queryByTestId('text')).toHaveSelection('no match'),
+ ).toThrowErrorMatchingInlineSnapshot(
+ `
+ expect(>element>).toHaveSelection(>no match>)>
+
+ Expected the element to have selection:
+ no match>
+ Received:
+ text>
+ `,
+ )
+ })
+
+ test('handles selection within text nodes', () => {
+ const {queryByTestId} = render(`
+