fix: resolve four frontend/backend issues

- Fix ProjectsPage tests by wrapping renders in MemoryRouter (9 passing)
- Improve session auto-naming to 'project / repo / tool' format
- Add missing /users/me/sessions endpoint for sidebar session loading
- Handle git history 500s: catch RuntimeError in endpoints, graceful empty repo handling
- Add git status badge and discard-changes button to FileEditor toolbar

Quality gates: tsc pass, build pass, Python syntax pass
This commit is contained in:
Developer
2026-06-03 08:30:28 +00:00
parent 543fee5d56
commit 5ed5e1c84b
9 changed files with 348 additions and 158 deletions
+17 -3
View File
@@ -3,7 +3,7 @@
import logging import logging
import uuid import uuid
from fastapi import APIRouter, Depends, Response, status from fastapi import APIRouter, Depends, HTTPException, Response, status
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from src.auth.dependencies import get_current_user, get_db_session, get_owned_project from src.auth.dependencies import get_current_user, get_db_session, get_owned_project
@@ -94,7 +94,14 @@ async def get_repository_history(
from src.services.git.repository import get_repo_and_validate, ensure_repo_on_disk from src.services.git.repository import get_repo_and_validate, ensure_repo_on_disk
repo = await get_repo_and_validate(session, repo_id, project_id) repo = await get_repo_and_validate(session, repo_id, project_id)
ensure_repo_on_disk(repo) ensure_repo_on_disk(repo)
return get_commit_history(repo.path, branch=branch, limit=limit, offset=offset) try:
return get_commit_history(repo.path, branch=branch, limit=limit, offset=offset)
except RuntimeError as e:
logger.warning("Git history failed for %s: %s", repo.path, str(e))
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Git history unavailable: {str(e)}",
) from e
@router.get("/{project_id}/repositories/{repo_id}/commits/{commit_hash}") @router.get("/{project_id}/repositories/{repo_id}/commits/{commit_hash}")
@@ -110,7 +117,14 @@ async def get_repository_commit(
from src.services.git.repository import get_repo_and_validate, ensure_repo_on_disk from src.services.git.repository import get_repo_and_validate, ensure_repo_on_disk
repo = await get_repo_and_validate(session, repo_id, project_id) repo = await get_repo_and_validate(session, repo_id, project_id)
ensure_repo_on_disk(repo) ensure_repo_on_disk(repo)
return get_commit_detail(repo.path, commit_hash) try:
return get_commit_detail(repo.path, commit_hash)
except RuntimeError as e:
logger.warning("Git commit detail failed for %s %s: %s", repo.path, commit_hash, str(e))
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Commit detail unavailable: {str(e)}",
) from e
# File browsing # File browsing
+41
View File
@@ -2,10 +2,13 @@ import uuid
from pathlib import Path from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, UploadFile, status from fastapi import APIRouter, Depends, HTTPException, UploadFile, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from src.auth.dependencies import get_current_user, get_db_session from src.auth.dependencies import get_current_user, get_db_session
from src.models.tool_instance import ToolInstance
from src.models.user import User from src.models.user import User
from src.schemas.tool_instance import SessionItemResponse, SessionListResponse
from src.schemas.user import UserProfileResponse, UserProfileUpdate from src.schemas.user import UserProfileResponse, UserProfileUpdate
router = APIRouter(prefix="/users", tags=["users"]) router = APIRouter(prefix="/users", tags=["users"])
@@ -131,3 +134,41 @@ async def upload_avatar(
await session.commit() await session.commit()
await session.refresh(user) await session.refresh(user)
return user return user
@router.get(
"/me/sessions",
response_model=SessionListResponse,
summary="Get current user sessions",
description="Retrieve all tool instances (sessions) for the authenticated user.",
)
async def get_user_sessions(
user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_db_session),
) -> SessionListResponse:
"""Return all tool instances for the current user with related names."""
result = await session.execute(
select(ToolInstance)
.where(ToolInstance.owner_id == user.id)
.order_by(ToolInstance.created_at.desc())
)
instances = result.scalars().all()
sessions = [
SessionItemResponse(
id=str(inst.id),
display_name=inst.display_name,
tool_type_name=inst.tool_type.display_name if inst.tool_type else "Unknown",
tool_icon=inst.tool_type.icon if inst.tool_type else None,
tool_type_interfaces=inst.tool_type.interfaces if inst.tool_type else [],
repository_name=inst.repository.name if inst.repository else "Unknown",
repository_id=str(inst.repository_id),
project_name=inst.project.name if inst.project else "Unknown",
project_id=str(inst.project_id),
status=inst.status,
url=inst.url,
)
for inst in instances
]
return SessionListResponse(sessions=sessions)
+24
View File
@@ -15,3 +15,27 @@ class CreateInstanceRequest(BaseModel):
config_profile_id: str | None = Field( config_profile_id: str | None = Field(
default=None, description="Optional config profile ID to apply to the instance" default=None, description="Optional config profile ID to apply to the instance"
) )
class SessionItemResponse(BaseModel):
"""Lightweight session summary for sidebar and dashboard."""
model_config = {"extra": "ignore"}
id: str = Field(description="Session (tool instance) ID")
display_name: str = Field(description="Display name of the session")
tool_type_name: str = Field(description="Name of the tool type")
tool_icon: str | None = Field(default=None, description="Icon URL for the tool type")
tool_type_interfaces: list[str] = Field(default_factory=list, description="Supported interfaces")
repository_name: str = Field(description="Name of the repository")
repository_id: str = Field(description="Repository ID")
project_name: str = Field(description="Name of the project")
project_id: str = Field(description="Project ID")
status: str = Field(description="Current status")
url: str | None = Field(default=None, description="Access URL")
class SessionListResponse(BaseModel):
"""Response wrapping a list of session summaries."""
sessions: list[SessionItemResponse]
+1 -1
View File
@@ -54,7 +54,7 @@ async def create_new_instance(
instance = ToolInstance( instance = ToolInstance(
name=instance_name, name=instance_name,
display_name=display_name or f"{tool_type.display_name} - {repo.name}", display_name=display_name or f"{project.name} / {repo.name} / {tool_type.display_name}",
tool_type_id=tool_type.id, tool_type_id=tool_type.id,
repository_id=repo.id, repository_id=repo.id,
project_id=project.id, project_id=project.id,
+21 -6
View File
@@ -60,9 +60,12 @@ def get_commit_history(repo_path: str, branch: str | None = None, limit: int = 1
Returns structured data including commits, branches, and graph information. Returns structured data including commits, branches, and graph information.
""" """
# Get list of branches # Get list of branches (may fail for empty repos)
branches_output = _run_git_command(repo_path, ["branch", "-a", "--format=%(refname:short)"]) try:
branches = [b.strip() for b in branches_output.strip().split("\n") if b.strip()] branches_output = _run_git_command(repo_path, ["branch", "-a", "--format=%(refname:short)"])
branches = [b.strip() for b in branches_output.strip().split("\n") if b.strip()]
except RuntimeError:
branches = []
# Build git log command - use NULL bytes as separators to avoid parsing issues # Build git log command - use NULL bytes as separators to avoid parsing issues
log_args = [ log_args = [
@@ -76,7 +79,16 @@ def get_commit_history(repo_path: str, branch: str | None = None, limit: int = 1
else: else:
log_args.append("--all") log_args.append("--all")
log_output = _run_git_command(repo_path, log_args) try:
log_output = _run_git_command(repo_path, log_args)
except RuntimeError:
# Empty repo or no commits
return {
"commits": [],
"branches": branches,
"total_commits": 0,
"graph_data": {"nodes": [], "edges": []},
}
# Get branch info for each commit # Get branch info for each commit
branch_map = _get_branch_map(repo_path) branch_map = _get_branch_map(repo_path)
@@ -113,8 +125,11 @@ def get_commit_history(repo_path: str, branch: str | None = None, limit: int = 1
) )
# Get total commit count # Get total commit count
count_output = _run_git_command(repo_path, ["rev-list", "--all", "--count"]) try:
total_commits = int(count_output.strip()) if count_output.strip() else 0 count_output = _run_git_command(repo_path, ["rev-list", "--all", "--count"])
total_commits = int(count_output.strip()) if count_output.strip() else 0
except RuntimeError:
total_commits = 0
# Build graph data and generate graph symbols # Build graph data and generate graph symbols
graph_data = _build_graph_data(commits) graph_data = _build_graph_data(commits)
@@ -9,14 +9,23 @@ import { Icon } from "../../ui/Icon";
import { SyntaxHighlighter } from "./SyntaxHighlighter"; import { SyntaxHighlighter } from "./SyntaxHighlighter";
import { detectLanguage } from "../../../utils/language"; import { detectLanguage } from "../../../utils/language";
interface GitFileStatus {
modified: string[];
added: string[];
deleted: string[];
untracked: string[];
}
interface FileEditorProps { interface FileEditorProps {
projectId: string; projectId: string;
repoId: string; repoId: string;
gitStatus?: GitFileStatus | null;
} }
export const FileEditor: React.FC<FileEditorProps> = ({ export const FileEditor: React.FC<FileEditorProps> = ({
projectId, projectId,
repoId, repoId,
gitStatus,
}) => { }) => {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const { user } = useAuth(); const { user } = useAuth();
@@ -31,9 +40,49 @@ export const FileEditor: React.FC<FileEditorProps> = ({
const [isBinary, setIsBinary] = useState(false); const [isBinary, setIsBinary] = useState(false);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const handleDiscard = async () => {
if (!filePath) return;
try {
const response = await apiClient.get(
`/projects/${projectId}/repositories/${repoId}/files/content`,
{
params: {
branch,
path: filePath,
},
}
);
const data = response.data;
if (data.is_binary) {
setIsBinary(true);
setContent("Binary file - cannot display");
setOriginalContent("");
} else {
setIsBinary(false);
setContent(data.content);
setOriginalContent(data.content);
}
setMode("view");
} catch {
setError("Failed to discard changes");
}
};
const branch = searchParams.get("branch") || "main"; const branch = searchParams.get("branch") || "main";
const filePath = searchParams.get("file"); const filePath = searchParams.get("file");
const fileStatus = gitStatus
? gitStatus.modified.includes(filePath || "")
? "modified"
: gitStatus.added.includes(filePath || "")
? "added"
: gitStatus.deleted.includes(filePath || "")
? "deleted"
: gitStatus.untracked.includes(filePath || "")
? "untracked"
: undefined
: undefined;
const loadFile = useCallback(async () => { const loadFile = useCallback(async () => {
if (!filePath) { if (!filePath) {
setContent(""); setContent("");
@@ -158,7 +207,7 @@ export const FileEditor: React.FC<FileEditorProps> = ({
<div className={styles.fileEditor}> <div className={styles.fileEditor}>
<div className={styles.fileEditorToolbar}> <div className={styles.fileEditorToolbar}>
<div className="file-breadcrumbs"> <div className="file-breadcrumbs">
{filePath.split("/").map((part, i, arr) => ( {filePath?.split("/").map((part, i, arr) => (
<span key={i}> <span key={i}>
{part} {part}
{i < arr.length - 1 && ( {i < arr.length - 1 && (
@@ -168,6 +217,11 @@ export const FileEditor: React.FC<FileEditorProps> = ({
))} ))}
</div> </div>
<div className={styles.fileActions}> <div className={styles.fileActions}>
{fileStatus && (
<span className={`git-status-badge ${fileStatus}`} title={fileStatus}>
{fileStatus === "modified" ? "M" : fileStatus === "added" ? "A" : fileStatus === "deleted" ? "D" : "?"}
</span>
)}
{mode === "view" && !isBinary && ( {mode === "view" && !isBinary && (
<button <button
className="btn-primary" className="btn-primary"
@@ -180,24 +234,33 @@ export const FileEditor: React.FC<FileEditorProps> = ({
)} )}
{mode === "edit" && ( {mode === "edit" && (
<> <>
<button <button
className="btn-primary" className="btn-primary"
onClick={handleSave} onClick={handleSave}
disabled={content === originalContent || saving} disabled={content === originalContent || saving}
type="button" type="button"
> >
{saving ? ( {saving ? (
<> <>
<Icon name="loading" size="sm" /> <Icon name="loading" size="sm" />
Saving... Saving...
</> </>
) : ( ) : (
<> <>
<Icon name="save" size="sm" /> <Icon name="save" size="sm" />
Save Save
</> </>
)} )}
</button> </button>
<button
className="btn-secondary"
onClick={handleDiscard}
type="button"
title="Revert to last committed version"
>
<Icon name="undo" size="sm" />
Discard
</button>
<button <button
className="btn-secondary" className="btn-secondary"
onClick={handleCancel} onClick={handleCancel}
+3 -1
View File
@@ -75,7 +75,8 @@ export type IconName =
| "play" | "play"
| "stop" | "stop"
| "terminal" | "terminal"
| "arrow-left"; | "arrow-left"
| "undo";
const iconMap: Record<IconName, React.ComponentType<{ size?: number | string; weight?: "thin" | "light" | "regular" | "bold" | "fill" | "duotone" }>> = { const iconMap: Record<IconName, React.ComponentType<{ size?: number | string; weight?: "thin" | "light" | "regular" | "bold" | "fill" | "duotone" }>> = {
dashboard: House, dashboard: House,
@@ -117,6 +118,7 @@ const iconMap: Record<IconName, React.ComponentType<{ size?: number | string; we
stop: Stop, stop: Stop,
terminal: Terminal, terminal: Terminal,
"arrow-left": ArrowLeft, "arrow-left": ArrowLeft,
undo: ClockCounterClockwise,
}; };
export interface IconProps { export interface IconProps {
+158 -127
View File
@@ -1,174 +1,205 @@
import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react"; 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 { afterEach, describe, expect, it, vi } from "vitest";
import { ProjectsPage } from "./ProjectsPage"; import { ProjectsPage } from "./ProjectsPage";
import * as projectsApi from "../api/projects"; import * as projectsApi from "../api/projects";
const renderWithRouter = (ui: React.ReactElement) =>
render(<MemoryRouter>{ui}</MemoryRouter>);
const mockProjects = [ const mockProjects = [
{ {
id: "proj-1", id: "proj-1",
name: "Alpha Project", name: "Alpha Project",
description: "First project", description: "First project",
owner_id: "user-1", owner_id: "user-1",
default_ssh_key_id: null, default_ssh_key_id: null,
}, },
{ {
id: "proj-2", id: "proj-2",
name: "Beta Project", name: "Beta Project",
description: null, description: null,
owner_id: "user-1", owner_id: "user-1",
default_ssh_key_id: null, default_ssh_key_id: null,
}, },
]; ];
afterEach(() => { afterEach(() => {
cleanup(); cleanup();
vi.restoreAllMocks(); vi.restoreAllMocks();
}); });
describe("ProjectsPage", () => { describe("ProjectsPage", () => {
it("renders loading state initially", () => { it("renders loading state initially", () => {
vi.spyOn(projectsApi, "listProjects").mockImplementation(() => new Promise(() => {})); vi.spyOn(projectsApi, "listProjects").mockImplementation(
render(<ProjectsPage />); () => new Promise(() => {}),
expect(screen.getByText(/loading projects/i)).toBeInTheDocument(); );
}); renderWithRouter(<ProjectsPage />);
expect(screen.getByText(/loading projects/i)).toBeInTheDocument();
});
it("renders project list after loading", async () => { it("renders project list after loading", async () => {
vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects); vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects);
render(<ProjectsPage />); renderWithRouter(<ProjectsPage />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText("Alpha Project")).toBeInTheDocument(); expect(screen.getByText("Alpha Project")).toBeInTheDocument();
}); });
expect(screen.getByText("Beta Project")).toBeInTheDocument(); expect(screen.getByText("Beta Project")).toBeInTheDocument();
expect(screen.getByText("First project")).toBeInTheDocument(); expect(screen.getByText("First project")).toBeInTheDocument();
}); });
it("renders empty state when no projects", async () => { it("renders empty state when no projects", async () => {
vi.spyOn(projectsApi, "listProjects").mockResolvedValue([]); vi.spyOn(projectsApi, "listProjects").mockResolvedValue([]);
render(<ProjectsPage />); renderWithRouter(<ProjectsPage />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText(/no projects yet/i)).toBeInTheDocument(); expect(screen.getByText(/no projects yet/i)).toBeInTheDocument();
}); });
}); });
it("renders error state with retry button", async () => { it("renders error state with retry button", async () => {
vi.spyOn(projectsApi, "listProjects").mockRejectedValue(new Error("fail")); vi.spyOn(projectsApi, "listProjects").mockRejectedValue(new Error("fail"));
render(<ProjectsPage />); renderWithRouter(<ProjectsPage />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText(/failed to load projects/i)).toBeInTheDocument(); expect(screen.getByText(/failed to load projects/i)).toBeInTheDocument();
}); });
expect(screen.getByRole("button", { name: /retry/i })).toBeInTheDocument(); expect(screen.getByRole("button", { name: /retry/i })).toBeInTheDocument();
}); });
it("opens create dialog and submits new project", async () => { it("opens create dialog and submits new project", async () => {
const listMock = vi.spyOn(projectsApi, "listProjects").mockResolvedValue([]); const listMock = vi
const createMock = vi.spyOn(projectsApi, "createProject").mockResolvedValue(mockProjects[0]); .spyOn(projectsApi, "listProjects")
.mockResolvedValue([]);
const createMock = vi
.spyOn(projectsApi, "createProject")
.mockResolvedValue(mockProjects[0]);
render(<ProjectsPage />); renderWithRouter(<ProjectsPage />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText(/no projects yet/i)).toBeInTheDocument(); expect(screen.getByText(/no projects yet/i)).toBeInTheDocument();
}); });
fireEvent.click(screen.getByRole("button", { name: /new project/i })); fireEvent.click(screen.getByRole("button", { name: /new project/i }));
expect(screen.getByRole("dialog")).toBeInTheDocument(); expect(screen.getByRole("dialog")).toBeInTheDocument();
fireEvent.change(screen.getByPlaceholderText(/project name/i), { fireEvent.change(screen.getByPlaceholderText(/project name/i), {
target: { value: "Gamma Project" }, target: { value: "Gamma Project" },
}); });
fireEvent.change(screen.getByPlaceholderText(/optional description/i), { fireEvent.change(screen.getByPlaceholderText(/optional description/i), {
target: { value: "A new project" }, target: { value: "A new project" },
}); });
fireEvent.click(screen.getByRole("button", { name: /create/i })); fireEvent.click(screen.getByRole("button", { name: /create/i }));
await waitFor(() => { await waitFor(() => {
expect(createMock).toHaveBeenCalledWith({ expect(createMock).toHaveBeenCalledWith({
name: "Gamma Project", name: "Gamma Project",
description: "A new project", description: "A new project",
}); });
}); });
expect(listMock).toHaveBeenCalledTimes(2); expect(listMock).toHaveBeenCalledTimes(2);
}); });
it("shows validation error when name is empty", async () => { it("shows validation error when name is empty", async () => {
vi.spyOn(projectsApi, "listProjects").mockResolvedValue([]); vi.spyOn(projectsApi, "listProjects").mockResolvedValue([]);
render(<ProjectsPage />); renderWithRouter(<ProjectsPage />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText(/no projects yet/i)).toBeInTheDocument(); expect(screen.getByText(/no projects yet/i)).toBeInTheDocument();
}); });
fireEvent.click(screen.getByRole("button", { name: /new project/i })); fireEvent.click(screen.getByRole("button", { name: /new project/i }));
fireEvent.click(screen.getByRole("button", { name: /create/i })); fireEvent.click(screen.getByRole("button", { name: /create/i }));
expect(screen.getByText(/project name is required/i)).toBeInTheDocument(); expect(screen.getByText(/project name is required/i)).toBeInTheDocument();
}); });
it("renders settings link for each project", async () => { it("renders settings link for each project", async () => {
vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects); vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects);
render(<ProjectsPage />); renderWithRouter(<ProjectsPage />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText("Alpha Project")).toBeInTheDocument(); expect(screen.getByText("Alpha Project")).toBeInTheDocument();
}); });
const alphaCard = screen.getByText("Alpha Project").closest(".project-card") as HTMLElement | null; const alphaCard = screen
if (!alphaCard) throw new Error("Card not found"); .getByText("Alpha Project")
.closest(".project-card") as HTMLElement | null;
if (!alphaCard) throw new Error("Card not found");
const settingsLink = within(alphaCard).getByRole("link", { name: /settings/i }); const settingsLink = within(alphaCard).getByRole("link", {
expect(settingsLink).toBeInTheDocument(); name: /settings/i,
expect(settingsLink).toHaveAttribute("href", "/projects/proj-1/settings"); });
}); expect(settingsLink).toBeInTheDocument();
expect(settingsLink).toHaveAttribute("href", "/projects/proj-1/settings");
});
it("renders open workspace link as rightmost action", async () => { it("renders open workspace link as rightmost action", async () => {
vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects); vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects);
render(<ProjectsPage />); renderWithRouter(<ProjectsPage />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText("Alpha Project")).toBeInTheDocument(); expect(screen.getByText("Alpha Project")).toBeInTheDocument();
}); });
const alphaCard = screen.getByText("Alpha Project").closest(".project-card") as HTMLElement | null; const alphaCard = screen
if (!alphaCard) throw new Error("Card not found"); .getByText("Alpha Project")
.closest(".project-card") as HTMLElement | null;
if (!alphaCard) throw new Error("Card not found");
const actions = alphaCard.querySelector(".project-actions"); const actions = alphaCard.querySelector(".project-actions");
if (!actions) throw new Error("Actions container not found"); if (!actions) throw new Error("Actions container not found");
const workspaceLink = within(alphaCard).getByRole("link", { name: /open workspace/i }); const workspaceLink = within(alphaCard).getByRole("link", {
expect(workspaceLink).toBeInTheDocument(); name: /open workspace/i,
expect(workspaceLink).toHaveAttribute("href", "/projects/proj-1"); });
expect(workspaceLink).toBeInTheDocument();
expect(workspaceLink).toHaveAttribute("href", "/projects/proj-1");
// Verify it's the last action in the container // Verify it's the last action in the container
const allActions = actions.querySelectorAll("a, button"); const allActions = actions.querySelectorAll("a, button");
const lastAction = allActions[allActions.length - 1]; const lastAction = allActions[allActions.length - 1];
expect(lastAction).toBe(workspaceLink); expect(lastAction).toBe(workspaceLink);
}); });
it("shows delete confirmation and deletes project", async () => { it("shows delete confirmation and deletes project", async () => {
const listMock = vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects); const listMock = vi
const deleteMock = vi.spyOn(projectsApi, "deleteProject").mockResolvedValue(undefined); .spyOn(projectsApi, "listProjects")
.mockResolvedValue(mockProjects);
const deleteMock = vi
.spyOn(projectsApi, "deleteProject")
.mockResolvedValue(undefined);
render(<ProjectsPage />); renderWithRouter(<ProjectsPage />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText("Alpha Project")).toBeInTheDocument(); expect(screen.getByText("Alpha Project")).toBeInTheDocument();
}); });
const alphaCard = screen.getByText("Alpha Project").closest(".project-card") as HTMLElement | null; const alphaCard = screen
if (!alphaCard) throw new Error("Card not found"); .getByText("Alpha Project")
.closest(".project-card") as HTMLElement | null;
if (!alphaCard) throw new Error("Card not found");
fireEvent.click(within(alphaCard).getByRole("button", { name: /delete/i })); fireEvent.click(within(alphaCard).getByRole("button", { name: /delete/i }));
expect(within(alphaCard).getByText(/are you sure/i)).toBeInTheDocument(); expect(within(alphaCard).getByText(/are you sure/i)).toBeInTheDocument();
fireEvent.click(within(alphaCard).getByRole("button", { name: /delete/i })); fireEvent.click(within(alphaCard).getByRole("button", { name: /delete/i }));
await waitFor(() => { await waitFor(() => {
expect(deleteMock).toHaveBeenCalledWith("proj-1"); expect(deleteMock).toHaveBeenCalledWith("proj-1");
}); });
expect(listMock).toHaveBeenCalledTimes(2); expect(listMock).toHaveBeenCalledTimes(2);
}); });
}); });
+1 -1
View File
@@ -198,7 +198,7 @@ export const RepoWorkspace = () => {
)} )}
<main className="workspace-main"> <main className="workspace-main">
{selectedRepoId && ( {selectedRepoId && (
<FileEditor projectId={projectId!} repoId={selectedRepoId} /> <FileEditor projectId={projectId!} repoId={selectedRepoId} gitStatus={gitStatus} />
)} )}
</main> </main>
</div> </div>