diff --git a/static/js/components/shared/ModelVersionsTab.js b/static/js/components/shared/ModelVersionsTab.js index 6b1e5364..b8061270 100644 --- a/static/js/components/shared/ModelVersionsTab.js +++ b/static/js/components/shared/ModelVersionsTab.js @@ -19,19 +19,77 @@ function escapeHtml(value) { .replace(/'/g, '''); } +function extractExtension(value) { + if (value == null) { + return ''; + } + const targets = []; + const stringValue = String(value); + if (stringValue) { + targets.push(stringValue); + stringValue.split(/[?&=]/).forEach(fragment => { + if (fragment) { + targets.push(fragment); + } + }); + } + + for (const target of targets) { + let candidate = target; + try { + candidate = decodeURIComponent(candidate); + } catch (error) { + // ignoring malformed sequences, fallback to raw value + } + const lastDot = candidate.lastIndexOf('.'); + if (lastDot === -1) { + continue; + } + const extension = candidate.slice(lastDot).toLowerCase(); + if (extension.includes('/') || extension.includes('\\')) { + continue; + } + return extension; + } + + return ''; +} + function isVideoUrl(url) { if (!url || typeof url !== 'string') { return false; } + + const candidates = new Set(); + const addCandidate = value => { + if (value == null) { + return; + } + const stringValue = String(value); + if (!stringValue) { + return; + } + candidates.add(stringValue); + }; + + addCandidate(url); + try { const parsed = new URL(url, window.location.origin); - const pathname = parsed.pathname || ''; - const extension = pathname.slice(pathname.lastIndexOf('.')).toLowerCase(); - return VIDEO_EXTENSIONS.includes(extension); + addCandidate(parsed.pathname); + parsed.searchParams.forEach(value => addCandidate(value)); } catch (error) { - const normalized = url.split('?')[0].toLowerCase(); - return VIDEO_EXTENSIONS.some(ext => normalized.endsWith(ext)); + // ignore parse errors and rely on fallbacks below } + + for (const candidate of candidates) { + const extension = extractExtension(candidate); + if (extension && VIDEO_EXTENSIONS.includes(extension)) { + return true; + } + } + + return false; } function formatDateLabel(value) { diff --git a/tests/frontend/components/modelVersionsTab.media.test.js b/tests/frontend/components/modelVersionsTab.media.test.js new file mode 100644 index 00000000..b8b3a287 --- /dev/null +++ b/tests/frontend/components/modelVersionsTab.media.test.js @@ -0,0 +1,149 @@ +import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest'; + +const { + MODEL_VERSIONS_MODULE, + API_FACTORY_MODULE, + DOWNLOAD_MANAGER_MODULE, + UI_HELPERS_MODULE, + STATE_MODULE, + I18N_HELPERS_MODULE, + UTILS_MODULE, +} = vi.hoisted(() => ({ + MODEL_VERSIONS_MODULE: new URL('../../../static/js/components/shared/ModelVersionsTab.js', import.meta.url).pathname, + API_FACTORY_MODULE: new URL('../../../static/js/api/modelApiFactory.js', import.meta.url).pathname, + DOWNLOAD_MANAGER_MODULE: new URL('../../../static/js/managers/DownloadManager.js', import.meta.url).pathname, + UI_HELPERS_MODULE: new URL('../../../static/js/utils/uiHelpers.js', import.meta.url).pathname, + STATE_MODULE: new URL('../../../static/js/state/index.js', import.meta.url).pathname, + I18N_HELPERS_MODULE: new URL('../../../static/js/utils/i18nHelpers.js', import.meta.url).pathname, + UTILS_MODULE: new URL('../../../static/js/components/shared/utils.js', import.meta.url).pathname, +})); + +vi.mock(DOWNLOAD_MANAGER_MODULE, () => ({ + downloadManager: { + downloadVersionWithDefaults: vi.fn(), + }, +})); + +vi.mock(UI_HELPERS_MODULE, () => ({ + showToast: vi.fn(), +})); + +const stateMock = { global: { settings: { autoplay_on_hover: false } } }; +vi.mock(STATE_MODULE, () => ({ + state: stateMock, +})); + +vi.mock(I18N_HELPERS_MODULE, () => ({ + translate: vi.fn((_, __, fallback) => fallback ?? ''), +})); + +vi.mock(UTILS_MODULE, () => ({ + formatFileSize: vi.fn(() => '1 MB'), +})); + +vi.mock(API_FACTORY_MODULE, () => ({ + getModelApiClient: vi.fn(), +})); + +describe('ModelVersionsTab media rendering', () => { + let getModelApiClient; + let fetchModelUpdateVersions; + + beforeEach(async () => { + vi.resetModules(); + document.body.innerHTML = ` +