diff --git a/tests/browser/workspace-images.spec.mjs b/tests/browser/workspace-images.spec.mjs index 9bba05e..87c8338 100644 --- a/tests/browser/workspace-images.spec.mjs +++ b/tests/browser/workspace-images.spec.mjs @@ -1592,6 +1592,30 @@ for (const width of [1600, 390]) { }); } +// Final review m3: D42 scrolls only when the alert meets the toast's band, never back up to one above the window. +test('Rescan does not scroll back to a discovery alert above the window', async ({ page }) => { + await page.setViewportSize({ width: 1600, height: 1100 }); + await page.route('**/prompt_manager/library/options*', route => route.abort('failed')); + await page.goto(workspace); + await expect(cards(page)).toHaveCount(2); + const alert = page.getByRole('alert'); + await expect(alert).toHaveText('Categories could not be loaded. Rescan from Import to retry.'); + await page.setViewportSize({ width: 1600, height: 260 }); + // Import is at the top; the page is scrolled down to the last card while its dialog is open. + await page.getByRole('button', { name: 'Import', exact: true }).click(); + await cards(page).last().evaluate(node => node.scrollIntoView({ block: 'end' })); + expect((await boxOf(alert)).bottom).toBeLessThan(0); + // The reload itself may move the page; what D42 must not do is scroll the alert back into view. + await alert.evaluate(node => { + window.alertScrolls = 0; + node.scrollIntoView = (...args) => { window.alertScrolls += 1; Element.prototype.scrollIntoView.apply(node, args); }; + }); + await importDialog(page).getByRole('button', { name: 'Rescan', exact: true }).click(); + await expect(toast(page)).toHaveText(/^Rescan finished\. /); + await expect(alert).toBeAttached(); + expect(await page.evaluate(() => window.alertScrolls)).toBe(0); +}); + // #249 final review m8: D36's alphabetical categories hold whatever order the server sends. test('toolbar categories are alphabetical regardless of server order', async ({ page }) => { await page.route('**/prompt_manager/library/options*', route => route.fulfill({ diff --git a/web/js/workspace/images.mjs b/web/js/workspace/images.mjs index e9d2aeb..9e41829 100644 --- a/web/js/workspace/images.mjs +++ b/web/js/workspace/images.mjs @@ -648,12 +648,15 @@ export function mountImages(main, api, parentScope, {embedded = false, fetchPage } } /** - * D42: the concept's toast stays fixed at the bottom, so a discovery alert the rescan leaves is scrolled, only when - * it must be, clear of the toast's band (its `scroll-margin-block-end`). + * D42: the concept's toast stays fixed at the bottom, so a discovery alert the rescan leaves is scrolled clear of + * the toast's band (its `scroll-margin-block-end`) only when it is in that band; elsewhere the page stays put. */ function announceRescan(message) { showWorkspaceToast(message); - if (!optionsStatus.hidden) optionsStatus.scrollIntoView({block:'nearest', inline:'nearest'}); + if (optionsStatus.hidden) return; + const {top, bottom} = optionsStatus.getBoundingClientRect(); + const band = parseFloat(getComputedStyle(optionsStatus).scrollMarginBlockEnd) || 0; + if (bottom > innerHeight - band && top < innerHeight) optionsStatus.scrollIntoView({block:'nearest', inline:'nearest'}); } async function refreshInspectionCollection() { if (scope.signal.aborted) return;