fix(images): keep the discovery alert clear of the Rescan toast
A rescan that leaves the Images discovery alert visible could show its fixed bottom toast over the alert on a short window. The concept's toast keeps its placement; the production-only alert now scrolls into view only as far as needed, with a 96px block-end scroll margin (D42).
This commit is contained in:
@@ -619,6 +619,13 @@ Recorded deviations on this page (spec §8):
|
||||
and no tag row. `positive_links` on `/__test__/concept/reset` links the newest
|
||||
images to their positive alone, so the inspector comparison sees the positive's
|
||||
category ("Positive prompt category: " hidden prefix, D21) and tag filters.
|
||||
- **D42**: the concept never shows a toast and a page alert together. Production keeps
|
||||
the concept's fixed toast (centred, 22px from the bottom, 12px at 700px and below)
|
||||
and, when an Import → Rescan toast leaves the Images discovery alert
|
||||
(`.options-status`), scrolls that alert into view only as far as needed, with
|
||||
`scroll-margin-block-end: 96px`, so it is never under the toast.
|
||||
`Rescan toast does not cover the discovery alert it leaves` ends the window inside
|
||||
the toast's band at 1600 and 390 pixels and measures both boxes.
|
||||
|
||||
`tests/browser/inspector-concept.spec.mjs` compares the side panel and the viewer's
|
||||
full details (#249 D4) for alpine_00001.png, at 1600x1100 and 390x844 in both themes:
|
||||
|
||||
@@ -1559,6 +1559,39 @@ test('unreachable sources and categories announce one error', async ({ page }) =
|
||||
await expect(page.getByRole('alert')).toHaveCount(0);
|
||||
});
|
||||
|
||||
// PR D-b Task 3 (D42): the shared toast keeps the concept's fixed bottom placement, so a discovery alert that a rescan
|
||||
// leaves visible must not stay under it. The window ends just below the alert, inside the toast's band, as on a short
|
||||
// window or a phone with its browser bars shown.
|
||||
const boxOf = locator => locator.evaluate(node => {
|
||||
const { top, right, bottom, left } = node.getBoundingClientRect();
|
||||
return { top, right, bottom, left };
|
||||
});
|
||||
const overlaps = (a, b) => a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom;
|
||||
for (const width of [1600, 390]) {
|
||||
test(`Rescan toast does not cover the discovery alert it leaves at ${width}px`, async ({ page }) => {
|
||||
await page.setViewportSize({ width, 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');
|
||||
const failed = 'Categories could not be loaded. Rescan from Import to retry.';
|
||||
await expect(alert).toHaveText(failed);
|
||||
const toastInset = width > 700 ? 22 : 12;
|
||||
await page.setViewportSize({ width, height: Math.ceil((await boxOf(alert)).bottom) + toastInset + 16 });
|
||||
await rescan(page);
|
||||
await expect(toast(page)).toHaveText(/^Rescan finished\. /);
|
||||
await expect(alert).toHaveText(failed);
|
||||
await expect.poll(async () => overlaps(await boxOf(alert), await boxOf(toast(page)))).toBe(false);
|
||||
const [shown, placed] = [await boxOf(alert), await boxOf(toast(page))];
|
||||
expect(shown.top).toBeGreaterThanOrEqual(0);
|
||||
expect(shown.bottom).toBeLessThanOrEqual(placed.top);
|
||||
// The concept's toast stays where the concept puts it: centred, its inset from the bottom.
|
||||
const viewport = page.viewportSize();
|
||||
expect(Math.round(viewport.height - placed.bottom)).toBe(toastInset);
|
||||
expect(Math.abs((placed.left + placed.right) / 2 - viewport.width / 2)).toBeLessThan(1);
|
||||
});
|
||||
}
|
||||
|
||||
// #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({
|
||||
|
||||
@@ -638,15 +638,23 @@ export function mountImages(main, api, parentScope, {embedded = false, fetchPage
|
||||
if (scope.signal.aborted) return;
|
||||
renderLoadStatus();
|
||||
if (found) reset();
|
||||
if (!scope.signal.aborted) showWorkspaceToast(rescanFinished(result));
|
||||
if (!scope.signal.aborted) announceRescan(rescanFinished(result));
|
||||
} catch {
|
||||
// A failed rescan leaves the collection and any discovery alert as they were.
|
||||
if (!scope.signal.aborted) showWorkspaceToast(RESCAN_FAILED);
|
||||
if (!scope.signal.aborted) announceRescan(RESCAN_FAILED);
|
||||
} finally {
|
||||
rescanning = false;
|
||||
importDialog?.setRescanBusy(false);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 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`).
|
||||
*/
|
||||
function announceRescan(message) {
|
||||
showWorkspaceToast(message);
|
||||
if (!optionsStatus.hidden) optionsStatus.scrollIntoView({block:'nearest', inline:'nearest'});
|
||||
}
|
||||
async function refreshInspectionCollection() {
|
||||
if (scope.signal.aborted) return;
|
||||
// Numeric roots may now name another source; retire the old stream first.
|
||||
|
||||
@@ -174,7 +174,9 @@ main:focus-visible { outline: 2px solid var(--ca-focus); outline-offset: 4px; }
|
||||
@container images-page (inline-size < 600px) {
|
||||
.image-workspace { grid-template-columns: minmax(0, 1fr); }
|
||||
}
|
||||
.options-status { font-size: 12px; padding-inline: 4px; }
|
||||
/* #249 D42: a discovery alert a rescan leaves scrolls clear of the concept's fixed toast (22px inset, 12px at 700px and
|
||||
below, two 12px lines at 390px), never under it; the toast itself keeps the concept's placement. */
|
||||
.options-status { font-size: 12px; padding-inline: 4px; scroll-margin-block-end: 96px; /* D42 */ }
|
||||
/* Until their pages rebuild (PR E Library, PR F history): the Library's and the recorded run's prompt blocks keep
|
||||
their spacing, the Archive negative dialog keeps `section.prompt-block`, and the shared prompt sender keeps its
|
||||
dialog and send button. */
|
||||
|
||||
Reference in New Issue
Block a user