Skip to content
Merged
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
1 change: 1 addition & 0 deletions apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"expo": "~54.0.37",
"expo-build-properties": "~1.0.10",
"expo-camera": "~17.0.10",
"expo-clipboard": "~8.0.8",
"expo-constants": "~18.0.14",
"expo-crypto": "~15.0.9",
"expo-dev-client": "~6.0.21",
Expand Down
15 changes: 15 additions & 0 deletions apps/mobile/src/navigation/root-navigation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ jest.mock("../screens/followed-projects-screen", () => {
const { Text } = jest.requireActual<typeof import("react-native")>("react-native");
return { FollowedProjectsScreen: () => <Text>Followed projects screen</Text> };
});
jest.mock("../screens/diff-screen", () => {
const { Text } = jest.requireActual<typeof import("react-native")>("react-native");
return { DiffScreen: () => <Text>Diff screen</Text> };
});
jest.mock("../screens/new-session-screen", () => {
const { Text } = jest.requireActual<typeof import("react-native")>("react-native");
return { NewSessionScreen: () => <Text>New session screen</Text> };
Expand Down Expand Up @@ -120,6 +124,17 @@ test("pushes session detail and presents workspace management routes over it", a
);
expect(await screen.findByText("Session screen")).toBeOnTheScreen();

act(() =>
navigation.navigate("Diff", {
connectionId: "connection-1",
location: { directory: "/workspace" },
mode: "working",
}),
);
expect(await screen.findByText("Diff screen")).toBeOnTheScreen();
act(() => navigation.goBack());
expect(await screen.findByText("Session screen")).toBeOnTheScreen();

act(() => navigation.navigate("NewSession"));
expect(await screen.findByText("New session screen")).toBeOnTheScreen();
act(() => navigation.goBack());
Expand Down
11 changes: 11 additions & 0 deletions apps/mobile/src/navigation/root-navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
SettingsScreen,
} from "../screens/app-shell";
import { ConnectionScreen } from "../screens/connection-screen";
import { DiffScreen } from "../screens/diff-screen";
import { FollowedProjectsScreen } from "../screens/followed-projects-screen";
import { NewSessionScreen } from "../screens/new-session-screen";
import { NotificationPairingScreen } from "../screens/notification-pairing-screen";
Expand All @@ -31,6 +32,11 @@ import { WorkspaceHeaderActions } from "./workspace-header-actions";

export type RootStackParamList = {
Connections: undefined;
Diff: {
connectionId: string;
location: LocationRef;
mode: "branch" | "working";
};
FollowedProjects: undefined;
NewSession: undefined;
Pending: undefined;
Expand Down Expand Up @@ -148,6 +154,11 @@ export function RootNavigation() {
title: "Session",
})}
/>
<Stack.Screen
component={DiffScreen}
name="Diff"
options={{ headerShown: true, title: "Current changes" }}
/>
<Stack.Screen
component={NewSessionScreen}
name="NewSession"
Expand Down
66 changes: 66 additions & 0 deletions apps/mobile/src/screens/app-shell.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect, jest, test } from "@jest/globals";
import { fireEvent, render, screen, waitFor } from "@testing-library/react-native";
import * as Clipboard from "expo-clipboard";
import { createElement } from "react";
import { Dimensions, Text } from "react-native";

Expand All @@ -19,6 +20,7 @@ jest.mock("../security/app-lock-context", () => ({ useAppLock: jest.fn() }));
jest.mock("../state/connection-runtime-context", () => ({ useConnectionRuntime: jest.fn() }));
jest.mock("../state/workspace-selection-context", () => ({ useWorkspaceSelection: jest.fn() }));
jest.mock("./form-request-list", () => ({ FormRequestList: () => null }));
jest.mock("expo-clipboard", () => ({ setStringAsync: jest.fn(async () => undefined) }));

test("communicates every transport status without relying on color", () => {
expect(getConnectionPresentation("connected", 0).label).toBe("LIVE");
Expand Down Expand Up @@ -95,6 +97,70 @@ test("uses native navigation on phones and retains the tablet rail", () => {
}
});

test("reveals and copies the full session branch name", async () => {
jest.mocked(useConnections).mockReturnValue({
profiles: [{ id: "connection-1", name: "Test server" }],
selectedProfileId: "connection-1",
} as never);
jest
.mocked(useConnectionRuntime)
.mockReturnValue({ reconnectAttempt: 0, status: "connected" } as never);
jest.mocked(useWorkspaceSelection).mockReturnValue({
attentionCoverage: { completeness: "complete" },
pendingCount: 0,
} as never);
const branchName = "docs/a-very-long-mobile-workflow-screenshots-branch";

const view = render(
createElement(
ShellFrame,
{
active: "Workspace",
branch: { name: branchName, state: "known" },
navigate: jest.fn(),
},
createElement(Text, null, "Session content"),
),
);

fireEvent.press(screen.getByRole("button", { name: `Current branch, ${branchName}` }));
expect(screen.getByRole("header", { name: "Current branch" })).toBeOnTheScreen();
fireEvent.press(screen.getByRole("button", { name: "Copy branch name" }));
await waitFor(() => expect(Clipboard.setStringAsync).toHaveBeenCalledWith(branchName));
expect(screen.getByRole("button", { name: "Copied" })).toBeOnTheScreen();
view.unmount();
});

test("reveals and copies the full server name", async () => {
jest.mocked(useConnections).mockReturnValue({
profiles: [{ id: "connection-1", name: "A server name too long for the metadata bar" }],
selectedProfileId: "connection-1",
} as never);
jest.mocked(useConnectionRuntime).mockReturnValue({
reconnectAttempt: 0,
status: "connected",
} as never);
jest.mocked(useWorkspaceSelection).mockReturnValue({
attentionCoverage: { completeness: "complete" },
pendingCount: 0,
} as never);
const serverName = "A server name too long for the metadata bar";

const view = render(
createElement(
ShellFrame,
{ active: "Workspace", navigate: jest.fn() },
createElement(Text, null, "Session content"),
),
);

fireEvent.press(screen.getByRole("button", { name: `Server, ${serverName}` }));
expect(screen.getByRole("header", { name: "Server name" })).toBeOnTheScreen();
fireEvent.press(screen.getByRole("button", { name: "Copy server name" }));
await waitFor(() => expect(Clipboard.setStringAsync).toHaveBeenCalledWith(serverName));
view.unmount();
});

test("allows a permission owned by a background child session from Pending", () => {
const replyPermission = jest.fn();
jest.mocked(useConnections).mockReturnValue({
Expand Down
Loading