feat(FN-3764): add manage view with edit modal for CLI printing press plugi
FN-3764 adds a manage view and edit draft modal to the CLI printing press plugin, wiring in draft storage, wizard routes, and plugin view registry integration so users can view and edit their printing press drafts directly from the dashboard. Includes corresponding tests, documentation updates, and Fusion-Task-Id: FN-3764
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { mkdtemp } from "node:fs/promises";
|
||||
import { mkdtemp, readdir } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createDraftStore } from "../storage/draft-store";
|
||||
import { createDraftStore, NotFoundError } from "../storage/draft-store";
|
||||
import type { ServiceDraft } from "../wizard/types";
|
||||
|
||||
function makeDraft(): ServiceDraft {
|
||||
@@ -22,4 +22,29 @@ describe("draft store", () => {
|
||||
await store.delete(created.id);
|
||||
expect(await store.get(created.id)).toBeNull();
|
||||
});
|
||||
|
||||
it("updates an existing draft and replaces endpoints", async () => {
|
||||
const rootDir = await mkdtemp(join(tmpdir(), "cli-printing-press-"));
|
||||
const store = createDraftStore({ rootDir });
|
||||
const created = await store.create(makeDraft());
|
||||
|
||||
const updated = await store.update(created.id, {
|
||||
name: "Renamed",
|
||||
endpoints: [{ id: "e2", name: "Health", method: "GET", path: "/health" }],
|
||||
});
|
||||
|
||||
expect(updated.name).toBe("Renamed");
|
||||
expect(updated.endpoints).toHaveLength(1);
|
||||
expect(updated.endpoints[0]?.id).toBe("e2");
|
||||
expect(updated.updatedAt).not.toBe(created.updatedAt);
|
||||
|
||||
const draftFiles = await readdir(join(rootDir, ".fusion", "plugins", "cli-printing-press", "drafts"));
|
||||
expect(draftFiles.some((entry) => entry.includes(".tmp-"))).toBe(false);
|
||||
});
|
||||
|
||||
it("throws NotFoundError on update for unknown ids", async () => {
|
||||
const rootDir = await mkdtemp(join(tmpdir(), "cli-printing-press-"));
|
||||
const store = createDraftStore({ rootDir });
|
||||
await expect(store.update("missing", { name: "Nope" })).rejects.toBeInstanceOf(NotFoundError);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
// @vitest-environment jsdom
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { CliPrintingPressManageView } from "../manage-view";
|
||||
import type { ServiceDraft } from "../wizard/types";
|
||||
|
||||
vi.mock("lucide-react", () => ({
|
||||
List: () => null,
|
||||
Pencil: () => null,
|
||||
RefreshCw: () => null,
|
||||
Trash2: () => null,
|
||||
}));
|
||||
|
||||
function makeDraft(id: string, name = "Demo"): ServiceDraft {
|
||||
const now = new Date().toISOString();
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
slug: id,
|
||||
description: "",
|
||||
baseUrl: `https://${id}.example.com`,
|
||||
transport: "http",
|
||||
endpoints: [{ id: `${id}-e1`, name: "Ping", method: "GET", path: "/ping" }],
|
||||
credential: { kind: "none" },
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
}
|
||||
|
||||
describe("CliPrintingPressManageView", () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("renders empty state", async () => {
|
||||
vi.stubGlobal("fetch", vi.fn(async () => ({ ok: true, json: async () => [] })));
|
||||
render(<CliPrintingPressManageView />);
|
||||
expect(await screen.findByText(/No saved drafts yet/i)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders list, detail, edit/save, regenerate, and delete", async () => {
|
||||
const draft1 = makeDraft("draft-1", "Draft One");
|
||||
const draft2 = makeDraft("draft-2", "Draft Two");
|
||||
const updated = { ...draft1, name: "Draft One Edited" };
|
||||
|
||||
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
const method = init?.method ?? "GET";
|
||||
|
||||
if (method === "GET" && url.endsWith("/drafts")) {
|
||||
return { ok: true, json: async () => ([
|
||||
{ id: draft1.id, name: draft1.name, slug: draft1.slug, updatedAt: draft1.updatedAt },
|
||||
{ id: draft2.id, name: draft2.name, slug: draft2.slug, updatedAt: draft2.updatedAt },
|
||||
]) };
|
||||
}
|
||||
if (method === "GET" && url.endsWith(`/drafts/${draft1.id}`)) return { ok: true, json: async () => draft1 };
|
||||
if (method === "GET" && url.endsWith(`/drafts/${draft2.id}`)) return { ok: true, json: async () => draft2 };
|
||||
if (method === "PUT" && url.endsWith(`/drafts/${draft1.id}`)) return { ok: true, json: async () => updated };
|
||||
if (method === "POST" && url.endsWith(`/drafts/${draft1.id}/regenerate`)) {
|
||||
return { ok: true, json: async () => ({ draft: { ...updated, regeneratedAt: new Date().toISOString() }, stub: true, message: "Regenerate stub — full generation lands in FN-3765/FN-3767" }) };
|
||||
}
|
||||
if (method === "DELETE" && url.endsWith(`/drafts/${draft1.id}`)) return { ok: true, status: 204, json: async () => ({}) };
|
||||
return { ok: false, status: 404, json: async () => ({ error: "Not found" }) };
|
||||
});
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
vi.spyOn(globalThis, "confirm").mockReturnValue(true);
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(<CliPrintingPressManageView />);
|
||||
|
||||
expect(await screen.findByText("Draft One")).toBeTruthy();
|
||||
expect(screen.getByText("Draft Two")).toBeTruthy();
|
||||
expect(await screen.findByText(/Base URL:/)).toBeTruthy();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /Edit/i }));
|
||||
await user.clear(screen.getByLabelText("Name"));
|
||||
await user.type(screen.getByLabelText("Name"), "Draft One Edited");
|
||||
await user.click(screen.getByRole("button", { name: "Next" }));
|
||||
await user.click(screen.getByRole("button", { name: "Next" }));
|
||||
await user.click(screen.getByRole("button", { name: "Next" }));
|
||||
await user.click(screen.getByRole("button", { name: "Next" }));
|
||||
await user.click(screen.getByRole("button", { name: "Save" }));
|
||||
|
||||
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining(`/drafts/${draft1.id}`), expect.objectContaining({ method: "PUT" })));
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /Regenerate/i }));
|
||||
expect(await screen.findByText(/Regenerate stub/)).toBeTruthy();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /Delete/i }));
|
||||
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining(`/drafts/${draft1.id}`), expect.objectContaining({ method: "DELETE" })));
|
||||
});
|
||||
});
|
||||
@@ -13,7 +13,7 @@ describe("cli-printing-press plugin", () => {
|
||||
expect(validation.valid).toBe(true);
|
||||
});
|
||||
|
||||
it("registers the wizard dashboard view", () => {
|
||||
it("registers the wizard and manage dashboard views", () => {
|
||||
expect(plugin.dashboardViews).toEqual([
|
||||
{
|
||||
viewId: "wizard",
|
||||
@@ -23,6 +23,14 @@ describe("cli-printing-press plugin", () => {
|
||||
placement: "primary",
|
||||
order: 60,
|
||||
},
|
||||
{
|
||||
viewId: "manage",
|
||||
label: "Manage Service CLIs",
|
||||
componentPath: "./manage-view",
|
||||
icon: "List",
|
||||
placement: "primary",
|
||||
order: 61,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,6 +35,23 @@ describe("wizard routes", () => {
|
||||
expect(invalidRes.status).toBe(400);
|
||||
expect((invalidRes.body as { errors: Record<string, string> }).errors.slug).toBeTruthy();
|
||||
|
||||
const putRes = await route("PUT", "/drafts/:id").handler({ params: { id }, body: { ...makeDraft(), id, name: "Renamed" } }, ctx);
|
||||
expect(putRes.status).toBe(200);
|
||||
expect((putRes.body as { name: string }).name).toBe("Renamed");
|
||||
|
||||
const invalidPutRes = await route("PUT", "/drafts/:id").handler({ params: { id }, body: { ...makeDraft(), id, baseUrl: "invalid-url" } }, ctx);
|
||||
expect(invalidPutRes.status).toBe(400);
|
||||
|
||||
const missingPutRes = await route("PUT", "/drafts/:id").handler({ params: { id: "missing" }, body: { ...makeDraft(), id: "missing" } }, ctx);
|
||||
expect(missingPutRes.status).toBe(404);
|
||||
|
||||
const regenRes = await route("POST", "/drafts/:id/regenerate").handler({ params: { id } }, ctx);
|
||||
expect(regenRes.status).toBe(200);
|
||||
expect((regenRes.body as { stub: boolean }).stub).toBe(true);
|
||||
|
||||
const missingRegenRes = await route("POST", "/drafts/:id/regenerate").handler({ params: { id: "missing" } }, ctx);
|
||||
expect(missingRegenRes.status).toBe(404);
|
||||
|
||||
const deleteRes = await route("DELETE", "/drafts/:id").handler({ params: { id } }, ctx);
|
||||
expect(deleteRes.status).toBe(204);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user