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",