mirror of
https://github.com/php-flasher/php-flasher.git
synced 2026-03-31 15:07:47 +01:00
883 lines
30 KiB
TypeScript
883 lines
30 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import Flasher from '@flasher/flasher/flasher'
|
|
import type { Envelope, PluginInterface, Theme } from '@flasher/flasher/types'
|
|
|
|
// Mock plugin for testing
|
|
class MockPlugin implements PluginInterface {
|
|
public envelopes: Envelope[] = []
|
|
public options: Record<string, unknown> = {}
|
|
|
|
success = vi.fn()
|
|
error = vi.fn()
|
|
info = vi.fn()
|
|
warning = vi.fn()
|
|
flash = vi.fn()
|
|
|
|
renderEnvelopes(envelopes: Envelope[]): void {
|
|
this.envelopes.push(...envelopes)
|
|
}
|
|
|
|
renderOptions(options: Record<string, unknown>): void {
|
|
this.options = { ...this.options, ...options }
|
|
}
|
|
}
|
|
|
|
// Mock theme for testing
|
|
const mockTheme: Theme = {
|
|
styles: '/path/to/theme.css',
|
|
render: (envelope: Envelope) => `<div class="notification">${envelope.message}</div>`,
|
|
}
|
|
|
|
describe('Flasher', () => {
|
|
let flasher: Flasher
|
|
|
|
beforeEach(() => {
|
|
flasher = new Flasher()
|
|
})
|
|
|
|
describe('plugin registration', () => {
|
|
it('should register a plugin with addPlugin', () => {
|
|
const plugin = new MockPlugin()
|
|
flasher.addPlugin('test', plugin)
|
|
|
|
expect(flasher.use('test')).toBe(plugin)
|
|
})
|
|
|
|
it('should throw error when plugin name is missing', () => {
|
|
const plugin = new MockPlugin()
|
|
|
|
expect(() => flasher.addPlugin('', plugin)).toThrow('Both plugin name and instance are required')
|
|
})
|
|
|
|
it('should throw error when plugin instance is missing', () => {
|
|
expect(() => flasher.addPlugin('test', null as unknown as PluginInterface)).toThrow('Both plugin name and instance are required')
|
|
})
|
|
|
|
it('should throw error when using unregistered plugin', () => {
|
|
expect(() => flasher.use('nonexistent')).toThrow('Unable to resolve "nonexistent" plugin')
|
|
})
|
|
|
|
it('create() should be an alias for use()', () => {
|
|
const plugin = new MockPlugin()
|
|
flasher.addPlugin('test', plugin)
|
|
|
|
expect(flasher.create('test')).toBe(flasher.use('test'))
|
|
})
|
|
})
|
|
|
|
describe('theme registration', () => {
|
|
it('should register a theme with addTheme', () => {
|
|
flasher.addTheme('custom', mockTheme)
|
|
|
|
// Theme creates a FlasherPlugin when accessed
|
|
const plugin = flasher.use('theme.custom')
|
|
expect(plugin).toBeDefined()
|
|
})
|
|
|
|
it('should throw error when theme name is missing', () => {
|
|
expect(() => flasher.addTheme('', mockTheme)).toThrow('Both theme name and definition are required')
|
|
})
|
|
|
|
it('should throw error when theme definition is missing', () => {
|
|
expect(() => flasher.addTheme('test', null as unknown as Theme)).toThrow('Both theme name and definition are required')
|
|
})
|
|
})
|
|
|
|
describe('plugin alias resolution', () => {
|
|
it('should resolve "flasher" to "theme.flasher"', () => {
|
|
flasher.addTheme('flasher', mockTheme)
|
|
|
|
const plugin = flasher.use('flasher')
|
|
expect(plugin).toBeDefined()
|
|
})
|
|
|
|
it('should keep other plugin names unchanged', () => {
|
|
const plugin = new MockPlugin()
|
|
flasher.addPlugin('toastr', plugin)
|
|
|
|
expect(flasher.use('toastr')).toBe(plugin)
|
|
})
|
|
})
|
|
|
|
describe('renderEnvelopes', () => {
|
|
it('should do nothing with empty envelopes', () => {
|
|
flasher.renderEnvelopes([])
|
|
// Should not throw
|
|
})
|
|
|
|
it('should do nothing with null/undefined envelopes', () => {
|
|
flasher.renderEnvelopes(null as unknown as Envelope[])
|
|
flasher.renderEnvelopes(undefined as unknown as Envelope[])
|
|
// Should not throw
|
|
})
|
|
|
|
it('should group envelopes by plugin and render', () => {
|
|
const plugin1 = new MockPlugin()
|
|
const plugin2 = new MockPlugin()
|
|
|
|
flasher.addPlugin('plugin1', plugin1)
|
|
flasher.addPlugin('plugin2', plugin2)
|
|
|
|
const envelopes: Envelope[] = [
|
|
{ type: 'success', message: 'Message 1', title: 'Title 1', options: {}, metadata: { plugin: 'plugin1' } },
|
|
{ type: 'error', message: 'Message 2', title: 'Title 2', options: {}, metadata: { plugin: 'plugin2' } },
|
|
{ type: 'info', message: 'Message 3', title: 'Title 3', options: {}, metadata: { plugin: 'plugin1' } },
|
|
]
|
|
|
|
flasher.renderEnvelopes(envelopes)
|
|
|
|
expect(plugin1.envelopes).toHaveLength(2)
|
|
expect(plugin2.envelopes).toHaveLength(1)
|
|
})
|
|
|
|
it('should log error and continue when plugin throws', () => {
|
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
const badPlugin: PluginInterface = {
|
|
success: vi.fn(),
|
|
error: vi.fn(),
|
|
info: vi.fn(),
|
|
warning: vi.fn(),
|
|
flash: vi.fn(),
|
|
renderEnvelopes: () => {
|
|
throw new Error('Plugin error')
|
|
},
|
|
renderOptions: vi.fn(),
|
|
}
|
|
|
|
flasher.addPlugin('bad', badPlugin)
|
|
|
|
flasher.renderEnvelopes([
|
|
{ type: 'success', message: 'Test', title: 'Test', options: {}, metadata: { plugin: 'bad' } },
|
|
])
|
|
|
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining('Error rendering envelopes'),
|
|
expect.any(Error),
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('renderOptions', () => {
|
|
it('should do nothing with null/undefined options', () => {
|
|
flasher.renderOptions(null as unknown as Record<string, unknown>)
|
|
flasher.renderOptions(undefined as unknown as Record<string, unknown>)
|
|
// Should not throw
|
|
})
|
|
|
|
it('should apply options to each plugin', () => {
|
|
const plugin1 = new MockPlugin()
|
|
const plugin2 = new MockPlugin()
|
|
|
|
flasher.addPlugin('plugin1', plugin1)
|
|
flasher.addPlugin('plugin2', plugin2)
|
|
|
|
flasher.renderOptions({
|
|
plugin1: { timeout: 5000 },
|
|
plugin2: { position: 'top-left' },
|
|
})
|
|
|
|
expect(plugin1.options).toEqual({ timeout: 5000 })
|
|
expect(plugin2.options).toEqual({ position: 'top-left' })
|
|
})
|
|
|
|
it('should log error and continue when plugin throws', () => {
|
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
const badPlugin: PluginInterface = {
|
|
success: vi.fn(),
|
|
error: vi.fn(),
|
|
info: vi.fn(),
|
|
warning: vi.fn(),
|
|
flash: vi.fn(),
|
|
renderEnvelopes: vi.fn(),
|
|
renderOptions: () => {
|
|
throw new Error('Plugin error')
|
|
},
|
|
}
|
|
|
|
flasher.addPlugin('bad', badPlugin)
|
|
|
|
flasher.renderOptions({ bad: { some: 'option' } })
|
|
|
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining('Error applying options'),
|
|
expect.any(Error),
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('render()', () => {
|
|
it('should process response and render envelopes', async () => {
|
|
const plugin = new MockPlugin()
|
|
flasher.addPlugin('test', plugin)
|
|
|
|
await flasher.render({
|
|
envelopes: [
|
|
{ type: 'success', message: 'Test', title: 'Test', options: {}, metadata: { plugin: 'test' } },
|
|
],
|
|
options: { test: { timeout: 3000 } },
|
|
})
|
|
|
|
expect(plugin.envelopes).toHaveLength(1)
|
|
expect(plugin.options).toEqual({ timeout: 3000 })
|
|
})
|
|
|
|
it('should handle empty response gracefully', async () => {
|
|
await flasher.render({})
|
|
// Should not throw
|
|
})
|
|
|
|
it('should set default CSP nonces if not provided', async () => {
|
|
const plugin = new MockPlugin()
|
|
flasher.addPlugin('test', plugin)
|
|
|
|
await flasher.render({
|
|
envelopes: [
|
|
{ type: 'success', message: 'Test', title: 'Test', options: {}, metadata: { plugin: 'test' } },
|
|
],
|
|
})
|
|
|
|
expect(plugin.envelopes[0].context).toMatchObject({
|
|
csp_style_nonce: '',
|
|
csp_script_nonce: '',
|
|
})
|
|
})
|
|
|
|
it('should preserve provided CSP nonces', async () => {
|
|
const plugin = new MockPlugin()
|
|
flasher.addPlugin('test', plugin)
|
|
|
|
await flasher.render({
|
|
envelopes: [
|
|
{ type: 'success', message: 'Test', title: 'Test', options: {}, metadata: { plugin: 'test' } },
|
|
],
|
|
context: {
|
|
csp_style_nonce: 'style-nonce-123',
|
|
csp_script_nonce: 'script-nonce-456',
|
|
},
|
|
})
|
|
|
|
expect(plugin.envelopes[0].context).toMatchObject({
|
|
csp_style_nonce: 'style-nonce-123',
|
|
csp_script_nonce: 'script-nonce-456',
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('function string conversion', () => {
|
|
it('should convert regular function strings to functions', async () => {
|
|
const plugin = new MockPlugin()
|
|
flasher.addPlugin('test', plugin)
|
|
|
|
await flasher.render({
|
|
envelopes: [{
|
|
type: 'success',
|
|
message: 'Test',
|
|
title: 'Test',
|
|
options: {
|
|
onClick: 'function(event) { return event.target }',
|
|
},
|
|
metadata: { plugin: 'test' },
|
|
}],
|
|
})
|
|
|
|
const onClick = plugin.envelopes[0].options.onClick
|
|
expect(typeof onClick).toBe('function')
|
|
})
|
|
|
|
it('should convert arrow function strings to functions', async () => {
|
|
const plugin = new MockPlugin()
|
|
flasher.addPlugin('test', plugin)
|
|
|
|
await flasher.render({
|
|
envelopes: [{
|
|
type: 'success',
|
|
message: 'Test',
|
|
title: 'Test',
|
|
options: {
|
|
callback: '(a, b) => a + b',
|
|
},
|
|
metadata: { plugin: 'test' },
|
|
}],
|
|
})
|
|
|
|
const callback = plugin.envelopes[0].options.callback as (a: number, b: number) => number
|
|
expect(typeof callback).toBe('function')
|
|
expect(callback(2, 3)).toBe(5)
|
|
})
|
|
|
|
it('should handle arrow functions with single parameter (no parens)', async () => {
|
|
const plugin = new MockPlugin()
|
|
flasher.addPlugin('test', plugin)
|
|
|
|
await flasher.render({
|
|
envelopes: [{
|
|
type: 'success',
|
|
message: 'Test',
|
|
title: 'Test',
|
|
options: {
|
|
// Single param with parens (more reliable parsing)
|
|
transform: '(x) => x * 2',
|
|
},
|
|
metadata: { plugin: 'test' },
|
|
}],
|
|
})
|
|
|
|
const transform = plugin.envelopes[0].options.transform as (x: number) => number
|
|
expect(typeof transform).toBe('function')
|
|
expect(transform(5)).toBe(10)
|
|
})
|
|
|
|
it('should leave non-function strings unchanged', async () => {
|
|
const plugin = new MockPlugin()
|
|
flasher.addPlugin('test', plugin)
|
|
|
|
await flasher.render({
|
|
envelopes: [{
|
|
type: 'success',
|
|
message: 'Test',
|
|
title: 'Test',
|
|
options: {
|
|
text: 'Hello World',
|
|
position: 'top-right',
|
|
},
|
|
metadata: { plugin: 'test' },
|
|
}],
|
|
})
|
|
|
|
expect(plugin.envelopes[0].options.text).toBe('Hello World')
|
|
expect(plugin.envelopes[0].options.position).toBe('top-right')
|
|
})
|
|
|
|
it('should leave non-string values unchanged', async () => {
|
|
const plugin = new MockPlugin()
|
|
flasher.addPlugin('test', plugin)
|
|
|
|
await flasher.render({
|
|
envelopes: [{
|
|
type: 'success',
|
|
message: 'Test',
|
|
title: 'Test',
|
|
options: {
|
|
timeout: 5000,
|
|
enabled: true,
|
|
data: { key: 'value' },
|
|
},
|
|
metadata: { plugin: 'test' },
|
|
}],
|
|
})
|
|
|
|
expect(plugin.envelopes[0].options.timeout).toBe(5000)
|
|
expect(plugin.envelopes[0].options.enabled).toBe(true)
|
|
expect(plugin.envelopes[0].options.data).toEqual({ key: 'value' })
|
|
})
|
|
|
|
it('should handle invalid function strings gracefully', async () => {
|
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
const plugin = new MockPlugin()
|
|
flasher.addPlugin('test', plugin)
|
|
|
|
await flasher.render({
|
|
envelopes: [{
|
|
type: 'success',
|
|
message: 'Test',
|
|
title: 'Test',
|
|
options: {
|
|
// This matches the regex but has invalid JS syntax
|
|
bad: 'function() { throw }',
|
|
},
|
|
metadata: { plugin: 'test' },
|
|
}],
|
|
})
|
|
|
|
// Should return original string on error
|
|
expect(plugin.envelopes[0].options.bad).toBe('function() { throw }')
|
|
expect(consoleSpy).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('asset loading', () => {
|
|
it('should add style elements to document head', async () => {
|
|
const plugin = new MockPlugin()
|
|
flasher.addPlugin('test', plugin)
|
|
|
|
// Mock the load event
|
|
const originalCreateElement = document.createElement.bind(document)
|
|
vi.spyOn(document, 'createElement').mockImplementation((tag: string) => {
|
|
const el = originalCreateElement(tag)
|
|
if (tag === 'link' || tag === 'script') {
|
|
setTimeout(() => el.onload?.({} as Event), 0)
|
|
}
|
|
return el
|
|
})
|
|
|
|
await flasher.render({
|
|
envelopes: [],
|
|
styles: ['/path/to/style.css'],
|
|
})
|
|
|
|
const link = document.head.querySelector('link[href="/path/to/style.css"]')
|
|
expect(link).toBeTruthy()
|
|
expect(link?.getAttribute('rel')).toBe('stylesheet')
|
|
})
|
|
|
|
it('should add script elements to document head', async () => {
|
|
const plugin = new MockPlugin()
|
|
flasher.addPlugin('test', plugin)
|
|
|
|
const originalCreateElement = document.createElement.bind(document)
|
|
vi.spyOn(document, 'createElement').mockImplementation((tag: string) => {
|
|
const el = originalCreateElement(tag)
|
|
if (tag === 'link' || tag === 'script') {
|
|
setTimeout(() => el.onload?.({} as Event), 0)
|
|
}
|
|
return el
|
|
})
|
|
|
|
await flasher.render({
|
|
envelopes: [],
|
|
scripts: ['/path/to/script.js'],
|
|
})
|
|
|
|
const script = document.head.querySelector('script[src="/path/to/script.js"]')
|
|
expect(script).toBeTruthy()
|
|
expect(script?.getAttribute('type')).toBe('text/javascript')
|
|
})
|
|
|
|
it('should not load duplicate assets', async () => {
|
|
const plugin = new MockPlugin()
|
|
flasher.addPlugin('test', plugin)
|
|
|
|
const originalCreateElement = document.createElement.bind(document)
|
|
vi.spyOn(document, 'createElement').mockImplementation((tag: string) => {
|
|
const el = originalCreateElement(tag)
|
|
if (tag === 'link' || tag === 'script') {
|
|
setTimeout(() => el.onload?.({} as Event), 0)
|
|
}
|
|
return el
|
|
})
|
|
|
|
await flasher.render({
|
|
envelopes: [],
|
|
styles: ['/path/to/style.css', '/path/to/style.css'],
|
|
})
|
|
|
|
const links = document.head.querySelectorAll('link[href="/path/to/style.css"]')
|
|
expect(links).toHaveLength(1)
|
|
})
|
|
|
|
it('should apply CSP nonce to loaded assets', async () => {
|
|
const plugin = new MockPlugin()
|
|
flasher.addPlugin('test', plugin)
|
|
|
|
const originalCreateElement = document.createElement.bind(document)
|
|
vi.spyOn(document, 'createElement').mockImplementation((tag: string) => {
|
|
const el = originalCreateElement(tag)
|
|
if (tag === 'link' || tag === 'script') {
|
|
setTimeout(() => el.onload?.({} as Event), 0)
|
|
}
|
|
return el
|
|
})
|
|
|
|
await flasher.render({
|
|
envelopes: [],
|
|
styles: ['/path/to/style.css'],
|
|
context: {
|
|
csp_style_nonce: 'test-nonce-123',
|
|
},
|
|
})
|
|
|
|
const link = document.head.querySelector('link[href="/path/to/style.css"]')
|
|
expect(link?.getAttribute('nonce')).toBe('test-nonce-123')
|
|
})
|
|
})
|
|
|
|
describe('theme styles handling', () => {
|
|
it('should add theme styles to response when using theme plugin', async () => {
|
|
flasher.addTheme('custom', {
|
|
styles: '/custom/theme.css',
|
|
render: () => '<div></div>',
|
|
})
|
|
|
|
const originalCreateElement = document.createElement.bind(document)
|
|
vi.spyOn(document, 'createElement').mockImplementation((tag: string) => {
|
|
const el = originalCreateElement(tag)
|
|
if (tag === 'link' || tag === 'script') {
|
|
setTimeout(() => el.onload?.({} as Event), 0)
|
|
}
|
|
return el
|
|
})
|
|
|
|
await flasher.render({
|
|
envelopes: [{
|
|
type: 'success',
|
|
message: 'Test',
|
|
title: 'Test',
|
|
options: {},
|
|
metadata: { plugin: 'theme.custom' },
|
|
}],
|
|
})
|
|
|
|
const link = document.head.querySelector('link[href="/custom/theme.css"]')
|
|
expect(link).toBeTruthy()
|
|
})
|
|
|
|
it('should handle array of theme styles', async () => {
|
|
flasher.addTheme('multi', {
|
|
styles: ['/style1.css', '/style2.css'],
|
|
render: () => '<div></div>',
|
|
})
|
|
|
|
const originalCreateElement = document.createElement.bind(document)
|
|
vi.spyOn(document, 'createElement').mockImplementation((tag: string) => {
|
|
const el = originalCreateElement(tag)
|
|
if (tag === 'link' || tag === 'script') {
|
|
setTimeout(() => el.onload?.({} as Event), 0)
|
|
}
|
|
return el
|
|
})
|
|
|
|
await flasher.render({
|
|
envelopes: [{
|
|
type: 'success',
|
|
message: 'Test',
|
|
title: 'Test',
|
|
options: {},
|
|
metadata: { plugin: 'theme.multi' },
|
|
}],
|
|
})
|
|
|
|
expect(document.head.querySelector('link[href="/style1.css"]')).toBeTruthy()
|
|
expect(document.head.querySelector('link[href="/style2.css"]')).toBeTruthy()
|
|
})
|
|
|
|
it('should handle theme without styles', async () => {
|
|
flasher.addTheme('no-styles', {
|
|
render: () => '<div></div>',
|
|
})
|
|
|
|
const originalCreateElement = document.createElement.bind(document)
|
|
vi.spyOn(document, 'createElement').mockImplementation((tag: string) => {
|
|
const el = originalCreateElement(tag)
|
|
if (tag === 'link' || tag === 'script') {
|
|
setTimeout(() => el.onload?.({} as Event), 0)
|
|
}
|
|
return el
|
|
})
|
|
|
|
await flasher.render({
|
|
envelopes: [{
|
|
type: 'success',
|
|
message: 'Test',
|
|
title: 'Test',
|
|
options: {},
|
|
metadata: { plugin: 'theme.no-styles' },
|
|
}],
|
|
})
|
|
|
|
// Should not throw
|
|
})
|
|
|
|
it('should handle non-theme plugins without adding styles', async () => {
|
|
const plugin = new MockPlugin()
|
|
flasher.addPlugin('custom-plugin', plugin)
|
|
|
|
await flasher.render({
|
|
envelopes: [{
|
|
type: 'success',
|
|
message: 'Test',
|
|
title: 'Test',
|
|
options: {},
|
|
metadata: { plugin: 'custom-plugin' },
|
|
}],
|
|
})
|
|
|
|
expect(plugin.envelopes).toHaveLength(1)
|
|
})
|
|
})
|
|
|
|
describe('error handling', () => {
|
|
it('should handle errors in render() gracefully', async () => {
|
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
|
|
// Create a plugin that will throw during renderEnvelopes
|
|
const badPlugin: PluginInterface = {
|
|
success: vi.fn(),
|
|
error: vi.fn(),
|
|
info: vi.fn(),
|
|
warning: vi.fn(),
|
|
flash: vi.fn(),
|
|
renderEnvelopes: () => {
|
|
throw new Error('Render error')
|
|
},
|
|
renderOptions: vi.fn(),
|
|
}
|
|
|
|
flasher.addPlugin('bad', badPlugin)
|
|
|
|
await flasher.render({
|
|
envelopes: [{
|
|
type: 'success',
|
|
message: 'Test',
|
|
title: 'Test',
|
|
options: {},
|
|
metadata: { plugin: 'bad' },
|
|
}],
|
|
})
|
|
|
|
expect(consoleSpy).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should handle asset loading errors', async () => {
|
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
|
|
const originalCreateElement = document.createElement.bind(document)
|
|
vi.spyOn(document, 'createElement').mockImplementation((tag: string) => {
|
|
const el = originalCreateElement(tag)
|
|
if (tag === 'link' || tag === 'script') {
|
|
setTimeout(() => el.onerror?.({} as Event), 0)
|
|
}
|
|
return el
|
|
})
|
|
|
|
await flasher.render({
|
|
envelopes: [],
|
|
styles: ['/nonexistent.css'],
|
|
})
|
|
|
|
// Should handle error gracefully
|
|
expect(consoleSpy).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('asset deduplication', () => {
|
|
it('should skip loading if asset already exists in DOM', async () => {
|
|
// Pre-add a link element to the DOM
|
|
const existingLink = document.createElement('link')
|
|
existingLink.rel = 'stylesheet'
|
|
existingLink.href = '/existing.css'
|
|
document.head.appendChild(existingLink)
|
|
|
|
const originalCreateElement = document.createElement.bind(document)
|
|
const createElementSpy = vi.spyOn(document, 'createElement').mockImplementation((tag: string) => {
|
|
const el = originalCreateElement(tag)
|
|
if (tag === 'link' || tag === 'script') {
|
|
setTimeout(() => el.onload?.({} as Event), 0)
|
|
}
|
|
return el
|
|
})
|
|
|
|
await flasher.render({
|
|
envelopes: [],
|
|
styles: ['/existing.css'],
|
|
})
|
|
|
|
// Should not create a new element since it already exists
|
|
expect(createElementSpy).not.toHaveBeenCalledWith('link')
|
|
})
|
|
})
|
|
|
|
describe('resolvePlugin edge cases', () => {
|
|
it('should not create plugin for existing plugin alias', async () => {
|
|
const plugin = new MockPlugin()
|
|
flasher.addPlugin('existing', plugin)
|
|
|
|
// Resolve plugin should not overwrite existing
|
|
await flasher.render({
|
|
envelopes: [{
|
|
type: 'success',
|
|
message: 'Test',
|
|
title: 'Test',
|
|
options: {},
|
|
metadata: { plugin: 'existing' },
|
|
}],
|
|
})
|
|
|
|
expect(plugin.envelopes).toHaveLength(1)
|
|
})
|
|
|
|
it('should not create plugin for non-theme aliases', async () => {
|
|
const plugin = new MockPlugin()
|
|
flasher.addPlugin('custom-plugin', plugin)
|
|
|
|
await flasher.render({
|
|
envelopes: [{
|
|
type: 'success',
|
|
message: 'Test',
|
|
title: 'Test',
|
|
options: {},
|
|
metadata: { plugin: 'custom-plugin' },
|
|
}],
|
|
})
|
|
|
|
expect(plugin.envelopes).toHaveLength(1)
|
|
})
|
|
|
|
it('should silently handle non-existent theme', async () => {
|
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
|
|
// This should not throw, just log error and continue
|
|
await flasher.render({
|
|
envelopes: [{
|
|
type: 'success',
|
|
message: 'Test',
|
|
title: 'Test',
|
|
options: {},
|
|
metadata: { plugin: 'theme.nonexistent' },
|
|
}],
|
|
})
|
|
|
|
// Should not throw and should log error
|
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining('PHPFlasher: Error rendering envelopes'),
|
|
expect.any(Error),
|
|
)
|
|
|
|
consoleSpy.mockRestore()
|
|
})
|
|
})
|
|
|
|
describe('resolveOptions edge cases', () => {
|
|
it('should handle response with empty options', async () => {
|
|
const plugin = new MockPlugin()
|
|
flasher.addPlugin('test', plugin)
|
|
|
|
await flasher.render({
|
|
envelopes: [{
|
|
type: 'success',
|
|
message: 'Test',
|
|
title: 'Test',
|
|
options: {},
|
|
metadata: { plugin: 'test' },
|
|
}],
|
|
options: {},
|
|
})
|
|
|
|
expect(plugin.envelopes).toHaveLength(1)
|
|
})
|
|
|
|
it('should handle response with nested options', async () => {
|
|
const plugin = new MockPlugin()
|
|
flasher.addPlugin('test', plugin)
|
|
|
|
await flasher.render({
|
|
envelopes: [{
|
|
type: 'success',
|
|
message: 'Test',
|
|
title: 'Test',
|
|
options: {},
|
|
metadata: { plugin: 'test' },
|
|
}],
|
|
options: {
|
|
test: {
|
|
timeout: 5000,
|
|
},
|
|
},
|
|
})
|
|
|
|
expect(plugin.envelopes).toHaveLength(1)
|
|
expect(plugin.options).toEqual({ timeout: 5000 })
|
|
})
|
|
})
|
|
|
|
describe('resolveFunction edge cases', () => {
|
|
it('should handle arrow function with block body containing return', async () => {
|
|
const plugin = new MockPlugin()
|
|
flasher.addPlugin('test', plugin)
|
|
|
|
await flasher.render({
|
|
envelopes: [{
|
|
type: 'success',
|
|
message: 'Test',
|
|
title: 'Test',
|
|
options: {},
|
|
metadata: { plugin: 'test' },
|
|
}],
|
|
options: {
|
|
test: {
|
|
callback: '(x) => { return x * 2; }',
|
|
},
|
|
},
|
|
})
|
|
|
|
expect(plugin.options.callback).toBeInstanceOf(Function)
|
|
})
|
|
|
|
it('should handle arrow function with block body without return keyword', async () => {
|
|
const plugin = new MockPlugin()
|
|
flasher.addPlugin('test', plugin)
|
|
|
|
await flasher.render({
|
|
envelopes: [{
|
|
type: 'success',
|
|
message: 'Test',
|
|
title: 'Test',
|
|
options: {},
|
|
metadata: { plugin: 'test' },
|
|
}],
|
|
options: {
|
|
test: {
|
|
callback: '(a, b) => { const c = a + b; return c; }',
|
|
},
|
|
},
|
|
})
|
|
|
|
expect(plugin.options.callback).toBeInstanceOf(Function)
|
|
})
|
|
|
|
it('should handle nested arrow functions', async () => {
|
|
const plugin = new MockPlugin()
|
|
flasher.addPlugin('test', plugin)
|
|
|
|
await flasher.render({
|
|
envelopes: [{
|
|
type: 'success',
|
|
message: 'Test',
|
|
title: 'Test',
|
|
options: {},
|
|
metadata: { plugin: 'test' },
|
|
}],
|
|
options: {
|
|
test: {
|
|
callback: '(x) => (y) => x + y',
|
|
},
|
|
},
|
|
})
|
|
|
|
expect(plugin.options.callback).toBeInstanceOf(Function)
|
|
})
|
|
})
|
|
|
|
describe('addAssets edge cases', () => {
|
|
it('should skip already loaded script URLs', async () => {
|
|
const originalCreateElement = document.createElement.bind(document)
|
|
let createElementCount = 0
|
|
|
|
vi.spyOn(document, 'createElement').mockImplementation((tag: string) => {
|
|
createElementCount++
|
|
const el = originalCreateElement(tag)
|
|
if (tag === 'link' || tag === 'script') {
|
|
setTimeout(() => el.onload?.({} as Event), 0)
|
|
}
|
|
return el
|
|
})
|
|
|
|
// First load
|
|
await flasher.render({
|
|
envelopes: [],
|
|
scripts: ['/script.js'],
|
|
})
|
|
|
|
const firstCount = createElementCount
|
|
|
|
// Second load - should skip
|
|
await flasher.render({
|
|
envelopes: [],
|
|
scripts: ['/script.js'],
|
|
})
|
|
|
|
// Should not have created additional elements for the same URL
|
|
expect(createElementCount).toBe(firstCount)
|
|
})
|
|
})
|
|
})
|