mirror of
https://github.com/php-flasher/php-flasher.git
synced 2026-03-31 15:07:47 +01:00
7d6e9b46b8
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).
312 lines
11 KiB
TypeScript
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()
|
|
})
|
|
})
|
|
})
|