Files
buzz/desktop/src/shared/api/useRelayResumeTriggers.test.mjs
Wes 59f613c404 fix(desktop): defer foreground resume work (#5696)
## Summary

- defer foreground resume work until the activation task has returned, a
frame has painted, and a trailing task gets a turn
- centralize app-focus subscribers and remove the broad TanStack
`refetchOnWindowFocus` fan-out
- coalesce relay recovery and preserve an explicit deferred refresh only
for workflow data without a polling/push freshness path
- defer the notification permission native check while keeping blur and
cheap correctness signals immediate

## Why

Buzz Desktop 0.5.10 can spend roughly 1.5 seconds in the WebKit
window-focus listener/microtask checkpoint before returning to the run
loop. Focus currently fans out into query refetches, React polling
updates, relay reconnect/replay, and native work in one activation turn.
This patch establishes an interaction-first foreground boundary rather
than letting those consumers compete with the activating input and first
paint.

## Validation

- focused foreground/workflow/relay tests: 18/18 passed before commit
- `pnpm --dir desktop typecheck`: passed before commit
- pre-commit desktop check and file-size gate: passed
- pre-push desktop check, typecheck, and full desktop unit suite:
4,743/4,743 passed at `704e7b4b6618fafce655bb2b07c7a9fe0fc8c643`
- Princess Donut independent adversarial review: PASS after two
lifecycle/freshness blockers were resolved

## Manual test

1. Install the PR build and use Buzz long enough to populate channels,
home, workflows, agents, and other polling surfaces.
2. Switch to another app for 30-60 seconds.
3. Return by clicking Buzz and immediately click a channel or scroll.
4. Confirm the first interaction and paint are prompt, then confirm
channels/home/workflows refresh and a degraded relay reconnects after
the activation boundary.
5. Repeat while rapidly switching away again to verify no resume work
starts after focus has been lost.

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-12 13:46:49 -07:00

142 lines
3.9 KiB
JavaScript

import assert from "node:assert/strict";
import { afterEach, describe, it } from "node:test";
import { installRelayResumeTriggers } from "./useRelayResumeTriggers.ts";
const originalDocument = globalThis.document;
const originalWindow = globalThis.window;
afterEach(() => {
if (originalDocument === undefined) delete globalThis.document;
else globalThis.document = originalDocument;
if (originalWindow === undefined) delete globalThis.window;
else globalThis.window = originalWindow;
});
function installEventStub() {
let visibilityState = "visible";
const documentListeners = new Map();
const windowListeners = new Map();
const add = (listeners, type, listener) => {
const values = listeners.get(type) ?? new Set();
values.add(listener);
listeners.set(type, values);
};
const remove = (listeners, type, listener) => {
const values = listeners.get(type);
values?.delete(listener);
if (values?.size === 0) listeners.delete(type);
};
globalThis.document = {
get visibilityState() {
return visibilityState;
},
addEventListener: (type, listener) =>
add(documentListeners, type, listener),
removeEventListener: (type, listener) =>
remove(documentListeners, type, listener),
};
globalThis.window = {
addEventListener: (type, listener) => add(windowListeners, type, listener),
removeEventListener: (type, listener) =>
remove(windowListeners, type, listener),
};
return {
documentListeners,
windowListeners,
emitDocument(type) {
for (const listener of documentListeners.get(type) ?? []) listener();
},
emitWindow(type) {
for (const listener of windowListeners.get(type) ?? []) listener();
},
setVisibility(value) {
visibilityState = value;
},
};
}
describe("relay resume triggers", () => {
it("defers and coalesces foreground signals", () => {
const stub = installEventStub();
const scheduled = [];
let attempts = 0;
const dispose = installRelayResumeTriggers(
() => attempts++,
(callback) => {
const pending = { callback, cancelled: false };
scheduled.push(pending);
return () => {
pending.cancelled = true;
};
},
);
stub.emitWindow("focus");
stub.emitWindow("online");
stub.emitDocument("visibilitychange");
assert.equal(attempts, 0, "resume is not attempted in the event turn");
assert.equal(scheduled.length, 1, "foreground burst is coalesced");
scheduled[0].callback();
assert.equal(attempts, 1);
dispose();
assert.equal(stub.documentListeners.size, 0);
assert.equal(stub.windowListeners.size, 0);
});
it("cancels a pending resume when hidden", () => {
const stub = installEventStub();
let pending;
let attempts = 0;
installRelayResumeTriggers(
() => attempts++,
(callback) => {
pending = {
cancelled: false,
run() {
if (!this.cancelled) callback();
},
};
return () => {
pending.cancelled = true;
};
},
);
stub.emitWindow("focus");
stub.setVisibility("hidden");
stub.emitDocument("visibilitychange");
assert.equal(pending.cancelled, true);
pending.run();
assert.equal(attempts, 0, "hidden state suppresses pending resume");
});
it("cancels a pending resume on dispose", () => {
const stub = installEventStub();
let pending;
let attempts = 0;
const dispose = installRelayResumeTriggers(
() => attempts++,
(callback) => {
pending = {
cancelled: false,
run() {
if (!this.cancelled) callback();
},
};
return () => {
pending.cancelled = true;
};
},
);
stub.emitWindow("focus");
dispose();
pending.run();
assert.equal(attempts, 0);
assert.equal(stub.documentListeners.size, 0);
assert.equal(stub.windowListeners.size, 0);
});
});