add vitest for JS/TS testing with comprehensive test coverage

This commit is contained in:
Younes ENNAJI
2026-02-25 15:52:21 +00:00
parent 62848e0fd1
commit d33de77835
21 changed files with 4641 additions and 67 deletions
+221
View File
@@ -0,0 +1,221 @@
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',
{ 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()
})
})
})