blob: 7f030505bfef718bc403b55757fa0df59a78fb1e [file] [edit]
/*!
* Maps the Jasmine API our specs use onto Vitest.
*
* The specs predate Vitest and are written against Jasmine, so this shim lets
* them run unchanged instead of forcing a rewrite of roughly 22,000 lines. New
* specs should prefer the plain Vitest API. The one behaviour to watch is
* `spyOn`, which stubs by default here to match Jasmine; `vi.spyOn` calls
* through.
* Copyright 2026 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
import { afterEach, expect, vi } from 'vitest'
import hammerSimulatorSource from 'hammer-simulator/index.js?raw'
/**
* hammer-simulator is from 2014 and sets `event.target`, which is a getter-only
* property. Karma bundled it into a classic script, where that assignment fails
* silently. ES modules are always strict mode, so the same line throws. Running
* the source through `new Function` restores the sloppy-mode behaviour the suite
* has always relied on. The assignment was always a no-op: `dispatchEvent` is
* what sets the real target.
*/
// eslint-disable-next-line no-new-func
new Function(hammerSimulatorSource)()
/**
* Spies
*
* This is the one behaviour that cannot be codemodded safely. Jasmine's `spyOn`
* replaces the method with a stub that returns undefined. Vitest's `vi.spyOn`
* keeps calling the real method. A spec that says `spyOn(el, 'focus')` expects
* the focus call to be swallowed, so the shim stubs by default and puts the
* real implementation back only when the spec asks with `.and.callThrough()`.
*/
const attachJasmineApi = (spy, original) => {
const callThrough = () => {
spy.mockImplementation(function (...args) {
return original.apply(this, args)
})
return spy
}
spy.and = {
callThrough,
stub() {
spy.mockImplementation(() => {})
return spy
},
returnValue(value) {
spy.mockImplementation(() => value)
return spy
},
returnValues(...values) {
let index = 0
spy.mockImplementation(() => values[index++])
return spy
},
callFake(fake) {
spy.mockImplementation(fake)
return spy
},
throwError(error) {
spy.mockImplementation(() => {
throw typeof error === 'string' ? new Error(error) : error
})
return spy
}
}
Object.defineProperty(spy, 'calls', {
configurable: true,
get() {
const { calls, results } = spy.mock
const entry = index => (
index in calls ? { args: calls[index], returnValue: results[index]?.value } : undefined
)
return {
count: () => calls.length,
any: () => calls.length > 0,
allArgs: () => calls,
argsFor: index => calls[index],
all: () => calls.map((_, index) => entry(index)),
first: () => entry(0),
mostRecent: () => entry(calls.length - 1),
reset: () => spy.mockClear()
}
}
})
return spy
}
globalThis.spyOn = (object, method) => {
const original = object[method]
const spy = vi.spyOn(object, method)
spy.mockImplementation(() => {})
return attachJasmineApi(spy, original)
}
globalThis.spyOnProperty = (object, property, accessType = 'get') => {
let original
for (let target = object; target; target = Object.getPrototypeOf(target)) {
const descriptor = Object.getOwnPropertyDescriptor(target, property)
if (descriptor) {
original = descriptor[accessType]
break
}
}
const spy = vi.spyOn(object, property, accessType)
spy.mockImplementation(() => {})
return attachJasmineApi(spy, original ?? (() => {}))
}
globalThis.jasmine = {
any: expected => expect.any(expected),
anything: () => expect.anything(),
objectContaining: expected => expect.objectContaining(expected),
arrayContaining: expected => expect.arrayContaining(expected),
stringMatching: expected => expect.stringMatching(expected),
createSpy(_name, fake) {
return attachJasmineApi(vi.fn(fake), fake ?? (() => {}))
},
clock: () => ({
install: () => vi.useFakeTimers(),
uninstall: () => vi.useRealTimers(),
tick: ms => vi.advanceTimersByTime(ms),
mockDate: date => vi.setSystemTime(date)
})
}
/**
* Matchers Jasmine 6 ships built in and Vitest does not
*/
const result = (pass, message) => ({ pass, message: () => message })
expect.extend({
toHaveClass(received, className) {
const pass = Boolean(received?.classList?.contains(className))
return result(pass, `expected ${received?.tagName ?? received} ${pass ? 'not ' : ''}to have class "${className}"`)
},
toBeTrue(received) {
return result(received === true, `expected ${received} to be exactly true`)
},
toBeFalse(received) {
return result(received === false, `expected ${received} to be exactly false`)
},
toHaveSize(received, size) {
const actual = received?.length ?? received?.size
return result(actual === size, `expected size ${size}, got ${actual}`)
},
nothing() {
return result(true, '')
}
})
/**
* Jasmine removes every spy after each spec. Vitest keeps them, so without this
* a stubbed method would stay stubbed for the rest of the file.
*/
afterEach(() => {
vi.restoreAllMocks()
vi.useRealTimers()
})