diff --git a/apps/mobile/src/features/workspace/WorkspaceRootScreen.android.tsx b/apps/mobile/src/features/workspace/WorkspaceRootScreen.android.tsx index ae7e6aa0842c0..1fa6a87bf596a 100644 --- a/apps/mobile/src/features/workspace/WorkspaceRootScreen.android.tsx +++ b/apps/mobile/src/features/workspace/WorkspaceRootScreen.android.tsx @@ -33,64 +33,45 @@ export function WorkspaceRootScreen() { const floating = floatingTabsMetrics(enabled.length + 1, scale, fabSize, window.width); const context = { ...workspace, - bottomClearance: floatingTabsEnabled - ? insets.bottom + - FLOATING_TABS.bottom + - FLOATING_TABS.edge + - Math.max( - floating.height, - workspace.state.selectedTab === "threads" ? floating.fabOffset + fabSize : 0, - ) - : workspace.state.selectedTab === "threads" - ? fabClearance + 16 - : 16, + bottomClearance: layout.usesSplitView + ? workspace.bottomClearance + : floatingTabsEnabled + ? insets.bottom + + FLOATING_TABS.bottom + + FLOATING_TABS.edge + + Math.max( + floating.height, + workspace.state.selectedTab === "threads" ? floating.fabOffset + fabSize : 0, + ) + : workspace.state.selectedTab === "threads" + ? fabClearance + 16 + : 16, }; const tab = workspace.state.selectedTab; - if (layout.usesSplitView) { - if (enabled.length === 0) return ; - return ( - - tab === "threads" ? ( - - ) : tab !== "pullRequests" ? ( - - ) : ( - <> - - - - - - - ) - } - /> - ); - } - return ( - - + + {!layout.usesSplitView ? ( + + ) : null} {enabled.length === 0 ? ( - + layout.usesSplitView ? ( + + ) : ( + + ) ) : ( ) : tab !== "pullRequests" ? ( + ) : layout.usesSplitView ? ( + <> + + + + + + ) : ( ( @@ -114,7 +107,7 @@ export function WorkspaceRootScreen() { /> )} - {floatingTabsEnabled ? ( + {layout.usesSplitView ? null : floatingTabsEnabled ? ( ) : ( diff --git a/apps/mobile/src/features/workspace/workspace-tabs-state.test.ts b/apps/mobile/src/features/workspace/workspace-tabs-state.test.ts index d0f72faa07727..8b1fccef6ebc1 100644 --- a/apps/mobile/src/features/workspace/workspace-tabs-state.test.ts +++ b/apps/mobile/src/features/workspace/workspace-tabs-state.test.ts @@ -1,4 +1,7 @@ -import { describe, expect, it } from "vite-plus/test"; +import { describe, expect, it, vi } from "vite-plus/test"; +import { act, createElement, createContext, useState, type ReactNode } from "react"; +import { createRoot } from "react-dom/client"; +import { WorkspaceRootScreen } from "./WorkspaceRootScreen.android"; import { initialWorkspaceTabsState, workspaceTabsReducer, @@ -161,3 +164,91 @@ describe("workspace root navigation", () => { expect(workspaceTabForNativeSlot(order, "removed")).toBeNull(); }); }); + +const mocks = vi.hoisted(() => ({ split: false, floating: false })); + +vi.mock("../settings/appearance/AppearancePreferencesProvider", () => ({ + useAndroidFloatingTabs: () => mocks.floating, +})); +vi.mock("react-native", () => ({ + useWindowDimensions: () => ({ width: 400, height: 800 }), + View: ({ children }: { children: ReactNode }) => createElement("div", null, children), +})); +vi.mock("react-native-safe-area-context", () => ({ + SafeAreaInsetsContext: createContext({ top: 0, right: 0, bottom: 24, left: 0 }), + useSafeAreaInsets: () => ({ top: 0, right: 0, bottom: 24, left: 0 }), +})); +vi.mock("../layout/AdaptiveWorkspaceLayout", () => ({ + useAdaptiveWorkspaceLayout: () => ({ + layout: { usesSplitView: mocks.split }, + startNewTask: () => undefined, + }), +})); +vi.mock("./navigation-tabs", () => ({ + useEnabledNavigationTabs: () => ["threads", "pullRequests", "usage"], +})); +vi.mock("./WorkspaceTabsContext", () => ({ + WorkspaceTabsContext: createContext(null), + useWorkspaceTabs: () => ({ state: { selectedTab: "usage" } }), +})); +vi.mock("./WorkspaceExtraTab", () => ({ + WorkspaceExtraTab: function UsageProbe() { + const [tab, setTab] = useState("Limits"); + return createElement("button", { onClick: () => setTab("Usage") }, tab); + }, +})); +vi.mock("../../native/StackHeader", () => ({ NativeStackScreenOptions: () => null })); +vi.mock("../../components/ui/AndroidScreenHeader", () => ({ AndroidScreenHeader: () => null })); +vi.mock("../../components/ui/EmptyState", () => ({ EmptyState: () => null })); +vi.mock("../../components/ui/ScreenContent", () => ({ + ScreenContent: ({ children }: { children: ReactNode }) => createElement("div", null, children), +})); +vi.mock("../../components/ui/useAndroidControlSizing", () => ({ + useAndroidControlSizing: () => ({ fabClearance: 64, scale: 1, fabSize: 56 }), +})); +vi.mock("../home/HomeRouteScreen", () => ({ HomeRouteScreen: () => null })); +vi.mock("../pull-requests/PullRequestFilterMenuContext", () => ({ + PullRequestFilterMenuProvider: ({ children }: { children: ReactNode }) => + createElement("div", null, children), +})); +vi.mock("../pull-requests/PullRequestsScreen", () => ({ PullRequestsScreen: () => null })); +vi.mock("../pull-requests/PullRequestToolbar.android", () => ({ PullRequestToolbar: () => null })); +vi.mock("./AndroidWorkspaceNavigation", () => ({ + AndroidWorkspaceNavigation: () => null, + FloatingWorkspaceTabs: () => null, +})); +vi.mock("./WorkspaceNoTabs", () => ({ WorkspaceNoTabs: () => null })); + +function setup(split: boolean, floating: boolean) { + mocks.split = split; + mocks.floating = floating; + vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); + return { [Symbol.dispose]: () => vi.unstubAllGlobals() }; +} + +it.each([ + [false, false], + [true, false], + [false, true], + [true, true], +])( + "keeps the Usage page mounted through layout transitions (split: %s, floating: %s)", + async (split, floating) => { + using _actEnvironment = setup(split, floating); + const container = document.createElement("div"); + const root = createRoot(container); + using _root = { [Symbol.dispose]: () => act(() => root.unmount()) }; + await act(async () => root.render(createElement(WorkspaceRootScreen))); + + const button = container.querySelector("button")!; + await act(async () => button.click()); + expect(button.textContent).toBe("Usage"); + + for (const nextSplit of [!split, split, !split, split]) { + mocks.split = nextSplit; + await act(async () => root.render(createElement(WorkspaceRootScreen))); + expect(container.querySelector("button")?.textContent).toBe("Usage"); + expect(container.querySelector("button")).toBe(button); + } + }, +); diff --git a/apps/mobile/vite.config.ts b/apps/mobile/vite.config.ts index 9ddb84efbc832..3b499ad7e3dc6 100644 --- a/apps/mobile/vite.config.ts +++ b/apps/mobile/vite.config.ts @@ -4,6 +4,7 @@ import { defineConfig, normalizePath } from "vite-plus"; import "vite-plus/test/config"; const reactTests = [ + "src/features/workspace/workspace-tabs-state.test.ts", "src/features/threads/git/GitBranchesSheet.occupancy.test.tsx", "src/features/files/useFileTreeEntries.test.tsx", "src/features/projects/AddProjectScreen.test.ts",