Skip to content
Source
Sign inSign up
Open History to see the last commit

appLinking.test.ts

182 lines6.0 KB

1import { describe, expect, it } from "vite-plus/test";
2​
3import type { NavigationState } from "@react-navigation/native";
4import {
5 findLinkedRoute,
6 findStackRoute,
7 restoreDraftLinkState,
8 shouldHandleAppLink,
9 updateLinkedRoute,
10} from "./appLinking";
11​
12describe("shouldHandleAppLink", () => {
13 it.each(["t3code://", "t3code:///", "t3code-dev://", "t3code-preview://"])(
14 "ignores scheme-only URL %s",
15 (url) => {
16 expect(shouldHandleAppLink(url, true)).toBe(false);
17 },
18 );
19​
20 it.each([
21 "t3code://threads/env-1/thread-1",
22 "t3code://pair?pairingUrl=x",
23 "t3code-dev://settings/usage?tab=limits",
24 "https://app.t3.codes/threads/env/thread?prompt=Fix%20expo-development-client%20and%20t3code://expo-sharing",
25 ])("handles path-bearing URL %s", (url) => {
26 expect(shouldHandleAppLink(url, true)).toBe(true);
27 });
28​
29 it.each(["t3code://expo-development-client/?url=x", "t3code://expo-sharing/anything"])(
30 "ignores lifecycle URL %s",
31 (url) => {
32 expect(shouldHandleAppLink(url, true)).toBe(false);
33 },
34 );
35});
36​
37describe("cloud-only deep links", () => {
38 it.each([
39 "t3code://connect-onboarding",
40 "t3code-dev://connect-onboarding/?source=x",
41 "t3code://settings/waitlist",
42 "t3code://settings/auth",
43 "exp://192.168.1.2:8081/--/settings/auth",
44 ])("ignores %s without cloud config", (url) => {
45 expect(shouldHandleAppLink(url, false)).toBe(false);
46 expect(shouldHandleAppLink(url, true)).toBe(true);
47 });
48​
49 it.each([
50 "t3code://settings",
51 "t3code://settings/authors",
52 "t3code://pair?next=settings/auth",
53 "t3code://threads/env/thread/files/scripts/connect-onboarding",
54 "t3code://new/draft/files/settings/auth",
55 "exp://192.168.1.2:8081/--/threads/env/thread/files/settings/waitlist",
56 ])("still handles %s without cloud config", (url) => {
57 expect(shouldHandleAppLink(url, false)).toBe(true);
58 });
59});
60​
61describe("draft deep links", () => {
62 it("opens the old draft URL on the thread screen with its draft identity", () => {
63 const draft = { projectId: "project-1", environmentId: "env-1", draftId: "draft-1" };
64 expect(
65 restoreDraftLinkState(
66 { routes: [{ name: "Home" }, { name: "Thread", params: draft }] },
67 "/new/draft?draftId=draft-1",
68 ),
69 ).toEqual({
70 index: 1,
71 routes: [{ name: "Home" }, { name: "Thread", params: { draft } }],
72 });
73 });
74​
75 it.each([
76 ["environment", "NewTaskEnvironment"],
77 ["branch", "NewTaskBranch"],
78 ["files", "NewTaskFiles"],
79 ["files/src/index.ts", "NewTaskFile"],
80 ["attachments/attachment-1", "NewTaskAttachment"],
81 ["settings", "NewTaskThreadSettings"],
82 ])("restores a draft beneath its %s screen", (path, name) => {
83 const params = { environmentId: "env-1", projectId: "project-1" };
84 const target = { name, params };
85 expect(
86 restoreDraftLinkState({ routes: [{ name: "Home" }, target] }, `/new/draft/${path}`),
87 ).toEqual({
88 index: 2,
89 routes: [{ name: "Home" }, { name: "Thread", params: { draft: params } }, target],
90 });
91 });
92​
93 it("leaves live-thread and project-selection links unchanged", () => {
94 const state = { routes: [{ name: "Thread", params: { threadId: "thread-1" } }] };
95 expect(restoreDraftLinkState(state, "/threads/env-1/thread-1")).toBe(state);
96 expect(restoreDraftLinkState(state, "/new")).toBe(state);
97 expect(restoreDraftLinkState(undefined, "/new/draft")).toBeUndefined();
98 });
99});
100​
101describe("nested link routes", () => {
102 const settingsLink = {
103 routes: [
104 { name: "Home" },
105 {
106 name: "SettingsSheet",
107 state: {
108 routes: [
109 { name: "Settings" },
110 { name: "SettingsEnvironmentNew", params: { pairingUrl: "x", autoConnect: "1" } },
111 ],
112 },
113 },
114 ],
115 };
116​
117 it("finds a route the link opens inside a nested navigator", () => {
118 expect(findLinkedRoute(settingsLink, ["SettingsEnvironmentNew"])?.params).toEqual({
119 pairingUrl: "x",
120 autoConnect: "1",
121 });
122 expect(findLinkedRoute(settingsLink, ["ConnectionsNew"])).toBeUndefined();
123 });
124​
125 it("rewrites a nested route without touching the rest of the state", () => {
126 const next = updateLinkedRoute(settingsLink, ["SettingsEnvironmentNew"], (route) => ({
127 ...route,
128 params: { pairingUrl: "x" },
129 }));
130 expect(findLinkedRoute(next, ["SettingsEnvironmentNew"])?.params).toEqual({ pairingUrl: "x" });
131 expect(next?.routes[0]).toBe(settingsLink.routes[0]);
132 expect(updateLinkedRoute(settingsLink, ["ConnectionsNew"], (route) => route)).toBe(
133 settingsLink,
134 );
135 });
136});
137​
138describe("findStackRoute", () => {
139 const stack = (key: string, routes: NavigationState["routes"]) =>
140 ({
141 key,
142 index: routes.length - 1,
143 routeNames: routes.map((route) => route.name),
144 routes,
145 stale: false,
146 type: "stack",
147 }) as NavigationState;
148​
149 it("finds a nested screen with the navigator that holds it", () => {
150 const root = stack("root", [
151 { key: "home", name: "Home" },
152 {
153 key: "settings",
154 name: "Settings",
155 state: stack("settings-stack", [{ key: "new-task", name: "NewTaskProject" }]),
156 },
157 ]);
158 expect(findStackRoute(root, "NewTaskProject")).toEqual({
159 navigatorKey: "settings-stack",
160 routeKey: "new-task",
161 focused: true,
162 });
163 });
164​
165 it("reports whether the screen is its navigator's focused route", () => {
166 const covered = stack("root", [
167 { key: "new-task", name: "NewTaskProject" },
168 { key: "add-project", name: "AddProject" },
169 ]);
170 expect(findStackRoute(covered, "NewTaskProject")?.focused).toBe(false);
171 expect(findStackRoute(covered, "AddProject")?.focused).toBe(true);
172 });
173​
174 it("prefers the top-most screen and reports a missing one", () => {
175 const root = stack("root", [
176 { key: "first", name: "NewTaskProject" },
177 { key: "second", name: "NewTaskProject" },
178 ]);
179 expect(findStackRoute(root, "NewTaskProject")?.routeKey).toBe("second");
180 expect(findStackRoute(root, "Thread")).toBeUndefined();
181 });
182});