import { describe, expect, it } from "vitest"; import { act, cleanup, createSchema, defineToolcraft, FileDrop, fireEvent, Panel, render, renderControlsPanelWithSchema, screen, vi, waitFor, } from "../testing/controls-panel-test-utils"; class ResizeObserverStub implements ResizeObserver { constructor(private readonly callback: ResizeObserverCallback) { resizeObservers.push(this); } disconnect(): void {} observe(): void {} unobserve(): void {} notify(): void { this.callback([], this); } } let resizeObservers: ResizeObserverStub[] = []; function createSectionHeaderSchema(description?: string) { return defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft fixture", }, canvas: { enabled: false }, panels: { controls: { sections: [ { id: "fixture-section-1", controls: { population: { applicability: { mode: "always" }, defaultValue: 24, label: "Population", max: 100, min: 1, target: "points.population", type: "slider", }, }, description, title: "Incremental Revenue", }, { id: "fixture-section-2", controls: { baseline: { applicability: { mode: "always" }, defaultValue: 12, label: "Baseline", max: 100, min: 1, target: "points.baseline", type: "slider", }, }, title: "Baseline Revenue", }, ], title: "Controls", }, }, }, modules: [], }); } describe("ControlsPanel collapsible section shell", () => { it("exposes stable schema target ownership without changing control layout", () => { const { container } = renderControlsPanelWithSchema(createSchema()); const opacityBoundary = container.querySelector( '[data-toolcraft-control-target="selectedLayer.opacity"]', ); const colorGroupBoundary = container.querySelector( "[data-toolcraft-control-targets]", ); expect(opacityBoundary?.className).toContain("contents"); expect(opacityBoundary?.querySelector('[data-slot="field"]')).toBeTruthy(); expect( JSON.parse( colorGroupBoundary?.getAttribute("data-toolcraft-control-targets") ?? "[]", ), ).toEqual(["style.fill", "style.stroke"]); expect( colorGroupBoundary?.querySelectorAll('[data-slot="field"]'), ).toHaveLength(2); }); it("renders titled body sections as 36px collapsible header rows", () => { const schema = createSchema(); const { container } = renderControlsPanelWithSchema(schema); const basicTitle = screen.getByText("Basic"); const header = basicTitle.closest('[data-slot="control-section-header"]'); const collapseButton = header?.querySelector( "[data-control-section-collapse-button]", ); const section = basicTitle.closest("section"); const controlList = section?.querySelector("[data-control-list]"); const body = section?.querySelector( '[data-slot="panel-section-collapsible-body"]', ); expect(header?.className).toContain("h-9"); expect(header?.getAttribute("data-collapsible")).toBe(""); expect(collapseButton?.getAttribute("aria-expanded")).toBe("true"); expect(collapseButton?.tagName).toBe("BUTTON"); expect(body?.className).toContain( "transition-[grid-template-rows,opacity]", ); expect(body?.className).toContain("grid-rows-[1fr]"); expect(controlList?.className).toContain("pt-2"); expect(controlList?.className).toContain("pb-6"); expect(section?.textContent).toContain("Prompt"); expect(section?.textContent).toContain("Opacity"); fireEvent.click(collapseButton!); expect(section?.getAttribute("data-collapsed")).toBe("true"); expect(collapseButton?.getAttribute("aria-expanded")).toBe("false"); expect(body?.getAttribute("aria-hidden")).toBe("true"); expect(body?.className).toContain("grid-rows-[0fr]"); expect(section?.textContent).toContain("Basic"); expect(section?.textContent).toContain("Prompt"); fireEvent.transitionEnd(body!); expect(section?.textContent).not.toContain("Prompt"); expect(section?.textContent).not.toContain("Opacity"); fireEvent.click(collapseButton!); expect(section?.getAttribute("data-collapsed")).toBe("false"); expect(collapseButton?.getAttribute("aria-expanded")).toBe("true"); expect( section?.querySelector('[data-slot="panel-section-collapsible-body"]'), ).toBeTruthy(); expect( container.querySelector('[data-slot="control-section-header"]'), ).toBeTruthy(); }); it("keeps section descriptions behind isolated header help controls", () => { const description = "Controls the point population and popup shown for Tab 3."; renderControlsPanelWithSchema(createSectionHeaderSchema(description)); const title = screen.getByText("Incremental Revenue"); const header = title.closest( '[data-slot="control-section-header"]', ); const section = title.closest("section"); const body = section?.querySelector( '[data-slot="panel-section-collapsible-body"]', ); const viewport = header?.querySelector( "[data-control-section-title-viewport]", ); const titleText = header?.querySelector( "[data-control-section-title-text]", ); const help = screen.getByRole("button", { name: "Incremental Revenue help", }); expect(viewport).toBeTruthy(); expect(titleText).toBeTruthy(); expect(viewport?.getAttribute("data-scroll-boundary-behavior")).toBe("chain"); expect(titleText?.className).toContain("whitespace-nowrap"); expect(title.className).toContain("whitespace-nowrap"); expect(titleText?.getAttribute("title")).toBeNull(); expect(header?.textContent).not.toContain(description); expect(body?.textContent).not.toContain(description); expect(screen.queryByText(description)).toBeNull(); expect(help.getAttribute("data-control-section-help")).toBe(""); expect(help.parentElement?.closest("button")).toBeNull(); const expectSectionExpanded = (): void => { expect( header ?.querySelector("[data-control-section-collapse-button]") ?.getAttribute("aria-expanded"), ).toBe("true"); expect(section?.getAttribute("data-collapsed")).toBe("false"); }; fireEvent.pointerDown(help); expectSectionExpanded(); fireEvent.click(help); expectSectionExpanded(); fireEvent.keyDown(help, { key: "Enter" }); expectSectionExpanded(); fireEvent.keyDown(help, { key: " " }); expectSectionExpanded(); help.focus(); expect(document.activeElement).toBe(help); const documentKeyDown = vi.fn(); document.addEventListener("keydown", documentKeyDown); try { fireEvent.keyDown(help, { ctrlKey: true, key: "z" }); expect(documentKeyDown).toHaveBeenCalledTimes(1); expectSectionExpanded(); } finally { document.removeEventListener("keydown", documentKeyDown); } expect( screen.queryByRole("button", { name: "Baseline Revenue help" }), ).toBeNull(); }); it("shows the section title fade and native hint only for measured overflow", () => { vi.stubGlobal("ResizeObserver", ResizeObserverStub); resizeObservers = []; let clientWidth = 160; let scrollWidth = 161; try { const { container } = renderControlsPanelWithSchema( createSectionHeaderSchema( "Controls the point population and popup shown for Tab 3.", ), ); const viewport = container.querySelector( "[data-control-section-title-viewport]", ); const titleText = container.querySelector( "[data-control-section-title-text]", ); if (!viewport || !titleText) { throw new Error( "Expected the runtime section title viewport and text.", ); } Object.defineProperties(viewport, { clientWidth: { configurable: true, get: () => clientWidth, }, scrollWidth: { configurable: true, get: () => scrollWidth, }, }); act(() => resizeObservers.forEach((observer) => observer.notify())); expect(titleText.getAttribute("title")).toBeNull(); expect(viewport.getAttribute("data-visible")).toBe("false"); scrollWidth = 162; act(() => resizeObservers.forEach((observer) => observer.notify())); expect(titleText.getAttribute("title")).toBe("Incremental Revenue"); expect(viewport.getAttribute("data-visible")).toBe("true"); clientWidth = 200; act(() => resizeObservers.forEach((observer) => observer.notify())); expect(titleText.getAttribute("title")).toBeNull(); expect(viewport.getAttribute("data-visible")).toBe("false"); } finally { resizeObservers = []; vi.unstubAllGlobals(); } }); it("suppresses child control transitions when a section re-expands", () => { const schema = createSchema(); renderControlsPanelWithSchema(schema); const basicTitle = screen.getByText("Basic"); const header = basicTitle.closest( '[data-slot="control-section-header"]', ); const collapseButton = header?.querySelector( "[data-control-section-collapse-button]", ); const section = basicTitle.closest("section"); const initialBody = section?.querySelector( '[data-slot="panel-section-collapsible-body"]', ); fireEvent.click(collapseButton!); fireEvent.transitionEnd(initialBody!); fireEvent.click(collapseButton!); const expandedBody = section?.querySelector( '[data-slot="panel-section-collapsible-body"]', ); const innerControls = expandedBody?.firstElementChild as HTMLElement | null; expect(expandedBody?.className).toContain( "transition-[grid-template-rows,opacity]", ); expect(expandedBody?.className).toContain("grid-rows-[1fr]"); expect( innerControls?.getAttribute("data-toolcraft-controls-mounting"), ).toBe("true"); }); it("persists ordinary section collapse state as a UI preference across remounts", async () => { const schema = createSchema(); const { unmount } = renderControlsPanelWithSchema(schema); const basicHeader = screen .getByText("Basic") .closest('[data-slot="control-section-header"]'); const collapseButton = basicHeader?.querySelector( "[data-control-section-collapse-button]", ); fireEvent.click(collapseButton!); expect( basicHeader?.closest("section")?.getAttribute("data-collapsed"), ).toBe("true"); if (schema.persistence.storage !== "localStorage") { throw new Error("Expected default local persistence"); } const persistenceKey = schema.persistence.key; const basicSectionId = schema.panels.controls?.sections.find( (section) => section.title === "Basic", )?.id; await waitFor(() => { const persisted = JSON.parse( window.localStorage.getItem(persistenceKey) ?? "{}", ); expect(persisted.state.panels.controls.collapsedSections).toEqual({ [basicSectionId!]: true, }); }); unmount(); cleanup(); renderControlsPanelWithSchema(schema); const restoredSection = screen.getByText("Basic").closest("section"); expect(restoredSection?.getAttribute("data-collapsed")).toBe("true"); fireEvent.click(screen.getByRole("button", { name: "Reset controls" })); expect( screen .getByText("Basic") .closest("section") ?.getAttribute("data-collapsed"), ).toBe("true"); }); it("puts ordinary implicit panel section spacing on the control list layer", () => { const { container } = render( , ); const section = container.querySelector("section"); const controlList = section?.querySelector("[data-control-list]"); expect(section?.className).toContain("py-0"); expect(controlList?.className).toContain("pt-2"); expect(controlList?.className).toContain("pb-6"); expect(controlList?.textContent).toContain("Click to upload an image"); }); });