mirror of
https://github.com/tabler/tabler.git
synced 2026-10-12 05:06:55 +04:00
258 lines
9.2 KiB
TypeScript
258 lines
9.2 KiB
TypeScript
import { describe, it, expect, beforeAll, afterEach, vi } from 'vitest'
|
|
import { clearFixture, getFixture } from '../helpers/fixture'
|
|
import Confetti from '../../src/confetti'
|
|
|
|
describe('Confetti', () => {
|
|
let fixtureEl: HTMLElement
|
|
|
|
beforeAll(() => {
|
|
fixtureEl = getFixture()
|
|
})
|
|
|
|
afterEach(() => {
|
|
// Let every burst land so one test's canvas never leaks into the next.
|
|
for (const element of fixtureEl.querySelectorAll<HTMLElement>('button, div')) {
|
|
Confetti.getInstance(element)?.dispose()
|
|
}
|
|
|
|
clearFixture()
|
|
vi.restoreAllMocks()
|
|
return vi.waitFor(() => expect(canvas()).toBeNull())
|
|
})
|
|
|
|
const button = (): HTMLElement => fixtureEl.querySelector('button')!
|
|
const canvas = (): HTMLCanvasElement | null => document.querySelector('.confetti-canvas')
|
|
|
|
// A short pour with fast pieces: the whole shower is over well within a second.
|
|
const QUICK = { count: 5, duration: 20, speed: 60 }
|
|
|
|
const reduceMotion = (matches: boolean): void => {
|
|
vi.spyOn(window, 'matchMedia').mockImplementation((query: string) => ({ matches, media: query }) as MediaQueryList)
|
|
}
|
|
|
|
describe('NAME', () => {
|
|
it('should return plugin name', () => {
|
|
expect(Confetti.NAME).toBe('confetti')
|
|
})
|
|
})
|
|
|
|
describe('Default', () => {
|
|
it('should expose the defaults and their types', () => {
|
|
expect(Confetti.Default.count).toBe(220)
|
|
expect(Confetti.Default.colors).toBeNull()
|
|
expect(Confetti.DefaultType.colors).toBe('(array|string|null)')
|
|
})
|
|
})
|
|
|
|
describe('burst', () => {
|
|
it('should mount one shared canvas and remove it after the pieces land', async () => {
|
|
fixtureEl.innerHTML = '<button type="button"></button>'
|
|
reduceMotion(false)
|
|
|
|
const instance = new Confetti(button(), QUICK)
|
|
instance.burst()
|
|
|
|
const mounted = canvas()!
|
|
expect(mounted).not.toBeNull()
|
|
expect(mounted.getAttribute('aria-hidden')).toBe('true')
|
|
expect(mounted.style.pointerEvents).toBe('none')
|
|
|
|
instance.burst()
|
|
expect(document.querySelectorAll('.confetti-canvas')).toHaveLength(1)
|
|
|
|
await vi.waitFor(() => expect(canvas()).toBeNull(), { timeout: 3000 })
|
|
})
|
|
|
|
it('should fire start and end events', async () => {
|
|
fixtureEl.innerHTML = '<button type="button"></button>'
|
|
reduceMotion(false)
|
|
|
|
const start = vi.fn()
|
|
const end = vi.fn()
|
|
button().addEventListener('start.bs.confetti', start)
|
|
button().addEventListener('end.bs.confetti', end)
|
|
|
|
new Confetti(button(), QUICK).burst()
|
|
expect(start).toHaveBeenCalledTimes(1)
|
|
expect(end).not.toHaveBeenCalled()
|
|
|
|
await vi.waitFor(() => expect(end).toHaveBeenCalledTimes(1), { timeout: 3000 })
|
|
})
|
|
|
|
it('should not start when the start event is prevented', () => {
|
|
fixtureEl.innerHTML = '<button type="button"></button>'
|
|
reduceMotion(false)
|
|
|
|
button().addEventListener('start.bs.confetti', (event) => event.preventDefault())
|
|
new Confetti(button(), QUICK).burst()
|
|
|
|
expect(canvas()).toBeNull()
|
|
})
|
|
|
|
it('should skip the animation but still end when the user prefers reduced motion', () => {
|
|
fixtureEl.innerHTML = '<button type="button"></button>'
|
|
reduceMotion(true)
|
|
|
|
const end = vi.fn()
|
|
button().addEventListener('end.bs.confetti', end)
|
|
new Confetti(button(), QUICK).burst()
|
|
|
|
expect(canvas()).toBeNull()
|
|
expect(end).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('should end at once without a canvas when there is no 2D context', () => {
|
|
fixtureEl.innerHTML = '<button type="button"></button>'
|
|
reduceMotion(false)
|
|
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(null)
|
|
|
|
const end = vi.fn()
|
|
button().addEventListener('end.bs.confetti', end)
|
|
new Confetti(button(), QUICK).burst()
|
|
|
|
expect(canvas()).toBeNull()
|
|
expect(end).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('should size the backing store to the viewport without the scrollbar', () => {
|
|
fixtureEl.innerHTML = '<button type="button"></button>'
|
|
reduceMotion(false)
|
|
document.documentElement.style.overflowY = 'scroll'
|
|
|
|
try {
|
|
new Confetti(button(), QUICK).burst()
|
|
|
|
const dpr = Math.min(window.devicePixelRatio || 1, 2)
|
|
expect(canvas()!.width).toBe(document.documentElement.clientWidth * dpr)
|
|
} finally {
|
|
document.documentElement.style.overflowY = ''
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('dispose', () => {
|
|
it('should not fire end once the instance is disposed', async () => {
|
|
fixtureEl.innerHTML = '<button type="button"></button>'
|
|
reduceMotion(false)
|
|
|
|
const end = vi.fn()
|
|
button().addEventListener('end.bs.confetti', end)
|
|
|
|
const instance = new Confetti(button(), QUICK)
|
|
instance.burst()
|
|
instance.dispose()
|
|
|
|
await vi.waitFor(() => expect(canvas()).toBeNull(), { timeout: 3000 })
|
|
expect(end).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('stop', () => {
|
|
it('should stop pouring and let the pieces in the air land', async () => {
|
|
fixtureEl.innerHTML = '<button type="button"></button>'
|
|
reduceMotion(false)
|
|
|
|
const end = vi.fn()
|
|
button().addEventListener('end.bs.confetti', end)
|
|
|
|
const instance = new Confetti(button(), { count: 50, duration: 60_000, speed: 60 })
|
|
instance.burst()
|
|
expect(canvas()).not.toBeNull()
|
|
|
|
instance.stop()
|
|
await vi.waitFor(() => expect(end).toHaveBeenCalledTimes(1), { timeout: 3000 })
|
|
expect(canvas()).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('colors', () => {
|
|
it('should accept a comma separated list from a data attribute', () => {
|
|
fixtureEl.innerHTML = '<button type="button" data-bs-colors=" #f00, #0f0 ,#00f"></button>'
|
|
|
|
const instance = new Confetti(button())
|
|
expect(instance._config.colors).toEqual(['#f00', '#0f0', '#00f'])
|
|
})
|
|
|
|
it('should read the Tabler palette from custom properties by default', () => {
|
|
fixtureEl.innerHTML = '<button type="button"></button>'
|
|
document.documentElement.style.setProperty('--tblr-blue', 'rgb(1, 2, 3)')
|
|
|
|
const colors = new Confetti(button())._colors()
|
|
document.documentElement.style.removeProperty('--tblr-blue')
|
|
|
|
expect(colors).toHaveLength(12)
|
|
expect(colors[0]).toBe('rgb(1, 2, 3)')
|
|
// No CSS is loaded in the test page: the other names fall back to hex.
|
|
expect(colors[1]).toBe('#4299e1')
|
|
})
|
|
})
|
|
|
|
describe('data-api', () => {
|
|
it('should burst on click without instantiating manually', async () => {
|
|
fixtureEl.innerHTML = '<button type="button" data-bs-toggle="confetti" data-bs-count="5" data-bs-duration="20" data-bs-speed="60"></button>'
|
|
reduceMotion(false)
|
|
|
|
button().click()
|
|
|
|
const instance = Confetti.getInstance(button()) as Confetti
|
|
expect(instance).not.toBeNull()
|
|
expect(instance._config.count).toBe(5)
|
|
expect(canvas()).not.toBeNull()
|
|
})
|
|
|
|
it('should burst on the target of a data-tblr-toggle trigger and prevent the default on a link', () => {
|
|
fixtureEl.innerHTML = '<a href="#" data-tblr-toggle="confetti" data-tblr-target="#party"></a><div id="party"></div>'
|
|
reduceMotion(true)
|
|
|
|
const start = vi.fn()
|
|
const target = fixtureEl.querySelector('#party')!
|
|
target.addEventListener('start.bs.confetti', start)
|
|
|
|
const event = new MouseEvent('click', { bubbles: true, cancelable: true })
|
|
fixtureEl.querySelector('a')!.dispatchEvent(event)
|
|
|
|
expect(event.defaultPrevented).toBe(true)
|
|
expect(start).toHaveBeenCalledTimes(1)
|
|
expect(Confetti.getInstance(fixtureEl.querySelector('a')!)).toBeNull()
|
|
})
|
|
|
|
it('should read options from the trigger even when data-bs-target points elsewhere', () => {
|
|
fixtureEl.innerHTML = '<button type="button" data-bs-toggle="confetti" data-bs-target="#order" data-bs-count="5" data-bs-colors="#f00"></button><div id="order"></div>'
|
|
reduceMotion(true)
|
|
|
|
button().click()
|
|
|
|
const instance = Confetti.getInstance(fixtureEl.querySelector('#order')!) as Confetti
|
|
expect(instance).not.toBeNull()
|
|
expect(instance._config.count).toBe(5)
|
|
expect(instance._config.colors).toEqual(['#f00'])
|
|
})
|
|
|
|
it("should apply each trigger's own options when several share a target", () => {
|
|
fixtureEl.innerHTML = '<button type="button" id="small" data-bs-toggle="confetti" data-bs-target="#order" data-bs-count="5"></button><button type="button" id="big" data-bs-toggle="confetti" data-bs-target="#order" data-bs-count="600"></button><div id="order"></div>'
|
|
reduceMotion(true)
|
|
const target = fixtureEl.querySelector('#order')!
|
|
|
|
fixtureEl.querySelector<HTMLElement>('#small')!.click()
|
|
expect((Confetti.getInstance(target) as Confetti)._config.count).toBe(5)
|
|
|
|
fixtureEl.querySelector<HTMLElement>('#big')!.click()
|
|
expect((Confetti.getInstance(target) as Confetti)._config.count).toBe(600)
|
|
|
|
fixtureEl.querySelector<HTMLElement>('#small')!.click()
|
|
expect((Confetti.getInstance(target) as Confetti)._config.count).toBe(5)
|
|
})
|
|
|
|
it('should ignore a disabled trigger', () => {
|
|
fixtureEl.innerHTML = '<button type="button" data-bs-toggle="confetti" disabled></button>'
|
|
reduceMotion(true)
|
|
|
|
const start = vi.fn()
|
|
button().addEventListener('start.bs.confetti', start)
|
|
button().click()
|
|
|
|
expect(start).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
})
|