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

124 lines
4.1 KiB
JavaScript

/** @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 <b>file</b> <script>evil()</script><img src=x onerror="alert(1)">' },
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('<b>file</b>');
// Scripts and event handlers removed
expect(html).not.toMatch(/<script/i);
expect(html).not.toMatch(/onerror=/i);
// Correlation reference present
expect(html).toMatch(/Ref: cid-abc123/);
});
it('uses correlation ID from header when both header and envelope provide values', async () => {
const headerCid = 'cid-header';
const bodyCid = 'cid-body';
const envelope = {
success: false,
error: { status: 400, code: 'http_error', message: 'Invalid type' },
correlation_id: bodyCid,
};
fetchMock.mockResolvedValueOnce(makeErrorResponse({ status: 400, envelope, headerCid }));
const wrapper = await uploadFileUI();
const html = wrapper.querySelector('.text-sm.mt-1.font-semibold').innerHTML;
expect(html).toMatch(/Ref: cid-header/);
expect(html).not.toMatch(/cid-body/);
});
it('falls back to basic text if alerts module is missing but our alerts is present; ensures container exists', async () => {
const cid = 'cid-xyz';
const envelope = {
success: false,
error: { status: 400, code: 'http_error', message: 'Bad <em>upload</em>' },
correlation_id: cid,
};
fetchMock.mockResolvedValueOnce(makeErrorResponse({ status: 400, envelope, headerCid: cid }));
const wrapper = await uploadFileUI();
const container = document.getElementById('notification-container');
expect(container).toBeTruthy();
expect(container.contains(wrapper)).toBe(true);
});
});