coming together
This commit is contained in:
123
static/js/__tests__/upload.ui.test.js
Normal file
123
static/js/__tests__/upload.ui.test.js
Normal file
@@ -0,0 +1,123 @@
|
||||
/** @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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user