diff --git a/src/app/smoke-tests/thread/[threadId]/page.tsx b/src/app/smoke-tests/thread/[threadId]/page.tsx index 5211d54..de7be1e 100644 --- a/src/app/smoke-tests/thread/[threadId]/page.tsx +++ b/src/app/smoke-tests/thread/[threadId]/page.tsx @@ -1,4 +1,5 @@ import { notFound } from "next/navigation"; +import LiveUnreadCountBadge from "@/components/LiveUnreadCountBadge"; interface Props { params: Promise<{ threadId: string }>; @@ -12,10 +13,17 @@ export default async function SmokeThreadPage({ params }: Props) { const { threadId } = await params; return ( -
+

Opened conversation {threadId}

+
+ Shared unread count + +
); } diff --git a/src/components/BrowserTabIndicator.tsx b/src/components/BrowserTabIndicator.tsx index b8e372e..483d1c5 100644 --- a/src/components/BrowserTabIndicator.tsx +++ b/src/components/BrowserTabIndicator.tsx @@ -1,5 +1,6 @@ "use client"; +import { usePathname } from "next/navigation"; import { useEffect, useRef } from "react"; import { useUnreadCount } from "@/components/UnreadCountProvider"; import { @@ -12,45 +13,49 @@ import { const MANAGED_FAVICON_SELECTOR = 'link[data-mail-unread-favicon="true"]'; export default function BrowserTabIndicator() { + const pathname = usePathname(); const unreadCount = useUnreadCount(); const faviconRef = useRef(null); useEffect(() => { - let favicon = document.head.querySelector( - MANAGED_FAVICON_SELECTOR, - ); - const createdFavicon = !favicon; - - if (!favicon) { - favicon = document.createElement("link"); - favicon.rel = "icon"; - favicon.type = "image/svg+xml"; - favicon.sizes = "any"; - favicon.dataset.mailUnreadFavicon = "true"; - document.head.append(favicon); - } - - faviconRef.current = favicon; - return () => { faviconRef.current = null; - if (createdFavicon) favicon.remove(); + document.head + .querySelectorAll(MANAGED_FAVICON_SELECTOR) + .forEach((favicon) => favicon.remove()); }; }, []); useEffect(() => { const unreadLabel = formatBrowserTabUnreadCount(unreadCount); - const favicon = faviconRef.current; + const connectedFavicon = faviconRef.current?.isConnected + ? faviconRef.current + : document.head.querySelector( + MANAGED_FAVICON_SELECTOR, + ); + const favicon = connectedFavicon ?? document.createElement("link"); + + if (!connectedFavicon) { + favicon.setAttribute("rel", "icon"); + favicon.setAttribute("type", "image/svg+xml"); + favicon.setAttribute("sizes", "any"); + favicon.setAttribute("data-mail-unread-favicon", "true"); + } document.title = getBrowserTabTitle(unreadCount); - - if (favicon) { - favicon.href = unreadLabel + favicon.setAttribute( + "href", + unreadLabel ? getUnreadFaviconDataUrl(unreadCount) - : DEFAULT_FAVICON_HREF; - favicon.dataset.unreadCount = unreadLabel; - } - }, [unreadCount]); + : DEFAULT_FAVICON_HREF, + ); + favicon.setAttribute("data-unread-count", unreadLabel); + faviconRef.current = favicon; + + // Next may rewrite static metadata while navigating. Keep this managed + // icon last so the browser continues to prefer the live unread state. + document.head.append(favicon); + }, [pathname, unreadCount]); return null; } diff --git a/tests/smoke/mail.spec.ts b/tests/smoke/mail.spec.ts index 3c550f6..1dadcea 100644 --- a/tests/smoke/mail.spec.ts +++ b/tests/smoke/mail.spec.ts @@ -41,6 +41,23 @@ test("keeps the browser tab in sync with live unread mail", async ({ await expect(page).toHaveTitle("Mail"); await expect(favicon).toHaveAttribute("href", /\/icon\.svg$/); await expect(favicon).toHaveAttribute("data-unread-count", ""); + + await page.getByRole("button", { name: "Set 99 unread" }).click(); + await page.getByRole("link", { name: "Inbox" }).click(); + await page + .locator('a[href="/smoke-tests/thread/thread-release"]') + .click(); + + await expect(page).toHaveURL("/smoke-tests/thread/thread-release"); + await expect(page.getByTestId("shared-unread-count")).toContainText("99"); + await expect(page).toHaveTitle("(99+) Mail"); + await expect(favicon).toHaveAttribute("data-unread-count", "99+"); + expect( + await page.evaluate(() => { + const icons = [...document.querySelectorAll('link[rel~="icon"]')]; + return icons.at(-1)?.getAttribute("data-mail-unread-favicon"); + }), + ).toBe("true"); }); test("loads sender artwork and falls back cleanly when it is unavailable", async ({