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
48 changes: 47 additions & 1 deletion apps/app/src/components/dialogs/ProjectPathDialog.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import {
type ProjectPathDialogTarget,
} from "./ProjectPathDialog";
import {
HOST_IDS,
HOST_NAMES,
makeHost,
PROJECT_IDS,
PROJECT_NAMES,
} from "../../../.ladle/story-fixtures";
Expand Down Expand Up @@ -30,6 +33,14 @@ const addSourceTarget: ProjectPathDialogTarget = {
projectName: PROJECT_NAMES.bb,
};

const connectedMachine = makeHost();
const offlineMachine = makeHost({
id: HOST_IDS.remote,
name: HOST_NAMES.remote,
status: "disconnected",
});
const offlineLocalMachine = makeHost({ status: "disconnected" });

export function Overview() {
return (
<StoryCard>
Expand Down Expand Up @@ -108,7 +119,42 @@ export function Overview() {
/>
</DialogStage>
</StoryRow>
<StoryRow label="pending" hint="submit in flight — input + submit disabled">
<StoryRow
label="machine picker"
hint='kind="create" with several machines — offline rows are disabled; the folder browser below needs a live host, so it reads as empty here'
>
<DialogStage>
<ProjectPathDialogContent
target={createTarget}
pending={false}
platform="darwin"
hostId={connectedMachine.id}
hostName={connectedMachine.name}
hosts={[connectedMachine, offlineMachine]}
onSubmit={noop}
/>
</DialogStage>
</StoryRow>
<StoryRow
label="machines all offline"
hint="no machine can be browsed — no row is selected and submit stays disabled"
>
<DialogStage>
<ProjectPathDialogContent
target={createTarget}
pending={false}
platform="darwin"
hostId={null}
hostName={null}
hosts={[offlineLocalMachine, offlineMachine]}
onSubmit={noop}
/>
</DialogStage>
</StoryRow>
<StoryRow
label="pending"
hint="submit in flight — input + submit disabled"
>
<DialogStage>
<ProjectPathDialogContent
target={updateTarget}
Expand Down
146 changes: 146 additions & 0 deletions apps/app/src/components/dialogs/ProjectPathDialog.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// @vitest-environment jsdom

import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import type { Host } from "@bb/domain";
import { afterEach, describe, expect, it, vi } from "vitest";
import { ProjectPathDialog } from "./ProjectPathDialog";

vi.mock("@/components/dialogs/RemotePathBrowser", () => ({
RemotePathBrowser: ({
hostId,
onDirectoryChange,
}: {
hostId: string;
onDirectoryChange: (directory: string) => void;
}) => (
<button
type="button"
onClick={() =>
onDirectoryChange(
hostId === "host_kunst"
? "/Users/amadad/projects/reachy_mini"
: "/home/deploy/repos/givecare",
)
}
>
Choose folder on {hostId}
</button>
),
}));

function host(overrides: Partial<Host> & Pick<Host, "id" | "name">): Host {
return {
type: "persistent",
status: "connected",
lastSeenAt: null,
lastRejectedProtocolVersion: null,
createdAt: 0,
updatedAt: 0,
...overrides,
};
}

const atum = host({ id: "host_atum", name: "atum" });
const kunst = host({ id: "host_kunst", name: "Kunst" });
const offline = host({
id: "host_offline",
name: "Offline Mac",
status: "disconnected",
});
const offlineKunst = host({ ...kunst, status: "disconnected" });

afterEach(() => {
cleanup();
vi.clearAllMocks();
});

describe("ProjectPathDialog machine selection", () => {
it("creates a project from a folder on the selected connected machine", () => {
const onSubmit = vi.fn();
render(
<ProjectPathDialog
target={{ kind: "create" }}
platform="linux"
hostId={atum.id}
hostName={atum.name}
hosts={[atum, kunst, offline]}
onOpenChange={vi.fn()}
onSubmit={onSubmit}
/>,
);

expect(
(screen.getByRole("radio", { name: "atum" }) as HTMLInputElement).checked,
).toBe(true);
expect(
screen
.getByRole("radio", { name: "Offline Mac" })
.hasAttribute("disabled"),
).toBe(true);

fireEvent.click(screen.getByRole("radio", { name: "Kunst" }));
fireEvent.click(
screen.getByRole("button", { name: "Choose folder on host_kunst" }),
);
fireEvent.click(screen.getByRole("button", { name: "Add project" }));

expect(onSubmit).toHaveBeenCalledWith(
{ kind: "create" },
"/Users/amadad/projects/reachy_mini",
"host_kunst",
);
});

it("preserves the direct single-machine folder flow", () => {
const onSubmit = vi.fn();
render(
<ProjectPathDialog
target={{ kind: "create" }}
platform="linux"
hostId={atum.id}
hostName={atum.name}
hosts={[atum]}
onOpenChange={vi.fn()}
onSubmit={onSubmit}
/>,
);

expect(screen.queryByRole("radiogroup", { name: "Machine" })).toBeNull();
fireEvent.click(
screen.getByRole("button", { name: "Choose folder on host_atum" }),
);
fireEvent.click(screen.getByRole("button", { name: "Add project" }));

expect(onSubmit).toHaveBeenCalledWith(
{ kind: "create" },
"/home/deploy/repos/givecare",
"host_atum",
);
});

// With machines listed but none selectable there is no host to resolve a
// path against, so the manual-path fallback must not invite a submit that
// the picker hook would drop without feedback.
it("blocks submission when every listed machine is offline", () => {
const onSubmit = vi.fn();
render(
<ProjectPathDialog
target={{ kind: "create" }}
platform="linux"
hostId={null}
hostName={null}
hosts={[offline, offlineKunst]}
onOpenChange={vi.fn()}
onSubmit={onSubmit}
/>,
);

expect(screen.queryByLabelText("Project path")).toBeNull();
expect(
screen
.getByRole("button", { name: "Add project" })
.hasAttribute("disabled"),
).toBe(true);
expect(onSubmit).not.toHaveBeenCalled();
});
});
Loading
Loading