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
Original file line number Diff line number Diff line change
@@ -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 }) => <div>{children}</div>,
}));
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 (
<div data-testid="usage-scroll">
<button onClick={() => setTab("Usage")}>{tab}</button>
<input value={filters} onChange={(event) => setFilters(event.target.value)} />
</div>
);
},
}));
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 }) => <div>{children}</div>,
}));
vi.mock("../../components/ui/useAndroidControlSizing", () => ({
useAndroidControlSizing: () => ({ fabClearance: 64 }),
}));
vi.mock("../home/HomeRouteScreen", () => ({ HomeRouteScreen: () => null }));
vi.mock("../pull-requests/PullRequestFilterMenuContext", () => ({
PullRequestFilterMenuProvider: ({ children }: { children: ReactNode }) => <div>{children}</div>,
}));
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(<WorkspaceRootScreen />));

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<HTMLElement>('[data-testid="usage-scroll"]')!;
scroll.scrollTop = 240;

for (const nextSplit of [!split, split, !split, split]) {
mocks.split = nextSplit;
await act(async () => root.render(<WorkspaceRootScreen />));
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);
}
},
);
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 @@ -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 <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 @@ -82,6 +63,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>
</>
) : (
<PullRequestFilterMenuProvider>
<PullRequestToolbar />
Expand All @@ -94,7 +87,7 @@ export function WorkspaceRootScreen() {
/>
)}
</SafeAreaInsetsContext>
<AndroidWorkspaceNavigation onNewThread={startNewTask} />
{!layout.usesSplitView ? <AndroidWorkspaceNavigation onNewThread={startNewTask} /> : null}
</View>
</WorkspaceTabsContext>
);
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading