254 lines
7.2 KiB
JavaScript
254 lines
7.2 KiB
JavaScript
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(`<tag a="1">it's & fine`), '<tag a="1">it's & fine');
|
|
assert.equal(escapeHtml(null), '');
|
|
assert.equal(html`<p>${'<unsafe>'}${['&', 'plain']}</p>`, '<p><unsafe>&plain</p>');
|
|
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: '<span>Copy</span>',
|
|
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: '<b>Done</b>',
|
|
successClass: 'copied',
|
|
});
|
|
assert.deepEqual(copied, ['copied text']);
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
assert.equal(button.innerHTML, '<span>Copy</span>');
|
|
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), '');
|
|
});
|