diff --git a/tests/test_search_filters.mjs b/tests/test_search_filters.mjs new file mode 100644 index 0000000..7316770 --- /dev/null +++ b/tests/test_search_filters.mjs @@ -0,0 +1,37 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { matchesSearchText, normalizeSearchToken } from '../web/resolver/utils/search_utils.js'; +import { baseModelAliasMethods } from '../web/resolver/search/base_model_alias_methods.js'; + +const projectRoot = path.resolve(import.meta.dirname, '..'); +const downloadTargetMethodsSource = fs.readFileSync( + path.join(projectRoot, 'web/resolver/search/download_target_methods.js'), + 'utf8' +); +const resolveDownloadMethodsSource = fs.readFileSync( + path.join(projectRoot, 'web/resolver/actions/resolve_download_methods.js'), + 'utf8' +); + +test('search text matching preserves raw, normalized, and empty-query behavior', () => { + assert.equal(matchesSearchText('Checkpoints Check Points', ''), true); + assert.equal(matchesSearchText('Flux.1 D', 'flux'), true); + assert.equal(matchesSearchText('Flux.1 D', 'flux1d'), true); + assert.equal(matchesSearchText('Flux.1 D', 'flux-1-d'), true); + assert.equal(matchesSearchText('Flux.1 D', 'sdxl'), false); + assert.equal(matchesSearchText('Flux.1 D', '---'), false); +}); + +test('base model token normalization uses the shared search token contract', () => { + const dialog = { ...baseModelAliasMethods }; + for (const value of ['', ' Flux.1 D ', 'SDXL 1.0', 'Pony XL']) { + assert.equal(dialog.normalizeBaseModelToken(value), normalizeSearchToken(value)); + } +}); + +test('download category and base model dropdowns use the shared matcher', () => { + assert.match(downloadTargetMethodsSource, /matchesSearchText\(`\$\{option\.value\} \$\{option\.label\}`, filter\)/); + assert.match(resolveDownloadMethodsSource, /matchesSearchText\(`\$\{option\.value\} \$\{option\.label\}`, filter\)/); +}); diff --git a/web/resolver/actions/resolve_download_methods.js b/web/resolver/actions/resolve_download_methods.js index 5bf8a59..c3e8db4 100644 --- a/web/resolver/actions/resolve_download_methods.js +++ b/web/resolver/actions/resolve_download_methods.js @@ -6,6 +6,7 @@ import { normalizeSha256 } from "../utils/hash_utils.js"; import { normalizePathIdentity } from "../utils/html_utils.js"; import { bindEventOnce, syncElementAttributes } from "../utils/dom_patch_utils.js"; import { bindDownloadActionHandlers } from "../utils/download_action_handlers.js"; +import { matchesSearchText } from "../utils/search_utils.js"; import { normalizeSourceKey } from "../utils/source_labels.js"; const log = createModuleLogger('resolve_download_methods'); @@ -3359,7 +3360,6 @@ export const resolveDownloadMethods = { const renderBaseOptions = (filterText = '') => { const filter = String(filterText || '').trim().toLowerCase(); - const normalizedFilter = this.normalizeBaseModelToken?.(filter) || filter.replace(/[^a-z0-9]+/g, ''); const options = this.getKnownBaseModelOptions() .map(option => { const label = option.value === 'auto' @@ -3367,14 +3367,7 @@ export const resolveDownloadMethods = { : option.label; return { ...option, label }; }) - .filter(option => { - if (!filter) return true; - const searchText = `${option.value} ${option.label}`.toLowerCase(); - const normalizedSearchText = this.normalizeBaseModelToken?.(searchText) - || searchText.replace(/[^a-z0-9]+/g, ''); - return searchText.includes(filter) - || (normalizedFilter && normalizedSearchText.includes(normalizedFilter)); - }); + .filter(option => matchesSearchText(`${option.value} ${option.label}`, filter)); baseList.innerHTML = options .map(option => { const label = option.label; diff --git a/web/resolver/search/base_model_alias_methods.js b/web/resolver/search/base_model_alias_methods.js index e7cf992..8ca9bd0 100644 --- a/web/resolver/search/base_model_alias_methods.js +++ b/web/resolver/search/base_model_alias_methods.js @@ -1,3 +1,5 @@ +import { normalizeSearchToken } from '../utils/search_utils.js'; + export const baseModelAliasMethods = { getBaseModelAliases() { const baseModelsList = this.baseModels?.base_models; @@ -22,9 +24,7 @@ export const baseModelAliasMethods = { }, normalizeBaseModelToken(value = '') { - return String(value || '') - .toLowerCase() - .replace(/[^a-z0-9]+/g, ''); + return normalizeSearchToken(value); }, getBaseModelTokenVariants(value = '') { diff --git a/web/resolver/search/download_target_methods.js b/web/resolver/search/download_target_methods.js index 022ec43..dd4cac4 100644 --- a/web/resolver/search/download_target_methods.js +++ b/web/resolver/search/download_target_methods.js @@ -4,6 +4,7 @@ import { createFloatingTreePicker } from "../utils/tree_picker.js"; import { normalizeDownloadCategoryValue } from "../utils/category_utils.js"; import { safeStorage } from "../utils/html_utils.js"; import { getSha256Field } from "../utils/hash_utils.js"; +import { matchesSearchText } from "../utils/search_utils.js"; const localStorage = safeStorage; const invalidWindowsPathCharacters = new RegExp( `[<>:"|?*${Array.from({ length: 32 }, (_, code) => String.fromCharCode(code)).join('')}]+`, @@ -2856,19 +2857,12 @@ export const downloadTargetMethods = { const populateCategoryOptions = (filterText = '') => { if (!categoryListEl) return; const filter = String(filterText || '').trim().toLowerCase(); - const normalizedFilter = filter.replace(/[^a-z0-9]+/g, ''); const options = this.getDownloadCategoryOptions(this.getDropdownValue(categoryEl) || 'checkpoints') .map(category => ({ value: category, label: this.getCategoryDisplayName(category) })) - .filter(option => { - if (!filter) return true; - const searchText = `${option.value} ${option.label}`.toLowerCase(); - const normalizedSearchText = searchText.replace(/[^a-z0-9]+/g, ''); - return searchText.includes(filter) - || (normalizedFilter && normalizedSearchText.includes(normalizedFilter)); - }); + .filter(option => matchesSearchText(`${option.value} ${option.label}`, filter)); renderOptions(categoryListEl, options, (value, label) => { this.setDropdownValue(categoryEl, value, label); subfolderEl.value = ''; diff --git a/web/resolver/utils/search_utils.js b/web/resolver/utils/search_utils.js new file mode 100644 index 0000000..57c2ed6 --- /dev/null +++ b/web/resolver/utils/search_utils.js @@ -0,0 +1,18 @@ +export function normalizeSearchToken(value = '') { + return String(value || '') + .toLowerCase() + .replace(/[^a-z0-9]+/g, ''); +} + +export function matchesSearchText(value = '', query = '') { + const filter = String(query || '').trim().toLowerCase(); + if (!filter) return true; + + const searchText = String(value || '').toLowerCase(); + const normalizedFilter = normalizeSearchToken(filter); + return searchText.includes(filter) + || Boolean( + normalizedFilter + && normalizeSearchToken(searchText).includes(normalizedFilter) + ); +}