test(frontend): add DOM patcher behavior coverage
This commit is contained in:
Generated
+113
-2
@@ -4,11 +4,11 @@
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "comfyui-model-resolver",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"eslint": "^10.7.0",
|
||||
"globals": "^17.7.0"
|
||||
"globals": "^17.7.0",
|
||||
"happy-dom": "^20.11.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
@@ -226,6 +226,33 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "26.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz",
|
||||
"integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~8.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/whatwg-mimetype": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
|
||||
"integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.17.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
|
||||
@@ -289,6 +316,19 @@
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-image-size": {
|
||||
"version": "0.6.4",
|
||||
"resolved": "https://registry.npmjs.org/buffer-image-size/-/buffer-image-size-0.6.4.tgz",
|
||||
"integrity": "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -329,6 +369,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
||||
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
@@ -595,6 +648,25 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/happy-dom": {
|
||||
"version": "20.11.1",
|
||||
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.11.1.tgz",
|
||||
"integrity": "sha512-XSt8tMzbW9ymE7687xztkO1ckR7qJNQ3LywY9vlYGhGi3zXrGBHuUo2Cl1ztZaICW+1eAGdkLbj6iwVqDT33kg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": ">=20.0.0",
|
||||
"@types/whatwg-mimetype": "^3.0.2",
|
||||
"@types/ws": "^8.18.1",
|
||||
"buffer-image-size": "^0.6.4",
|
||||
"entities": "^7.0.1",
|
||||
"whatwg-mimetype": "^3.0.0",
|
||||
"ws": "^8.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -862,6 +934,13 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
|
||||
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uri-js": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
@@ -872,6 +951,16 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-mimetype": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
|
||||
"integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
@@ -898,6 +987,28 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.21.2",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.2.tgz",
|
||||
"integrity": "sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
||||
+2
-1
@@ -11,6 +11,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"eslint": "^10.7.0",
|
||||
"globals": "^17.7.0"
|
||||
"globals": "^17.7.0",
|
||||
"happy-dom": "^20.11.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { Window } from 'happy-dom';
|
||||
|
||||
const projectRoot = path.resolve(import.meta.dirname, '..');
|
||||
const resolveDownloadMethodsSource = fs.readFileSync(
|
||||
path.join(projectRoot, 'web/resolver/actions/resolve_download_methods.js'),
|
||||
'utf8'
|
||||
);
|
||||
const queueMethodsSource = fs.readFileSync(
|
||||
path.join(projectRoot, 'web/resolver/actions/queue_methods.js'),
|
||||
'utf8'
|
||||
);
|
||||
const renderFormatMethodsSource = fs.readFileSync(
|
||||
path.join(projectRoot, 'web/resolver/utils/render_format_methods.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) {
|
||||
const char = source[index];
|
||||
if (char === '{') depth += 1;
|
||||
if (char === '}') depth -= 1;
|
||||
if (depth === 0) {
|
||||
return `${isAsync ? 'async ' : ''}function ${methodName}(${params}) ${source.slice(braceStart, index + 1)}`;
|
||||
}
|
||||
}
|
||||
throw new Error(`Could not parse ${methodName}`);
|
||||
}
|
||||
|
||||
const patchSearchResultsContainer = eval(`(${extractMethod(resolveDownloadMethodsSource, 'patchSearchResultsContainer')})`);
|
||||
const patchSearchProgressElement = eval(`(${extractMethod(resolveDownloadMethodsSource, 'patchSearchProgressElement')})`);
|
||||
const patchSearchProgressItem = eval(`(${extractMethod(resolveDownloadMethodsSource, 'patchSearchProgressItem')})`);
|
||||
const patchSearchProgressActions = eval(`(${extractMethod(resolveDownloadMethodsSource, 'patchSearchProgressActions')})`);
|
||||
const patchSearchResultsTable = eval(`(${extractMethod(resolveDownloadMethodsSource, 'patchSearchResultsTable')})`);
|
||||
const syncSearchElementAttributes = eval(`(${extractMethod(resolveDownloadMethodsSource, 'syncSearchElementAttributes')})`);
|
||||
const getSearchResultsPatchKind = eval(`(${extractMethod(resolveDownloadMethodsSource, 'getSearchResultsPatchKind')})`);
|
||||
const wireSearchDownloadButtons = eval(`(${extractMethod(resolveDownloadMethodsSource, 'wireSearchDownloadButtons')})`);
|
||||
const patchLocalMatchesContainer = eval(`(${extractMethod(resolveDownloadMethodsSource, 'patchLocalMatchesContainer')})`);
|
||||
const patchQueuedSelections = eval(`(${extractMethod(queueMethodsSource, 'patchQueuedSelections')})`);
|
||||
const patchDownloadsPanelElement = eval(`(${extractMethod(queueMethodsSource, 'patchDownloadsPanelElement')})`);
|
||||
const patchLoadedModelsProgress = eval(`(${extractMethod(renderFormatMethodsSource, 'patchLoadedModelsProgress')})`);
|
||||
|
||||
function createDialog(window) {
|
||||
return {
|
||||
syncSearchElementAttributes,
|
||||
patchSearchProgressActions,
|
||||
patchSearchProgressItem,
|
||||
patchSearchProgressElement,
|
||||
patchSearchResultsTable,
|
||||
getSearchResultsPatchKind,
|
||||
patchSearchResultsContainer,
|
||||
patchLocalMatchesContainer,
|
||||
patchQueuedSelections,
|
||||
patchDownloadsPanelElement,
|
||||
patchLoadedModelsProgress,
|
||||
wireSearchDownloadButtons,
|
||||
window,
|
||||
wireSearchHashMatchHighlights() {},
|
||||
};
|
||||
}
|
||||
|
||||
test('search patcher preserves keyed progress items and result rows', () => {
|
||||
const window = new Window();
|
||||
const dialog = createDialog(window);
|
||||
const container = window.document.createElement('div');
|
||||
const firstHtml = `
|
||||
<div class="mr-search-progress-list">
|
||||
<div class="mr-search-progress-item mr-search-progress-running" data-search-progress-source="civitai">
|
||||
<span class="mr-search-progress-source">CivitAI</span>
|
||||
<span class="mr-search-progress-status">Searching... 10%</span>
|
||||
<div class="mr-search-progress-bar" role="progressbar" aria-valuenow="10"><div class="mr-search-progress-fill" style="width: 10%;"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mr-search-results-table-wrap">
|
||||
<table class="mr-search-results-table"><tbody>
|
||||
<tr data-search-result-key="civitai%3Amodel"><td>Old result</td></tr>
|
||||
</tbody></table>
|
||||
</div>
|
||||
`;
|
||||
const secondHtml = `
|
||||
<div class="mr-search-progress-list">
|
||||
<div class="mr-search-progress-item mr-search-progress-running" data-search-progress-source="civitai">
|
||||
<span class="mr-search-progress-source">CivitAI</span>
|
||||
<span class="mr-search-progress-status">Searching... 55%</span>
|
||||
<div class="mr-search-progress-bar" role="progressbar" aria-valuenow="55"><div class="mr-search-progress-fill" style="width: 55%;"></div></div>
|
||||
</div>
|
||||
<div class="mr-search-progress-item mr-search-progress-pending" data-search-progress-source="huggingface">
|
||||
<span class="mr-search-progress-source">HuggingFace</span>
|
||||
<span class="mr-search-progress-status">Queued</span>
|
||||
<div class="mr-search-progress-bar" role="progressbar" aria-valuenow="0"><div class="mr-search-progress-fill" style="width: 0%;"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mr-search-results-table-wrap">
|
||||
<table class="mr-search-results-table"><tbody>
|
||||
<tr data-search-result-key="civitai%3Amodel"><td>Updated result</td></tr>
|
||||
</tbody></table>
|
||||
</div>
|
||||
`;
|
||||
|
||||
dialog.patchSearchResultsContainer(container, firstHtml);
|
||||
const progressItem = container.querySelector('[data-search-progress-source="civitai"]');
|
||||
const resultRow = container.querySelector('[data-search-result-key="civitai%3Amodel"]');
|
||||
dialog.patchSearchResultsContainer(container, secondHtml);
|
||||
|
||||
assert.strictEqual(
|
||||
container.querySelector('[data-search-progress-source="civitai"]'),
|
||||
progressItem
|
||||
);
|
||||
assert.strictEqual(
|
||||
container.querySelector('[data-search-result-key="civitai%3Amodel"]'),
|
||||
resultRow
|
||||
);
|
||||
assert.equal(progressItem.querySelector('.mr-search-progress-status').textContent, 'Searching... 55%');
|
||||
assert.equal(progressItem.querySelector('.mr-search-progress-fill').style.width, '55%');
|
||||
assert.equal(resultRow.textContent.trim(), 'Updated result');
|
||||
assert.ok(container.querySelector('[data-search-progress-source="huggingface"]'));
|
||||
});
|
||||
|
||||
test('local match patcher preserves rows and collapsed alternatives', () => {
|
||||
const window = new Window();
|
||||
const dialog = createDialog(window);
|
||||
const container = window.document.createElement('div');
|
||||
const firstHtml = `
|
||||
<div class="mr-match-row" data-local-match-key="path%3A%2Fmodels%2Fone"><span>One</span></div>
|
||||
<button class="mr-local-alternatives-toggle" aria-expanded="false">Alternatives</button>
|
||||
<div id="more-matches" class="mr-stack-sm mr-hidden">
|
||||
<div class="mr-match-row" data-local-match-key="path%3A%2Fmodels%2Ftwo"><span>Two</span></div>
|
||||
</div>
|
||||
`;
|
||||
const secondHtml = `
|
||||
<div class="mr-match-row" data-local-match-key="path%3A%2Fmodels%2Fone"><span>One updated</span></div>
|
||||
<button class="mr-local-alternatives-toggle" aria-expanded="true">Alternatives</button>
|
||||
<div id="more-matches" class="mr-stack-sm">
|
||||
<div class="mr-match-row" data-local-match-key="path%3A%2Fmodels%2Ftwo"><span>Two updated</span></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
dialog.patchLocalMatchesContainer(container, firstHtml);
|
||||
const mainRow = container.querySelector('[data-local-match-key="path%3A%2Fmodels%2Fone"]');
|
||||
const alternativeRow = container.querySelector('[data-local-match-key="path%3A%2Fmodels%2Ftwo"]');
|
||||
const group = container.querySelector('#more-matches');
|
||||
dialog.patchLocalMatchesContainer(container, secondHtml);
|
||||
|
||||
assert.strictEqual(container.querySelector('[data-local-match-key="path%3A%2Fmodels%2Fone"]'), mainRow);
|
||||
assert.strictEqual(container.querySelector('[data-local-match-key="path%3A%2Fmodels%2Ftwo"]'), alternativeRow);
|
||||
assert.strictEqual(container.querySelector('#more-matches'), group);
|
||||
assert.equal(mainRow.textContent, 'One updated');
|
||||
assert.equal(alternativeRow.textContent, 'Two updated');
|
||||
assert.ok(group.classList.contains('mr-hidden'), 'user-collapsed alternatives should stay collapsed');
|
||||
});
|
||||
|
||||
test('loaded progress patcher preserves the progress root and fill element', () => {
|
||||
const window = new Window();
|
||||
const dialog = createDialog(window);
|
||||
const container = window.document.createElement('div');
|
||||
const makeProgress = (message, percent) => `
|
||||
<div class="mr-download-section">
|
||||
<div class="mr-status-inline"><span class="mr-download-info">${message}</span></div>
|
||||
<div class="mr-progress-container"><div class="mr-progress-bar"><div class="mr-progress-fill" style="width: ${percent}%;"></div></div><div class="mr-progress-text"><span>1 / 2</span><span>${percent}%</span></div></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
dialog.patchLoadedModelsProgress(container, makeProgress('Loading one', 10));
|
||||
const root = container.firstElementChild;
|
||||
const fill = container.querySelector('.mr-progress-fill');
|
||||
dialog.patchLoadedModelsProgress(container, makeProgress('Loading two', 60));
|
||||
|
||||
assert.strictEqual(container.firstElementChild, root);
|
||||
assert.strictEqual(container.querySelector('.mr-progress-fill'), fill);
|
||||
assert.equal(container.querySelector('.mr-download-info').textContent, 'Loading two');
|
||||
assert.equal(fill.style.width, '60%');
|
||||
});
|
||||
|
||||
test('download queue patcher preserves keyed cards', () => {
|
||||
const window = new Window();
|
||||
const dialog = createDialog(window);
|
||||
const queueList = window.document.createElement('div');
|
||||
dialog.queueList = queueList;
|
||||
queueList.innerHTML = '<div class="mr-queue-items"><div class="mr-queue-item" data-queue-key="one"><span>One</span></div></div>';
|
||||
dialog.patchQueuedSelections('<div class="mr-queue-items"><div class="mr-queue-item" data-queue-key="one"><span>Updated</span></div><div class="mr-queue-item" data-queue-key="two"><span>Two</span></div></div>');
|
||||
|
||||
const item = queueList.querySelector('[data-queue-key="one"]');
|
||||
assert.equal(item.textContent, 'Updated');
|
||||
assert.strictEqual(queueList.querySelectorAll('[data-queue-key="one"]').length, 1);
|
||||
assert.ok(queueList.querySelector('[data-queue-key="two"]'));
|
||||
});
|
||||
|
||||
test('downloads panel patcher preserves a keyed card node', () => {
|
||||
const window = new Window();
|
||||
const dialog = createDialog(window);
|
||||
const current = window.document.createElement('div');
|
||||
const next = window.document.createElement('div');
|
||||
current.innerHTML = '<div class="mr-download-queue-item" data-download-id="download-1"><span>10%</span></div>';
|
||||
next.innerHTML = '<div class="mr-download-queue-item" data-download-id="download-1"><span>20%</span></div>';
|
||||
const card = current.firstElementChild;
|
||||
|
||||
dialog.patchDownloadsPanelElement(current, next);
|
||||
|
||||
assert.strictEqual(current.firstElementChild, card);
|
||||
assert.equal(card.textContent, '20%');
|
||||
});
|
||||
|
||||
test('search download button wiring remains idempotent on the same node', () => {
|
||||
const window = new Window();
|
||||
const dialog = createDialog(window);
|
||||
const container = window.document.createElement('div');
|
||||
container.innerHTML = '<button class="search-download-btn" data-url="https://example.test/model" data-filename="model.safetensors" data-category="checkpoints"></button>';
|
||||
let calls = 0;
|
||||
dialog.downloadFromSearch = () => { calls += 1; };
|
||||
|
||||
dialog.wireSearchDownloadButtons(container, {});
|
||||
dialog.wireSearchDownloadButtons(container, {});
|
||||
container.querySelector('.search-download-btn').click();
|
||||
|
||||
assert.equal(calls, 1);
|
||||
});
|
||||
Reference in New Issue
Block a user