diff --git a/locales/de.json b/locales/de.json index 15b4cf47..e6a43fdc 100644 --- a/locales/de.json +++ b/locales/de.json @@ -938,6 +938,18 @@ "viewLocalVersions": "Alle lokalen Versionen anzeigen", "viewLocalTooltip": "Demnächst verfügbar" }, + "filters": { + "label": "Basisfilter", + "state": { + "showAll": "Alle Versionen", + "showSameBase": "Gleiches Basismodell" + }, + "tooltip": { + "showAllVersions": "Wechseln, um alle Versionen anzuzeigen", + "showSameBaseVersions": "Wechseln, um nur Versionen mit demselben Basismodell anzuzeigen" + }, + "empty": "Keine Versionen entsprechen dem Filter für das aktuelle Basismodell." + }, "empty": "Noch keine Versionshistorie für dieses Modell vorhanden.", "error": "Versionen konnten nicht geladen werden.", "missingModelId": "Für dieses Modell ist keine Civitai-Model-ID vorhanden.", diff --git a/locales/en.json b/locales/en.json index 7ac844da..19ed4b4f 100644 --- a/locales/en.json +++ b/locales/en.json @@ -938,6 +938,18 @@ "viewLocalVersions": "View all local versions", "viewLocalTooltip": "Coming soon" }, + "filters": { + "label": "Base filter", + "state": { + "showAll": "All versions", + "showSameBase": "Same base" + }, + "tooltip": { + "showAllVersions": "Switch to showing all versions", + "showSameBaseVersions": "Switch to showing only versions that match the current base model" + }, + "empty": "No versions match the current base model filter." + }, "empty": "No version history available for this model yet.", "error": "Failed to load versions.", "missingModelId": "This model is missing a Civitai model id.", diff --git a/locales/es.json b/locales/es.json index 89333ca8..4395c7e7 100644 --- a/locales/es.json +++ b/locales/es.json @@ -938,6 +938,18 @@ "viewLocalVersions": "Ver todas las versiones locales", "viewLocalTooltip": "Disponible pronto" }, + "filters": { + "label": "Filtro base", + "state": { + "showAll": "Todas las versiones", + "showSameBase": "Mismo modelo base" + }, + "tooltip": { + "showAllVersions": "Cambiar para mostrar todas las versiones", + "showSameBaseVersions": "Cambiar para mostrar solo versiones del mismo modelo base" + }, + "empty": "Ninguna versión coincide con el filtro del modelo base actual." + }, "empty": "Aún no hay historial de versiones para este modelo.", "error": "No se pudieron cargar las versiones.", "missingModelId": "Este modelo no tiene un ID de modelo de Civitai.", diff --git a/locales/fr.json b/locales/fr.json index cf3c8ce3..68261795 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -938,6 +938,18 @@ "viewLocalVersions": "Voir toutes les versions locales", "viewLocalTooltip": "Bientôt disponible" }, + "filters": { + "label": "Filtre de base", + "state": { + "showAll": "Toutes les versions", + "showSameBase": "Même modèle de base" + }, + "tooltip": { + "showAllVersions": "Passer à l'affichage de toutes les versions", + "showSameBaseVersions": "Passer à l'affichage des versions du même modèle de base" + }, + "empty": "Aucune version ne correspond au filtre du modèle de base actuel." + }, "empty": "Aucun historique de versions n'est disponible pour ce modèle pour le moment.", "error": "Échec du chargement des versions.", "missingModelId": "Ce modèle ne possède pas d'identifiant de modèle Civitai.", diff --git a/locales/he.json b/locales/he.json index ac9571c6..ec33f7db 100644 --- a/locales/he.json +++ b/locales/he.json @@ -938,6 +938,18 @@ "viewLocalVersions": "הצג את כל הגרסאות המקומיות", "viewLocalTooltip": "יגיע בקרוב" }, + "filters": { + "label": "מסנן בסיס", + "state": { + "showAll": "כל הגרסאות", + "showSameBase": "אותו מודל בסיס" + }, + "tooltip": { + "showAllVersions": "החלף להצגת כל הגרסאות", + "showSameBaseVersions": "החלף להצגת גרסאות עם אותו מודל בסיס" + }, + "empty": "אין גרסאות התואמות את המסנן של מודל הבסיס הנוכחי." + }, "empty": "אין עדיין היסטוריית גרסאות למודל זה.", "error": "טעינת הגרסאות נכשלה.", "missingModelId": "למודל זה אין מזהה מודל של Civitai.", diff --git a/locales/ja.json b/locales/ja.json index ad5b300f..45ff3c10 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -938,6 +938,18 @@ "viewLocalVersions": "ローカルの全バージョンを表示", "viewLocalTooltip": "近日対応予定" }, + "filters": { + "label": "ベースフィルター", + "state": { + "showAll": "すべてのバージョン", + "showSameBase": "同じベース" + }, + "tooltip": { + "showAllVersions": "すべてのバージョンを表示する", + "showSameBaseVersions": "同じベースモデルのバージョンのみ表示する" + }, + "empty": "現在のベースモデルフィルターに一致するバージョンがありません。" + }, "empty": "このモデルにはまだバージョン履歴がありません。", "error": "バージョンの読み込みに失敗しました。", "missingModelId": "このモデルにはCivitaiのモデルIDがありません。", diff --git a/locales/ko.json b/locales/ko.json index 2fef79b2..ccedfc57 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -938,6 +938,18 @@ "viewLocalVersions": "로컬 버전 모두 보기", "viewLocalTooltip": "곧 제공 예정" }, + "filters": { + "label": "기본 필터", + "state": { + "showAll": "모든 버전", + "showSameBase": "같은 베이스" + }, + "tooltip": { + "showAllVersions": "모든 버전을 표시하도록 전환", + "showSameBaseVersions": "같은 베이스 모델 버전만 표시하도록 전환" + }, + "empty": "현재 베이스 모델 필터와 일치하는 버전이 없습니다." + }, "empty": "이 모델에는 아직 버전 기록이 없습니다.", "error": "버전을 불러오지 못했습니다.", "missingModelId": "이 모델에는 Civitai 모델 ID가 없습니다.", diff --git a/locales/ru.json b/locales/ru.json index 2d739730..d703ca4e 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -938,6 +938,18 @@ "viewLocalVersions": "Показать все локальные версии", "viewLocalTooltip": "Скоро появится" }, + "filters": { + "label": "Фильтр по базе", + "state": { + "showAll": "Все версии", + "showSameBase": "Тот же базовый" + }, + "tooltip": { + "showAllVersions": "Переключиться на отображение всех версий", + "showSameBaseVersions": "Переключиться на отображение только версий с тем же базовым" + }, + "empty": "Нет версий, соответствующих текущему фильтру базовой модели." + }, "empty": "Для этой модели пока нет истории версий.", "error": "Не удалось загрузить версии.", "missingModelId": "У этой модели отсутствует идентификатор модели Civitai.", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 1742ab69..e3804de8 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -938,6 +938,18 @@ "viewLocalVersions": "查看所有本地版本", "viewLocalTooltip": "敬请期待" }, + "filters": { + "label": "基础筛选", + "state": { + "showAll": "全部版本", + "showSameBase": "相同基模型" + }, + "tooltip": { + "showAllVersions": "切换为显示所有版本", + "showSameBaseVersions": "仅显示与当前基模型匹配的版本" + }, + "empty": "没有与当前基模型筛选匹配的版本。" + }, "empty": "该模型还没有版本历史。", "error": "加载版本失败。", "missingModelId": "该模型缺少 Civitai 模型 ID。", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index c8404531..9416fd8a 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -938,6 +938,18 @@ "viewLocalVersions": "檢視所有本地版本", "viewLocalTooltip": "敬請期待" }, + "filters": { + "label": "基礎篩選", + "state": { + "showAll": "所有版本", + "showSameBase": "相同基礎模型" + }, + "tooltip": { + "showAllVersions": "切換為顯示所有版本", + "showSameBaseVersions": "僅顯示與目前基礎模型相符的版本" + }, + "empty": "沒有符合目前基礎模型篩選的版本。" + }, "empty": "此模型尚無版本歷史。", "error": "載入版本失敗。", "missingModelId": "此模型缺少 Civitai 模型 ID。", diff --git a/static/css/components/lora-modal/versions.css b/static/css/components/lora-modal/versions.css index 1eaa9eac..929bf6d4 100644 --- a/static/css/components/lora-modal/versions.css +++ b/static/css/components/lora-modal/versions.css @@ -24,12 +24,29 @@ color: var(--text-color); } +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} + .versions-toolbar-info p { margin: 0; font-size: 0.85rem; color: var(--text-muted); } +.versions-toolbar-info-heading { + display: flex; + align-items: center; + gap: var(--space-2); +} + .versions-toolbar-actions { display: flex; flex-wrap: wrap; @@ -68,6 +85,41 @@ color: var(--text-color); } +.versions-filter-toggle { + appearance: none; + border: 1px solid var(--border-color); + border-radius: var(--border-radius-sm); + padding: 0; + margin-bottom: 4px; + width: 30px; + height: 30px; + background: color-mix(in oklch, var(--card-bg) 80%, var(--bg-color)); + align-self: center; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--text-muted); + transition: border-color 0.2s ease, background 0.2s ease, color 0.2s ease, transform 0.2s ease; + position: relative; + cursor: pointer; +} + +.versions-filter-toggle i { + font-size: 1rem; +} + +.versions-filter-toggle:hover:not(:disabled) { + border-color: var(--text-color); + color: var(--text-color); + transform: translateY(-1px); +} + +.versions-filter-toggle[data-filter-active="true"] { + border-color: color-mix(in oklch, var(--lora-accent) 65%, transparent); + color: var(--lora-accent); + background: color-mix(in oklch, var(--lora-accent) 20%, var(--card-bg) 80%); +} + .versions-toolbar-btn:disabled { opacity: 0.6; cursor: not-allowed; diff --git a/static/js/components/shared/ModelVersionsTab.js b/static/js/components/shared/ModelVersionsTab.js index 8aff2efd..8b04e25c 100644 --- a/static/js/components/shared/ModelVersionsTab.js +++ b/static/js/components/shared/ModelVersionsTab.js @@ -152,6 +152,81 @@ function buildBadge(label, tone) { return `${escapeHtml(label)}`; } +const DISPLAY_FILTER_MODES = Object.freeze({ + SAME_BASE: 'same_base', + ANY: 'any', +}); + +const FILTER_LABEL_KEY = 'modals.model.versions.filters.label'; +const FILTER_STATE_KEYS = { + [DISPLAY_FILTER_MODES.SAME_BASE]: 'modals.model.versions.filters.state.showSameBase', + [DISPLAY_FILTER_MODES.ANY]: 'modals.model.versions.filters.state.showAll', +}; +const FILTER_TOOLTIP_KEYS = { + [DISPLAY_FILTER_MODES.SAME_BASE]: 'modals.model.versions.filters.tooltip.showAllVersions', + [DISPLAY_FILTER_MODES.ANY]: 'modals.model.versions.filters.tooltip.showSameBaseVersions', +}; + +function normalizeBaseModelName(value) { + if (typeof value !== 'string') { + return null; + } + const trimmed = value.trim(); + if (!trimmed) { + return null; + } + return trimmed.toLowerCase(); +} + +function getToggleLabelText() { + return translate(FILTER_LABEL_KEY, {}, 'Base filter'); +} + +function getToggleStateText(mode) { + const key = FILTER_STATE_KEYS[mode] || FILTER_STATE_KEYS[DISPLAY_FILTER_MODES.ANY]; + const fallback = + mode === DISPLAY_FILTER_MODES.SAME_BASE ? 'Same base' : 'All versions'; + return translate(key, {}, fallback); +} + +function getToggleTooltipText(mode) { + const key = + FILTER_TOOLTIP_KEYS[mode] || FILTER_TOOLTIP_KEYS[DISPLAY_FILTER_MODES.ANY]; + const fallback = + mode === DISPLAY_FILTER_MODES.SAME_BASE + ? 'Switch to showing all versions' + : 'Switch to showing only versions with the current base model'; + return translate(key, {}, fallback); +} + +function getDefaultDisplayMode() { + const strategy = state?.global?.settings?.update_flag_strategy; + return strategy === DISPLAY_FILTER_MODES.SAME_BASE + ? DISPLAY_FILTER_MODES.SAME_BASE + : DISPLAY_FILTER_MODES.ANY; +} + +function getCurrentVersionBaseModel(record, versionId) { + if (!record || typeof versionId !== 'number' || !Array.isArray(record.versions)) { + return { + normalized: null, + raw: null, + }; + } + const currentVersion = record.versions.find(v => v.versionId === versionId); + if (!currentVersion) { + return { + normalized: null, + raw: null, + }; + } + const baseModelRaw = currentVersion.baseModel ?? null; + return { + normalized: normalizeBaseModelName(baseModelRaw), + raw: baseModelRaw, + }; +} + function getAutoplaySetting() { try { return Boolean(state?.global?.settings?.autoplay_on_hover); @@ -314,7 +389,7 @@ function getLatestLibraryVersionId(record) { return Math.max(...record.inLibraryVersionIds); } -function renderToolbar(record) { +function renderToolbar(record, toolbarState = {}) { const ignoreText = record.shouldIgnore ? translate('modals.model.versions.actions.resumeModelUpdates', {}, 'Resume updates for this model') : translate('modals.model.versions.actions.ignoreModelUpdates', {}, 'Ignore updates for this model'); @@ -325,10 +400,23 @@ function renderToolbar(record) { 'Track and manage every version of this model in one place.' ); + const displayMode = toolbarState.displayMode || DISPLAY_FILTER_MODES.ANY; + const toggleLabel = getToggleLabelText(); + const toggleState = getToggleStateText(displayMode); + const toggleTooltip = getToggleTooltipText(displayMode); + const filterActive = toolbarState.isFilteringActive ? 'true' : 'false'; + const screenReaderText = [toggleLabel, toggleState].filter(Boolean).join(': '); + return `
-

${translate('modals.model.versions.heading', {}, 'Model versions')}

+
+

${translate('modals.model.versions.heading', {}, 'Model versions')}

+ +

${escapeHtml(infoText)}

@@ -353,6 +441,20 @@ function renderEmptyState(container) { `; } +function renderFilteredEmptyState(baseModelLabel) { + const message = translate( + 'modals.model.versions.filters.empty', + { baseModel: baseModelLabel }, + 'No versions match the current base model filter.' + ); + return ` +
+ +

${escapeHtml(message)}

+
+ `; +} + function renderErrorState(container, message) { const fallback = translate('modals.model.versions.error', {}, 'Failed to load versions.'); container.innerHTML = ` @@ -391,6 +493,8 @@ export function initVersionsTab({ record: null, }; + let displayMode = getDefaultDisplayMode(); + let apiClient; function ensureClient() { @@ -414,55 +518,92 @@ export function initVersionsTab({ `; } - function render(record) { - controller.record = record; - controller.hasLoaded = true; +function render(record) { + controller.record = record; + controller.hasLoaded = true; - if (!record || !Array.isArray(record.versions) || record.versions.length === 0) { - renderEmptyState(container); - return; - } - - const latestLibraryVersionId = getLatestLibraryVersionId(record); - let dividerInserted = false; - - const sortedVersions = [...record.versions].sort( - (a, b) => Number(b.versionId) - Number(a.versionId) - ); - - const rowsMarkup = sortedVersions - .map(version => { - const isNewer = - typeof latestLibraryVersionId === 'number' && - version.versionId > latestLibraryVersionId; - let markup = ''; - if ( - !dividerInserted && - typeof latestLibraryVersionId === 'number' && - !isNewer - ) { - dividerInserted = true; - markup += ''; - } - markup += renderRow(version, { - latestLibraryVersionId, - currentVersionId: normalizedCurrentVersionId, - modelId: record?.modelId ?? modelId, - }); - return markup; - }) - .join(''); - - container.innerHTML = ` - ${renderToolbar(record)} -
- ${rowsMarkup} -
- `; - - setupMediaHoverInteractions(container); + if (!record || !Array.isArray(record.versions) || record.versions.length === 0) { + renderEmptyState(container); + return; } + const latestLibraryVersionId = getLatestLibraryVersionId(record); + const { normalized: currentBaseModelNormalized, raw: currentBaseModelLabel } = + getCurrentVersionBaseModel(record, normalizedCurrentVersionId); + const isFilteringActive = + displayMode === DISPLAY_FILTER_MODES.SAME_BASE && + Boolean(currentBaseModelNormalized); + + const sortedVersions = [...record.versions].sort( + (a, b) => Number(b.versionId) - Number(a.versionId) + ); + + const filteredVersions = sortedVersions.filter(version => { + if (!isFilteringActive) { + return true; + } + return normalizeBaseModelName(version.baseModel) === currentBaseModelNormalized; + }); + + const dividerThresholdVersionId = (() => { + if (!isFilteringActive) { + return latestLibraryVersionId; + } + const baseLocalVersionIds = record.versions + .filter( + version => + version.isInLibrary && + normalizeBaseModelName(version.baseModel) === currentBaseModelNormalized && + typeof version.versionId === 'number' + ) + .map(version => version.versionId); + if (!baseLocalVersionIds.length) { + return null; + } + return Math.max(...baseLocalVersionIds); + })(); + + let dividerInserted = false; + + const rowsMarkup = filteredVersions + .map(version => { + const isNewer = + typeof latestLibraryVersionId === 'number' && + version.versionId > latestLibraryVersionId; + let markup = ''; + if ( + !dividerInserted && + typeof dividerThresholdVersionId === 'number' && + !(version.versionId > dividerThresholdVersionId) + ) { + dividerInserted = true; + markup += ''; + } + markup += renderRow(version, { + latestLibraryVersionId, + currentVersionId: normalizedCurrentVersionId, + modelId: record?.modelId ?? modelId, + }); + return markup; + }) + .join(''); + + const listContent = + rowsMarkup || renderFilteredEmptyState(currentBaseModelLabel); + + container.innerHTML = ` + ${renderToolbar(record, { + displayMode, + isFilteringActive, + })} +
+ ${listContent} +
+ `; + + setupMediaHoverInteractions(container); +} + async function loadVersions({ forceRefresh = false, eager = false } = {}) { if (controller.isLoading) { return; @@ -531,6 +672,17 @@ export function initVersionsTab({ } } + function handleToggleVersionDisplayMode() { + displayMode = + displayMode === DISPLAY_FILTER_MODES.SAME_BASE + ? DISPLAY_FILTER_MODES.ANY + : DISPLAY_FILTER_MODES.SAME_BASE; + if (!controller.record) { + return; + } + render(controller.record); + } + async function handleToggleVersionIgnore(button, versionId) { if (!controller.record) { return; @@ -799,9 +951,17 @@ export function initVersionsTab({ const toolbarAction = event.target.closest('[data-versions-action]'); if (toolbarAction) { const action = toolbarAction.dataset.versionsAction; - if (action === 'toggle-model-ignore') { - event.preventDefault(); - await handleToggleModelIgnore(toolbarAction); + switch (action) { + case 'toggle-model-ignore': + event.preventDefault(); + await handleToggleModelIgnore(toolbarAction); + break; + case 'toggle-version-display-mode': + event.preventDefault(); + handleToggleVersionDisplayMode(); + break; + default: + break; } return; } diff --git a/tests/frontend/components/modelVersionsTab.media.test.js b/tests/frontend/components/modelVersionsTab.media.test.js index b8b3a287..a4c6af7c 100644 --- a/tests/frontend/components/modelVersionsTab.media.test.js +++ b/tests/frontend/components/modelVersionsTab.media.test.js @@ -28,7 +28,14 @@ vi.mock(UI_HELPERS_MODULE, () => ({ showToast: vi.fn(), })); -const stateMock = { global: { settings: { autoplay_on_hover: false } } }; +const stateMock = { + global: { + settings: { + autoplay_on_hover: false, + update_flag_strategy: 'any', + }, + }, +}; vi.mock(STATE_MODULE, () => ({ state: stateMock, })); @@ -59,6 +66,7 @@ describe('ModelVersionsTab media rendering', () => {
`; stateMock.global.settings.autoplay_on_hover = false; + stateMock.global.settings.update_flag_strategy = 'any'; ({ getModelApiClient } = await import(API_FACTORY_MODULE)); fetchModelUpdateVersions = vi.fn(); getModelApiClient.mockReturnValue({ @@ -146,4 +154,133 @@ describe('ModelVersionsTab media rendering', () => { expect(imageElement?.getAttribute('src')).toBe(previewUrl); expect(document.querySelector('.version-media video')).toBeFalsy(); }); + + it('shows a stable label with a short state indicator', async () => { + stateMock.global.settings.update_flag_strategy = 'any'; + fetchModelUpdateVersions.mockResolvedValue({ + success: true, + record: { + shouldIgnore: false, + inLibraryVersionIds: [5], + versions: [ + { + versionId: 5, + name: 'base', + baseModel: 'SDXL', + previewUrl: '/api/lm/previews/v-base.png', + sizeBytes: 1024, + isInLibrary: true, + shouldIgnore: false, + }, + ], + }, + }); + + const { initVersionsTab } = await import(MODEL_VERSIONS_MODULE); + const controller = initVersionsTab({ + modalId: 'model-versions-modal', + modelType: 'loras', + modelId: 321, + currentVersionId: 5, + }); + + await controller.load(); + + const toggleText = document.querySelector('.versions-filter-toggle .sr-only'); + expect(toggleText?.textContent?.trim()).toBe('Base filter: All versions'); + }); + + it('filters versions to the current base model when strategy is same_base', async () => { + stateMock.global.settings.update_flag_strategy = 'same_base'; + fetchModelUpdateVersions.mockResolvedValue({ + success: true, + record: { + shouldIgnore: false, + inLibraryVersionIds: [10], + versions: [ + { + versionId: 10, + name: 'v1.0', + baseModel: 'SDXL', + previewUrl: '/api/lm/previews/v1.png', + sizeBytes: 1024, + isInLibrary: true, + shouldIgnore: false, + }, + { + versionId: 11, + name: 'v1.1', + baseModel: 'Realistic', + previewUrl: '/api/lm/previews/v1-1.png', + sizeBytes: 2048, + isInLibrary: false, + shouldIgnore: false, + }, + ], + }, + }); + + const { initVersionsTab } = await import(MODEL_VERSIONS_MODULE); + const controller = initVersionsTab({ + modalId: 'model-versions-modal', + modelType: 'loras', + modelId: 789, + currentVersionId: 10, + }); + + await controller.load(); + + expect(document.querySelectorAll('.model-version-row').length).toBe(1); + }); + + it('toggle button can switch to display all versions', async () => { + stateMock.global.settings.update_flag_strategy = 'same_base'; + fetchModelUpdateVersions.mockResolvedValue({ + success: true, + record: { + shouldIgnore: false, + inLibraryVersionIds: [10], + versions: [ + { + versionId: 10, + name: 'v1.0', + baseModel: 'SDXL', + previewUrl: '/api/lm/previews/v1.png', + sizeBytes: 1024, + isInLibrary: true, + shouldIgnore: false, + }, + { + versionId: 11, + name: 'v1.1', + baseModel: 'Realistic', + previewUrl: '/api/lm/previews/v1-1.png', + sizeBytes: 2048, + isInLibrary: false, + shouldIgnore: false, + }, + ], + }, + }); + + const { initVersionsTab } = await import(MODEL_VERSIONS_MODULE); + const controller = initVersionsTab({ + modalId: 'model-versions-modal', + modelType: 'loras', + modelId: 987, + currentVersionId: 10, + }); + + await controller.load(); + + expect(document.querySelectorAll('.model-version-row').length).toBe(1); + const toggleButton = document.querySelector('[data-versions-action="toggle-version-display-mode"]'); + expect(toggleButton).toBeTruthy(); + const toggleTextBefore = document.querySelector('.versions-filter-toggle .sr-only'); + expect(toggleTextBefore?.textContent?.trim()).toContain('Same base'); + toggleButton?.click(); + expect(document.querySelectorAll('.model-version-row').length).toBe(2); + const toggleTextAfter = document.querySelector('.versions-filter-toggle .sr-only'); + expect(toggleTextAfter?.textContent?.trim()).toContain('All versions'); + }); });