1import { describe, expect, it } from "vitest";
2
3import {
4 ASK_AI_ROWS,
5 BACKGROUND_COLOR_TOKENS,
6 buildBlockMenu,
7 convertToRows,
8 moreRows,
9 turnIntoId,
10 turnIntoRows,
11 TEXT_BLOCK_TYPES,
12 TEXT_COLOR_TOKENS,
13} from "./context-menu.js";
14
15describe("context menu (PRODUCT-SPEC §7 shape)", () => {
16 it("builds the exact top-level row order", () => {
17 const rows = buildBlockMenu({ blockIds: ["b1"], type: "paragraph" });
18 expect(rows.map((r) => r.id)).toEqual([
19 "turn_into",
20 "duplicate",
21 "copy",
22 "copy_link",
23 "move_to",
24 "delete",
25 "sep-1",
26 "comment",
27 "ask_ai",
28 "sep-2",
29 "color",
30 "background",
31 "sep-3",
32 "convert_to",
33 "sep-4",
34 "more",
35 ]);
36 });
37
38 it("pluralizes the duplicate label for a selection", () => {
39 const single = buildBlockMenu({ blockIds: ["b1"], type: "paragraph" });
40 const many = buildBlockMenu({ blockIds: ["a", "b", "c"], type: "paragraph" });
41 expect(single.find((r) => r.id === "duplicate")?.label).toBe("Duplicate");
42 expect(many.find((r) => r.id === "duplicate")?.label).toBe("Duplicate 3 blocks");
43 });
44
45 it("disables mutating rows in read-only mode but keeps read rows", () => {
46 const rows = buildBlockMenu({ blockIds: ["b1"], type: "paragraph", readOnly: true });
47 const byId = new Map(rows.map((r) => [r.id, r]));
48 expect(byId.get("duplicate")?.disabled).toBe(true);
49 expect(byId.get("delete")?.disabled).toBe(true);
50 expect(byId.get("turn_into")?.disabled).toBe(true);
51 expect(byId.get("copy")?.disabled).toBe(false);
52 expect(byId.get("copy_link")?.disabled).toBe(false);
53 });
54
55 it("Turn into lists every text-capable type and disables the current one", () => {
56 const rows = turnIntoRows("heading_2");
57 expect(rows.map((r) => r.id)).toEqual(TEXT_BLOCK_TYPES.map(turnIntoId));
58 expect(rows.find((r) => r.id === turnIntoId("heading_2"))?.disabled).toBe(true);
59 expect(rows.find((r) => r.id === turnIntoId("paragraph"))?.disabled).toBe(false);
60 });
61
62 it("Convert to is the quick set (h1–3 / quote / callout / todo)", () => {
63 expect(convertToRows().map((r) => r.id)).toEqual([
64 "convert_to:heading_1",
65 "convert_to:heading_2",
66 "convert_to:heading_3",
67 "convert_to:quote",
68 "convert_to:callout",
69 "convert_to:todo",
70 ]);
71 });
72
73 it("Ask AI is the frozen Phase-12 shape (all disabled, same tooltip)", () => {
74 expect(ASK_AI_ROWS).toHaveLength(4);
75 for (const row of ASK_AI_ROWS) {
76 expect(row.disabled).toBe(true);
77 expect(row.tooltip).toBe("Available in Phase 12");
78 }
79 });
80
81 it("color/background submenus have 9 swatches each (closed token palette)", () => {
82 expect(TEXT_COLOR_TOKENS).toHaveLength(9);
83 expect(BACKGROUND_COLOR_TOKENS).toHaveLength(9);
84 const rows = buildBlockMenu({ blockIds: ["b1"], type: "paragraph" });
85 const color = rows.find((r) => r.id === "color")?.submenu;
86 const bg = rows.find((r) => r.id === "background")?.submenu;
87 expect(color).toHaveLength(9);
88 expect(bg).toHaveLength(9);
89
90 expect(color?.every((r) => r.run !== undefined)).toBe(true);
91 });
92
93 it("More rows: synced + template disabled; permissions gated on page root", () => {
94 expect(moreRows(false).find((r) => r.id === "more:permissions")?.disabled).toBe(true);
95 expect(moreRows(true).find((r) => r.id === "more:permissions")?.disabled).toBe(false);
96 expect(moreRows(true).find((r) => r.id === "more:synced")?.disabled).toBe(true);
97 expect(moreRows(true).find((r) => r.id === "more:template")?.disabled).toBe(true);
98 });
99});
100