mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-09 07:25:44 +02:00
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.
This commit is contained in:
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 one or more lines are too long
+1
-1
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user