1import { describe, expect, it } from "vite-plus/test";
2
3import { restoreSettingsLinkState } from "../../settings/settings-navigation";
4import {
5 workspaceSelectionsIn,
6 workspaceTabColumns,
7 workspaceTabNavigationState,
8} from "./workspace-tab-navigation";
9
10const home = { key: "home", name: "Home" };
11const thread = {
12 key: "thread",
13 name: "Thread",
14 params: { environmentId: "env", threadId: "thread-a" },
15};
16const pullRequest = {
17 key: "pr",
18 name: "PullRequest",
19 params: { environmentId: "env", projectId: "project", repository: "o/r", number: 7 },
20};
21const settings = { key: "settings", name: "Settings" };
22const available = { thread: () => true, pullRequest: () => true };
23const names = (state: { routes: readonly { name: string }[] }) =>
24 state.routes.map((route) => route.name);
25
26describe("workspace tab selections", () => {
27 it("remembers the chat, pull request and Settings page each tab showed", () => {
28 const afterThread = workspaceSelectionsIn([home, thread], {});
29 const afterPullRequest = workspaceSelectionsIn([home, pullRequest], afterThread);
30 expect(afterPullRequest).toMatchObject({
31 threads: thread.params,
32 pullRequests: pullRequest.params,
33 });
34 const afterSettings = workspaceSelectionsIn(
35 [home, settings, { key: "appearance", name: "SettingsAppearance" }],
36 afterPullRequest,
37 );
38 expect(afterSettings.settings).toBe("SettingsAppearance");
39 expect(workspaceSelectionsIn([home, thread], afterSettings)).toBe(afterSettings);
40 });
41
42 it("doesn't remember pages a Settings row doesn't open", () => {
43 for (const name of ["SettingsEnvironmentNew", "SettingsProjectOverview", "SettingsAuth"]) {
44 expect(workspaceSelectionsIn([home, settings, { key: name, name }], {}).settings).toBe(
45 undefined,
46 );
47 }
48 });
49});
50
51describe("opening a workspace tab", () => {
52 const columns = { columns: true, check: available };
53
54 it("opens Android Settings beside its default page only in a wide window", () => {
55 const open = (wide: boolean) =>
56 workspaceTabNavigationState(
57 { routes: [home, thread] },
58 "settings",
59 {},
60 {
61 columns: workspaceTabColumns("android", "settings", wide),
62 check: available,
63 },
64 );
65 expect(names(open(false))).toEqual(["Home", "Settings"]);
66 expect(names(open(true))).toEqual(["Home", "Settings", "SettingsEnvironments"]);
67 });
68
69 it("keeps Android list tabs and iOS remembered-detail navigation unchanged", () => {
70 const open = (platform: string) =>
71 workspaceTabNavigationState(
72 { routes: [home, thread] },
73 "threads",
74 { threads: thread.params },
75 {
76 columns: workspaceTabColumns(platform, "threads", true),
77 check: available,
78 },
79 );
80 expect(names(open("android"))).toEqual(["Home"]);
81 expect(names(open("ios"))).toEqual(["Home", "Thread"]);
82 });
83
84 it("opens the remembered page beside the list with columns, and the list without", () => {
85 const state = { routes: [home, thread] };
86 const selections = { threads: thread.params, pullRequests: pullRequest.params };
87 expect(names(workspaceTabNavigationState(state, "pullRequests", selections, columns))).toEqual([
88 "Home",
89 "PullRequest",
90 ]);
91 expect(
92 names(
93 workspaceTabNavigationState(state, "pullRequests", selections, {
94 columns: false,
95 check: available,
96 }),
97 ),
98 ).toEqual(["Home"]);
99 expect(names(workspaceTabNavigationState(state, "settings", {}, columns))).toEqual([
100 "Home",
101 "Settings",
102 "SettingsEnvironments",
103 ]);
104 expect(
105 names(
106 workspaceTabNavigationState(state, "settings", { settings: "SettingsMicrophone" }, columns),
107 ),
108 ).toEqual(["Home", "Settings", "SettingsVoice"]);
109 });
110
111 it("keeps the route that is already open, so re-selecting a tab doesn't rebuild it", () => {
112 const next = workspaceTabNavigationState(
113 { routes: [home, thread] },
114 "threads",
115 { threads: thread.params },
116 columns,
117 );
118 expect(next.routes.map((route) => route.key)).toEqual(["home", "thread"]);
119 });
120
121 it("reopens a tab's page under the key it had, so its kept screen is reused", () => {
122 const columnsAvailable = { columns: true, check: available };
123 let selections = workspaceSelectionsIn([home, thread], {});
124 const away = workspaceTabNavigationState(
125 { routes: [home, thread] },
126 "pullRequests",
127 selections,
128 {
129 columns: false,
130 check: available,
131 },
132 );
133 selections = workspaceSelectionsIn(away.routes, selections);
134 const back = workspaceTabNavigationState(away, "threads", selections, columnsAvailable);
135 expect(back.routes.at(-1)).toMatchObject({ key: "thread", params: thread.params });
136 });
137
138 it("reopens a draft the tab showed after its last chat, until another chat opens", () => {
139 const columns = { columns: true, check: available };
140 const draft = { key: "draft", name: "Thread", params: { draft: { requestId: "r" } } };
141 const withDraft = workspaceSelectionsIn(
142 [home, draft],
143 workspaceSelectionsIn([home, thread], {}),
144 );
145 const back = workspaceTabNavigationState(
146 { routes: [home, settings] },
147 "threads",
148 withDraft,
149 columns,
150 );
151 expect(back.routes.at(-1)).toEqual(draft);
152 const afterChat = workspaceSelectionsIn([home, thread], withDraft);
153 const again = workspaceTabNavigationState(
154 { routes: [home, settings] },
155 "threads",
156 afterChat,
157 columns,
158 );
159 expect(again.routes.at(-1)).toMatchObject({ key: "thread", params: thread.params });
160 });
161
162 it("falls back to the list for a chat that no longer exists, or an unknown Settings page", () => {
163 const missing = { thread: () => false, pullRequest: () => true };
164 expect(
165 names(
166 workspaceTabNavigationState(
167 { routes: [home] },
168 "threads",
169 { threads: thread.params },
170 {
171 columns: true,
172 check: missing,
173 },
174 ),
175 ),
176 ).toEqual(["Home"]);
177 expect(
178 names(
179 workspaceTabNavigationState(
180 { routes: [home] },
181 "settings",
182 { settings: "SettingsGone" },
183 columns,
184 ),
185 ),
186 ).toEqual(["Home", "Settings", "SettingsEnvironments"]);
187 });
188
189 it("keeps an open sheet on top", () => {
190 const sheet = { key: "sheet", name: "ConnectOnboarding" };
191 const next = workspaceTabNavigationState(
192 { routes: [home] },
193 "threads",
194 { threads: thread.params },
195 {
196 ...columns,
197 overlays: [sheet],
198 },
199 );
200 expect(names(next)).toEqual(["Home", "Thread", "ConnectOnboarding"]);
201 expect(next.index).toBe(2);
202 });
203});
204
205describe("Settings links", () => {
206 it("puts the Settings list under a linked page, and a page beside a bare link with columns", () => {
207 const linked = restoreSettingsLinkState(
208 { routes: [{ name: "Home" }, { name: "SettingsKeyboard" }] },
209 undefined,
210 );
211 expect(names(linked!)).toEqual(["Home", "Settings", "SettingsKeyboard"]);
212 const bare = { routes: [{ name: "Home" }, { name: "Settings" }] };
213 expect(names(restoreSettingsLinkState(bare, "SettingsAppearance")!)).toEqual([
214 "Home",
215 "Settings",
216 "SettingsAppearance",
217 ]);
218 expect(restoreSettingsLinkState(bare, undefined)).toBe(bare);
219 });
220
221 it("opens the sign-in link as a modal over the workspace", () => {
222 const state = { routes: [{ name: "Home" }, { name: "SettingsAuth" }] };
223 expect(restoreSettingsLinkState(state, "SettingsAppearance")).toBe(state);
224 });
225});