/** @jest-environment jsdom */ const path = require('path'); // Install a fetch mock BEFORE loading the fetch-wrapper so it captures our mock const fetchMock = jest.fn(); global.fetch = fetchMock; // Load sanitizer and alerts (IIFE attaches to window) require(path.join(__dirname, '..', 'sanitizer.js')); require(path.join(__dirname, '..', 'alerts.js')); // Load fetch wrapper (captures current global fetch as originalFetch) require(path.join(__dirname, '..', 'fetch-wrapper.js')); // Polyfill requestAnimationFrame for jsdom animations in alerts beforeAll(() => { if (!global.requestAnimationFrame) { global.requestAnimationFrame = (cb) => setTimeout(cb, 0); } }); beforeEach(() => { document.body.innerHTML = ''; fetchMock.mockReset(); }); // Minimal UI helper that simulates the upload UI error handling async function uploadFileUI() { // Attempt an upload; on failure, surface the envelope via alerts.error with a sanitized HTML message const resp = await window.http.wrappedFetch('/api/documents/upload/FILE-123', { method: 'POST', body: new FormData(), }); if (!resp.ok) { const err = await window.http.toError(resp, 'Upload failed'); const msg = window.http.formatAlert(err, 'Upload failed'); // html: true to allow basic markup from server but sanitized; duration 0 so it stays for assertions return window.alerts.error(msg, { html: true, duration: 0, id: 'upload-error' }); } return null; } function makeErrorResponse({ status = 400, envelope, headerCid = null }) { return { ok: false, status, headers: { get: (name) => { if (name && name.toLowerCase() === 'x-correlation-id') return headerCid || null; return null; }, }, clone() { return this; }, async json() { return envelope; }, async text() { try { return JSON.stringify(envelope); } catch (_) { return ''; } }, }; } describe('Upload UI error handling', () => { it('displays server error via alerts.error, sanitizes HTML, and includes correlation ID', async () => { const cid = 'cid-abc123'; const envelope = { success: false, error: { status: 400, code: 'http_error', message: 'Invalid file ' }, correlation_id: cid, }; fetchMock.mockResolvedValueOnce(makeErrorResponse({ status: 400, envelope, headerCid: cid })); const wrapper = await uploadFileUI(); expect(wrapper).toBeTruthy(); expect(wrapper.id).toBe('upload-error'); const content = wrapper.querySelector('.text-sm.mt-1.font-semibold'); expect(content).toBeTruthy(); const html = content.innerHTML; // Preserves safe markup expect(html).toContain('file'); // Scripts and event handlers removed expect(html).not.toMatch(/