Skip to content

Commit 0deb383

Browse files
test(mobile): pairing link test runs the app's real link parsing (#26326)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 8e76f94 commit 0deb383

4 files changed

Lines changed: 104 additions & 66 deletions

File tree

‎apps/mobile/src/App.tsx‎

Lines changed: 5 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
createPathConfigForStaticNavigation,
1414
createStaticNavigation,
1515
getActionFromState,
16-
getStateFromPath,
1716
type NavigationAction,
1817
type NavigationState,
1918
} from "@react-navigation/native";
@@ -49,22 +48,12 @@ import { subscribeComposerDraftModelReconciliation } from "./state/use-composer-
4948
import { environmentThreadShells } from "./state/threads";
5049
import { environmentShell } from "./state/shell";
5150
import { OverlayPortalHost } from "./components/ui/OverlayPortal";
52-
import { shouldHandleAppLink, restoreDraftLinkState } from "./lib/appLinking";
51+
import { shouldHandleAppLink } from "./lib/appLinking";
5352
import { hasCloudPublicConfig } from "./features/cloud/publicConfig";
54-
import { takeScheduledTaskNewLink } from "./features/settings/scheduled-task-editor-state";
55-
import {
56-
pairingFormLinkAction,
57-
startDevAutoConnect,
58-
withoutPairingTarget,
59-
} from "./features/connection/devAutoConnect";
60-
import {
61-
resolvePendingNewTaskPromptLink,
62-
takeNewTaskPromptLink,
63-
} from "./features/prompt-links/newTaskPromptLink";
64-
import {
65-
resolvePendingThreadPromptLink,
66-
takeThreadPromptLink,
67-
} from "./features/prompt-links/threadPromptLink";
53+
import { pairingFormLinkAction } from "./features/connection/devAutoConnect";
54+
import { resolvePendingNewTaskPromptLink } from "./features/prompt-links/newTaskPromptLink";
55+
import { resolvePendingThreadPromptLink } from "./features/prompt-links/threadPromptLink";
56+
import { getAppStateFromPath } from "./lib/appLinkState";
6857
import {
6958
disposeOnFoundationReplace,
7059
type FoundationHotModule,
@@ -74,12 +63,10 @@ import { installUiRuntimeMemoryWarningGc } from "./lib/useUiRuntimeMemoryWarning
7463
import { SubscriptionUsageCoordinator } from "./widgets/SubscriptionUsageCoordinator";
7564
import { installSubscriptionUsageRefresh } from "./widgets/useSubscriptionUsage";
7665
import { GlobalVoiceInputControl } from "./features/voice-input/GlobalVoiceInputControl";
77-
import { restoreSettingsLinkState } from "./features/settings/settings-navigation";
7866
import {
7967
hydrateWorkspaceSelections,
8068
launchWorkspaceURL,
8169
recordWorkspaceSelections,
82-
settingsLinkPage,
8370
} from "./features/workspace/workspace-selection-store";
8471
import { workspaceDetailLinkAction } from "./features/workspace/workspace-detail-selection";
8572
import { NativeLayoutMetricsProvider } from "./native/native-layout-metrics";
@@ -107,23 +94,6 @@ const linkingConfig = {
10794
screens: createPathConfigForStaticNavigation(RootStack, { initialRouteName: "Home" }) ?? {},
10895
} as const;
10996

110-
function getAppStateFromPath(path: string, options: Parameters<typeof getStateFromPath>[1]) {
111-
return takeScheduledTaskNewLink(
112-
startDevAutoConnect(
113-
withoutPairingTarget(
114-
takeNewTaskPromptLink(
115-
takeThreadPromptLink(
116-
restoreSettingsLinkState(
117-
restoreDraftLinkState(getStateFromPath(path, options), path),
118-
settingsLinkPage(),
119-
),
120-
),
121-
),
122-
),
123-
),
124-
);
125-
}
126-
12797
const appLinking = {
12898
prefixes: [Linking.createURL("/"), "t3code://", "t3code-dev://", "t3code-preview://"],
12999
config: { initialRouteName: "Home" },

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

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,12 @@ vi.mock("../../lib/rootNavigation", () => ({
6363
},
6464
}));
6565

66-
import { findLinkedRoute } from "../../lib/appLinking";
6766
import { appAtomRegistry } from "../../state/atom-registry";
6867
import {
6968
autoConnectReport,
7069
devAutoConnectAttemptAtom,
7170
pairingFormLinkAction,
7271
startDevAutoConnect,
73-
withoutPairingTarget,
7472
} from "./devAutoConnect";
7573

7674
const PAIRING_URL = "http://192.168.1.10:3773/pair#token=abc";
@@ -256,32 +254,3 @@ it("a pairing link fills the pairing form already on screen instead of stacking
256254
params: { pairingUrl: PAIRING_URL },
257255
});
258256
});
259-
260-
it("a link cannot aim a pairing at a saved environment", () => {
261-
vi.stubGlobal("__DEV__", true);
262-
harness.pair.mockClear();
263-
harness.pair.mockResolvedValue({ ok: false, error: null });
264-
const linking = {
265-
initialRouteName: "Home",
266-
screens: {
267-
Home: "",
268-
Settings: { path: "settings", screens: { SettingsEnvironmentNew: "environment-new" } },
269-
ConnectionsNew: "connections/new",
270-
},
271-
};
272-
for (const path of ["connections/new", "settings/environment-new"]) {
273-
const link = withoutPairingTarget(
274-
getStateFromPath(
275-
`${path}?routeFor=victim&pairingUrl=${encodeURIComponent(PAIRING_URL)}&autoConnect=1`,
276-
linking,
277-
),
278-
);
279-
expect(findLinkedRoute(link, ["ConnectionsNew", "SettingsEnvironmentNew"])?.params).toEqual({
280-
pairingUrl: PAIRING_URL,
281-
autoConnect: "1",
282-
});
283-
284-
startDevAutoConnect(link);
285-
expect(harness.pair).toHaveBeenLastCalledWith(PAIRING_URL);
286-
}
287-
});
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import * as NodeModule from "node:module";
2+
import { expect, it, vi } from "vite-plus/test";
3+
4+
const harness = vi.hoisted(() => {
5+
vi.stubGlobal("__DEV__", true);
6+
return { pair: vi.fn() };
7+
});
8+
9+
// The native package pulls in React Native; its link parsing and actions live in core and routers.
10+
vi.mock("@react-navigation/native", () => {
11+
const requireFromNative = NodeModule.createRequire(
12+
NodeModule.createRequire(import.meta.url).resolve("@react-navigation/native/package.json"),
13+
);
14+
const requireFromCore = NodeModule.createRequire(
15+
requireFromNative.resolve("@react-navigation/core/package.json"),
16+
);
17+
const { getStateFromPath } = requireFromNative("@react-navigation/core");
18+
const { CommonActions, StackActions } = requireFromCore("@react-navigation/routers");
19+
return { getStateFromPath, CommonActions, StackActions };
20+
});
21+
22+
vi.mock("./rootNavigation", () => ({
23+
rootNavigationRef: { isReady: () => false },
24+
whenRootNavigationReady: () => {},
25+
}));
26+
vi.mock("../components/ui/Dialog", () => ({ alert: () => {} }));
27+
vi.mock("../features/prompt-links/prompt-link-actions", () => ({}));
28+
// Stores the prompt-link steps read only when a link carries a prompt; they load the native runtime.
29+
vi.mock("../state/projects", () => ({}));
30+
vi.mock("../state/pull-requests", () => ({}));
31+
vi.mock("../state/server", () => ({}));
32+
vi.mock("../state/threads", () => ({}));
33+
vi.mock("../state/use-composer-drafts", () => ({}));
34+
vi.mock("../state/workspace", () => ({}));
35+
vi.mock("../state/use-remote-environment-registry", () => ({ pairEnvironment: harness.pair }));
36+
vi.mock("../features/workspace/workspace-selection-store", () => ({
37+
settingsLinkPage: () => undefined,
38+
workspaceHasColumns: () => false,
39+
}));
40+
41+
import { findLinkedRoute } from "./appLinking";
42+
import { getAppStateFromPath } from "./appLinkState";
43+
44+
const PAIRING_URL = "http://192.168.1.10:3773/pair#token=abc";
45+
46+
// RootStack imports every screen, which Node cannot load; these are its paths for the pairing form.
47+
const linking = {
48+
initialRouteName: "Home",
49+
screens: {
50+
Home: "",
51+
Settings: "settings",
52+
SettingsEnvironmentNew: "settings/environment-new",
53+
ConnectionsNew: "connections/new",
54+
},
55+
};
56+
57+
it("a link cannot aim a pairing at a saved environment", () => {
58+
harness.pair.mockResolvedValue({ ok: false, error: null });
59+
for (const path of ["connections/new", "settings/environment-new"]) {
60+
harness.pair.mockClear();
61+
const state = getAppStateFromPath(
62+
`${path}?routeFor=victim&pairingUrl=${encodeURIComponent(PAIRING_URL)}&autoConnect=1`,
63+
linking,
64+
);
65+
66+
expect(findLinkedRoute(state, ["ConnectionsNew", "SettingsEnvironmentNew"])?.params).toEqual({
67+
pairingUrl: PAIRING_URL,
68+
autoConnect: "1",
69+
});
70+
expect(harness.pair).toHaveBeenCalledExactlyOnceWith(PAIRING_URL);
71+
}
72+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { getStateFromPath } from "@react-navigation/native";
2+
3+
import { startDevAutoConnect, withoutPairingTarget } from "../features/connection/devAutoConnect";
4+
import { takeNewTaskPromptLink } from "../features/prompt-links/newTaskPromptLink";
5+
import { takeThreadPromptLink } from "../features/prompt-links/threadPromptLink";
6+
import { takeScheduledTaskNewLink } from "../features/settings/scheduled-task-editor-state";
7+
import { restoreSettingsLinkState } from "../features/settings/settings-navigation";
8+
import { settingsLinkPage } from "../features/workspace/workspace-selection-store";
9+
import { restoreDraftLinkState } from "./appLinking";
10+
11+
/** Parses an app link into navigation state, taking each link-owned side effect on the way. */
12+
export function getAppStateFromPath(path: string, options: Parameters<typeof getStateFromPath>[1]) {
13+
return takeScheduledTaskNewLink(
14+
startDevAutoConnect(
15+
withoutPairingTarget(
16+
takeNewTaskPromptLink(
17+
takeThreadPromptLink(
18+
restoreSettingsLinkState(
19+
restoreDraftLinkState(getStateFromPath(path, options), path),
20+
settingsLinkPage(),
21+
),
22+
),
23+
),
24+
),
25+
),
26+
);
27+
}

0 commit comments

Comments
 (0)