From 070585a8097ea71af718280231f279890353d40b Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Sun, 2 Aug 2026 14:37:39 +0200 Subject: [PATCH] test(frontend): cover html utility fallbacks --- tests/test_html_utils.mjs | 253 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 tests/test_html_utils.mjs diff --git a/tests/test_html_utils.mjs b/tests/test_html_utils.mjs new file mode 100644 index 0000000..093dd4a --- /dev/null +++ b/tests/test_html_utils.mjs @@ -0,0 +1,253 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { + copyTextWithFeedback, + escapeHtml, + escapeJsString, + getFilenameFromPath, + html, + normalizePathIdentity, + pollBackgroundTask, + safeStorage, + sanitizeDescriptionHtml, +} from '../web/resolver/utils/html_utils.js'; + +function installGlobal(name, value) { + const previousDescriptor = Object.getOwnPropertyDescriptor(globalThis, name); + Object.defineProperty(globalThis, name, { + configurable: true, + enumerable: true, + value, + writable: true, + }); + return () => { + if (previousDescriptor) { + Object.defineProperty(globalThis, name, previousDescriptor); + } else { + delete globalThis[name]; + } + }; +} + +test('html utilities escape strings, arrays, and inline JavaScript safely', () => { + assert.equal(escapeHtml(`it's & fine`), '<tag a="1">it's & fine'); + assert.equal(escapeHtml(null), ''); + assert.equal(html`

${''}${['&', 'plain']}

`, '

<unsafe>&plain

'); + assert.equal(html`before${null}after${undefined}`, 'beforeafter'); + assert.equal(escapeJsString('line"break'), '"line\\"break"'); +}); + +test('html utilities normalize paths and extract filenames across platforms', () => { + assert.equal(normalizePathIdentity(' Models\\Checkpoints///Model.safetensors/ '), 'models/checkpoints/model.safetensors'); + assert.equal(normalizePathIdentity(''), ''); + assert.equal(getFilenameFromPath('models\\checkpoints\\model.safetensors'), 'model.safetensors'); + assert.equal(getFilenameFromPath('models/checkpoints/model.safetensors'), 'model.safetensors'); + assert.equal(getFilenameFromPath(''), ''); + assert.equal(getFilenameFromPath('filename.safetensors'), 'filename.safetensors'); +}); + +test('safe storage reads, writes, and removes values through localStorage', () => { + const values = new Map(); + const restoreStorage = installGlobal('localStorage', { + getItem(key) { + return values.has(key) ? values.get(key) : null; + }, + setItem(key, value) { + values.set(key, String(value)); + }, + removeItem(key) { + values.delete(key); + }, + }); + + try { + safeStorage.setItem('html-utils-key', 42); + assert.equal(safeStorage.getItem('html-utils-key'), '42'); + assert.equal(safeStorage.getItem('missing-key', 'fallback'), 'fallback'); + safeStorage.removeItem('html-utils-key'); + assert.equal(safeStorage.getItem('html-utils-key'), null); + } finally { + restoreStorage(); + } +}); + +test('safe storage falls back to memory when browser storage fails', () => { + const restoreStorage = installGlobal('localStorage', { + getItem() { + throw new Error('read failed'); + }, + setItem(key) { + if (key === '__test_storage__') return; + throw new Error('write failed'); + }, + removeItem(key) { + if (key === '__test_storage__') return; + throw new Error('remove failed'); + }, + }); + + try { + safeStorage.setItem('fallback-key', 'value'); + assert.equal(safeStorage.getItem('fallback-key'), 'value'); + safeStorage.removeItem('fallback-key'); + assert.equal(safeStorage.getItem('fallback-key'), null); + } finally { + restoreStorage(); + } +}); + +test('background task polling reports progress, ignores filtered statuses, and stops at terminal state', async () => { + const responses = [ + { status: 'queued' }, + { status: 'ignored' }, + { status: 'completed', result: 42 }, + ]; + const progress = []; + const terminal = []; + let fetchCount = 0; + + await pollBackgroundTask({ + endpoint: '/task/1', + tokenCheck: () => true, + onProgress: data => progress.push(data.status), + isTerminal: data => data.status === 'completed', + onTerminal: data => terminal.push(data.result), + onError: error => { + throw error; + }, + intervalMs: 0, + fetchJson: async () => responses[fetchCount++], + filterIgnoredStatus: data => data.status === 'ignored', + }); + + assert.deepEqual(progress, ['queued', 'completed']); + assert.deepEqual(terminal, [42]); + assert.equal(fetchCount, 3); +}); + +test('background task polling stops before fetching when its token is cancelled', async () => { + let fetchCount = 0; + await pollBackgroundTask({ + endpoint: '/task/cancelled', + tokenCheck: () => false, + onProgress: () => { + throw new Error('progress should not run'); + }, + isTerminal: () => false, + onTerminal: () => { + throw new Error('terminal should not run'); + }, + onError: () => { + throw new Error('error should not run'); + }, + fetchJson: async () => { + fetchCount += 1; + return {}; + }, + }); + assert.equal(fetchCount, 0); +}); + +test('background task polling reports active errors and ignores stale errors', async () => { + const errors = []; + await pollBackgroundTask({ + endpoint: '/task/error', + tokenCheck: () => true, + onProgress: () => {}, + isTerminal: () => false, + onTerminal: () => {}, + onError: error => errors.push(error.message), + intervalMs: 0, + fetchJson: async () => { + throw new Error('request failed'); + }, + }); + assert.deepEqual(errors, ['request failed']); + + let active = true; + const staleErrors = []; + await pollBackgroundTask({ + endpoint: '/task/stale-error', + tokenCheck: () => active, + onProgress: () => {}, + isTerminal: () => false, + onTerminal: () => {}, + onError: error => staleErrors.push(error.message), + fetchJson: async () => { + active = false; + throw new Error('stale request failed'); + }, + }); + assert.deepEqual(staleErrors, []); +}); + +function createButton() { + const classes = new Set(); + return { + innerHTML: 'Copy', + textContent: '', + isConnected: true, + classList: { + add(value) { + classes.add(value); + }, + remove(value) { + classes.delete(value); + }, + has(value) { + return classes.has(value); + }, + }, + }; +} + +test('copy feedback updates and restores a button after a successful copy', async () => { + const copied = []; + const restoreNavigator = installGlobal('navigator', { + clipboard: { + async writeText(value) { + copied.push(value); + }, + }, + }); + const button = createButton(); + + try { + await copyTextWithFeedback('copied text', button, { + duration: 1, + successHtml: 'Done', + successClass: 'copied', + }); + assert.deepEqual(copied, ['copied text']); + await new Promise(resolve => setTimeout(resolve, 10)); + assert.equal(button.innerHTML, 'Copy'); + assert.equal(button.classList.has('copied'), false); + } finally { + restoreNavigator(); + } +}); + +test('copy feedback displays an error and handles missing buttons', async () => { + const restoreNavigator = installGlobal('navigator', { + clipboard: { + async writeText() { + throw new Error('clipboard unavailable'); + }, + }, + }); + const button = createButton(); + + try { + await copyTextWithFeedback('text', button, { duration: 1, errorText: 'Copy failed' }); + assert.equal(button.textContent, 'Copy failed'); + await copyTextWithFeedback('text', null); + } finally { + restoreNavigator(); + } +}); + +test('description sanitizer returns an empty string for empty input', () => { + assert.equal(sanitizeDescriptionHtml(' '), ''); + assert.equal(sanitizeDescriptionHtml(null), ''); +});