Files
delphi-database/static/js/__tests__/alerts.ui.test.js
2025-08-13 18:53:35 -05:00

134 lines
4.9 KiB
JavaScript

/** @jest-environment jsdom */
const path = require('path');
// Load sanitizer and alerts modules (IIFE attaches to window)
require(path.join(__dirname, '..', 'sanitizer.js'));
require(path.join(__dirname, '..', 'alerts.js'));
// Polyfill requestAnimationFrame for jsdom
beforeAll(() => {
if (!global.requestAnimationFrame) {
global.requestAnimationFrame = (cb) => setTimeout(cb, 0);
}
});
beforeEach(() => {
document.body.innerHTML = '';
});
describe('alerts.show UI behavior', () => {
it('creates a container and renders a notification', () => {
const wrapper = window.alerts.show('Hello world', 'info', { duration: 0 });
const container = document.getElementById('notification-container');
expect(container).toBeTruthy();
expect(container.contains(wrapper)).toBe(true);
expect(wrapper.className).toMatch(/alert-notification/);
});
it('applies styling based on type and aliases', () => {
const s = window.alerts.show('ok', 'success', { duration: 0 });
const e = window.alerts.show('bad', 'error', { duration: 0 }); // alias to danger
const w = window.alerts.show('warn', 'warning', { duration: 0 });
const i = window.alerts.show('info', 'info', { duration: 0 });
expect(s.className).toContain('bg-green-50');
expect(e.className).toContain('bg-red-50');
expect(w.className).toContain('bg-yellow-50');
expect(i.className).toContain('bg-blue-50');
});
it('renders title when provided', () => {
const wrapper = window.alerts.show('Body', 'info', { title: 'My Title', duration: 0 });
const titleEl = wrapper.querySelector('p.text-sm.font-bold');
expect(titleEl).toBeTruthy();
expect(titleEl.textContent).toBe('My Title');
});
it('sanitizes HTML when html option is true', () => {
const wrapper = window.alerts.show('<img src=x onerror=alert(1)><script>evil()</script><p>hi</p>', 'info', { html: true, duration: 0 });
const textEl = wrapper.querySelector('.text-sm.mt-1.font-semibold');
expect(textEl).toBeTruthy();
const html = textEl.innerHTML;
expect(html).toContain('<img');
expect(html).toContain('<p>hi</p>');
expect(html).not.toMatch(/<script/i);
expect(html).not.toMatch(/onerror/i);
});
it('supports Node message content without sanitization', () => {
const span = document.createElement('span');
span.textContent = 'node message';
const wrapper = window.alerts.show(span, 'info', { duration: 0 });
const textEl = wrapper.querySelector('.text-sm.mt-1.font-semibold');
expect(textEl.textContent).toContain('node message');
});
it('is dismissible by default and calls onClose', () => {
const onClose = jest.fn();
const wrapper = window.alerts.show('dismiss me', 'info', { onClose, duration: 0 });
const btn = wrapper.querySelector('button[aria-label="Close"]');
expect(btn).toBeTruthy();
btn.click();
expect(document.body.contains(wrapper)).toBe(false);
expect(onClose).toHaveBeenCalledTimes(1);
});
it('can be non-dismissible', () => {
const wrapper = window.alerts.show('stay', 'info', { dismissible: false, duration: 0 });
const btn = wrapper.querySelector('button[aria-label="Close"]');
expect(btn).toBeNull();
});
it('auto-closes after duration', () => {
jest.useFakeTimers();
const wrapper = window.alerts.show('timeout', 'info', { duration: 50 });
expect(document.body.contains(wrapper)).toBe(true);
jest.advanceTimersByTime(400);
expect(document.body.contains(wrapper)).toBe(false);
jest.useRealTimers();
});
it('renders actions and handles clicks (autoClose true by default)', () => {
const onClick = jest.fn();
const wrapper = window.alerts.show('with action', 'info', {
duration: 0,
actions: [
{ label: 'Retry', onClick }
]
});
const buttons = Array.from(wrapper.querySelectorAll('button'));
const retryBtn = buttons.find((b) => b.textContent === 'Retry');
expect(retryBtn).toBeTruthy();
retryBtn.click();
expect(onClick).toHaveBeenCalledTimes(1);
expect(document.body.contains(wrapper)).toBe(false);
});
it('respects action.autoClose = false', () => {
const onClick = jest.fn();
const wrapper = window.alerts.show('stay open', 'info', {
duration: 0,
actions: [
{ label: 'Stay', onClick, autoClose: false }
]
});
const buttons = Array.from(wrapper.querySelectorAll('button'));
const stayBtn = buttons.find((b) => b.textContent === 'Stay');
expect(stayBtn).toBeTruthy();
stayBtn.click();
expect(onClick).toHaveBeenCalledTimes(1);
expect(document.body.contains(wrapper)).toBe(true);
});
it('supports custom containerId and element id', () => {
const wrapper = window.alerts.show('custom', 'info', { duration: 0, containerId: 'alt-container', id: 'toast-1' });
const container = document.getElementById('alt-container');
expect(container).toBeTruthy();
expect(wrapper.id).toBe('toast-1');
expect(container.contains(wrapper)).toBe(true);
});
});