From cf4697ddd81c6fb75645023a8d1b888fe767c72f Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Thu, 6 Aug 2026 22:30:15 +0200 Subject: [PATCH] refactor shared frontend size parsing --- tests/test_downloads_tab_workflow_route.mjs | 45 +++++++++++++++++++++ tests/test_size_utils.mjs | 18 +++++++++ web/resolver/search/search_panel.js | 5 ++- web/resolver/utils/size_utils.js | 6 +++ web/resolver/views/model_info_methods.js | 5 ++- 5 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 tests/test_size_utils.mjs create mode 100644 web/resolver/utils/size_utils.js diff --git a/tests/test_downloads_tab_workflow_route.mjs b/tests/test_downloads_tab_workflow_route.mjs index 9f8e5e2..bad3fec 100644 --- a/tests/test_downloads_tab_workflow_route.mjs +++ b/tests/test_downloads_tab_workflow_route.mjs @@ -31,6 +31,7 @@ import { } from '../web/resolver/utils/category_aliases.generated.js'; import { getSha256Field, normalizeSha256 } from '../web/resolver/utils/hash_utils.js'; import { normalizeSearchToken } from '../web/resolver/utils/search_utils.js'; +import { parseFiniteNumber } from '../web/resolver/utils/size_utils.js'; import { getSourceDisplayLabel, normalizeSourceKey } from '../web/resolver/utils/source_labels.js'; import { baseModelAliasMethods } from '../web/resolver/search/base_model_alias_methods.js'; import { searchHashMethods } from '../web/resolver/search/search_hash_methods.js'; @@ -6963,6 +6964,50 @@ test('CivitAI hash search renders sizeKB when byte size is unavailable', () => { ); }); +test('search result size formatting preserves aliases and fallback values', () => { + const formatSearchResultSize = eval(`(${extractMethod(searchPanelMethodsSource, 'formatSearchResultSize')})`); + const dialog = { + formatBytes(value) { + return `${Number(value)} B`; + }, + }; + + assert.equal(formatSearchResultSize.call(dialog, { size: 2048 }), '2048 B'); + assert.equal(formatSearchResultSize.call(dialog, { size: '5.7 GB' }), '5.7 GB'); + assert.equal( + formatSearchResultSize.call(dialog, { + size: null, + file_info: { sizeBytes: 1024 }, + }), + '1024 B' + ); + assert.equal(formatSearchResultSize.call(dialog, { size: null, sizeKB: '2' }), '2048 B'); + assert.equal(formatSearchResultSize.call(dialog, { size: 0 }), '0 B'); + assert.equal(formatSearchResultSize.call(dialog, { size: null, sizeKB: 'invalid' }), ''); + assert.equal(formatSearchResultSize.call(dialog, { size: null, sizeKB: Number.MAX_VALUE }), ''); +}); + +test('info dialog size formatting preserves numeric, nested and label values', () => { + const formatInfoDialogSize = eval(`(${extractMethod(modelInfoMethodsSource, 'formatInfoDialogSize')})`); + const dialog = { + formatBytes(value) { + return `${Number(value)} B`; + }, + }; + + assert.equal(formatInfoDialogSize.call(dialog, { size: 2048 }), '2048 B'); + assert.equal(formatInfoDialogSize.call(dialog, { size: '2048' }), '2048 B'); + assert.equal( + formatInfoDialogSize.call(dialog, { file: { sizeBytes: 1024 } }), + '1024 B' + ); + assert.equal(formatInfoDialogSize.call(dialog, { size: '5.7 GB' }), '5.7 GB'); + assert.equal(formatInfoDialogSize.call(dialog, { size: 0 }), '0 B'); + assert.equal(formatInfoDialogSize.call(dialog, { size: '0' }), '0 B'); + assert.equal(formatInfoDialogSize.call(dialog, { size: 'invalid' }), 'invalid'); + assert.equal(formatInfoDialogSize.call(dialog, {}), ''); +}); + test('link/name controls place the add or search action before the input switch', () => { const renderSearchControls = eval(`(${extractMethod(searchPanelMethodsSource, 'renderSearchControls')})`); const dialog = { diff --git a/tests/test_size_utils.mjs b/tests/test_size_utils.mjs new file mode 100644 index 0000000..1e4ad0e --- /dev/null +++ b/tests/test_size_utils.mjs @@ -0,0 +1,18 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { parseFiniteNumber } from '../web/resolver/utils/size_utils.js'; + +test('parseFiniteNumber normalizes numeric size values', () => { + assert.equal(parseFiniteNumber(2048), 2048); + assert.equal(parseFiniteNumber('2048'), 2048); + assert.equal(parseFiniteNumber('0'), 0); + assert.equal(parseFiniteNumber(0), 0); +}); + +test('parseFiniteNumber rejects empty, invalid and non-finite values', () => { + assert.equal(parseFiniteNumber(null), null); + assert.equal(parseFiniteNumber(undefined), null); + assert.equal(parseFiniteNumber(''), null); + assert.equal(parseFiniteNumber('invalid'), null); + assert.equal(parseFiniteNumber(Infinity), null); +}); diff --git a/web/resolver/search/search_panel.js b/web/resolver/search/search_panel.js index 048e4b7..659abe7 100644 --- a/web/resolver/search/search_panel.js +++ b/web/resolver/search/search_panel.js @@ -4,6 +4,7 @@ import { getSourceDisplayLabel, normalizeSourceKey } from "../utils/source_label import { getCivitaiModelUrl } from "../globals.js"; import { safeStorage, normalizePathIdentity } from "../utils/html_utils.js"; import { getSha256Field } from "../utils/hash_utils.js"; +import { parseFiniteNumber } from "../utils/size_utils.js"; import { bindEventOnce } from "../utils/dom_patch_utils.js"; import { classifyLocalMatches, @@ -1552,8 +1553,8 @@ export const searchPanelMethods = { const sizeKb = candidate.sizeKB ?? candidate.size_kb; if (sizeKb !== undefined && sizeKb !== null && sizeKb !== '') { - const sizeBytes = Number(sizeKb) * 1024; - if (Number.isFinite(sizeBytes)) return this.formatBytes(sizeBytes); + const sizeBytes = parseFiniteNumber(sizeKb * 1024); + if (sizeBytes !== null) return this.formatBytes(sizeBytes); } } diff --git a/web/resolver/utils/size_utils.js b/web/resolver/utils/size_utils.js new file mode 100644 index 0000000..4da1c34 --- /dev/null +++ b/web/resolver/utils/size_utils.js @@ -0,0 +1,6 @@ +export function parseFiniteNumber(value) { + if (value === null || value === undefined || value === '') return null; + + const numericValue = Number(value); + return Number.isFinite(numericValue) ? numericValue : null; +} diff --git a/web/resolver/views/model_info_methods.js b/web/resolver/views/model_info_methods.js index a752fc4..ecd37e7 100644 --- a/web/resolver/views/model_info_methods.js +++ b/web/resolver/views/model_info_methods.js @@ -4,6 +4,7 @@ import { escapeHtml, escapeJsString, getFilenameFromPath, sanitizeDescriptionHtm import { getModelCardUrl } from "../utils/url_utils.js"; import { extractComfyWorkflow } from "../utils/workflow_metadata.js"; import { getSha256Field, normalizeSha256 } from "../utils/hash_utils.js"; +import { parseFiniteNumber } from "../utils/size_utils.js"; import { getSourceDisplayLabel } from "../utils/source_labels.js"; import { getCivitaiModelUrl } from "../globals.js"; export const modelInfoMethods = { @@ -1263,8 +1264,8 @@ export const modelInfoMethods = { if (rawSize === null || rawSize === undefined || rawSize === '') return ''; if (rawSize === 0 || rawSize === '0') return '0 B'; - const numericSize = Number(rawSize); - if (Number.isFinite(numericSize)) { + const numericSize = parseFiniteNumber(rawSize); + if (numericSize !== null) { return typeof this.formatBytes === 'function' ? this.formatBytes(numericSize) : `${numericSize} B`;