import { describe, expect, it } from "vitest"; import { defineToolcraft, fireEvent, renderControlsPanelWithSchema, screen, waitFor, } from "../testing/controls-panel-test-utils"; describe("ControlsPanel collection actions", () => { it("renders collection actions as add/remove controls for runtime item arrays", () => { const schema = defineToolcraft({ canvas: { enabled: false }, panels: { controls: { sections: [ { controls: { colors: { defaultValue: [{ hex: "#DFFF1A" }, { hex: "#8CFF3A" }], itemControl: { defaultValue: { hex: "#C1FF00" }, label: "Color", type: "color", }, label: "Colors", minItems: 1, recommendedMaxItems: 2, target: "bead.colors", type: "collectionActions", }, }, title: "Bead Colors", }, ], title: "Controls", }, }, }); const { container } = renderControlsPanelWithSchema(schema); const addButton = screen.getByRole("button", { name: "Add Color" }); const removeButton = screen.getByRole("button", { name: "Remove Color" }); const collectionHeader = container.querySelector( '[data-slot="collection-actions-control-header"]', ); const colorGrid = container.querySelector( '[data-slot="collection-actions-items-grid"]', ); expect(collectionHeader?.textContent).toContain("Colors"); expect(collectionHeader?.lastElementChild?.contains(removeButton)).toBe(true); expect(collectionHeader?.lastElementChild?.contains(addButton)).toBe(true); expect(colorGrid).toBeTruthy(); expect(colorGrid?.children).toHaveLength(2); expect( container.querySelector('[data-slot="collection-item-groups"]'), ).toBeNull(); expect(screen.queryByText("Color 1")).toBeNull(); expect(screen.queryByText("Color 2")).toBeNull(); fireEvent.click(addButton); fireEvent.click(addButton); let values = JSON.parse(screen.getByTestId("values-json").textContent ?? "{}"); expect(values["bead.colors"]).toHaveLength(4); expect(screen.queryByText("Color 4")).toBeNull(); expect(colorGrid?.children).toHaveLength(4); expect((addButton as HTMLButtonElement).disabled).toBe(false); fireEvent.click(removeButton); fireEvent.click(removeButton); fireEvent.click(removeButton); values = JSON.parse(screen.getByTestId("values-json").textContent ?? "{}"); expect(values["bead.colors"]).toHaveLength(1); expect((removeButton as HTMLButtonElement).disabled).toBe(true); }); it("adds, edits, separates, and removes complete logical item records", () => { const schema = defineToolcraft({ canvas: { enabled: false }, panels: { controls: { sections: [ { controls: { layers: { defaultValue: [ { invert: true, rendererToken: "keep", strength: 0.4, }, ], itemControls: { strength: { defaultValue: 0.5, label: "Strength", max: 1, min: 0, step: 0.1, type: "slider", }, invert: { defaultValue: false, label: "Invert", type: "switch", }, }, itemLabel: "Layer", minItems: 1, target: "surface.layers", type: "collectionActions", }, }, title: "Surface Layers", }, ], title: "Controls", }, }, }); const { container } = renderControlsPanelWithSchema(schema); expect( container.querySelectorAll('[data-slot="collection-item-group"]'), ).toHaveLength(1); expect(screen.queryByText(/Item 1/u)).toBeNull(); fireEvent.click(screen.getByRole("button", { name: "Add Layer" })); let values = JSON.parse( screen.getByTestId("values-json").textContent ?? "{}", ); expect(values["surface.layers"]).toEqual([ { invert: true, rendererToken: "keep", strength: 0.4 }, { invert: false, strength: 0.5 }, ]); const groups = container.querySelectorAll( '[data-slot="collection-item-group"]', ); expect(groups).toHaveLength(2); expect(groups[0]?.className).not.toContain("border-t"); expect(groups[1]?.className).toContain("border-t"); fireEvent.click( screen.getAllByRole("button", { name: "Edit Strength value" })[0]!, ); const editor = screen.getByRole("textbox", { name: "Strength value" }); editor.textContent = "0.7"; fireEvent.keyDown(editor, { key: "Enter" }); values = JSON.parse(screen.getByTestId("values-json").textContent ?? "{}"); expect(values["surface.layers"][0]).toEqual({ invert: true, rendererToken: "keep", strength: 0.7, }); fireEvent.click(screen.getByRole("button", { name: "Remove Layer" })); expect( container.querySelectorAll('[data-slot="collection-item-group"]'), ).toHaveLength(1); }); it("renders collection item controls beyond color lists", () => { const schema = defineToolcraft({ canvas: { enabled: false }, panels: { controls: { sections: [ { controls: { labels: { defaultValue: ["Alpha"], itemControl: { defaultValue: "New label", label: "Label", type: "text", }, minItems: 1, target: "glyph.labels", type: "collectionActions", }, }, title: "Glyph Labels", }, ], title: "Controls", }, }, }); renderControlsPanelWithSchema(schema); fireEvent.click(screen.getByRole("button", { name: "Add Label" })); const values = JSON.parse(screen.getByTestId("values-json").textContent ?? "{}"); expect(values["glyph.labels"]).toEqual(["Alpha", "New label"]); expect(screen.getByDisplayValue("Alpha")).toBeTruthy(); expect(screen.getByDisplayValue("New label")).toBeTruthy(); }); it("keeps a collection font picker color popover open while editing its color surface", async () => { const schema = defineToolcraft({ canvas: { enabled: false }, panels: { controls: { sections: [ { controls: { textStyles: { defaultValue: [ { color: "#FFFFFF", fontId: "inter", fontSize: 16, fontWeight: "400", letterSpacing: "normal", lineHeight: "normal", opacity: 100, textCase: "original", }, ], itemControl: { defaultValue: { color: "#FFFFFF", fontId: "inter", fontSize: 16, fontWeight: "400", letterSpacing: "normal", lineHeight: "normal", opacity: 100, textCase: "original", }, label: "Font", type: "fontPicker", }, label: "Text styles", minItems: 1, target: "text.styles", type: "collectionActions", }, }, title: "Typography", }, ], title: "Controls", }, }, }); const { container } = renderControlsPanelWithSchema(schema); fireEvent.click(screen.getByRole("button", { name: "Pick Color" })); const surface = await waitFor(() => { const nextSurface = container.ownerDocument.body.querySelector( '[data-slot="style-guide-color-surface"]', ); if (!nextSurface) { throw new Error("Expected color surface to be open."); } return nextSurface; }); const surfaceRect = { bottom: 100, height: 100, left: 0, right: 100, toJSON: () => ({}), top: 0, width: 100, x: 0, y: 0, } as DOMRect; Object.defineProperty(surface, "getBoundingClientRect", { configurable: true, value: () => surfaceRect, }); fireEvent.pointerDown(surface, { buttons: 1, clientX: 72, clientY: 28, pointerId: 1, }); expect( container.ownerDocument.body.querySelector('[data-slot="style-guide-color-surface"]'), ).toBeTruthy(); const values = JSON.parse(screen.getByTestId("values-json").textContent ?? "{}"); expect(values["text.styles"][0].color).not.toBe("#FFFFFF"); }); });