Skip to content
Draft
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
8 changes: 8 additions & 0 deletions packages/core/src/canvas/dashboardSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ export const dashboardFileMetaSchema = z.object({
// the FileSystem row has no updated_at column to sort the dashboards list by.
createdAt: z.number().optional(),
updatedAt: z.number().optional(),
// Channel folders only: the file-system id of the channel's home canvas (the
// auto-created freeform board shown when the channel name is clicked). Stored
// on the folder's meta because the FileSystem model has no column for it.
homeCanvasId: z.string().optional(),
});
export type DashboardFileMeta = z.infer<typeof dashboardFileMetaSchema>;

Expand Down Expand Up @@ -102,6 +106,10 @@ export const saveFreeformInput = z.object({

export const dashboardIdInput = z.object({ id: z.string().min(1) });

export const ensureHomeCanvasInput = z.object({
channelId: z.string().min(1),
});

// The active time window a dashboard's time-based queries run against. `from`
// and `to` are epoch ms; `name` is the picker label (e.g. "Last 7 days"). Stored
// on the spec under `state.dateRange` so it survives reload and the toolbar
Expand Down
196 changes: 196 additions & 0 deletions packages/core/src/canvas/dashboardsService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import type { DashboardQueryService } from "./dashboardQueryService";
import { DashboardsService } from "./dashboardsService";
import type { DesktopFsClient, FsEntryBase } from "./desktopFsClient";

// ensureHomeCanvas fetches the signed-in user's label via posthogApi; stub it so
// the service doesn't reach the network in tests.
vi.mock("./posthogApi", () => ({
fetchCurrentUser: vi.fn(async () => ({ label: "Tester" })),
}));

// A dashboard FS row carrying our payload under `meta`, as the backend returns it.
function dashboardRow(
id: string,
Expand Down Expand Up @@ -68,3 +74,193 @@ describe("DashboardsService.list", () => {
expect(result[0]).toMatchObject({ name: "Newer", channelId: "chan-1" });
});
});

// A stateful fake exposing getEntry + fetch, enough for create/saveFreeform/PATCH.
// POST "" assigns an id and stores the row; PATCH "<id>/" merges meta/path.
function statefulFs(initial: Record<string, Record<string, unknown>>) {
const entries: Record<string, Record<string, unknown>> = { ...initial };
let seq = 0;
const fetch = vi.fn(
async (suffix: string, init?: { method?: string; body?: string }) => {
const method = init?.method ?? "GET";
const body = init?.body ? JSON.parse(init.body) : undefined;
if (suffix === "" && method === "POST") {
const id = `new-${++seq}`;
const entry = {
id,
path: body.path,
type: body.type,
meta: body.meta ?? {},
};
entries[id] = entry;
return { ok: true, status: 200, json: async () => entry } as Response;
}
const id = decodeURIComponent(suffix.replace(/\/$/, ""));
const prev = entries[id] ?? { id, path: "", meta: {} };
const next = { ...prev };
if (body?.meta) next.meta = body.meta;
if (body?.path) next.path = body.path;
entries[id] = next;
return { ok: true, status: 200, json: async () => next } as Response;
},
);
const getEntry = vi.fn(async (id: string) => entries[id] ?? null);
const fs = { getEntry, fetch } as unknown as DesktopFsClient;
return { fs, fetch, entries };
}

describe("DashboardsService.ensureHomeCanvas", () => {
it("creates + seeds a freeform canvas and records it on the channel folder", async () => {
const { fs, entries } = statefulFs({
"chan-1": {
id: "chan-1",
path: "marketing",
type: "folder",
meta: {},
},
});
const service = new DashboardsService(
fs,
{} as DashboardQueryService,
{} as never,
);

const record = await service.ensureHomeCanvas("chan-1");

// The freeform canvas was created under the channel folder.
expect(record.id).toBe("new-1");
expect(record.kind).toBe("freeform");
expect(entries["new-1"]?.path).toBe("marketing/Home");

// Its seeded source queries the file_system system table and bakes both ids.
const meta = entries["new-1"]?.meta as { code?: string };
expect(meta.code).toContain("system.file_system");
expect(meta.code).toContain("chan-1");
expect(meta.code).toContain("new-1");

// The channel folder now points at the home canvas.
const folderMeta = entries["chan-1"]?.meta as { homeCanvasId?: string };
expect(folderMeta.homeCanvasId).toBe("new-1");
});

it("seeds source that transpiles as valid TSX", async () => {
const { fs, entries } = statefulFs({
"chan-1": { id: "chan-1", path: "marketing", type: "folder", meta: {} },
});
const service = new DashboardsService(
fs,
{} as DashboardQueryService,
{} as never,
);

await service.ensureHomeCanvas("chan-1");
const code = (entries["new-1"]?.meta as { code?: string }).code ?? "";

// The sandbox transpiles the seeded code with Babel at runtime; mirror that
// here with esbuild so a syntax error is caught in CI, not in the iframe.
const { transform } = await import("esbuild");
await expect(
transform(code, { loader: "tsx", format: "esm" }),
).resolves.toBeDefined();
});

Comment on lines +83 to +166

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 resetHomeCanvas has no test coverage

Three cases exercise ensureHomeCanvas (create-and-seed, valid TSX, idempotency), but resetHomeCanvas — the user-visible "Reset to default" button — has none. At a minimum, a test verifying the new version is appended (not overwriting history) and that the returned record carries the reset code would protect the undo flow.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/core/src/canvas/dashboardsService.test.ts
Line: 83-166

Comment:
**`resetHomeCanvas` has no test coverage**

Three cases exercise `ensureHomeCanvas` (create-and-seed, valid TSX, idempotency), but `resetHomeCanvas` — the user-visible "Reset to default" button — has none. At a minimum, a test verifying the new version is appended (not overwriting history) and that the returned record carries the reset code would protect the undo flow.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

it("is idempotent: returns the existing home canvas without creating another", async () => {
const { fs, fetch, entries } = statefulFs({
"chan-1": {
id: "chan-1",
path: "marketing",
type: "folder",
meta: { homeCanvasId: "home-x" },
},
"home-x": {
id: "home-x",
path: "marketing/Home",
type: "dashboard",
meta: { channelId: "chan-1", kind: "freeform", code: "// seeded" },
},
});
const service = new DashboardsService(
fs,
{} as DashboardQueryService,
{} as never,
);

const record = await service.ensureHomeCanvas("chan-1");

expect(record.id).toBe("home-x");
// No create/patch happened — the folder already had a live home canvas.
expect(fetch).not.toHaveBeenCalled();
expect(Object.keys(entries)).toEqual(["chan-1", "home-x"]);
});
});

describe("DashboardsService.resetHomeCanvas", () => {
it("appends a fresh default version without dropping history", async () => {
const { fs, entries } = statefulFs({
"chan-1": {
id: "chan-1",
path: "marketing",
type: "folder",
meta: { homeCanvasId: "home-x" },
},
"home-x": {
id: "home-x",
path: "marketing/Home",
type: "dashboard",
meta: {
channelId: "chan-1",
kind: "freeform",
code: "// edited by the user",
versions: [{ id: "v1", code: "// edited by the user", createdAt: 1 }],
currentVersionId: "v1",
},
},
});
const service = new DashboardsService(
fs,
{} as DashboardQueryService,
{} as never,
);

const record = await service.resetHomeCanvas("chan-1");

// The returned record carries the regenerated default source (queries the
// file_system table and bakes both ids), not the user's edit.
expect(record.id).toBe("home-x");
expect(record.code).toContain("system.file_system");
expect(record.code).toContain("chan-1");
expect(record.code).toContain("home-x");
expect(record.code).not.toContain("// edited by the user");

// History is preserved: the prior version stays and the default is appended
// as the new head, so Undo can restore the user's edit.
expect(record.versions?.map((v) => v.id)).toEqual([
"v1",
record.currentVersionId,
]);
expect(record.currentVersionId).not.toBe("v1");
expect(record.versions?.at(-1)?.code).toBe(record.code);

// Persisted to the same canvas (no new canvas created).
expect(Object.keys(entries)).toEqual(["chan-1", "home-x"]);
});

it("creates a home canvas if the channel has none yet", async () => {
const { fs, entries } = statefulFs({
"chan-1": { id: "chan-1", path: "marketing", type: "folder", meta: {} },
});
const service = new DashboardsService(
fs,
{} as DashboardQueryService,
{} as never,
);

const record = await service.resetHomeCanvas("chan-1");

expect(record.id).toBe("new-1");
expect(record.code).toContain("system.file_system");
expect(
(entries["chan-1"]?.meta as { homeCanvasId?: string }).homeCanvasId,
).toBe("new-1");
});
});
Loading
Loading