Skip to content

Commit

Permalink
feat(expect): toContain can handle classList and Node.contains (#4239)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va committed Nov 15, 2023
1 parent 969f185 commit ce84f06
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 6 deletions.
8 changes: 7 additions & 1 deletion docs/api/expect.md
Expand Up @@ -422,14 +422,20 @@ test('structurally the same, but semantically different', () => {

- **Type:** `(received: string) => Awaitable<void>`

`toContain` asserts if the actual value is in an array. `toContain` can also check whether a string is a substring of another string.
`toContain` asserts if the actual value is in an array. `toContain` can also check whether a string is a substring of another string. Since Vitest 1.0, if you are running tests in a browser-like environment, this assertion can also check if class is contained in a `classList`, or an element is inside another one.

```ts
import { expect, test } from 'vitest'
import { getAllFruits } from './stocks.js'

test('the fruit list contains orange', () => {
expect(getAllFruits()).toContain('orange')

const element = document.querySelector('#el')
// element has a class
expect(element.classList).toContain('flex')
// element is inside another one
expect(document.querySelector('#wrapper')).toContain(element)
})
```

Expand Down
47 changes: 43 additions & 4 deletions packages/expect/src/jest-expect.ts
Expand Up @@ -10,6 +10,15 @@ import { diff, stringify } from './jest-matcher-utils'
import { JEST_MATCHERS_OBJECT } from './constants'
import { recordAsyncExpect, wrapSoft } from './utils'

// polyfill globals because expect can be used in node environment
declare class Node {
contains(item: unknown): boolean
}
declare class DOMTokenList {
value: string
contains(item: unknown): boolean
}

// Jest Expect Compact
export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
const { AssertionError } = chai
Expand Down Expand Up @@ -164,6 +173,36 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
return this.match(expected)
})
def('toContain', function (item) {
const actual = this._obj as Iterable<unknown> | string | Node | DOMTokenList

if (typeof Node !== 'undefined' && actual instanceof Node) {
if (!(item instanceof Node))
throw new TypeError(`toContain() expected a DOM node as the argument, but got ${typeof item}`)

return this.assert(
actual.contains(item),
'expected #{this} to contain element #{exp}',
'expected #{this} not to contain element #{exp}',
item,
actual,
)
}

if (typeof DOMTokenList !== 'undefined' && actual instanceof DOMTokenList) {
assertTypes(item, 'class name', ['string'])
const isNot = utils.flag(this, 'negate') as boolean
const expectedClassList = isNot ? actual.value.replace(item, '').trim() : `${actual.value} ${item}`
return this.assert(
actual.contains(item),
`expected "${actual.value}" to contain "${item}"`,
`expected "${actual.value}" not to contain "${item}"`,
expectedClassList,
actual.value,
)
}
// make "actual" indexable to have compatibility with jest
if (actual != null && typeof actual !== 'string')
utils.flag(this, 'object', Array.from(actual as Iterable<unknown>))
return this.contain(item)
})
def('toContainEqual', function (expected) {
Expand Down Expand Up @@ -200,7 +239,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
)
})
def('toBeGreaterThan', function (expected: number | bigint) {
const actual = this._obj
const actual = this._obj as number | bigint
assertTypes(actual, 'actual', ['number', 'bigint'])
assertTypes(expected, 'expected', ['number', 'bigint'])
return this.assert(
Expand All @@ -213,7 +252,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
)
})
def('toBeGreaterThanOrEqual', function (expected: number | bigint) {
const actual = this._obj
const actual = this._obj as number | bigint
assertTypes(actual, 'actual', ['number', 'bigint'])
assertTypes(expected, 'expected', ['number', 'bigint'])
return this.assert(
Expand All @@ -226,7 +265,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
)
})
def('toBeLessThan', function (expected: number | bigint) {
const actual = this._obj
const actual = this._obj as number | bigint
assertTypes(actual, 'actual', ['number', 'bigint'])
assertTypes(expected, 'expected', ['number', 'bigint'])
return this.assert(
Expand All @@ -239,7 +278,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
)
})
def('toBeLessThanOrEqual', function (expected: number | bigint) {
const actual = this._obj
const actual = this._obj as number | bigint
assertTypes(actual, 'actual', ['number', 'bigint'])
assertTypes(expected, 'expected', ['number', 'bigint'])
return this.assert(
Expand Down
75 changes: 74 additions & 1 deletion test/core/test/environments/jsdom.spec.ts
@@ -1,6 +1,12 @@
// @vitest-environment jsdom

import { expect, test } from 'vitest'
import { createColors, getDefaultColors, setupColors } from '@vitest/utils'
import { processError } from '@vitest/utils/error'
import { afterEach, expect, test } from 'vitest'

afterEach(() => {
setupColors(createColors(true))
})

const nodeMajor = Number(process.version.slice(1).split('.')[0])

Expand All @@ -21,3 +27,70 @@ test.runIf(nodeMajor >= 18)('fetch, Request, Response, and BroadcastChannel are
expect(TextDecoder).toBeDefined()
expect(BroadcastChannel).toBeDefined()
})

test('toContain correctly handles DOM nodes', () => {
const wrapper = document.createElement('div')
const child = document.createElement('div')
const external = document.createElement('div')
wrapper.appendChild(child)

const parent = document.createElement('div')
parent.appendChild(wrapper)
parent.appendChild(external)

document.body.appendChild(parent)
const divs = document.querySelectorAll('div')

expect(divs).toContain(wrapper)
expect(divs).toContain(parent)
expect(divs).toContain(external)

expect(wrapper).toContain(child)
expect(wrapper).not.toContain(external)

wrapper.classList.add('flex', 'flex-col')

expect(wrapper.classList).toContain('flex-col')
expect(wrapper.classList).not.toContain('flex-row')

expect(() => {
expect(wrapper).toContain('some-element')
}).toThrowErrorMatchingInlineSnapshot(`[TypeError: toContain() expected a DOM node as the argument, but got string]`)

expect(() => {
expect(wrapper.classList).toContain('flex-row')
}).toThrowErrorMatchingInlineSnapshot(`[AssertionError: expected "flex flex-col" to contain "flex-row"]`)
expect(() => {
expect(wrapper.classList).toContain(2)
}).toThrowErrorMatchingInlineSnapshot(`[TypeError: class name value must be string, received "number"]`)

setupColors(getDefaultColors())

try {
expect(wrapper.classList).toContain('flex-row')
expect.unreachable()
}
catch (err: any) {
expect(processError(err).diff).toMatchInlineSnapshot(`
"- Expected
+ Received
- flex flex-col flex-row
+ flex flex-col"
`)
}

try {
expect(wrapper.classList).not.toContain('flex')
expect.unreachable()
}
catch (err: any) {
expect(processError(err).diff).toMatchInlineSnapshot(`
"- Expected
+ Received
- flex-col
+ flex flex-col"
`)
}
})

0 comments on commit ce84f06

Please sign in to comment.