Welcome to the EpicWeb.dev Workshop app!

This is the deployed version. Run locally for full experience.

Custom Assertions

You know assertions? I'm talking about this stuff:
expect(1 + 1).toBe(2)
Well, there are a bunch of great built-in assertions. But sometimes you have some state that's a little more complex or use-case specific. One solution is to have a set of assertions that you wrap up in a function:
function assertLoggedIn(userName: string) {
	expect(screen.getByRole('link', { name: userName })).toBeTruthy()
	expect(screen.getByRole('button', { name: /logout/i })).toBeTruthy()
}
So then you want to use this in multiple tests, you can move it to a separate file, but then the code frame you get from Vitest will point to the function, not where it was called.
What would be cooler is if you could make your own assertions, which you can do! Via the expect.extend API.
Here's what that assertLoggedIn function would look like as a custom matcher:
expect.extend({
	toBeLoggedIn(userName: string) {
		const link = screen.queryByRole('link', { name: userName })
		const button = screen.queryByRole('button', { name: /logout/i })

		return {
			pass: Boolean(link && button),
			message: () => `Expected to be logged in as ${userName}`,
		}
	},
})
You can even support negation (.not). Just change the message based on this.isNot:
expect.extend({
	toBeLoggedIn(userName: string) {
		const link = screen.queryByRole('link', { name: userName })
		const button = screen.queryByRole('button', { name: /logout/i })

		return {
			// you don't change "pass" based on isNot because vitest will do that for you.
			pass: Boolean(link && button),
			// you only change the message
			message: () => {
				return `Expected ${
					this.isNot ? 'not ' : ''
				}to be logged in as ${userName}`
			},
		}
	},
})
And then you can use some utils to color the output as well:
expect.extend({
	toBeLoggedIn(userName: string) {
		const link = screen.queryByRole('link', { name: userName })
		const button = screen.queryByRole('button', { name: /logout/i })

		return {
			pass: Boolean(link && button),
			message: () => {
				return `Expected ${
					this.isNot ? 'not ' : ''
				}to be logged in as ${this.utils.printExpected(userName)}`
			},
		}
	},
})
And then you can add this to the TypeScript definitions for expect via a module augmentation:
import {
	expect,
	type Assertion,
	type AsymmetricMatchersContaining,
} from 'vitest'

expect.extend({
	toBeLoggedIn(userName: string) {
		const link = screen.queryByRole('link', { name: userName })
		const button = screen.queryByRole('button', { name: /logout/i })

		return {
			pass: Boolean(link && button),
			message: () => {
				return `Expected ${
					this.isNot ? 'not ' : ''
				}to be logged in as ${this.utils.printExpected(userName)}`
			},
		}
	},
})

interface CustomMatchers<R = unknown> {
	toBeLoggedIn(userName: string): R
}

declare module 'vitest' {
	interface Assertion<T = any> extends CustomMatchers<T> {}
	interface AsymmetricMatchersContaining extends CustomMatchers {}
}
There's a lot of power to this API and entire libraries have been developed to handle common use cases for assertions to make them easier to write and improve the error messages. For example, jest-dom which includes a bunch of matchers specific to DOM testing.