Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 29 additions & 36 deletions apps/mobile/src/features/workspace/WorkspaceRootScreen.android.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,48 +30,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 <View className="flex-1 bg-screen" />;
return (
<WorkspaceTabPanes
tabs={TABS}
selected={tab}
renderTab={(tab) =>
tab === "threads" ? (
<HomeRouteScreen />
) : tab !== "pullRequests" ? (
<WorkspaceExtraTab tab={tab} />
) : (
<>
<NativeStackScreenOptions options={{ headerShown: false }} />
<AndroidScreenHeader title="Pull requests" />
<View className="flex-1 items-center justify-center px-10">
<EmptyState
title="Select a pull request"
detail="Choose a pull request from the sidebar to review it."
variant="plain"
/>
</View>
</>
)
}
/>
);
}

return (
<WorkspaceTabsContext value={context}>
<View className="flex-1 bg-header">
<NativeStackScreenOptions options={{ headerShown: false }} />
<SafeAreaInsetsContext value={{ ...insets, bottom: 0 }}>
<View className={layout.usesSplitView ? "flex-1 bg-screen" : "flex-1 bg-header"}>
{!layout.usesSplitView ? (
<NativeStackScreenOptions options={{ headerShown: false }} />
) : null}
<SafeAreaInsetsContext value={layout.usesSplitView ? insets : { ...insets, bottom: 0 }}>
{enabled.length === 0 ? (
<WorkspaceNoTabs />
layout.usesSplitView ? (
<View className="flex-1 bg-screen" />
) : (
<WorkspaceNoTabs />
)
) : (
<WorkspaceTabPanes
tabs={TABS}
Expand All @@ -81,6 +62,18 @@ export function WorkspaceRootScreen() {
<HomeRouteScreen />
) : tab !== "pullRequests" ? (
<WorkspaceExtraTab tab={tab} />
) : layout.usesSplitView ? (
<>
<NativeStackScreenOptions options={{ headerShown: false }} />
<AndroidScreenHeader title="Pull requests" />
<View className="flex-1 items-center justify-center px-10">
<EmptyState
title="Select a pull request"
detail="Choose a pull request from the sidebar to review it."
variant="plain"
/>
</View>
</>
) : (
<PullRequestsScreen
renderFrame={(filters, list) => (
Expand All @@ -95,7 +88,7 @@ export function WorkspaceRootScreen() {
/>
)}
</SafeAreaInsetsContext>
<AndroidWorkspaceNavigation onNewThread={startNewTask} />
{!layout.usesSplitView ? <AndroidWorkspaceNavigation onNewThread={startNewTask} /> : null}
</View>
</WorkspaceTabsContext>
);
Expand Down
80 changes: 79 additions & 1 deletion apps/mobile/src/features/workspace/workspace-tabs-state.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -161,3 +164,78 @@ describe("workspace root navigation", () => {
expect(workspaceTabForNativeSlot(order, "removed")).toBeNull();
});
});

const mocks = vi.hoisted(() => ({ split: false }));

vi.mock("react-native", () => ({
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 }),
}));
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 }));
vi.mock("./WorkspaceNoTabs", () => ({ WorkspaceNoTabs: () => null }));

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 mounted through layout 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(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);
}
},
);
1 change: 1 addition & 0 deletions apps/mobile/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/files/useFileTreeEntries.test.tsx",
"src/features/projects/AddProjectScreen.test.ts",
"src/lib/useMediaVideoPlayback.test.tsx",
Expand Down
Loading