From 34ca14d7fcd78eb752c9fac9f082c2c699c8c979 Mon Sep 17 00:00:00 2001 From: Will Miao Date: Tue, 1 Sep 2026 22:54:36 +0800 Subject: [PATCH] fix(showcase): reset gallery position when loading a model's examples The module-level galleryState kept activeIndex/expanded across models (the modal is a singleton), so opening model B after navigating model A started B's gallery at A's last index. Reset activeIndex, expanded and lastNavDirection in loadExampleImages, the per-model entry point. --- .../shared/showcase/ShowcaseView.js | 7 +++++ .../components/showcase.gallery.test.js | 29 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/static/js/components/shared/showcase/ShowcaseView.js b/static/js/components/shared/showcase/ShowcaseView.js index 3406cc12..f8f4c794 100644 --- a/static/js/components/shared/showcase/ShowcaseView.js +++ b/static/js/components/shared/showcase/ShowcaseView.js @@ -54,6 +54,13 @@ export async function loadExampleImages(images, modelHash, previewUrl = '') { const showcaseTab = document.getElementById('showcase-tab'); if (!showcaseTab) return; + // Fresh load of a model's examples: reset the gallery position so a + // previously viewed model's active index / expansion state never leaks + // into this one (the modal is a singleton, state is module-level) + galleryState.activeIndex = 0; + galleryState.expanded = false; + lastNavDirection = 1; + // First fetch local example files let localFiles = []; diff --git a/tests/frontend/components/showcase.gallery.test.js b/tests/frontend/components/showcase.gallery.test.js index 57883c3d..79e19c58 100644 --- a/tests/frontend/components/showcase.gallery.test.js +++ b/tests/frontend/components/showcase.gallery.test.js @@ -288,6 +288,35 @@ describe('Showcase gallery', () => { vi.unstubAllGlobals(); }); + it('resets the gallery position when a new model is loaded', async () => { + const { renderShowcaseContent, loadExampleImages, updateMainDisplay } = await import(SHOWCASE_MODULE); + + // Model A: expand and navigate to the third example + document.body.innerHTML = `
${renderShowcaseContent(IMAGES, [], PREVIEW_URL, true)}
`; + updateMainDisplay(2); + expect(document.querySelector('.gallery-thumb.active')?.dataset.index).toBe('2'); + + // Model B opens: loadExampleImages is the per-model entry point + const modelBImages = [0, 1, 2, 3].map(i => ({ + url: `https://image.civitai.com/reset/${i}.jpeg`, width: 100, height: 100, nsfwLevel: 0, + })); + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ + json: () => Promise.resolve({ success: true, files: [] }), + })); + await loadExampleImages(modelBImages, 'model-b-hash', ''); + vi.unstubAllGlobals(); + + // The leaked index (2) must not carry over: model B starts at example 1 + const gallery = document.querySelector('.showcase-gallery'); + expect(gallery).toBeTruthy(); + expect(document.querySelector('.gallery-indicator-bar')).toBeTruthy(); + // Expand model B's gallery: it renders from index 0, not the leaked 2 + // (loadExampleImages already bound the controls via initShowcaseContent) + document.querySelector('#galleryShowBtn').click(); + expect(document.querySelector('.gallery-thumb.active')?.dataset.index).toBe('0'); + expect(document.querySelector('#galleryPosition')?.textContent).toBe('1 / 4'); + }); + it('defers video thumbnail metadata fetches until the strip shows them', async () => { const { renderShowcaseContent, initShowcaseContent } = await import(SHOWCASE_MODULE);