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).
323 lines
11 KiB
TypeScript
323 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 { mockShow, mockNotyInstance, MockNoty } = vi.hoisted(() => {
|
|
const mockShow = vi.fn()
|
|
const mockNotyInstance = {
|
|
show: mockShow,
|
|
layoutDom: { dataset: {} as DOMStringMap },
|
|
}
|
|
const MockNoty = Object.assign(
|
|
vi.fn().mockImplementation(() => mockNotyInstance),
|
|
{ overrideDefaults: vi.fn() },
|
|
)
|
|
return { mockShow, mockNotyInstance, MockNoty }
|
|
})
|
|
|
|
vi.mock('noty', () => ({
|
|
default: MockNoty,
|
|
}))
|
|
|
|
// Import after mocks
|
|
import NotyPlugin from '@flasher/flasher-noty/noty'
|
|
|
|
const createEnvelope = (overrides: Partial<Envelope> = {}): Envelope => ({
|
|
type: 'success',
|
|
message: 'Test message',
|
|
title: 'Test title',
|
|
options: {},
|
|
metadata: { plugin: 'noty' },
|
|
...overrides,
|
|
})
|
|
|
|
describe('NotyPlugin', () => {
|
|
let plugin: NotyPlugin
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
// Restore mock implementations after clearing
|
|
MockNoty.mockImplementation(() => mockNotyInstance)
|
|
mockNotyInstance.layoutDom = { dataset: {} as DOMStringMap }
|
|
plugin = new NotyPlugin()
|
|
})
|
|
|
|
describe('renderEnvelopes', () => {
|
|
it('should do nothing with empty envelopes', () => {
|
|
plugin.renderEnvelopes([])
|
|
|
|
expect(MockNoty).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(MockNoty).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should create Noty instance with envelope data', () => {
|
|
plugin.renderEnvelopes([createEnvelope({
|
|
type: 'success',
|
|
message: 'Hello World',
|
|
})])
|
|
|
|
expect(MockNoty).toHaveBeenCalledWith(expect.objectContaining({
|
|
text: 'Hello World',
|
|
type: 'success',
|
|
}))
|
|
})
|
|
|
|
it('should call show() on Noty instance', () => {
|
|
plugin.renderEnvelopes([createEnvelope()])
|
|
|
|
expect(mockShow).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should include default timeout option', () => {
|
|
plugin.renderEnvelopes([createEnvelope()])
|
|
|
|
expect(MockNoty).toHaveBeenCalledWith(expect.objectContaining({
|
|
timeout: 10000,
|
|
}))
|
|
})
|
|
|
|
it('should merge envelope options', () => {
|
|
plugin.renderEnvelopes([createEnvelope({
|
|
options: { timeout: 5000, layout: 'topRight' },
|
|
})])
|
|
|
|
expect(MockNoty).toHaveBeenCalledWith(expect.objectContaining({
|
|
timeout: 5000,
|
|
layout: 'topRight',
|
|
}))
|
|
})
|
|
|
|
it('should render multiple envelopes', () => {
|
|
plugin.renderEnvelopes([
|
|
createEnvelope({ message: 'First' }),
|
|
createEnvelope({ message: 'Second' }),
|
|
])
|
|
|
|
expect(MockNoty).toHaveBeenCalledTimes(2)
|
|
expect(mockShow).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
it('should set Turbo compatibility on layoutDom', () => {
|
|
plugin.renderEnvelopes([createEnvelope()])
|
|
|
|
expect(mockNotyInstance.layoutDom.dataset.turboTemporary).toBe('')
|
|
})
|
|
|
|
it('should handle missing layoutDom gracefully', () => {
|
|
MockNoty.mockImplementationOnce(() => ({
|
|
show: mockShow,
|
|
layoutDom: null,
|
|
}))
|
|
|
|
// Should not throw
|
|
expect(() => plugin.renderEnvelopes([createEnvelope()])).not.toThrow()
|
|
})
|
|
|
|
it('should handle errors gracefully', () => {
|
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
MockNoty.mockImplementationOnce(() => {
|
|
throw new Error('Noty error')
|
|
})
|
|
|
|
plugin.renderEnvelopes([createEnvelope()])
|
|
|
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining('Error rendering notification'),
|
|
expect.any(Error),
|
|
expect.any(Object),
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('renderOptions', () => {
|
|
it('should do nothing with null/undefined options', () => {
|
|
plugin.renderOptions(null as unknown as Record<string, unknown>)
|
|
plugin.renderOptions(undefined as unknown as Record<string, unknown>)
|
|
|
|
expect(MockNoty.overrideDefaults).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should call Noty.overrideDefaults with merged options', () => {
|
|
plugin.renderOptions({ timeout: 8000, layout: 'bottomLeft' })
|
|
|
|
expect(MockNoty.overrideDefaults).toHaveBeenCalledWith(expect.objectContaining({
|
|
timeout: 8000,
|
|
layout: 'bottomLeft',
|
|
}))
|
|
})
|
|
|
|
it('should preserve existing default options', () => {
|
|
plugin.renderOptions({ layout: 'topLeft' })
|
|
plugin.renderOptions({ theme: 'mint' })
|
|
|
|
// Get the last call arguments
|
|
const lastCall = MockNoty.overrideDefaults.mock.calls[MockNoty.overrideDefaults.mock.calls.length - 1][0]
|
|
|
|
expect(lastCall).toMatchObject({
|
|
timeout: 10000, // default
|
|
layout: 'topLeft', // from first call
|
|
theme: 'mint', // from second call
|
|
})
|
|
})
|
|
|
|
it('should use options in subsequent renderEnvelopes', () => {
|
|
plugin.renderOptions({ animation: { open: 'fadeIn', close: 'fadeOut' } })
|
|
plugin.renderEnvelopes([createEnvelope()])
|
|
|
|
expect(MockNoty).toHaveBeenCalledWith(expect.objectContaining({
|
|
animation: { open: 'fadeIn', close: 'fadeOut' },
|
|
}))
|
|
})
|
|
})
|
|
|
|
describe('convenience methods (inherited from AbstractPlugin)', () => {
|
|
it('success() should create success notification', () => {
|
|
plugin.success('Success message')
|
|
|
|
expect(MockNoty).toHaveBeenCalledWith(expect.objectContaining({
|
|
type: 'success',
|
|
}))
|
|
})
|
|
|
|
it('error() should create error notification', () => {
|
|
plugin.error('Error message')
|
|
|
|
expect(MockNoty).toHaveBeenCalledWith(expect.objectContaining({
|
|
type: 'error',
|
|
}))
|
|
})
|
|
|
|
it('info() should create info notification', () => {
|
|
plugin.info('Info message')
|
|
|
|
expect(MockNoty).toHaveBeenCalledWith(expect.objectContaining({
|
|
type: 'info',
|
|
}))
|
|
})
|
|
|
|
it('warning() should create warning notification', () => {
|
|
plugin.warning('Warning message')
|
|
|
|
expect(MockNoty).toHaveBeenCalledWith(expect.objectContaining({
|
|
type: 'warning',
|
|
}))
|
|
})
|
|
})
|
|
|
|
describe('notification types', () => {
|
|
it('should support all standard Noty types', () => {
|
|
const types = ['alert', 'success', 'error', 'warning', 'info']
|
|
|
|
types.forEach((type) => {
|
|
MockNoty.mockClear()
|
|
plugin.renderEnvelopes([createEnvelope({ type })])
|
|
|
|
expect(MockNoty).toHaveBeenCalledWith(expect.objectContaining({
|
|
type,
|
|
}))
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('event dispatching', () => {
|
|
it('should set up event callbacks that dispatch custom events', () => {
|
|
plugin.renderEnvelopes([createEnvelope()])
|
|
|
|
// Verify callbacks are set up in options
|
|
const calledOptions = MockNoty.mock.calls[0][0]
|
|
expect(calledOptions.callbacks).toBeDefined()
|
|
expect(calledOptions.callbacks.onShow).toBeInstanceOf(Function)
|
|
expect(calledOptions.callbacks.onClick).toBeInstanceOf(Function)
|
|
expect(calledOptions.callbacks.onClose).toBeInstanceOf(Function)
|
|
expect(calledOptions.callbacks.onHover).toBeInstanceOf(Function)
|
|
})
|
|
|
|
it('should dispatch flasher:noty:show event when onShow callback is called', () => {
|
|
const eventHandler = vi.fn()
|
|
window.addEventListener('flasher:noty:show', eventHandler)
|
|
|
|
plugin.renderEnvelopes([createEnvelope({ message: 'Test' })])
|
|
|
|
const calledOptions = MockNoty.mock.calls[0][0]
|
|
calledOptions.callbacks.onShow()
|
|
|
|
expect(eventHandler).toHaveBeenCalledWith(expect.objectContaining({
|
|
detail: expect.objectContaining({
|
|
envelope: expect.objectContaining({ message: 'Test' }),
|
|
}),
|
|
}))
|
|
|
|
window.removeEventListener('flasher:noty:show', eventHandler)
|
|
})
|
|
|
|
it('should dispatch flasher:noty:click event when onClick callback is called', () => {
|
|
const eventHandler = vi.fn()
|
|
window.addEventListener('flasher:noty:click', eventHandler)
|
|
|
|
plugin.renderEnvelopes([createEnvelope({ message: 'Click test' })])
|
|
|
|
const calledOptions = MockNoty.mock.calls[0][0]
|
|
calledOptions.callbacks.onClick()
|
|
|
|
expect(eventHandler).toHaveBeenCalledWith(expect.objectContaining({
|
|
detail: expect.objectContaining({
|
|
envelope: expect.objectContaining({ message: 'Click test' }),
|
|
}),
|
|
}))
|
|
|
|
window.removeEventListener('flasher:noty:click', eventHandler)
|
|
})
|
|
|
|
it('should dispatch flasher:noty:close event when onClose callback is called', () => {
|
|
const eventHandler = vi.fn()
|
|
window.addEventListener('flasher:noty:close', eventHandler)
|
|
|
|
plugin.renderEnvelopes([createEnvelope()])
|
|
|
|
const calledOptions = MockNoty.mock.calls[0][0]
|
|
calledOptions.callbacks.onClose()
|
|
|
|
expect(eventHandler).toHaveBeenCalled()
|
|
|
|
window.removeEventListener('flasher:noty:close', eventHandler)
|
|
})
|
|
|
|
it('should dispatch flasher:noty:hover event when onHover callback is called', () => {
|
|
const eventHandler = vi.fn()
|
|
window.addEventListener('flasher:noty:hover', eventHandler)
|
|
|
|
plugin.renderEnvelopes([createEnvelope()])
|
|
|
|
const calledOptions = MockNoty.mock.calls[0][0]
|
|
calledOptions.callbacks.onHover()
|
|
|
|
expect(eventHandler).toHaveBeenCalled()
|
|
|
|
window.removeEventListener('flasher:noty:hover', eventHandler)
|
|
})
|
|
|
|
it('should call original callbacks if provided', () => {
|
|
const originalOnClick = vi.fn()
|
|
|
|
plugin.renderEnvelopes([createEnvelope({
|
|
options: {
|
|
callbacks: {
|
|
onClick: originalOnClick,
|
|
},
|
|
},
|
|
})])
|
|
|
|
const calledOptions = MockNoty.mock.calls[0][0]
|
|
calledOptions.callbacks.onClick()
|
|
|
|
expect(originalOnClick).toHaveBeenCalled()
|
|
})
|
|
})
|
|
})
|