diff --git a/tests/test_dom_patchers.mjs b/tests/test_dom_patchers.mjs index ddfb8e3..d5e2de7 100644 --- a/tests/test_dom_patchers.mjs +++ b/tests/test_dom_patchers.mjs @@ -2,6 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import test from 'node:test'; import assert from 'node:assert/strict'; +import { performance } from 'node:perf_hooks'; import { Window } from 'happy-dom'; import { bindEventOnce, @@ -480,3 +481,99 @@ test('search UI refreshes coalesce within one animation frame', () => { else globalThis.cancelAnimationFrame = previousCancelAnimationFrame; } }); + +test('DOM patchers stay within the local benchmark budget', () => { + const window = new Window(); + const dialog = createDialog(window); + const itemCount = 80; + const iterations = 30; + const elementFromHtml = html => { + const wrapper = window.document.createElement('div'); + wrapper.innerHTML = html; + return wrapper.firstElementChild; + }; + const benchmark = (label, operation) => { + const start = performance.now(); + for (let iteration = 0; iteration < iterations; iteration += 1) { + operation(iteration); + } + const elapsedMs = performance.now() - start; + assert.ok(elapsedMs < 5000, `${label} exceeded 5000ms: ${elapsedMs.toFixed(1)}ms`); + return elapsedMs; + }; + + const searchHtml = iteration => ` +
+ ${Array.from({ length: itemCount }, (_, index) => ` +
+
+ Source ${index} + ${iteration}% +
+
+
+ `).join('')} +
`; + const searchCurrent = elementFromHtml(searchHtml(0)); + const searchNext = elementFromHtml(searchHtml(0)); + const searchFirstItem = searchCurrent.firstElementChild; + const searchMs = benchmark('search progress patcher', iteration => { + searchNext.querySelectorAll('.mr-search-progress-status').forEach(status => { + status.textContent = `${iteration}%`; + }); + dialog.patchSearchProgressElement(searchCurrent, searchNext); + }); + assert.strictEqual(searchCurrent.firstElementChild, searchFirstItem); + + const localContainer = window.document.createElement('div'); + const localHtml = iteration => ` + ${Array.from({ length: itemCount }, (_, index) => ` +
${iteration}-${index}
+ `).join('')}`; + dialog.patchLocalMatchesContainer(localContainer, localHtml(0)); + const localFirstRow = localContainer.firstElementChild; + const localMs = benchmark('local match patcher', iteration => { + dialog.patchLocalMatchesContainer(localContainer, localHtml(iteration)); + }); + assert.strictEqual(localContainer.firstElementChild, localFirstRow); + + const progressContainer = window.document.createElement('div'); + const progressHtml = iteration => ` +
+
Loading ${iteration}
+
+
${iteration} / 100${iteration}%
+
`; + dialog.patchLoadedModelsProgress(progressContainer, progressHtml(0)); + const progressRoot = progressContainer.firstElementChild; + const progressMs = benchmark('loaded progress patcher', iteration => { + dialog.patchLoadedModelsProgress(progressContainer, progressHtml(iteration)); + }); + assert.strictEqual(progressContainer.firstElementChild, progressRoot); + + const downloadsCurrent = elementFromHtml(` +
+ ${Array.from({ length: itemCount }, (_, index) => ` +
0%
+ `).join('')} +
`); + const downloadsFirstCard = downloadsCurrent.firstElementChild; + const downloadsMs = benchmark('downloads panel patcher', iteration => { + const downloadsNext = elementFromHtml(` +
+ ${Array.from({ length: itemCount }, (_, index) => ` +
${iteration}%
+ `).join('')} +
`); + dialog.patchDownloadsPanelElement(downloadsCurrent, downloadsNext); + }); + assert.strictEqual(downloadsCurrent.firstElementChild, downloadsFirstCard); + + console.info( + `[dom-benchmark] search=${searchMs.toFixed(1)}ms ` + + `local=${localMs.toFixed(1)}ms ` + + `progress=${progressMs.toFixed(1)}ms ` + + `downloads=${downloadsMs.toFixed(1)}ms ` + + `(${itemCount} items x ${iterations} updates)` + ); +});