diff --git a/tests/browser/server.py b/tests/browser/server.py index d30119c..f1e36d0 100644 --- a/tests/browser/server.py +++ b/tests/browser/server.py @@ -145,6 +145,13 @@ def create_app(root: Path) -> web.Application: await api._run_in_executor(seed, extra, include_video) return web.json_response({"success": True}) + @routes.post("/__test__/add-nested-image") + async def add_nested_image(request: web.Request) -> web.Response: + destination = root / "output" / "project" / "day" / "new-output.png" + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(root / "output" / "shared" / "same.png", destination) + return web.json_response({"success": True}) + @routes.get("/api/settings/Comfy.ColorPalette") async def palette(request: web.Request) -> web.Response: return web.json_response("dark") diff --git a/tests/browser/workspace-images.spec.mjs b/tests/browser/workspace-images.spec.mjs index b28d0ca..9cd9931 100644 --- a/tests/browser/workspace-images.spec.mjs +++ b/tests/browser/workspace-images.spec.mjs @@ -433,3 +433,50 @@ test('refresh releases the previous inspector video source', async ({ page, requ await expect.poll(() => page.evaluate(() => !window.__oldInspectorVideo.isConnected)).toBe(true); expect(await page.evaluate(() => window.__oldInspectorVideo.getAttribute('src'))).toBeNull(); }); + +test('Refresh rescans cached files and preserves the collection when rescan fails', async ({ page, request }) => { + await page.goto(workspace); + await expect(cards(page)).toHaveCount(2); + await expect(await request.post('/__test__/add-nested-image')).toBeOK(); + await page.route('**/prompt_manager/gallery/rescan', route => route.fulfill({ + status: 503, json: { success: false, error: 'Rescan unavailable' }, + }), { times: 1 }); + await page.getByRole('button', { name: 'Refresh', exact: true }).click(); + await expect(page.getByRole('alert')).toContainText('could not be refreshed'); + await expect(cards(page)).toHaveCount(2); + await page.getByRole('button', { name: 'Refresh', exact: true }).click(); + await expect(cards(page)).toHaveCount(3); + await expect(cards(page).filter({ hasText: 'new-output.png' })).toHaveCount(1); + await expect(page.getByRole('alert')).toBeHidden(); +}); + +test('folder discovery offers ancestors that filter nested images', async ({ page, request }) => { + await expect(await request.post('/__test__/add-nested-image')).toBeOK(); + await page.goto(workspace); + await expect(cards(page)).toHaveCount(3); + const folder = page.getByRole('combobox', { name: 'Folder', exact: true }); + await expect(folder.locator('option[value="project"]')).toHaveCount(1); + await folder.selectOption('project'); + await expect(cards(page)).toHaveCount(1); + await expect(cards(page)).toContainText('new-output.png'); +}); + +test('leaving during Refresh cancels it without replacing the next workspace', async ({ page }) => { + let release; + const pending = new Promise(resolve => { release = resolve; }); + await page.route('**/prompt_manager/gallery/rescan', async route => { + await pending; + await route.fulfill({ json: { success: true } }).catch(() => {}); + }); + await page.goto(workspace); + await expect(cards(page)).toHaveCount(2); + const requested = page.waitForRequest('**/prompt_manager/gallery/rescan'); + await page.getByRole('button', { name: 'Refresh', exact: true }).click(); + await requested; + await page.getByRole('navigation', { name: 'Workspace', exact: true }) + .getByRole('link', { name: 'Overview', exact: true }).click(); + await expect(page.locator('#workspace-main h1')).toHaveText('Overview'); + release(); + await expect(page.getByRole('heading', { name: 'Continue creating', exact: true })).toBeVisible(); + await expect(page.getByText('Images could not be refreshed. Try Refresh again.', { exact: true })).toHaveCount(0); +}); diff --git a/web/js/workspace/images.mjs b/web/js/workspace/images.mjs index 340af76..bdf3d2e 100644 --- a/web/js/workspace/images.mjs +++ b/web/js/workspace/images.mjs @@ -61,7 +61,24 @@ export function mountImages(main, api, parentScope) { layouts.append(button); } const refresh = createButton('Refresh'); - scope.listen(refresh, 'click', () => { loadOptions(); reset(); }); + scope.listen(refresh, 'click', async () => { + if (refresh.disabled) return; + refresh.disabled = true; + optionsRequest?.abort(); optionsRequest = null; + optionsStatus.hidden = true; + try { + await api.post('/prompt_manager/gallery/rescan', {}, {signal:scope.signal}); + if (scope.signal.aborted) return; + await loadOptions(); + if (!scope.signal.aborted) reset(); + } catch { + if (scope.signal.aborted) return; + optionsStatus.textContent = 'Images could not be refreshed. Try Refresh again.'; + optionsStatus.hidden = false; + } finally { + refresh.disabled = false; + } + }); const currentGallery = el('a', 'Current gallery', 'workspace-link'); currentGallery.href = resolveUrl('/prompt_manager/gallery'); toolbar.append(count, layouts, refresh, currentGallery); @@ -307,7 +324,7 @@ export function mountImages(main, api, parentScope) { optionsRequest?.abort(); const request = new AbortController(); optionsRequest = request; try { - const params = new URLSearchParams({mode:'workspace'}); + const params = new URLSearchParams({mode:'workspace', include_ancestors:'true'}); if (filters.root !== 'all') params.set('root', filters.root); const data = await api.get(`/prompt_manager/gallery/subfolders?${params}`, {signal:request.signal}); if (scope.signal.aborted || request !== optionsRequest) return;