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, } 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 => ({ 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() }) }) })