diff --git a/apps/mobile/src/features/workspace/WorkspaceRootScreen.android.test.tsx b/apps/mobile/src/features/workspace/WorkspaceRootScreen.android.test.tsx new file mode 100644 index 0000000000000..aedbe9ac3e207 --- /dev/null +++ b/apps/mobile/src/features/workspace/WorkspaceRootScreen.android.test.tsx @@ -0,0 +1,99 @@ +import { act, createContext, useState, type ReactNode } from "react"; +import { createRoot } from "react-dom/client"; +import { expect, it, vi } from "vite-plus/test"; + +const mocks = vi.hoisted(() => ({ split: false })); + +vi.mock("react-native", () => ({ + View: ({ children }: { children: ReactNode }) =>
{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"); + const [filters, setFilters] = useState("30d / Cost / All environments"); + return ( +
+ + setFilters(event.target.value)} /> +
+ ); + }, +})); +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 }) =>
{children}
, +})); +vi.mock("../../components/ui/useAndroidControlSizing", () => ({ + useAndroidControlSizing: () => ({ fabClearance: 64 }), +})); +vi.mock("../home/HomeRouteScreen", () => ({ HomeRouteScreen: () => null })); +vi.mock("../pull-requests/PullRequestFilterMenuContext", () => ({ + PullRequestFilterMenuProvider: ({ children }: { children: ReactNode }) =>
{children}
, +})); +vi.mock("../pull-requests/PullRequestsScreen", () => ({ PullRequestsScreen: () => null })); +vi.mock("../pull-requests/PullRequestToolbar.android", () => ({ PullRequestToolbar: () => null })); +vi.mock("./AndroidWorkspaceNavigation", () => ({ AndroidWorkspaceNavigation: () => null })); +vi.mock("./WorkspaceNoTabs", () => ({ WorkspaceNoTabs: () => null })); + +import { WorkspaceRootScreen } from "./WorkspaceRootScreen.android"; + +function setup(split: boolean) { + mocks.split = split; + vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); + return { [Symbol.dispose]: () => vi.unstubAllGlobals() }; +} + +it.each([false, true])( + "keeps the Usage page, filters, and scroll container through fold transitions (split: %s)", + async (split) => { + using _actEnvironment = setup(split); + const container = document.createElement("div"); + const root = createRoot(container); + using _root = { [Symbol.dispose]: () => act(() => root.unmount()) }; + await act(async () => root.render()); + + const button = container.querySelector("button")!; + await act(async () => button.click()); + const input = container.querySelector("input")!; + const filters = "7d / Tokens / Demo Workstation"; + await act(async () => { + Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!.set!.call( + input, + filters, + ); + input.dispatchEvent(new Event("input", { bubbles: true })); + }); + expect(button.textContent).toBe("Usage"); + expect(input.value).toBe(filters); + const scroll = container.querySelector('[data-testid="usage-scroll"]')!; + scroll.scrollTop = 240; + + for (const nextSplit of [!split, split, !split, split]) { + mocks.split = nextSplit; + await act(async () => root.render()); + expect(container.querySelector("button")?.textContent).toBe("Usage"); + expect(container.querySelector("input")?.value).toBe(filters); + expect(container.querySelector('[data-testid="usage-scroll"]')).toBe(scroll); + expect(scroll.scrollTop).toBe(240); + } + }, +); diff --git a/apps/mobile/src/features/workspace/WorkspaceRootScreen.android.tsx b/apps/mobile/src/features/workspace/WorkspaceRootScreen.android.tsx index 1173a08e867e9..b156f1fcafbe5 100644 --- a/apps/mobile/src/features/workspace/WorkspaceRootScreen.android.tsx +++ b/apps/mobile/src/features/workspace/WorkspaceRootScreen.android.tsx @@ -31,48 +31,29 @@ export function WorkspaceRootScreen() { const context = useMemo( () => ({ ...workspace, - bottomClearance: workspace.state.selectedTab === "threads" ? fabClearance + 16 : 16, + bottomClearance: layout.usesSplitView + ? workspace.bottomClearance + : workspace.state.selectedTab === "threads" + ? fabClearance + 16 + : 16, }), - [workspace, fabClearance], + [workspace, fabClearance, layout.usesSplitView], ); 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 ? ( + <> + + + + + + ) : ( @@ -94,7 +87,7 @@ export function WorkspaceRootScreen() { /> )} - + {!layout.usesSplitView ? : null} ); diff --git a/apps/mobile/vite.config.ts b/apps/mobile/vite.config.ts index 6313768d04587..337ad0a8ae24c 100644 --- a/apps/mobile/vite.config.ts +++ b/apps/mobile/vite.config.ts @@ -7,6 +7,7 @@ const reactTests = [ "src/components/ui/Dialog.android.test.tsx", "src/features/settings/SettingsServerControlsRouteScreen.test.tsx", "src/features/settings/components/BranchNamingSettings.test.tsx", + "src/features/workspace/WorkspaceRootScreen.android.test.tsx", "src/native/NativeHeaderToolbar.test.tsx", "src/native/useRestingTopOffset.test.tsx", "src/native/useRetryingImageSource.test.tsx",