1import { expect, it, vi } from "vite-plus/test";
2import type { NavigationContainerRef } from "@react-navigation/native";
3
4vi.mock("@react-navigation/native", () => ({
5 createNavigationContainerRef: () => ({
6 current: null as { isReady(): boolean } | null,
7 isReady() {
8 return this.current?.isReady() ?? false;
9 },
10 dispatched: [] as Array<unknown>,
11 dispatch(action: unknown) {
12 this.dispatched.push(action);
13 },
14 getRootState: () => ({ key: "root" }),
15 }),
16}));
17
18const {
19 attachRootNavigation,
20 flushRootNavigationQueue,
21 linkToRootPath,
22 rootNavigationRef,
23 whenRootNavigationReady,
24} = await import("./rootNavigation");
25
26function fakeContainer() {
27 return { isReady: () => true } as NavigationContainerRef<ReactNavigation.RootParamList>;
28}
29
30it("keeps the newer container when an older root unmounts after it attached", () => {
31 // Android activity replacement mounts the new root before the old one unmounts.
32 const detachPrevious = attachRootNavigation(fakeContainer());
33 const current = fakeContainer();
34 const detachCurrent = attachRootNavigation(current);
35
36 detachPrevious();
37 expect(rootNavigationRef.current).toBe(current);
38 expect(rootNavigationRef.isReady()).toBe(true);
39
40 detachCurrent();
41 expect(rootNavigationRef.current).toBeNull();
42});
43
44it("holds navigation until the container is ready, then runs it once", () => {
45 const navigate = vi.fn();
46 const failing = vi.fn(() => {
47 throw new Error("unknown route");
48 });
49 const logged = vi.spyOn(console, "error").mockImplementation(() => undefined);
50 whenRootNavigationReady(failing);
51 whenRootNavigationReady(navigate);
52 expect(navigate).not.toHaveBeenCalled();
53
54 const detach = attachRootNavigation(fakeContainer());
55 flushRootNavigationQueue();
56 flushRootNavigationQueue();
57 expect(failing).toHaveBeenCalledOnce();
58 expect(navigate).toHaveBeenCalledOnce();
59 expect(logged).toHaveBeenCalledOnce();
60
61 whenRootNavigationReady(navigate);
62 expect(navigate).toHaveBeenCalledTimes(2);
63 detach();
64 logged.mockRestore();
65});
66
67it("rejects an href without a leading slash, as useLinkTo does", () => {
68 const toAction = vi.fn(() => ({ type: "NAVIGATE" }));
69 const detach = attachRootNavigation(fakeContainer());
70 const { dispatched } = rootNavigationRef as unknown as { dispatched: Array<unknown> };
71
72 expect(() => linkToRootPath("threads/env/t1", toAction)).toThrow(
73 "The href must start with '/' (threads/env/t1).",
74 );
75 expect(toAction).not.toHaveBeenCalled();
76
77 linkToRootPath("/threads/env/t1", toAction);
78 expect(toAction).toHaveBeenCalledExactlyOnceWith("/threads/env/t1");
79 expect(dispatched).toEqual([{ target: "root", type: "NAVIGATE" }]);
80 detach();
81});