fix: constrain dialog content to the viewport

- Add dynamic viewport bounds and scrollable body regions to dialogs and modals\n- Make tool-launch popups use the shared scrollable body pattern\n- Keep mobile sheets, action sheets, and notification popups scroll-contained\n- Restore the ProjectsPage test setup required for the frontend suite\n\nOpenSpec: fix-dialog-scroll-containment\nQuality gates: npm run typecheck, npm run lint, npm test (88 passed), npm run build
This commit is contained in:
Developer
2026-07-15 10:20:20 +00:00
parent 5e06a2a226
commit 415aecc0dd
9 changed files with 160 additions and 62 deletions
+22 -27
View File
@@ -1,17 +1,26 @@
import "@testing-library/jest-dom/vitest";
import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { afterEach, describe, expect, it, vi } from "vitest";
import { ProjectsPage } from "./ProjectsPage";
import * as projectsApi from "../api/projects";
import { SessionsProvider } from "../state/sessions";
import type { ProjectWithRepos } from "../types";
const mockProjects = [
vi.mock("../api/sessions", () => ({
getUserSessions: vi.fn().mockResolvedValue([]),
}));
const mockProjects: ProjectWithRepos[] = [
{
id: "proj-1",
name: "Alpha Project",
description: "First project",
owner_id: "user-1",
default_ssh_key_id: null,
repositories: [],
created_at: "2026-07-01T00:00:00Z",
},
{
id: "proj-2",
@@ -19,6 +28,8 @@ const mockProjects = [
description: null,
owner_id: "user-1",
default_ssh_key_id: null,
repositories: [],
created_at: "2026-07-01T00:00:00Z",
},
];
@@ -31,9 +42,7 @@ describe("ProjectsPage", () => {
it("renders loading state initially", () => {
vi.spyOn(projectsApi, "listProjects").mockImplementation(() => new Promise(() => {}));
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
);
expect(screen.getByText(/loading projects/i)).toBeInTheDocument();
});
@@ -41,9 +50,7 @@ describe("ProjectsPage", () => {
it("renders project list after loading", async () => {
vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects);
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
);
await waitFor(() => {
@@ -56,9 +63,7 @@ describe("ProjectsPage", () => {
it("renders empty state when no projects", async () => {
vi.spyOn(projectsApi, "listProjects").mockResolvedValue([]);
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
);
await waitFor(() => {
@@ -69,9 +74,7 @@ describe("ProjectsPage", () => {
it("renders error state with retry button", async () => {
vi.spyOn(projectsApi, "listProjects").mockRejectedValue(new Error("fail"));
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
);
await waitFor(() => {
@@ -85,9 +88,7 @@ describe("ProjectsPage", () => {
const createMock = vi.spyOn(projectsApi, "createProject").mockResolvedValue(mockProjects[0]);
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
);
await waitFor(() => {
@@ -118,9 +119,7 @@ describe("ProjectsPage", () => {
vi.spyOn(projectsApi, "listProjects").mockResolvedValue([]);
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
);
await waitFor(() => {
@@ -138,16 +137,14 @@ describe("ProjectsPage", () => {
const updateMock = vi.spyOn(projectsApi, "updateProject").mockResolvedValue(mockProjects[0]);
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
);
await waitFor(() => {
expect(screen.getByText("Alpha Project")).toBeInTheDocument();
});
const alphaCard = screen.getByText("Alpha Project").closest(".project-card") as HTMLElement | null;
const alphaCard = screen.getByText("Alpha Project").closest(".project-list-item") as HTMLElement | null;
if (!alphaCard) throw new Error("Card not found");
fireEvent.click(within(alphaCard).getByRole("button", { name: /edit/i }));
@@ -171,16 +168,14 @@ describe("ProjectsPage", () => {
const deleteMock = vi.spyOn(projectsApi, "deleteProject").mockResolvedValue(undefined);
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
);
await waitFor(() => {
expect(screen.getByText("Alpha Project")).toBeInTheDocument();
});
const alphaCard = screen.getByText("Alpha Project").closest(".project-card") as HTMLElement | null;
const alphaCard = screen.getByText("Alpha Project").closest(".project-list-item") as HTMLElement | null;
if (!alphaCard) throw new Error("Card not found");
fireEvent.click(within(alphaCard).getByRole("button", { name: /delete/i }));