174 lines
5.3 KiB
JavaScript
174 lines
5.3 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
const searchPanelSource = fs.readFileSync(
|
|
path.resolve(import.meta.dirname, '../web/resolver/search/search_panel.js'),
|
|
'utf8'
|
|
);
|
|
|
|
function extractMethod(source, methodName, paramsPattern = '[^)]*') {
|
|
const signatureRegex = new RegExp(`\\n\\s+(async\\s+)?${methodName}\\s*\\(${paramsPattern}\\)\\s*\\{`);
|
|
const match = signatureRegex.exec(source);
|
|
assert.ok(match, `Could not find ${methodName}`);
|
|
const isAsync = Boolean(match[1]);
|
|
const parenStart = source.indexOf('(', match.index);
|
|
const parenEnd = source.indexOf(')', parenStart);
|
|
const params = source.slice(parenStart + 1, parenEnd);
|
|
const braceStart = source.indexOf('{', parenEnd);
|
|
let depth = 0;
|
|
for (let index = braceStart; index < source.length; index += 1) {
|
|
if (source[index] === '{') depth += 1;
|
|
if (source[index] === '}') depth -= 1;
|
|
if (depth === 0) {
|
|
return `${isAsync ? 'async ' : ''}function ${methodName}(${params}) ${source.slice(braceStart, index + 1)}`;
|
|
}
|
|
}
|
|
throw new Error(`Could not parse ${methodName}`);
|
|
}
|
|
|
|
globalThis.getSvgIcon = () => '';
|
|
const renderSearchProgressItem = eval(`(${extractMethod(searchPanelSource, 'renderSearchProgressItem', '[\\s\\S]*?')})`);
|
|
const renderSearchProgress = eval(`(${extractMethod(searchPanelSource, 'renderSearchProgress')})`);
|
|
const wireSearchProgressRetryButtons = eval(`(${extractMethod(searchPanelSource, 'wireSearchProgressRetryButtons', '[\\s\\S]*?')})`);
|
|
|
|
function createDialog() {
|
|
return {
|
|
renderSearchProgressItem,
|
|
renderSearchProgress,
|
|
wireSearchProgressRetryButtons,
|
|
getSearchSourceLabel(source) {
|
|
return {
|
|
civarchive: 'CivArchive',
|
|
civitai: 'CivitAI',
|
|
}[source] || source;
|
|
},
|
|
hasActiveSearchProgress(state) {
|
|
return Object.values(state.sourceProgress || {}).some(progress => (
|
|
progress?.status === 'pending' || progress?.status === 'running'
|
|
));
|
|
},
|
|
escapeHtml(value) {
|
|
return String(value).replace(/[&<>"']/g, char => ({
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": ''',
|
|
}[char]));
|
|
},
|
|
};
|
|
}
|
|
|
|
test('retryable provider failures render a source-specific retry action', () => {
|
|
const dialog = createDialog();
|
|
const html = dialog.renderSearchProgress({
|
|
sourceProgress: {
|
|
civarchive: {
|
|
status: 'error',
|
|
message: 'CivArchive may be overloaded or temporarily unavailable. Please try again.',
|
|
error: '... Details: HTTP 522',
|
|
providerState: 'unavailable',
|
|
retryable: true,
|
|
},
|
|
civitai: {
|
|
status: 'error',
|
|
message: 'CivitAI search failed.',
|
|
retryable: false,
|
|
},
|
|
},
|
|
});
|
|
|
|
assert.match(html, /mr-search-progress-retry/);
|
|
assert.match(html, /data-source="civarchive"/);
|
|
assert.match(html, /Retry CivArchive search/);
|
|
assert.doesNotMatch(html, /data-source="civitai"[^>]*>.*Retry/);
|
|
});
|
|
|
|
test('active search progress renders the full progress rows', () => {
|
|
const dialog = createDialog();
|
|
const html = dialog.renderSearchProgress({
|
|
activeSearchRunId: 'run-42',
|
|
sourceProgress: {
|
|
civitai: {
|
|
status: 'running',
|
|
percent: 42,
|
|
message: 'Querying models',
|
|
},
|
|
civarchive: {
|
|
status: 'error',
|
|
message: 'Search failed',
|
|
error: 'HTTP 522',
|
|
retryable: true,
|
|
},
|
|
},
|
|
});
|
|
|
|
assert.match(html, /class="mr-search-progress-list"/);
|
|
assert.doesNotMatch(html, /mr-search-progress-list-compact/);
|
|
assert.match(html, /aria-valuenow="42"/);
|
|
assert.match(html, /width: 42%/);
|
|
assert.match(html, /mr-search-progress-cancel/);
|
|
assert.match(html, /data-run-id="run-42"/);
|
|
assert.match(html, /mr-search-progress-retry/);
|
|
});
|
|
|
|
test('inactive search progress renders compact rows without progress controls', () => {
|
|
const dialog = createDialog();
|
|
const html = dialog.renderSearchProgress({
|
|
sourceProgress: {
|
|
civitai: {
|
|
status: 'found',
|
|
message: 'Found',
|
|
},
|
|
civarchive: {
|
|
status: 'error',
|
|
message: 'Search failed',
|
|
retryable: true,
|
|
},
|
|
},
|
|
});
|
|
|
|
assert.match(html, /mr-search-progress-list-compact/);
|
|
assert.doesNotMatch(html, /mr-search-progress-bar/);
|
|
assert.doesNotMatch(html, /mr-search-progress-cancel/);
|
|
assert.match(html, /mr-search-progress-retry/);
|
|
});
|
|
|
|
test('source retry action searches only the failed provider with force search', () => {
|
|
const dialog = createDialog();
|
|
const listeners = {};
|
|
const button = {
|
|
dataset: { source: 'civarchive' },
|
|
addEventListener(eventName, callback) {
|
|
listeners[eventName] = callback;
|
|
},
|
|
};
|
|
const container = {
|
|
querySelectorAll(selector) {
|
|
assert.equal(selector, '.mr-search-progress-retry');
|
|
return [button];
|
|
},
|
|
};
|
|
const missing = { key: 'missing-key' };
|
|
const state = {
|
|
sourceProgress: {
|
|
civarchive: { status: 'error', retryable: true },
|
|
},
|
|
};
|
|
const calls = [];
|
|
dialog.searchOnline = (...args) => calls.push(args);
|
|
|
|
dialog.wireSearchProgressRetryButtons(container, missing, state, {
|
|
workflowKey: 'workflow-key',
|
|
});
|
|
listeners.click({ preventDefault() {}, stopPropagation() {} });
|
|
|
|
assert.deepEqual(calls, [[missing, {
|
|
workflowKey: 'workflow-key',
|
|
source: 'civarchive',
|
|
forceSearch: true,
|
|
}]]);
|
|
});
|