Sfoglia il codice sorgente

fix(tab-progress): drop the redundant status poll and quieten the test suite

The hook is mounted globally in WebSocketProvider, so refetchInterval on its
per-printer status queries added one request per printer every 30s on every
page. The Printers page already runs that fallback on the same query key, and
useWebSocket writes ['printerStatus', id] straight into the cache, so the poll
bought nothing outside the Printers page and cost a request per printer per
tab everywhere else.

Also captures document.title at mount instead of restoring to a hardcoded
'Bambuddy', so the default no longer has to be kept in sync with index.html.

jsdom has no canvas backend, so getContext('2d') logged a "Not implemented"
jsdomError with a full React stack on every run of the hook's tests, and the
favicon branch bailed on the null context and went untested. Stubbing
getContext/toDataURL removes the noise and lets the ring code run, so the
favicon swap and the restore-on-toggle-off path are now asserted.
maziggy 1 mese fa
parent
commit
24322c71cb

+ 53 - 2
frontend/src/__tests__/hooks/usePrintProgressTitle.test.tsx

@@ -75,13 +75,47 @@ function wrapper() {
   );
 }
 
+// jsdom has no canvas backend — calling getContext('2d') logs a "Not implemented"
+// jsdomError (with a full React stack) into the suite output on every run, and
+// leaves the favicon path untested because it bails on the null context. Stub
+// both canvas calls so the ring code actually executes and the swap is assertable.
+const RING_URL = 'data:image/png;base64,ring';
+const fakeCtx = {
+  beginPath: vi.fn(),
+  arc: vi.fn(),
+  stroke: vi.fn(),
+  lineWidth: 0,
+  strokeStyle: '',
+  lineCap: 'butt',
+} as unknown as CanvasRenderingContext2D;
+
+const realGetContext = HTMLCanvasElement.prototype.getContext;
+const realToDataURL = HTMLCanvasElement.prototype.toDataURL;
+
+function faviconHref(): string {
+  return document.querySelector<HTMLLinkElement>('link[rel~="icon"]')!.href;
+}
+
 describe('usePrintProgressTitle effect', () => {
   beforeEach(() => {
     h.getPrinters.mockReset();
     h.getPrinterStatus.mockReset();
+
+    HTMLCanvasElement.prototype.getContext = (() =>
+      fakeCtx) as typeof HTMLCanvasElement.prototype.getContext;
+    HTMLCanvasElement.prototype.toDataURL = (() =>
+      RING_URL) as typeof HTMLCanvasElement.prototype.toDataURL;
+
+    // Replaces <title> too, so set the title after wiring the head up — the hook
+    // captures document.title at mount.
+    document.head.innerHTML = '<link rel="icon" href="/favicon.svg">';
     document.title = 'Bambuddy';
   });
-  afterEach(() => cleanup());
+  afterEach(() => {
+    cleanup();
+    HTMLCanvasElement.prototype.getContext = realGetContext;
+    HTMLCanvasElement.prototype.toDataURL = realToDataURL;
+  });
 
   it('is inert while the pref is off — never touches the tab title', async () => {
     h.theme.value = { progressInTitle: false, resolvedMode: 'dark', darkAccent: 'green', lightAccent: 'green' };
@@ -91,10 +125,11 @@ describe('usePrintProgressTitle effect', () => {
 
     await new Promise((r) => setTimeout(r, 20));
     expect(document.title).toBe('Something Else');
+    expect(faviconHref()).toContain('/favicon.svg');
     expect(h.getPrinters).not.toHaveBeenCalled();
   });
 
-  it('shows the active print percentage in the title when enabled', async () => {
+  it('shows the active print percentage in the title and swaps the favicon', async () => {
     h.theme.value = { progressInTitle: true, resolvedMode: 'dark', darkAccent: 'green', lightAccent: 'green' };
     h.getPrinters.mockResolvedValue([{ id: 1 }]);
     h.getPrinterStatus.mockResolvedValue({ state: 'RUNNING', progress: 42, remaining_time: 600 });
@@ -102,5 +137,21 @@ describe('usePrintProgressTitle effect', () => {
     renderHook(() => usePrintProgressTitle(), { wrapper: wrapper() });
 
     await waitFor(() => expect(document.title).toBe('42% · Bambuddy'));
+    expect(faviconHref()).toBe(RING_URL);
+  });
+
+  it('restores the original title and favicon when the pref is switched off', async () => {
+    h.theme.value = { progressInTitle: true, resolvedMode: 'dark', darkAccent: 'green', lightAccent: 'green' };
+    h.getPrinters.mockResolvedValue([{ id: 1 }]);
+    h.getPrinterStatus.mockResolvedValue({ state: 'RUNNING', progress: 42, remaining_time: 600 });
+
+    const { rerender } = renderHook(() => usePrintProgressTitle(), { wrapper: wrapper() });
+    await waitFor(() => expect(document.title).toBe('42% · Bambuddy'));
+
+    h.theme.value = { ...h.theme.value, progressInTitle: false };
+    rerender();
+
+    await waitFor(() => expect(document.title).toBe('Bambuddy'));
+    expect(faviconHref()).toContain('/favicon.svg');
   });
 });

+ 1 - 1
frontend/src/contexts/ThemeContext.tsx

@@ -19,7 +19,7 @@ interface ThemeContextType {
   lightStyle: ThemeStyle;
   lightBackground: LightBackground;
   lightAccent: ThemeAccent;
-  // Show live print progress (% + green ring favicon) in the browser tab
+  // Show live print progress (% + accent-coloured ring favicon) in the browser tab
   progressInTitle: boolean;
   setProgressInTitle: (v: boolean) => void;
   // Actions

+ 12 - 5
frontend/src/hooks/usePrintProgressTitle.ts

@@ -3,7 +3,6 @@ import { useEffect, useRef } from 'react';
 import { api } from '../api/client';
 import { useTheme } from '../contexts/ThemeContext';
 
-const DEFAULT_TITLE = 'Bambuddy';
 const FALLBACK_ACCENT = '#00ae42'; // Bambuddy green, if --accent can't be read (e.g. jsdom)
 
 // A remaining_time <= 0 means "ETA not known yet" (the backend defaults it to 0,
@@ -111,15 +110,22 @@ export function usePrintProgressTitle() {
     enabled: progressInTitle,
   });
 
+  // No refetchInterval here on purpose. This hook is mounted globally, so a
+  // poll would add one request per printer every interval on every page — the
+  // Printers page already runs its own 30s fallback on this exact key. The
+  // WebSocket writes ['printerStatus', id] directly (useWebSocket), which keeps
+  // the tab live; a cosmetic title going stale during a WS outage is fine.
   const statusQueries = useQueries({
     queries: (progressInTitle ? printers ?? [] : []).map((p) => ({
       queryKey: ['printerStatus', p.id],
       queryFn: () => api.getPrinterStatus(p.id),
-      refetchInterval: 30000, // fallback; WebSocket drives live updates
     })),
   });
 
   const originalsRef = useRef<Map<HTMLLinkElement, string>>(new Map());
+  // The tab's own title, captured before we ever touch it, so restoring doesn't
+  // depend on a constant matching index.html.
+  const defaultTitleRef = useRef(document.title);
   // Whether we currently own the tab title/favicon. Lets us stay inert while
   // off (never touch the tab) yet still restore once if we ever took it over.
   const ownsRef = useRef(false);
@@ -129,12 +135,12 @@ export function usePrintProgressTitle() {
 
   useEffect(() => {
     if (progressInTitle && pct != null) {
-      document.title = `${pct}% · ${DEFAULT_TITLE}`;
+      document.title = `${pct}% · ${defaultTitleRef.current}`;
       setFavicon(drawProgressFavicon(pct), originalsRef.current);
       ownsRef.current = true;
     } else if (ownsRef.current) {
       // Disabled or idle after having taken over — hand the tab back.
-      document.title = DEFAULT_TITLE;
+      document.title = defaultTitleRef.current;
       setFavicon(null, originalsRef.current);
       ownsRef.current = false;
     }
@@ -145,9 +151,10 @@ export function usePrintProgressTitle() {
   useEffect(() => {
     const originals = originalsRef.current;
     const owns = ownsRef;
+    const defaultTitle = defaultTitleRef.current;
     return () => {
       if (owns.current) {
-        document.title = DEFAULT_TITLE;
+        document.title = defaultTitle;
         setFavicon(null, originals);
       }
     };

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-BSGkCTjg.js


+ 1 - 1
static/index.html

@@ -26,7 +26,7 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-utEp4da9.js"></script>
+    <script type="module" crossorigin src="/assets/index-BSGkCTjg.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-D4bpNaiw.css">
   </head>
   <body>

Some files were not shown because too many files changed in this diff