Skip to content

Commit 47bcbe1

Browse files
fix(mobile): dev auto-connect success leaves Settings the way Connect does
A successful settings/environment-new auto-connect only popped the form, which on columns left Settings with an empty detail. It now exits through leaveSettingsPage, as main and the form's own Connect do, so the new environment shows on Environments. The deferred success callback also checks that its own attempt is still current before clearing it, so a newer link without autoConnect keeps its form open. Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent 12de752 commit 47bcbe1

2 files changed

Lines changed: 82 additions & 10 deletions

File tree

‎apps/mobile/src/features/connection/devAutoConnect.test.ts‎

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,43 @@ import { expect, it, vi } from "vite-plus/test";
44
const harness = vi.hoisted(() => ({
55
pair: vi.fn(),
66
goBack: vi.fn(),
7-
currentRoute: "SettingsEnvironmentNew",
7+
dispatch: vi.fn(),
8+
columns: false,
9+
// The root stack the form sits on when pairing succeeds.
10+
rootState: {
11+
key: "root",
12+
index: 2,
13+
routes: [
14+
{ key: "home", name: "Home" },
15+
{ key: "settings", name: "Settings" },
16+
{ key: "new", name: "SettingsEnvironmentNew" },
17+
],
18+
},
19+
// Navigation that is not ready yet holds callbacks here until the test flushes them.
20+
deferred: null as Array<() => void> | null,
821
}));
922

1023
vi.mock("@react-navigation/native", () => ({ StackActions: { replace: vi.fn() } }));
1124
vi.mock("../../state/use-remote-environment-registry", () => ({
1225
pairEnvironment: harness.pair,
1326
}));
27+
vi.mock("../workspace/workspace-selection-store", () => ({
28+
workspaceHasColumns: () => harness.columns,
29+
}));
1430
vi.mock("../../lib/rootNavigation", () => ({
1531
rootNavigationRef: {
16-
getCurrentRoute: () => ({ name: harness.currentRoute }),
32+
getCurrentRoute: () => harness.rootState.routes[harness.rootState.index],
33+
getRootState: () => harness.rootState,
34+
getState: () => harness.rootState,
35+
getParent: () => undefined,
1736
canGoBack: () => true,
1837
goBack: harness.goBack,
19-
dispatch: vi.fn(),
38+
dispatch: harness.dispatch,
39+
},
40+
whenRootNavigationReady: (navigate: () => void) => {
41+
if (harness.deferred) harness.deferred.push(navigate);
42+
else navigate();
2043
},
21-
whenRootNavigationReady: (navigate: () => void) => navigate(),
2244
}));
2345

2446
import { appAtomRegistry } from "../../state/atom-registry";
@@ -122,3 +144,50 @@ it("keeps Connect busy while the auto-connect runs, even after the user edits th
122144
expect(autoConnectReport(failed, PAIRING_URL, true).error).toBeNull();
123145
expect(autoConnectReport(connecting, "http://other/pair#token=x", false).connecting).toBe(false);
124146
});
147+
148+
async function pairSuccessfully(link: PartialState<NavigationState>) {
149+
const succeeded = Promise.withResolvers<{ ok: boolean; error: string | null }>();
150+
harness.pair.mockReturnValueOnce(succeeded.promise);
151+
startDevAutoConnect(link);
152+
succeeded.resolve({ ok: true, error: null });
153+
await succeeded.promise;
154+
await Promise.resolve();
155+
}
156+
157+
it("on columns, a successful settings link shows Environments instead of an empty detail", async () => {
158+
vi.stubGlobal("__DEV__", true);
159+
harness.goBack.mockClear();
160+
harness.dispatch.mockClear();
161+
harness.columns = true;
162+
await pairSuccessfully(settingsLink({ pairingUrl: PAIRING_URL, autoConnect: "1" }));
163+
harness.columns = false;
164+
165+
expect(harness.goBack).not.toHaveBeenCalled();
166+
expect(harness.dispatch).toHaveBeenCalledExactlyOnceWith({
167+
type: "RESET",
168+
target: "root",
169+
payload: expect.objectContaining({
170+
index: 2,
171+
routes: [
172+
{ key: "home", name: "Home" },
173+
{ key: "settings", name: "Settings" },
174+
{ name: "SettingsEnvironments", params: undefined },
175+
],
176+
}),
177+
});
178+
});
179+
180+
it("a success that waited for navigation leaves a newer link's form open", async () => {
181+
vi.stubGlobal("__DEV__", true);
182+
harness.goBack.mockClear();
183+
harness.dispatch.mockClear();
184+
harness.deferred = [];
185+
await pairSuccessfully(settingsLink({ pairingUrl: PAIRING_URL, autoConnect: "1" }));
186+
startDevAutoConnect(settingsLink({ pairingUrl: "http://192.168.1.11:3773/pair#token=def" }));
187+
188+
const deferred = harness.deferred;
189+
harness.deferred = null;
190+
for (const navigate of deferred) navigate();
191+
expect(harness.goBack).not.toHaveBeenCalled();
192+
expect(harness.dispatch).not.toHaveBeenCalled();
193+
});

‎apps/mobile/src/features/connection/devAutoConnect.ts‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { Atom } from "effect/reactivity";
66
import { findLinkedRoute } from "../../lib/appLinking";
77
import { rootNavigationRef, whenRootNavigationReady } from "../../lib/rootNavigation";
88
import { appAtomRegistry } from "../../state/atom-registry";
9+
import { leaveSettingsPage } from "../settings/settings-navigation";
10+
import { workspaceHasColumns } from "../workspace/workspace-selection-store";
911
import { pairEnvironment } from "../../state/use-remote-environment-registry";
1012

1113
export type DevAutoConnectAttempt =
@@ -56,15 +58,16 @@ async function connect(pairingUrl: string, expectedEnvironmentId: EnvironmentId
5658
});
5759
return;
5860
}
59-
appAtomRegistry.set(devAutoConnectAttemptAtom, null);
6061
whenRootNavigationReady(() => {
61-
// A link that arrived before navigation was ready owns the form now.
62-
if (appAtomRegistry.get(devAutoConnectAttemptAtom) !== null) return;
62+
// A newer link (with or without the flag) owns the form now.
63+
if (appAtomRegistry.get(devAutoConnectAttemptAtom) !== attempt) return;
64+
appAtomRegistry.set(devAutoConnectAttemptAtom, null);
6365
const current = rootNavigationRef.getCurrentRoute()?.name;
6466
if (current === undefined || !PAIRING_FORM_ROUTES.includes(current)) return;
65-
// Return to whatever the link opened the sheet over.
66-
if (rootNavigationRef.canGoBack()) rootNavigationRef.goBack();
67-
else rootNavigationRef.dispatch(StackActions.replace("Home"));
67+
// Same exit as the form's own Connect: return to the opener, and on columns show the
68+
// new environment on Environments instead of an empty detail.
69+
if (!rootNavigationRef.canGoBack()) rootNavigationRef.dispatch(StackActions.replace("Home"));
70+
else leaveSettingsPage(rootNavigationRef, workspaceHasColumns(), "SettingsEnvironments");
6871
});
6972
}
7073

0 commit comments

Comments
 (0)