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

settingsScopeNavigation.test.ts

286 lines11.3 KB

1import { EnvironmentId, ProviderInstanceId, ScheduledTaskId } from "@t3tools/contracts";
2import {
3 createMemoryHistory,
4 createRootRoute,
5 createRoute,
6 createRouter,
7 redirect,
8} from "@tanstack/react-router";
9import { describe, expect, it } from "vite-plus/test";
10​
11import { resolveSettingsScope } from "./settingsScope";
12import { retainSettingsScope, validateSettingsRouteSearch } from "./settingsScopeNavigation";
13​
14import { validateScheduledTasksSearch } from "./scheduledTasksSettings.logic";
15​
16const checkoutSearch = {
17 project: "repository:t3code",
18 machine: "remote-server",
19 checkout: "remote-server:/home/user/T3 Code",
20};
21​
22function createSettingsRouter(initialEntry = "/settings/general") {
23 const root = createRootRoute();
24 const settings = createRoute({
25 getParentRoute: () => root,
26 path: "settings",
27 validateSearch: validateSettingsRouteSearch,
28 search: { middlewares: [retainSettingsScope] },
29 beforeLoad: ({ location }) => {
30 if (location.pathname === "/settings") {
31 throw redirect({ to: "/settings/general", replace: true });
32 }
33 },
34 });
35 const general = createRoute({ getParentRoute: () => settings, path: "general" });
36 const projects = createRoute({ getParentRoute: () => settings, path: "projects" });
37 const integrations = createRoute({ getParentRoute: () => settings, path: "integrations" });
38 const sourceControl = createRoute({ getParentRoute: () => settings, path: "source-control" });
39 const providers = createRoute({
40 getParentRoute: () => settings,
41 path: "providers",
42 validateSearch: (raw: Record<string, unknown>) => ({
43 ...(typeof raw.environmentId === "string" && raw.environmentId.trim()
44 ? { environmentId: EnvironmentId.make(raw.environmentId) }
45 : {}),
46 ...(typeof raw.instanceId === "string" && raw.instanceId.trim()
47 ? { instanceId: ProviderInstanceId.make(raw.instanceId) }
48 : {}),
49 }),
50 });
51 const scheduledTasks = createRoute({
52 getParentRoute: () => settings,
53 path: "scheduled-tasks",
54 validateSearch: validateScheduledTasksSearch,
55 });
56 const legacyProject = createRoute({
57 getParentRoute: () => root,
58 path: "projects/$projectKey",
59 beforeLoad: ({ params }) => {
60 throw redirect({
61 to: "/settings/projects",
62 search: { project: params.projectKey, machine: undefined },
63 replace: true,
64 });
65 },
66 });
67 return createRouter({
68 routeTree: root.addChildren([
69 settings.addChildren([
70 general,
71 projects,
72 integrations,
73 sourceControl,
74 providers,
75 scheduledTasks,
76 ]),
77 legacyProject,
78 ]),
79 history: createMemoryHistory({ initialEntries: [initialEntry] }),
80 });
82​
83describe("settings scope navigation", () => {
84 it("keeps an explicit page tab in its pane through category and scope navigation", async () => {
85 const router = createSettingsRouter();
86 await router.navigate({
87 to: "/settings/general",
88 search: { ...checkoutSearch, pane: 1 },
89 });
90 await router.navigate({ to: "/settings/integrations" });
91 expect(router.state.location.search).toEqual({ ...checkoutSearch, pane: 1 });
92 await router.navigate({ to: "/settings/projects", search: { project: "another-project" } });
93 expect(router.state.location.search).toEqual({ project: "another-project", pane: 1 });
94 });
95​
96 it("opens default settings navigation fullscreen while retaining its selected scope", async () => {
97 const router = createSettingsRouter();
98 await router.navigate({
99 to: "/settings/general",
100 search: { ...checkoutSearch, pane: 1 },
101 });
102 await router.navigate({ to: "/settings", search: { pane: undefined } });
103 expect(router.state.redirect).not.toBeUndefined();
104 await router.navigate(router.state.redirect!.options);
105 expect(router.state.location.pathname).toBe("/settings/general");
106 expect(router.state.location.search).toEqual(checkoutSearch);
107 });
108​
109 it("clears a checkout when selecting all environments and all projects", async () => {
110 const router = createSettingsRouter();
111 await router.navigate({ to: "/settings/general", search: checkoutSearch, hash: "old-setting" });
112 await router.navigate({
113 to: "/settings/general",
114 search: { project: undefined, machine: undefined, checkout: undefined },
115 hash: "",
116 });
117 expect(router.state.location.search).toEqual({});
118 expect(router.state.location.hash).toBe("");
119 });
120​
121 it("preserves the checkout through category and settings-search navigation", async () => {
122 const router = createSettingsRouter();
123 await router.navigate({ to: "/settings/general", search: checkoutSearch, hash: "new-threads" });
124 await router.navigate({ to: "/settings/integrations", hash: "", replace: true });
125 expect(router.state.location.search).toEqual(checkoutSearch);
126 expect(router.state.location.hash).toBe("");
127 await router.navigate({ to: "/settings/source-control", hash: "source-control-writing-style" });
128 expect(router.state.location.search).toEqual(checkoutSearch);
129 expect(router.state.location.hash).toBe("source-control-writing-style");
130 await router.navigate({ to: "/settings/projects", hash: "project-defaults" });
131 expect(router.state.location.search).toEqual(checkoutSearch);
132 });
133​
134 it.each(["/settings/projects", "/settings/scheduled-tasks"] as const)(
135 "keeps %s when regrouping or selecting a target from the shared settings layout",
136 async (to) => {
137 const router = createSettingsRouter();
138 await router.navigate({ to, search: checkoutSearch });
139​
140 const regroupedCheckout = { ...checkoutSearch, project: "separate:t3code" };
141 await router.navigate({
142 from: "/settings",
143 to: router.state.location.pathname,
144 search: () => regroupedCheckout,
145 replace: true,
146 hashScrollIntoView: false,
147 });
148 expect(router.state.location.pathname).toBe(to);
149 expect(router.state.location.search).toEqual(regroupedCheckout);
150 expect(router.state.redirect).toBeUndefined();
151​
152 await router.navigate({
153 from: "/settings",
154 to: router.state.location.pathname,
155 search: () => ({ machine: "another-server" }),
156 hash: "",
157 resetScroll: false,
158 });
159 expect(router.state.location.pathname).toBe(to);
160 expect(router.state.location.search).toEqual({ machine: "another-server" });
161 expect(router.state.location.hash).toBe("");
162​
163 const selectedCheckout = {
164 project: "another-project",
165 machine: "another-server",
166 checkout: "another-server:/home/user/Another checkout",
167 };
168 await router.navigate({
169 from: "/settings",
170 to: router.state.location.pathname,
171 search: () => selectedCheckout,
172 hash: "requested-setting",
173 });
174 expect(router.state.location.pathname).toBe(to);
175 expect(router.state.location.search).toEqual(selectedCheckout);
176 expect(router.state.location.hash).toBe("requested-setting");
177 },
178 );
179​
180 it("honors an explicit provider environment and drops its instance on category navigation", async () => {
181 const router = createSettingsRouter();
182 await router.navigate({ to: "/settings/general", search: checkoutSearch });
183 await router.navigate({
184 to: "/settings/providers",
185 search: {
186 environmentId: EnvironmentId.make("provider-server"),
187 instanceId: ProviderInstanceId.make("codex-work"),
188 },
189 });
190 expect(router.state.location.search).toEqual({
191 machine: "provider-server",
192 environmentId: "provider-server",
193 instanceId: "codex-work",
194 });
195 await router.navigate({ to: "/settings/general", hash: "" });
196 expect(router.state.location.search).toEqual({ machine: "provider-server" });
197 });
198​
199 it("preserves the environment from an initially loaded legacy provider URL", async () => {
200 const router = createSettingsRouter(
201 "/settings/providers?environmentId=provider-server&instanceId=codex-work",
202 );
203 await router.load();
204 await router.navigate({ to: "/settings/general" });
205 expect(router.state.location.search).toEqual({ machine: "provider-server" });
206 });
207​
208 it("retains an explicit unavailable target rather than reviving the previous environment", async () => {
209 const router = createSettingsRouter();
210 await router.navigate({ to: "/settings/general", search: { machine: "online" } });
211 await router.navigate({ to: "/settings/general", search: { machine: "removed" } });
212 expect(router.state.location.search).toEqual({ machine: "removed" });
213 expect(
214 resolveSettingsScope(
215 router.state.location.search,
216 [],
217 [{ environmentId: EnvironmentId.make("online"), label: "Online" }],
218 ),
219 ).toMatchObject({
220 kind: "unavailable",
221 reason: "environment-missing",
222 environmentIds: [],
223 });
224 });
225​
226 it("preserves escaped checkout identifiers across reload and browser history", async () => {
227 const router = createSettingsRouter();
228 await router.navigate({ to: "/settings/general", search: checkoutSearch });
229 const checkoutHref = router.state.location.href;
230 await router.navigate({ to: "/settings/general", search: { machine: "another" } });
231 router.history.back();
232 await router.load();
233 expect(router.state.location.search).toEqual(checkoutSearch);
234 const reloaded = createSettingsRouter(checkoutHref);
235 await reloaded.load();
236 expect(reloaded.state.location.search).toEqual(checkoutSearch);
237 await reloaded.navigate({ to: "/settings/integrations", hash: "agent-browser-access" });
238 expect(reloaded.state.location.search).toEqual(checkoutSearch);
239 });
240​
241 it("redirects legacy project links without carrying the prior checkout scope", async () => {
242 const router = createSettingsRouter();
243 await router.navigate({ to: "/settings/general", search: checkoutSearch });
244 await router.navigate({
245 to: "/projects/$projectKey",
246 params: { projectKey: "legacy-project" },
247 });
248 expect(router.state.redirect).not.toBeUndefined();
249 await router.navigate(router.state.redirect!.options);
250 expect(router.state.location.pathname).toBe("/settings/projects");
251 expect(router.state.location.search).toEqual({ project: "legacy-project" });
252 });
253​
254 it("keeps scope through the settings index redirect", async () => {
255 const router = createSettingsRouter();
256 await router.navigate({ to: "/settings", search: { machine: "remote-server" } });
257 expect(router.state.redirect).not.toBeUndefined();
258 await router.navigate(router.state.redirect!.options);
259 expect(router.state.location.pathname).toBe("/settings/general");
260 expect(router.state.location.search).toEqual({ machine: "remote-server" });
261 });
262});
263​
264describe("scheduled task scope navigation", () => {
265 it("opens a task link on its owning environment, then clears the task when changing filters", async () => {
266 const router = createSettingsRouter();
267 await router.navigate({
268 to: "/settings/scheduled-tasks",
269 search: {
270 environmentId: EnvironmentId.make("remote-server"),
271 taskId: ScheduledTaskId.make("task-1"),
272 },
273 });
274 expect(router.state.matches.at(-1)?.search).toEqual({
275 machine: "remote-server",
276 environmentId: "remote-server",
277 taskId: "task-1",
278 });
279 await router.navigate({
280 from: "/settings",
281 to: router.state.location.pathname,
282 search: () => ({ machine: undefined, project: undefined, checkout: undefined }),
283 });
284 expect(router.state.matches.at(-1)?.search).toEqual({});
285 });
286});