Skip to content

Commit 13b6231

Browse files
committed
feat(web): polish mobile fullscreen terminal
1 parent bafe296 commit 13b6231

8 files changed

Lines changed: 769 additions & 257 deletions

File tree

packages/web/src/features/terminal-panel/__tests__/terminal-panel.test.tsx

Lines changed: 291 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Topics } from "@coder-studio/core";
22
import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
33
import userEvent from "@testing-library/user-event";
44
import { createStore, Provider } from "jotai";
5+
import { type ReactNode, useState } from "react";
56
import { MemoryRouter } from "react-router-dom";
67
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
78
import { localeAtom } from "../../../atoms/app-ui";
@@ -602,7 +603,7 @@ describe("TerminalPanel", () => {
602603
expect(consoleSpy).not.toHaveBeenCalled();
603604
});
604605

605-
it("renders a lighter mobile fullscreen terminal toolbar when requested by the mobile sheet", async () => {
606+
it("leaves the mobile fullscreen title slot blank when no terminal exists", async () => {
606607
const store = createStore();
607608
const subscribe = vi.fn((topics: string[], handler: EventHandler) => {
608609
handlers.push(handler);
@@ -629,14 +630,90 @@ describe("TerminalPanel", () => {
629630
store.set(bottomPanelHeightAtom, 240);
630631
setEnglishLocale(store);
631632
store.set(wsClientAtom, { subscribe, sendCommand } as never);
632-
store.set(terminalMetaAtomFamily("term_1"), {
633-
id: "term_1",
634-
workspaceId: "ws-test",
635-
kind: "shell",
636-
alive: true,
637-
title: "Workspace Shell",
633+
render(
634+
<Provider store={store}>
635+
<MemoryRouter>
636+
<div className="mobile-terminal-sheet mobile-terminal-sheet--fullscreen">
637+
<TerminalPanel chrome="mobile-fullscreen" />
638+
</div>
639+
</MemoryRouter>
640+
</Provider>
641+
);
642+
643+
expect(
644+
document.querySelector(".bottom-terminal--mobile-fullscreen .terminal-toolbar")
645+
).toBeTruthy();
646+
expect(
647+
document.querySelector(".bottom-terminal--mobile-fullscreen .terminal-toolbar-mobile-row")
648+
).toBeTruthy();
649+
expect(
650+
document.querySelector(
651+
".bottom-terminal--mobile-fullscreen .terminal-toolbar-mobile-placeholder"
652+
)
653+
).toBeTruthy();
654+
expect(screen.queryByRole("button", { name: "Close Terminal" })).not.toBeInTheDocument();
655+
expect(screen.getAllByRole("button", { name: "New Terminal" }).length).toBeGreaterThan(0);
656+
});
657+
658+
it("renders the mobile fullscreen terminal toolbar as a single compact chrome row", async () => {
659+
const store = createStore();
660+
const subscribe = vi.fn((topics: string[], handler: EventHandler) => {
661+
handlers.push(handler);
662+
return () => {
663+
handlers = handlers.filter((candidate) => candidate !== handler);
664+
};
665+
});
666+
const sendCommand = vi.fn().mockImplementation((op: string) => {
667+
if (op === "terminal.list") {
668+
return Promise.resolve([
669+
{
670+
id: "term_1",
671+
workspaceId: "ws-test",
672+
kind: "shell",
673+
title: "Workspace Shell",
674+
cwd: "/tmp/ws-test",
675+
argv: ["/bin/bash"],
676+
cols: 120,
677+
rows: 30,
678+
alive: true,
679+
createdAt: 1,
680+
},
681+
{
682+
id: "term_2",
683+
workspaceId: "ws-test",
684+
kind: "shell",
685+
title: "Workspace Shell 2",
686+
cwd: "/tmp/ws-test",
687+
argv: ["/bin/bash"],
688+
cols: 120,
689+
rows: 30,
690+
alive: true,
691+
createdAt: 2,
692+
},
693+
]);
694+
}
695+
696+
return Promise.resolve([]);
638697
});
639698

699+
seedReadyWorkspaceState(store, {
700+
"ws-test": {
701+
id: "ws-test",
702+
path: "/tmp/ws-test",
703+
targetRuntime: "native",
704+
openedAt: 1,
705+
lastActiveAt: 1,
706+
uiState: {
707+
leftPanelWidth: 280,
708+
bottomPanelHeight: 200,
709+
focusMode: false,
710+
},
711+
},
712+
});
713+
store.set(bottomPanelHeightAtom, 240);
714+
setEnglishLocale(store);
715+
store.set(wsClientAtom, { subscribe, sendCommand } as never);
716+
640717
render(
641718
<Provider store={store}>
642719
<MemoryRouter>
@@ -647,11 +724,216 @@ describe("TerminalPanel", () => {
647724
</Provider>
648725
);
649726

650-
expect(screen.queryByText("TERMINAL")).not.toBeInTheDocument();
651-
expect(screen.queryByText("Workspace Shell")).not.toBeInTheDocument();
727+
await waitFor(() => {
728+
expect(screen.getByTestId("xterm-host")).toHaveTextContent("term_1");
729+
});
730+
731+
const toolbar = document.querySelector(".bottom-terminal--mobile-fullscreen .terminal-toolbar");
732+
const mobileRow = document.querySelector(
733+
".bottom-terminal--mobile-fullscreen .terminal-toolbar-mobile-row"
734+
);
735+
const selector = document.querySelector(
736+
".bottom-terminal--mobile-fullscreen .terminal-selector"
737+
);
738+
const actions = document.querySelector(
739+
".bottom-terminal--mobile-fullscreen .terminal-toolbar-actions"
740+
);
741+
742+
expect(toolbar).toBeTruthy();
743+
expect(mobileRow).toBeTruthy();
744+
expect(toolbar?.querySelector(".terminal-toolbar-left")).toBeNull();
745+
expect(toolbar?.querySelector(".terminal-toolbar-right")).toBeNull();
746+
expect(selector).toBeTruthy();
747+
expect(actions).toBeTruthy();
748+
});
749+
750+
it("renders a static mobile fullscreen title when only one terminal exists", async () => {
751+
const store = createStore();
752+
const subscribe = vi.fn((topics: string[], handler: EventHandler) => {
753+
handlers.push(handler);
754+
return () => {
755+
handlers = handlers.filter((candidate) => candidate !== handler);
756+
};
757+
});
758+
const sendCommand = vi.fn().mockImplementation((op: string) => {
759+
if (op === "terminal.list") {
760+
return Promise.resolve([
761+
{
762+
id: "term_1",
763+
workspaceId: "ws-test",
764+
kind: "shell",
765+
title: "Workspace Shell",
766+
cwd: "/tmp/ws-test",
767+
argv: ["/bin/bash"],
768+
cols: 120,
769+
rows: 30,
770+
alive: true,
771+
createdAt: 1,
772+
},
773+
]);
774+
}
775+
776+
return Promise.resolve([]);
777+
});
778+
779+
seedReadyWorkspaceState(store, {
780+
"ws-test": {
781+
id: "ws-test",
782+
path: "/tmp/ws-test",
783+
targetRuntime: "native",
784+
openedAt: 1,
785+
lastActiveAt: 1,
786+
uiState: {
787+
leftPanelWidth: 280,
788+
bottomPanelHeight: 200,
789+
focusMode: false,
790+
},
791+
},
792+
});
793+
store.set(bottomPanelHeightAtom, 240);
794+
setEnglishLocale(store);
795+
store.set(wsClientAtom, { subscribe, sendCommand } as never);
796+
797+
render(
798+
<Provider store={store}>
799+
<MemoryRouter>
800+
<div className="mobile-terminal-sheet mobile-terminal-sheet--fullscreen">
801+
<TerminalPanel chrome="mobile-fullscreen" />
802+
</div>
803+
</MemoryRouter>
804+
</Provider>
805+
);
806+
807+
await waitFor(() => {
808+
expect(screen.getByTestId("xterm-host")).toHaveTextContent("term_1");
809+
});
810+
811+
expect(screen.getByText("Workspace Shell")).toBeInTheDocument();
812+
expect(
813+
document.querySelector(".bottom-terminal--mobile-fullscreen .terminal-selector-btn--static")
814+
).toBeTruthy();
815+
expect(
816+
document.querySelector(
817+
".bottom-terminal--mobile-fullscreen .terminal-toolbar-mobile-placeholder"
818+
)
819+
).toBeNull();
820+
expect(screen.getByRole("button", { name: "Close Terminal" })).toBeInTheDocument();
652821
expect(screen.getAllByRole("button", { name: "New Terminal" }).length).toBeGreaterThan(0);
653822
});
654823

824+
it("keeps mobile fullscreen terminal actions together in the toolbar when header actions are available", async () => {
825+
const store = createStore();
826+
const subscribe = vi.fn((topics: string[], handler: EventHandler) => {
827+
handlers.push(handler);
828+
return () => {
829+
handlers = handlers.filter((candidate) => candidate !== handler);
830+
};
831+
});
832+
const sendCommand = vi.fn().mockImplementation((op: string) => {
833+
if (op === "terminal.list") {
834+
return Promise.resolve([
835+
{
836+
id: "term_1",
837+
workspaceId: "ws-test",
838+
kind: "shell",
839+
title: "Workspace Shell",
840+
cwd: "/tmp/ws-test",
841+
argv: ["/bin/bash"],
842+
cols: 120,
843+
rows: 30,
844+
alive: true,
845+
createdAt: 1,
846+
},
847+
{
848+
id: "term_2",
849+
workspaceId: "ws-test",
850+
kind: "shell",
851+
title: "Workspace Shell 2",
852+
cwd: "/tmp/ws-test",
853+
argv: ["/bin/bash"],
854+
cols: 120,
855+
rows: 30,
856+
alive: true,
857+
createdAt: 2,
858+
},
859+
]);
860+
}
861+
862+
return Promise.resolve([]);
863+
});
864+
865+
seedReadyWorkspaceState(store, {
866+
"ws-test": {
867+
id: "ws-test",
868+
path: "/tmp/ws-test",
869+
targetRuntime: "native",
870+
openedAt: 1,
871+
lastActiveAt: 1,
872+
uiState: {
873+
leftPanelWidth: 280,
874+
bottomPanelHeight: 200,
875+
focusMode: false,
876+
},
877+
},
878+
});
879+
store.set(bottomPanelHeightAtom, 240);
880+
setEnglishLocale(store);
881+
store.set(wsClientAtom, { subscribe, sendCommand } as never);
882+
883+
function HeaderActionHarness() {
884+
const [headerActions, setHeaderActions] = useState<ReactNode>(null);
885+
886+
return (
887+
<>
888+
<div data-testid="terminal-header-actions">{headerActions}</div>
889+
<div className="mobile-terminal-sheet mobile-terminal-sheet--fullscreen">
890+
<TerminalPanel
891+
chrome="mobile-fullscreen"
892+
onMobileHeaderActionsChange={setHeaderActions}
893+
/>
894+
</div>
895+
</>
896+
);
897+
}
898+
899+
render(
900+
<Provider store={store}>
901+
<MemoryRouter>
902+
<HeaderActionHarness />
903+
</MemoryRouter>
904+
</Provider>
905+
);
906+
907+
await waitFor(() => {
908+
expect(screen.getByTestId("xterm-host")).toHaveTextContent("term_1");
909+
});
910+
911+
const headerActions = screen.getByTestId("terminal-header-actions");
912+
const toolbar = document.querySelector(
913+
".bottom-terminal--mobile-fullscreen .terminal-toolbar"
914+
) as HTMLElement | null;
915+
916+
expect(
917+
within(headerActions).queryByRole("button", { name: "New Terminal" })
918+
).not.toBeInTheDocument();
919+
expect(
920+
within(headerActions).queryByRole("button", { name: "Close Terminal" })
921+
).not.toBeInTheDocument();
922+
expect(toolbar).toBeTruthy();
923+
expect(
924+
within(toolbar as HTMLElement).getByRole("button", { name: "Close Terminal" })
925+
).toBeInTheDocument();
926+
expect(
927+
within(toolbar as HTMLElement).getByRole("button", { name: "New Terminal" })
928+
).toBeInTheDocument();
929+
expect(
930+
document.querySelector(".bottom-terminal--mobile-fullscreen .terminal-selector")
931+
).toBeTruthy();
932+
expect(
933+
document.querySelectorAll(".bottom-terminal--mobile-fullscreen .terminal-toolbar-actions")
934+
).toHaveLength(2);
935+
});
936+
655937
it("keeps only one terminal switcher in mobile fullscreen mode", async () => {
656938
const store = createStore();
657939
const subscribe = vi.fn((topics: string[], handler: EventHandler) => {

0 commit comments

Comments
 (0)