diff --git a/tests/browser/workspace-images.spec.mjs b/tests/browser/workspace-images.spec.mjs index 0affb9ea..e9ebf2b7 100644 --- a/tests/browser/workspace-images.spec.mjs +++ b/tests/browser/workspace-images.spec.mjs @@ -1435,3 +1435,26 @@ test('Import Rescan shows that it runs, ignores a second press and announces the await expect(cards(page)).toHaveCount(3); await expect(caption(page)).toContainText('3 of 3 images'); }); + +// #249 final review I2: a polite live region re-announces replaced text, so an unchanged status is never rewritten. +test('a star press leaves the continuation status, count and caption date untouched', async ({ page }) => { + await page.goto(workspace); + await expect(cards(page)).toHaveCount(2); + await expect(page.locator('#gallery-continuation p')).toHaveText(/caught up/); + await expect(caption(page).locator('span').nth(1)).toBeVisible(); + await page.evaluate(() => { + window.__statusMutations = []; + const observer = new MutationObserver(records => { + window.__statusMutations.push(...records.map(record => `${record.type}:${record.target.nodeName}`)); + }); + for (const node of [document.querySelector('#gallery-continuation p'), document.querySelector('#result-count'), + document.querySelector('.section-caption > span:nth-child(2)')]) { + observer.observe(node, { childList: true, characterData: true, subtree: true, attributes: true }); + } + }); + const card = page.locator('article[data-image-key="0:shared/same.png"]'); + await star(card, 'same.png').click(); + await expectStar(star(card, 'same.png'), true); + await expect(toast(page)).toHaveText('Added to favorites.'); + expect(await page.evaluate(() => window.__statusMutations)).toEqual([]); +}); diff --git a/web/js/workspace/images.mjs b/web/js/workspace/images.mjs index e3b7a9e5..c5ab1c92 100644 --- a/web/js/workspace/images.mjs +++ b/web/js/workspace/images.mjs @@ -20,6 +20,17 @@ function el(tag, text, className) { return element; } +/** + * Live regions re-announce replaced text even when it is the same, and a patched publish (a favorite) repeats the + * same status: write text and attributes only when they change. + */ +function setText(element, text) { + if (element.textContent !== text) element.textContent = text; +} +function setAttr(element, name, value) { + if (element.getAttribute(name) !== value) element.setAttribute(name, value); +} + function selectOf(id, options, value) { const select = el('select'); select.id = id; @@ -458,10 +469,10 @@ export function mountImages(main, api, parentScope, {embedded = false, fetchPage for (let index = state?.items.length || 0; index < next.items.length; index++) itemIndexes.set(imageKey(next.items[index]), index); } state = next; - count.textContent = embedded ? `${next.loaded.toLocaleString('en-US')} loaded · ${next.total.toLocaleString('en-US')} ${noun(next.total, 'linked image')}` : countLine(next); + setText(count, embedded ? `${next.loaded.toLocaleString('en-US')} loaded · ${next.total.toLocaleString('en-US')} ${noun(next.total, 'linked image')}` : countLine(next)); if (!embedded) { - captionDay.textContent = captionDate(next.items, filters.sort); - captionDay.hidden = !captionDay.textContent; + setText(captionDay, captionDate(next.items, filters.sort)); + if (captionDay.hidden !== !captionDay.textContent) captionDay.hidden = !captionDay.textContent; } if (changed) updateLayout(); // The concept's Load more toast (app.js loadMoreImages) for every batch appended to a loaded collection. @@ -469,8 +480,8 @@ export function mountImages(main, api, parentScope, {embedded = false, fetchPage const added = next.loaded - previous.loaded; showWorkspaceToast(`${added.toLocaleString('en-US')} more ${noun(added, 'image')} loaded. ${next.loaded.toLocaleString('en-US')} of ${next.total.toLocaleString('en-US')}.`); } - message.textContent = continuationText(next); - if (next.error) message.setAttribute('role', 'alert'); else message.setAttribute('role', 'status'); + setText(message, continuationText(next)); + setAttr(message, 'role', next.error ? 'alert' : 'status'); retry.hidden = !next.error; more.hidden = !next.hasMore || !!next.error; more.setAttribute('aria-disabled', String(next.loading || recovering));