chore(web): lint and format cleanup for push client files

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Claude Code
2026-06-11 09:45:49 +02:00
parent 9a1b4ca3d7
commit d2c8be9162
5 changed files with 52 additions and 26 deletions
+46 -14
View File
@@ -24,29 +24,44 @@ function wrap() {
let subscribeMock: ReturnType<typeof vi.fn>;
let getSubMock: ReturnType<typeof vi.fn>;
type MockNotification = {
permission: NotificationPermission;
requestPermission: () => Promise<NotificationPermission>;
};
const g = globalThis as unknown as {
PushManager?: unknown;
Notification?: MockNotification;
};
const nav = navigator as unknown as { serviceWorker?: unknown };
beforeEach(() => {
apiMock.mockReset();
cacheVapidMock.mockReset();
subscribeMock = vi.fn(async () => ({ toJSON: () => ({ endpoint: "https://x", keys: {} }) }));
getSubMock = vi.fn(async () => null);
(globalThis as any).PushManager = function () {};
(navigator as any).serviceWorker = {
g.PushManager = function () {};
nav.serviceWorker = {
register: vi.fn(),
ready: Promise.resolve({ pushManager: { subscribe: subscribeMock, getSubscription: getSubMock } }),
ready: Promise.resolve({
pushManager: { subscribe: subscribeMock, getSubscription: getSubMock },
}),
};
g.Notification = {
permission: "default",
requestPermission: vi.fn(async (): Promise<NotificationPermission> => "granted"),
};
(globalThis as any).Notification = { permission: "default", requestPermission: vi.fn(async () => "granted") };
});
afterEach(() => vi.restoreAllMocks());
describe("usePushDevice", () => {
it("reports unsupported when PushManager missing", () => {
delete (globalThis as any).PushManager;
delete g.PushManager;
const { result } = renderHook(() => usePushDevice(), { wrapper: wrap() });
expect(result.current.status).toBe("unsupported");
});
it("reports blocked when permission denied", () => {
(globalThis as any).Notification.permission = "denied";
g.Notification!.permission = "denied";
const { result } = renderHook(() => usePushDevice(), { wrapper: wrap() });
expect(result.current.status).toBe("blocked");
});
@@ -57,7 +72,9 @@ describe("usePushDevice", () => {
return Promise.resolve(undefined); // PUT device
});
const { result } = renderHook(() => usePushDevice(), { wrapper: wrap() });
await act(async () => { await result.current.enable(); });
await act(async () => {
await result.current.enable();
});
expect(subscribeMock).toHaveBeenCalledOnce();
expect(cacheVapidMock).toHaveBeenCalledWith("AQID");
const putCall = apiMock.mock.calls.find((c) => c[0] === "/notifications/push/device");
@@ -67,9 +84,13 @@ describe("usePushDevice", () => {
});
it("enable(): permission denied short-circuits, no subscribe", async () => {
(globalThis as any).Notification.requestPermission = vi.fn(async () => "denied");
g.Notification!.requestPermission = vi.fn(
async (): Promise<NotificationPermission> => "denied",
);
const { result } = renderHook(() => usePushDevice(), { wrapper: wrap() });
await act(async () => { await result.current.enable(); });
await act(async () => {
await result.current.enable();
});
expect(subscribeMock).not.toHaveBeenCalled();
expect(result.current.status).toBe("blocked");
});
@@ -79,9 +100,13 @@ describe("usePushDevice", () => {
getSubMock.mockResolvedValue({ unsubscribe: unsub });
apiMock.mockResolvedValue(undefined);
const { result } = renderHook(() => usePushDevice(), { wrapper: wrap() });
await act(async () => { await result.current.disable(); });
await act(async () => {
await result.current.disable();
});
expect(unsub).toHaveBeenCalled();
const del = apiMock.mock.calls.find((c) => c[0] === "/notifications/push/device" && c[1]?.method === "DELETE");
const del = apiMock.mock.calls.find(
(c) => c[0] === "/notifications/push/device" && c[1]?.method === "DELETE",
);
expect(del).toBeTruthy();
await waitFor(() => expect(result.current.status).toBe("off"));
});
@@ -103,16 +128,23 @@ describe("usePushDevice", () => {
// Start enable() without awaiting — it stalls at the webpush-key call (gen=1).
let innerEnableResolve!: () => void;
const innerEnableDone = new Promise<void>((res) => { innerEnableResolve = res; });
const innerEnableDone = new Promise<void>((res) => {
innerEnableResolve = res;
});
act(() => {
void result.current.enable().then(innerEnableResolve, innerEnableResolve);
});
// Drain microtasks so enable() reaches the webpush-key await (past requestPermission + sw.ready).
await act(async () => { await Promise.resolve(); await Promise.resolve(); });
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
// Run disable() to full completion — increments gen to 2, sets status "off".
await act(async () => { await result.current.disable(); });
await act(async () => {
await result.current.disable();
});
// Reject the stale enable()'s webpush-key call — gen=1 is superseded so
// the catch's set("off") is a no-op; disable's "off" (gen=2) must survive.
@@ -48,10 +48,7 @@ describe("PushSettings", () => {
it("reflects configured status on non-password fields", () => {
render(<PushSettings />);
// SettingField renders placeholder "configured" for a configured field.
expect(screen.getByLabelText("VAPID Public Key")).toHaveAttribute(
"placeholder",
"configured",
);
expect(screen.getByLabelText("VAPID Public Key")).toHaveAttribute("placeholder", "configured");
expect(screen.getByLabelText("Key ID")).toHaveAttribute("placeholder", "configured");
// A field that is not in the configured list does not get the indicator.
expect(screen.getByLabelText("Team ID")).not.toHaveAttribute("placeholder", "configured");
@@ -60,7 +60,7 @@ function CredentialField({
placeholder={sensitiveConfigured ? "configured" : hint}
aria-describedby={hint ? hintId : undefined}
rows={5}
className="border-input bg-background ring-offset-background focus-visible:ring-ring max-w-md w-full rounded-md border px-3 py-2 font-mono text-xs focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
className="border-input bg-background ring-offset-background focus-visible:ring-ring w-full max-w-md rounded-md border px-3 py-2 font-mono text-xs focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
/>
{hint && (
<p id={hintId} className="text-muted-foreground text-xs">
@@ -242,9 +242,7 @@ function FcmCard() {
const [serviceAccountJson, setServiceAccountJson] = useState("");
const jsonConfigured = new Set(sensitive?.configured ?? []).has(
"push.fcm.service_account_json",
);
const jsonConfigured = new Set(sensitive?.configured ?? []).has("push.fcm.service_account_json");
return (
<div className="border-border bg-surface max-w-2xl rounded-lg border px-5 py-4">
@@ -295,9 +295,7 @@ describe("NotificationsSettings", () => {
await render(<NotificationsSettings />);
expect(container.textContent).toContain(
"This browser doesn’t support push notifications.",
);
expect(container.textContent).toContain("This browser doesn’t support push notifications.");
const labels = Array.from(container.querySelectorAll("label"));
const deviceLabel = labels.find((l) => l.textContent?.trim() === "Push on this device");
@@ -129,7 +129,8 @@ export default function NotificationsSettings() {
{devicesLoading ? null : devices && devices.length > 0 ? (
devices.map((d) => {
const registered = d.registered_at
? (timeAgo(d.registered_at, "Registered") ?? `Registered ${new Date(d.registered_at).toLocaleDateString()}`)
? (timeAgo(d.registered_at, "Registered") ??
`Registered ${new Date(d.registered_at).toLocaleDateString()}`)
: null;
return (
<SettingRow