Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(expect): toContain can handle classList and Node.contains #4239

Merged
merged 6 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 7 additions & 1 deletion docs/api/expect.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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>))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this Array.from is causing bugs. If actual is an object, then it will always return an empty array which is an issue.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I found that this is expected and I should instead use toMatchObject rather than toContain.

https://stackoverflow.com/questions/47754777/how-can-i-test-for-object-keys-and-values-equality-using-jest

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
Original file line number Diff line number Diff line change
@@ -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"
`)
}
})
Loading