diff --git a/apps/web/src/components/features/project/repositories-settings-tab.test.tsx b/apps/web/src/components/features/project/repositories-settings-tab.test.tsx index 65a74cc..0701780 100644 --- a/apps/web/src/components/features/project/repositories-settings-tab.test.tsx +++ b/apps/web/src/components/features/project/repositories-settings-tab.test.tsx @@ -1,4 +1,10 @@ -import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { + cleanup, + fireEvent, + render, + screen, + waitFor, +} from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { RepositoriesSettingsTab } from "./repositories-settings-tab"; @@ -6,119 +12,146 @@ import * as gitRepositoriesApi from "../../../api/git-repositories"; import * as sshKeysApi from "../../../api/ssh-keys"; const mockRepositories = [ - { - id: "repo-1", - name: "Main Repo", - path: "/repos/main", - project_id: "proj-1", - owner_id: "user-1", - is_mirror: false, - remote_url: null, - last_push: null, - created_at: null, - }, + { + id: "repo-1", + name: "Main Repo", + path: "/repos/main", + project_id: "proj-1", + owner_id: "user-1", + is_mirror: false, + remote_url: null, + last_push: null, + created_at: null, + }, ]; vi.mock("react-router-dom", async () => { - const actual = await vi.importActual("react-router-dom"); - return { - ...actual, - useParams: () => ({ projectId: "proj-1" }), - }; + const actual = + await vi.importActual( + "react-router-dom", + ); + return { + ...actual, + useParams: () => ({ projectId: "proj-1" }), + }; }); afterEach(() => { - cleanup(); - vi.restoreAllMocks(); + cleanup(); + vi.restoreAllMocks(); }); describe("RepositoriesSettingsTab", () => { - it("opens create dialog and clones an existing repository", async () => { - const listMock = vi.spyOn(gitRepositoriesApi, "listRepositories").mockResolvedValue(mockRepositories); - const createMock = vi.spyOn(gitRepositoriesApi, "createRepository").mockResolvedValue(mockRepositories[0]); - vi.spyOn(sshKeysApi, "listSSHKeys").mockResolvedValue([ - { id: "key-1", name: "My Key", public_key: "ssh-ed25519 AAA...", created_at: "2024-01-01" }, - ]); + it("opens create dialog and clones an existing repository", async () => { + const listMock = vi + .spyOn(gitRepositoriesApi, "listRepositories") + .mockResolvedValue(mockRepositories); + const createMock = vi + .spyOn(gitRepositoriesApi, "createRepository") + .mockResolvedValue(mockRepositories[0]); + vi.spyOn(sshKeysApi, "listSSHKeys").mockResolvedValue([ + { + id: "key-1", + name: "My Key", + public_key: "ssh-ed25519 AAA...", + created_at: "2024-01-01", + }, + ]); - render(); + render(); - await waitFor(() => { - expect(screen.getByText("Main Repo")).toBeInTheDocument(); - }); + await waitFor(() => { + expect(screen.getByText("Main Repo")).toBeInTheDocument(); + }); - fireEvent.click(screen.getByRole("button", { name: /add repository/i })); - fireEvent.change(screen.getByPlaceholderText(/repository-name/i), { - target: { value: "New Repo" }, - }); - fireEvent.change(screen.getByPlaceholderText(/owner/i), { - target: { value: "alice" }, - }); - fireEvent.change(screen.getByPlaceholderText(/repo-name/i), { - target: { value: "demo" }, - }); - const sshSelect = screen.getByRole("combobox", { name: /ssh key/i }); - await waitFor(() => expect(sshSelect.querySelector('option[value="key-1"]')).toBeTruthy()); - (sshSelect as HTMLSelectElement).value = "key-1"; - fireEvent.change(sshSelect); - fireEvent.click(screen.getByRole("button", { name: /clone repository/i })); + fireEvent.click(screen.getByRole("button", { name: /add repository/i })); + fireEvent.change(screen.getByPlaceholderText(/repository-name/i), { + target: { value: "New Repo" }, + }); + fireEvent.change(screen.getByPlaceholderText(/owner/i), { + target: { value: "alice" }, + }); + fireEvent.change(screen.getByPlaceholderText(/repo-name/i), { + target: { value: "demo" }, + }); + const sshSelect = screen.getByRole("combobox", { name: /ssh key/i }); + await waitFor(() => + expect(sshSelect.querySelector('option[value="key-1"]')).toBeTruthy(), + ); + (sshSelect as HTMLSelectElement).value = "key-1"; + fireEvent.change(sshSelect); + fireEvent.click(screen.getByRole("button", { name: /clone repository/i })); - await waitFor(() => { - expect(createMock).toHaveBeenCalledWith("proj-1", { - name: "New Repo", - remote_url: "git@git.commumedia.org:alice/demo.git", - ssh_key_id: "key-1", - }); - }); - expect(listMock).toHaveBeenCalledTimes(2); - }); + await waitFor(() => { + expect(createMock).toHaveBeenCalledWith("proj-1", { + name: "New Repo", + remote_url: "git@git.commumedia.org:alice/demo.git", + ssh_key_id: "key-1", + }); + }); + expect(listMock).toHaveBeenCalledTimes(2); + }); - it("uses advanced url fallback when requested", async () => { - const listMock = vi.spyOn(gitRepositoriesApi, "listRepositories").mockResolvedValue(mockRepositories); - const createMock = vi.spyOn(gitRepositoriesApi, "createRepository").mockResolvedValue(mockRepositories[0]); - vi.spyOn(sshKeysApi, "listSSHKeys").mockResolvedValue([]); + it("uses advanced url fallback when requested", async () => { + const listMock = vi + .spyOn(gitRepositoriesApi, "listRepositories") + .mockResolvedValue(mockRepositories); + const createMock = vi + .spyOn(gitRepositoriesApi, "createRepository") + .mockResolvedValue(mockRepositories[0]); + vi.spyOn(sshKeysApi, "listSSHKeys").mockResolvedValue([]); - render(); + render(); - await waitFor(() => { - expect(screen.getByText("Main Repo")).toBeInTheDocument(); - }); + await waitFor(() => { + expect(screen.getByText("Main Repo")).toBeInTheDocument(); + }); - fireEvent.click(screen.getByRole("button", { name: /add repository/i })); - fireEvent.change(screen.getByPlaceholderText(/repository-name/i), { - target: { value: "New Repo" }, - }); - fireEvent.click(screen.getByRole("button", { name: /use full url instead/i })); - fireEvent.change(screen.getByPlaceholderText(/https:\/\/github.com\/user\/repo.git/i), { - target: { value: "https://github.com/user/repo.git" }, - }); - fireEvent.click(screen.getByRole("button", { name: /clone repository/i })); + fireEvent.click(screen.getByRole("button", { name: /add repository/i })); + fireEvent.change(screen.getByPlaceholderText(/repository-name/i), { + target: { value: "New Repo" }, + }); + fireEvent.click( + screen.getByRole("button", { name: /use full url instead/i }), + ); + fireEvent.change( + screen.getByPlaceholderText(/https:\/\/github.com\/user\/repo.git/i), + { + target: { value: "https://github.com/user/repo.git" }, + }, + ); + fireEvent.click(screen.getByRole("button", { name: /clone repository/i })); - await waitFor(() => { - expect(createMock).toHaveBeenCalledWith("proj-1", { - name: "New Repo", - remote_url: "https://github.com/user/repo.git", - }); - }); - expect(listMock).toHaveBeenCalledTimes(2); - }); + await waitFor(() => { + expect(createMock).toHaveBeenCalledWith("proj-1", { + name: "New Repo", + remote_url: "https://github.com/user/repo.git", + }); + }); + expect(listMock).toHaveBeenCalledTimes(2); + }); - it("shows validation when cloning without a remote url", async () => { - vi.spyOn(gitRepositoriesApi, "listRepositories").mockResolvedValue(mockRepositories); - render(); + it("shows validation when cloning without a remote url", async () => { + vi.spyOn(gitRepositoriesApi, "listRepositories").mockResolvedValue( + mockRepositories, + ); + render(); - await waitFor(() => { - expect(screen.getByText("Main Repo")).toBeInTheDocument(); - }); + await waitFor(() => { + expect(screen.getByText("Main Repo")).toBeInTheDocument(); + }); - fireEvent.click(screen.getByRole("button", { name: /add repository/i })); - fireEvent.change(screen.getByPlaceholderText(/repository-name/i), { - target: { value: "New Repo" }, - }); - fireEvent.change(screen.getByPlaceholderText(/owner/i), { - target: { value: "" }, - }); - fireEvent.click(screen.getByRole("button", { name: /clone repository/i })); + fireEvent.click(screen.getByRole("button", { name: /add repository/i })); + fireEvent.change(screen.getByPlaceholderText(/repository-name/i), { + target: { value: "New Repo" }, + }); + fireEvent.change(screen.getByPlaceholderText(/owner/i), { + target: { value: "" }, + }); + fireEvent.click(screen.getByRole("button", { name: /clone repository/i })); - expect(screen.getByText(/owner and repository name are required/i)).toBeInTheDocument(); - }); + expect( + screen.getByText(/owner and repository name are required/i), + ).toBeInTheDocument(); + }); }); diff --git a/apps/web/src/components/features/session/session-card.tsx b/apps/web/src/components/features/session/session-card.tsx index 79255f6..e2c38a6 100644 --- a/apps/web/src/components/features/session/session-card.tsx +++ b/apps/web/src/components/features/session/session-card.tsx @@ -138,7 +138,7 @@ export function SessionCard({ )} {session.url && (

- + {session.url}

@@ -158,7 +158,7 @@ export function SessionCard({ {openHref ? ( @@ -215,7 +215,7 @@ export function SessionCard({ {openHref ? ( diff --git a/apps/web/src/components/features/tool/instance-list.tsx b/apps/web/src/components/features/tool/instance-list.tsx index 2a1eed0..8c89616 100644 --- a/apps/web/src/components/features/tool/instance-list.tsx +++ b/apps/web/src/components/features/tool/instance-list.tsx @@ -282,7 +282,7 @@ export const InstanceList = ({ ? instance.url : `${API_BASE_URL}${instance.url}` } - target="_blank" + target={`instance-${instance.id}`} rel="noopener noreferrer" className="secondary-button small" > diff --git a/apps/web/src/components/features/workspace/workspace-instance-chips.tsx b/apps/web/src/components/features/workspace/workspace-instance-chips.tsx index 27ee546..8cb7fdb 100644 --- a/apps/web/src/components/features/workspace/workspace-instance-chips.tsx +++ b/apps/web/src/components/features/workspace/workspace-instance-chips.tsx @@ -43,7 +43,7 @@ export function WorkspaceInstanceChips({ {inst.status === "running" && inst.url && ( e.stopPropagation()} > diff --git a/apps/web/src/hooks/use-instance-actions.ts b/apps/web/src/hooks/use-instance-actions.ts index 08cd397..0d411fd 100644 --- a/apps/web/src/hooks/use-instance-actions.ts +++ b/apps/web/src/hooks/use-instance-actions.ts @@ -35,15 +35,16 @@ export function useInstanceActions( const [dirtyDeleteFiles, setDirtyDeleteFiles] = useState([]); const handleOpen = useCallback((session: Session) => { + const tabName = `session-${session.id}`; if (session.url) { - window.open(session.url, "_blank", "noopener,noreferrer"); + window.open(session.url, tabName, "noopener,noreferrer"); return; } if (session.tool_type_interfaces?.includes("terminal")) { - window.open(`/instances/${session.id}/terminal`, "_blank", "noopener,noreferrer"); + window.open(`/instances/${session.id}/terminal`, tabName, "noopener,noreferrer"); return; } - window.open(`/projects/${session.project_id}`, "_blank", "noopener,noreferrer"); + window.open(`/projects/${session.project_id}`, tabName, "noopener,noreferrer"); }, []); const handleStart = useCallback(