From 72d030521f677f6876dc8e5be48f462931039f91 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Wed, 21 Oct 2026 19:23:58 -0500 Subject: [PATCH 1/4] test(mobile): reproduce late worktree base status Co-Authored-By: GPT-6.1 Sol --- .../threads/git/GitBranchesSheet.test.tsx | 132 ++++++++++++++++++ apps/mobile/vite.config.ts | 1 + 2 files changed, 133 insertions(+) create mode 100644 apps/mobile/src/features/threads/git/GitBranchesSheet.test.tsx diff --git a/apps/mobile/src/features/threads/git/GitBranchesSheet.test.tsx b/apps/mobile/src/features/threads/git/GitBranchesSheet.test.tsx new file mode 100644 index 0000000000000..6f030e788eaa3 --- /dev/null +++ b/apps/mobile/src/features/threads/git/GitBranchesSheet.test.tsx @@ -0,0 +1,132 @@ +import { act, type ReactNode } from "react"; +import { createRoot } from "react-dom/client"; +import { expect, it, vi } from "vite-plus/test"; + +type InputProps = { + accessibilityLabel: string; + value: string; + onChangeText: (value: string) => void; +}; +type ActionProps = { label: string; disabled: boolean; onPress: () => void }; +const state = vi.hoisted(() => ({ + refName: null as string | null, + branch: null as string | null, + inputs: new Map(), + actions: new Map(), + worktrees: [] as Array<{ baseBranch: string; newBranch: string }>, +})); + +vi.mock("@react-navigation/native", () => ({ useNavigation: () => ({ goBack() {} }) })); +vi.mock("react-native", () => ({ + Platform: { OS: "android" }, + useWindowDimensions: () => ({ height: 800 }), + View: ({ children }: { children: ReactNode }) => children, + ScrollView: ({ children }: { children: ReactNode }) => children, + Pressable: () => null, +})); +vi.mock("react-native-safe-area-context", () => ({ + useSafeAreaInsets: () => ({ bottom: 0 }), +})); +vi.mock("../../../components/ui/AndroidScreenHeader", () => ({ AndroidSheetHeader: () => null })); +vi.mock("../../../components/ui/ScreenContent", () => ({ + ScreenContent: ({ children }: { children: ReactNode }) => children, +})); +vi.mock("../../../native/StackHeader", () => ({ NativeStackScreenOptions: () => null })); +vi.mock("../../../components/ui/AppText", () => ({ + AppText: ({ children }: { children: ReactNode }) => children, + AppTextInput: (props: InputProps) => { + state.inputs.set(props.accessibilityLabel, props); + return null; + }, +})); +vi.mock("../../../state/query", () => ({ + useEnvironmentQuery: () => ({ data: state.refName === null ? undefined : { refName: state.refName } }), +})); +vi.mock("../../../state/use-thread-selection", () => ({ + useThreadSelection: () => ({ + selectedThread: { environmentId: "environment", branch: state.branch }, + }), +})); +vi.mock("../../../state/use-selected-thread-worktree", () => ({ + useSelectedThreadWorktree: () => ({ selectedThreadCwd: "/repo", selectedThreadWorktreePath: null }), +})); +vi.mock("../../../state/use-selected-thread-git-state", () => ({ + useSelectedThreadGitState: () => ({ + selectedThreadBranches: [], + selectedThreadBranchesLoading: false, + gitOperationLabel: null, + }), +})); +vi.mock("../../../state/use-selected-thread-git-actions", () => ({ + useSelectedThreadGitActions: () => ({ + canChangeThreadBranch: true, + onCreateSelectedThreadWorktree: async (input: { baseBranch: string; newBranch: string }) => { + state.worktrees.push(input); + return null; + }, + }), +})); +vi.mock("../../../state/vcs", () => ({ vcsEnvironment: { status: () => null } })); +vi.mock("./gitSheetComponents", () => ({ + SheetActionButton: (props: ActionProps) => { + state.actions.set(props.label, props); + return null; + }, +})); + +import { GitBranchesSheet } from "./GitBranchesSheet"; + +function setup(branch: string | null = null) { + (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + state.refName = null; + state.branch = branch; + state.inputs.clear(); + state.actions.clear(); + state.worktrees = []; + const root = createRoot(document.createElement("div")); + const render = () => + act(() => { + root.render(); + }); + render(); + return { + render, + edit(label: string, value: string) { + act(() => state.inputs.get(label)!.onChangeText(value)); + }, + create() { + act(() => state.actions.get("Create worktree")!.onPress()); + }, + [Symbol.dispose]() { + act(() => root.unmount()); + }, + }; +} + +it.each([null, "stale-branch"])( + "uses the loaded checkout branch when thread metadata is %s", + (branch) => { + using screen = setup(branch); + screen.edit("Worktree branch name", "feature/new-worktree"); + state.refName = "release/stable"; + screen.render(); + screen.create(); + expect(state.worktrees).toEqual([ + { baseBranch: "release/stable", newBranch: "feature/new-worktree" }, + ]); + }, +); + +it.each(["custom-base", ""])("preserves an edited base %j when status arrives", (base) => { + using screen = setup(); + screen.edit("Worktree base branch", base); + screen.edit("Worktree branch name", "feature/new-worktree"); + state.refName = "release/stable"; + screen.render(); + expect(state.inputs.get("Worktree base branch")!.value).toBe(base); + expect(state.actions.get("Create worktree")!.disabled).toBe(base === ""); + screen.create(); + expect(state.worktrees).toEqual( + base === "" ? [] : [{ baseBranch: base, newBranch: "feature/new-worktree" }], + ); +}); diff --git a/apps/mobile/vite.config.ts b/apps/mobile/vite.config.ts index 3f66714c68815..9c09411d18247 100644 --- a/apps/mobile/vite.config.ts +++ b/apps/mobile/vite.config.ts @@ -2,6 +2,7 @@ import { defineConfig } from "vite-plus"; import "vite-plus/test/config"; const reactTests = [ + "src/features/threads/git/GitBranchesSheet.test.tsx", "src/components/ui/Dialog.android.test.tsx", "src/features/settings/SettingsServerControlsRouteScreen.test.tsx", "src/features/settings/components/BranchNamingSettings.test.tsx", From a42bfe6d7033189764545e24a44dfa44bf4972eb Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Wed, 21 Oct 2026 19:37:53 -0500 Subject: [PATCH 2/4] fix(mobile): follow loaded status for untouched worktree base Co-Authored-By: GPT-6.1 Sol --- .../mobile/src/features/threads/git/GitBranchesSheet.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/mobile/src/features/threads/git/GitBranchesSheet.tsx b/apps/mobile/src/features/threads/git/GitBranchesSheet.tsx index d1d39357f2fa8..3bdb2b0c69d48 100644 --- a/apps/mobile/src/features/threads/git/GitBranchesSheet.tsx +++ b/apps/mobile/src/features/threads/git/GitBranchesSheet.tsx @@ -48,9 +48,10 @@ export function GitBranchesSheet(_props: GitBranchesSheetProps) { const busy = gitState.gitOperationLabel !== null; const [newBranchName, setNewBranchName] = useState(""); - const [worktreeBaseBranch, setWorktreeBaseBranch] = useState( - currentBranchLabel === "Detached HEAD" ? "main" : currentBranchLabel, - ); + const [worktreeBaseBranchOverride, setWorktreeBaseBranchOverride] = useState(null); + const worktreeBaseBranch = + worktreeBaseBranchOverride ?? + (currentBranchLabel === "Detached HEAD" ? "main" : currentBranchLabel); const [worktreeBranchName, setWorktreeBranchName] = useState(""); const disabledExistingBranchNames: Array = []; @@ -133,7 +134,7 @@ export function GitBranchesSheet(_props: GitBranchesSheetProps) { ) : null} Date: Wed, 21 Oct 2026 19:49:01 -0500 Subject: [PATCH 3/4] test(mobile): format worktree status regression coverage Co-Authored-By: GPT-6.1 Sol --- .../threads/git/GitBranchesSheet.test.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/mobile/src/features/threads/git/GitBranchesSheet.test.tsx b/apps/mobile/src/features/threads/git/GitBranchesSheet.test.tsx index 6f030e788eaa3..3216584e78aea 100644 --- a/apps/mobile/src/features/threads/git/GitBranchesSheet.test.tsx +++ b/apps/mobile/src/features/threads/git/GitBranchesSheet.test.tsx @@ -40,7 +40,9 @@ vi.mock("../../../components/ui/AppText", () => ({ }, })); vi.mock("../../../state/query", () => ({ - useEnvironmentQuery: () => ({ data: state.refName === null ? undefined : { refName: state.refName } }), + useEnvironmentQuery: () => ({ + data: state.refName === null ? undefined : { refName: state.refName }, + }), })); vi.mock("../../../state/use-thread-selection", () => ({ useThreadSelection: () => ({ @@ -48,7 +50,10 @@ vi.mock("../../../state/use-thread-selection", () => ({ }), })); vi.mock("../../../state/use-selected-thread-worktree", () => ({ - useSelectedThreadWorktree: () => ({ selectedThreadCwd: "/repo", selectedThreadWorktreePath: null }), + useSelectedThreadWorktree: () => ({ + selectedThreadCwd: "/repo", + selectedThreadWorktreePath: null, + }), })); vi.mock("../../../state/use-selected-thread-git-state", () => ({ useSelectedThreadGitState: () => ({ @@ -86,7 +91,11 @@ function setup(branch: string | null = null) { const root = createRoot(document.createElement("div")); const render = () => act(() => { - root.render(); + root.render( + , + ); }); render(); return { From 02dbeb0e187c09bac030a15816c7c429902bf2a3 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Wed, 21 Oct 2026 20:10:19 -0500 Subject: [PATCH 4/4] test(mobile): isolate native sheet header chrome Co-Authored-By: GPT-6.1 Sol --- apps/mobile/src/features/threads/git/GitBranchesSheet.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/mobile/src/features/threads/git/GitBranchesSheet.test.tsx b/apps/mobile/src/features/threads/git/GitBranchesSheet.test.tsx index 3216584e78aea..bc999e3e1cb4c 100644 --- a/apps/mobile/src/features/threads/git/GitBranchesSheet.test.tsx +++ b/apps/mobile/src/features/threads/git/GitBranchesSheet.test.tsx @@ -28,6 +28,7 @@ vi.mock("react-native-safe-area-context", () => ({ useSafeAreaInsets: () => ({ bottom: 0 }), })); vi.mock("../../../components/ui/AndroidScreenHeader", () => ({ AndroidSheetHeader: () => null })); +vi.mock("../../../components/ui/SheetHeader", () => ({ SheetHeader: () => null })); vi.mock("../../../components/ui/ScreenContent", () => ({ ScreenContent: ({ children }: { children: ReactNode }) => children, }));