fix(app): show late archive failures after the sidebar unmounts

An archive keeps the toast API it started with. Unmounting the sidebar (focus
mode, a layout change, a plugin view) left the ref saying it was visible, so a
failure arriving afterwards went to the removed host and the user saw nothing.
This commit is contained in:
Mohamed Boudra
2026-11-08 12:36:51 +01:00
parent 01a40957ea
commit 1228a6c4a1
2 changed files with 77 additions and 2 deletions
@@ -0,0 +1,71 @@
// @vitest-environment jsdom
import { render } from "@testing-library/react";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { describe, expect, it, vi } from "vitest";
import type { ToastApi } from "@/components/toast-host";
import { ToastApiProvider, useToast } from "@/contexts/toast-context";
import { SidebarToastRegion } from "./sidebar-toast-region";
const safeAreaMetrics = {
frame: { x: 0, y: 0, width: 800, height: 600 },
insets: { top: 0, left: 0, right: 0, bottom: 0 },
};
function createFakeToast(): ToastApi {
return { show: vi.fn(() => () => {}), copied: vi.fn(), error: vi.fn() };
}
function renderRegion(active: boolean) {
const appToast = createFakeToast();
let regionToast: ToastApi | null = null;
function CaptureToast() {
regionToast = useToast();
return null;
}
const tree = (isActive: boolean) => (
<SafeAreaProvider initialMetrics={safeAreaMetrics}>
<ToastApiProvider api={appToast}>
<SidebarToastRegion active={isActive}>
<CaptureToast />
</SidebarToastRegion>
</ToastApiProvider>
</SafeAreaProvider>
);
const view = render(tree(active));
return {
appToast,
toast: () => regionToast as unknown as ToastApi,
setActive: (next: boolean) => view.rerender(tree(next)),
unmount: view.unmount,
};
}
describe("SidebarToastRegion", () => {
it("keeps toasts in the sidebar while it is visible", () => {
const { appToast, toast } = renderRegion(true);
toast().error("Archive failed");
expect(appToast.error).not.toHaveBeenCalled();
});
it("sends toasts to the app while the sidebar is hidden", () => {
const { appToast, toast, setActive } = renderRegion(true);
setActive(false);
toast().error("Archive failed");
expect(appToast.error).toHaveBeenCalledWith("Archive failed");
});
it("sends a late failure to the app after the sidebar unmounts", () => {
const { appToast, toast, unmount } = renderRegion(true);
const savedToast = toast();
unmount();
savedToast.error("Archive failed");
expect(appToast.error).toHaveBeenCalledWith("Archive failed");
});
});
@@ -18,8 +18,12 @@ export function SidebarToastRegion({ active, children }: { active: boolean; chil
useEffect(() => {
activeRef.current = active;
// A toast must not outlive its visibility: its dismissal is what ends an archive's undo.
return active ? dismiss : undefined;
return () => {
// An archive holds on to this API, so a failure that lands later needs a host on screen.
activeRef.current = false;
// A toast must not outlive its visibility: its dismissal is what ends an archive's undo.
dismiss();
};
}, [active, dismiss]);
const api = useMemo<ToastApi>(() => {