Files
php-flasher/tests/adapters/toastr.test.ts
T
Younes ENNAJI 7d6e9b46b8 add event dispatching system for Livewire integration
Implement consistent event dispatching across Noty, Notyf, Toastr adapters and
themes, following the existing SweetAlert pattern. This enables Livewire
integration for all notification types.

JavaScript Events:
- Noty: flasher:noty:show, flasher:noty:click, flasher:noty:close, flasher:noty:hover
- Notyf: flasher:notyf:click, flasher:notyf:dismiss
- Toastr: flasher:toastr:show, flasher:toastr:click, flasher:toastr:close, flasher:toastr:hidden
- Themes: flasher:theme:click (generic), flasher:theme:{name}:click (specific)

PHP Livewire Listeners:
- LivewireListener for each adapter (Noty, Notyf, Toastr)
- ThemeLivewireListener for theme click events
- Registered in service providers when Livewire is bound

This allows Livewire users to listen for notification events and react
accordingly (e.g., noty:click, theme:flasher:click).
2026-03-01 21:05:10 +00:00

312 lines
11 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { Envelope } from '@flasher/flasher/types'
// Use vi.hoisted to define mocks that will be available during vi.mock hoisting
const { mockToastr, mockJQuery } = vi.hoisted(() => {
const mockToastr = {
success: vi.fn().mockReturnValue({ parent: vi.fn().mockReturnValue({ attr: vi.fn() }) }),
error: vi.fn().mockReturnValue({ parent: vi.fn().mockReturnValue({ attr: vi.fn() }) }),
info: vi.fn().mockReturnValue({ parent: vi.fn().mockReturnValue({ attr: vi.fn() }) }),
warning: vi.fn().mockReturnValue({ parent: vi.fn().mockReturnValue({ attr: vi.fn() }) }),
options: {} as Record<string, unknown>,
}
const mockJQuery = vi.fn()
return { mockToastr, mockJQuery }
})
vi.mock('toastr', () => ({
default: mockToastr,
}))
// Import after mocks are set up
import ToastrPlugin from '@flasher/flasher-toastr/toastr'
const createEnvelope = (overrides: Partial<Envelope> = {}): Envelope => ({
type: 'success',
message: 'Test message',
title: 'Test title',
options: {},
metadata: { plugin: 'toastr' },
...overrides,
})
describe('ToastrPlugin', () => {
let plugin: ToastrPlugin
beforeEach(() => {
plugin = new ToastrPlugin()
vi.clearAllMocks()
mockToastr.options = {}
// Set up jQuery mock
;(window as any).jQuery = mockJQuery
;(window as any).$ = mockJQuery
})
describe('renderEnvelopes', () => {
it('should do nothing with empty envelopes', () => {
plugin.renderEnvelopes([])
expect(mockToastr.success).not.toHaveBeenCalled()
})
it('should do nothing with null/undefined envelopes', () => {
plugin.renderEnvelopes(null as unknown as Envelope[])
plugin.renderEnvelopes(undefined as unknown as Envelope[])
expect(mockToastr.success).not.toHaveBeenCalled()
})
it('should call toastr with correct type', () => {
plugin.renderEnvelopes([createEnvelope({ type: 'success' })])
expect(mockToastr.success).toHaveBeenCalled()
plugin.renderEnvelopes([createEnvelope({ type: 'error' })])
expect(mockToastr.error).toHaveBeenCalled()
plugin.renderEnvelopes([createEnvelope({ type: 'info' })])
expect(mockToastr.info).toHaveBeenCalled()
plugin.renderEnvelopes([createEnvelope({ type: 'warning' })])
expect(mockToastr.warning).toHaveBeenCalled()
})
it('should pass message, title, and options to toastr', () => {
plugin.renderEnvelopes([createEnvelope({
message: 'Hello World',
title: 'Greeting',
options: { timeOut: 5000 },
})])
expect(mockToastr.success).toHaveBeenCalledWith(
'Hello World',
'Greeting',
expect.objectContaining({ timeOut: 5000 }),
)
})
it('should render multiple envelopes', () => {
plugin.renderEnvelopes([
createEnvelope({ type: 'success', message: 'First' }),
createEnvelope({ type: 'error', message: 'Second' }),
])
expect(mockToastr.success).toHaveBeenCalledTimes(1)
expect(mockToastr.error).toHaveBeenCalledTimes(1)
})
it('should set Turbo compatibility attribute', () => {
const mockParent = { attr: vi.fn() }
mockToastr.success.mockReturnValue({ parent: () => mockParent })
plugin.renderEnvelopes([createEnvelope()])
expect(mockParent.attr).toHaveBeenCalledWith('data-turbo-temporary', '')
})
it('should log error when jQuery is not available', () => {
delete (window as any).jQuery
delete (window as any).$
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
plugin.renderEnvelopes([createEnvelope()])
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('jQuery is required'),
)
expect(mockToastr.success).not.toHaveBeenCalled()
// Restore jQuery for other tests
;(window as any).jQuery = mockJQuery
;(window as any).$ = mockJQuery
})
it('should handle toastr errors gracefully', () => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
mockToastr.success.mockImplementationOnce(() => {
throw new Error('Toastr error')
})
plugin.renderEnvelopes([createEnvelope()])
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('Error rendering notification'),
expect.any(Error),
expect.any(Object),
)
})
it('should handle missing parent method gracefully', () => {
mockToastr.success.mockReturnValueOnce({})
// Should not throw
expect(() => plugin.renderEnvelopes([createEnvelope()])).not.toThrow()
})
it('should handle parent() returning null gracefully', () => {
mockToastr.success.mockReturnValueOnce({ parent: () => null })
// Should not throw
expect(() => plugin.renderEnvelopes([createEnvelope()])).not.toThrow()
})
})
describe('renderOptions', () => {
it('should set toastr options with defaults', () => {
plugin.renderOptions({ closeButton: true })
expect(mockToastr.options).toMatchObject({
timeOut: 10000,
progressBar: true,
closeButton: true,
})
})
it('should override default timeOut', () => {
plugin.renderOptions({ timeOut: 5000 })
expect(mockToastr.options.timeOut).toBe(5000)
})
it('should override default progressBar', () => {
plugin.renderOptions({ progressBar: false })
expect(mockToastr.options.progressBar).toBe(false)
})
it('should log error when jQuery is not available', () => {
delete (window as any).jQuery
delete (window as any).$
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
plugin.renderOptions({ timeOut: 5000 })
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('jQuery is required'),
)
// Restore jQuery
;(window as any).jQuery = mockJQuery
;(window as any).$ = mockJQuery
})
})
describe('convenience methods (inherited from AbstractPlugin)', () => {
it('success() should create success notification', () => {
plugin.success('Success message')
expect(mockToastr.success).toHaveBeenCalled()
})
it('error() should create error notification', () => {
plugin.error('Error message')
expect(mockToastr.error).toHaveBeenCalled()
})
it('info() should create info notification', () => {
plugin.info('Info message')
expect(mockToastr.info).toHaveBeenCalled()
})
it('warning() should create warning notification', () => {
plugin.warning('Warning message')
expect(mockToastr.warning).toHaveBeenCalled()
})
})
describe('event dispatching', () => {
it('should set up event callbacks that dispatch custom events', () => {
plugin.renderEnvelopes([createEnvelope()])
// Verify options passed to toastr include event callbacks
const calledOptions = mockToastr.success.mock.calls[0][2]
expect(calledOptions.onShown).toBeInstanceOf(Function)
expect(calledOptions.onclick).toBeInstanceOf(Function)
expect(calledOptions.onCloseClick).toBeInstanceOf(Function)
expect(calledOptions.onHidden).toBeInstanceOf(Function)
})
it('should dispatch flasher:toastr:show event when onShown callback is called', () => {
const eventHandler = vi.fn()
window.addEventListener('flasher:toastr:show', eventHandler)
plugin.renderEnvelopes([createEnvelope({ message: 'Show test' })])
const calledOptions = mockToastr.success.mock.calls[0][2]
calledOptions.onShown()
expect(eventHandler).toHaveBeenCalledWith(expect.objectContaining({
detail: expect.objectContaining({
envelope: expect.objectContaining({ message: 'Show test' }),
}),
}))
window.removeEventListener('flasher:toastr:show', eventHandler)
})
it('should dispatch flasher:toastr:click event when onclick callback is called', () => {
const eventHandler = vi.fn()
window.addEventListener('flasher:toastr:click', eventHandler)
plugin.renderEnvelopes([createEnvelope({ message: 'Click test' })])
const calledOptions = mockToastr.success.mock.calls[0][2]
calledOptions.onclick()
expect(eventHandler).toHaveBeenCalledWith(expect.objectContaining({
detail: expect.objectContaining({
envelope: expect.objectContaining({ message: 'Click test' }),
}),
}))
window.removeEventListener('flasher:toastr:click', eventHandler)
})
it('should dispatch flasher:toastr:close event when onCloseClick callback is called', () => {
const eventHandler = vi.fn()
window.addEventListener('flasher:toastr:close', eventHandler)
plugin.renderEnvelopes([createEnvelope()])
const calledOptions = mockToastr.success.mock.calls[0][2]
calledOptions.onCloseClick()
expect(eventHandler).toHaveBeenCalled()
window.removeEventListener('flasher:toastr:close', eventHandler)
})
it('should dispatch flasher:toastr:hidden event when onHidden callback is called', () => {
const eventHandler = vi.fn()
window.addEventListener('flasher:toastr:hidden', eventHandler)
plugin.renderEnvelopes([createEnvelope()])
const calledOptions = mockToastr.success.mock.calls[0][2]
calledOptions.onHidden()
expect(eventHandler).toHaveBeenCalled()
window.removeEventListener('flasher:toastr:hidden', eventHandler)
})
it('should call original callbacks if provided', () => {
const originalOnClick = vi.fn()
plugin.renderEnvelopes([createEnvelope({
options: { onclick: originalOnClick },
})])
const calledOptions = mockToastr.success.mock.calls[0][2]
calledOptions.onclick()
expect(originalOnClick).toHaveBeenCalled()
})
})
})