From 9d0da99c742673f6f6ba52ff53c05207b9d302f0 Mon Sep 17 00:00:00 2001 From: alfredorubin96 Date: Tue, 28 Apr 2026 18:12:32 +0200 Subject: [PATCH 1/2] test(app): add widget-editor sub-component tests (#578) Add 55 unit tests across 4 test files covering: - resolveInternalParamType / reverseParamTypeMapping pure functions (16 tests) - ChartTypeSelector component rendering and callbacks (10 tests) - ParameterConfigSection UI: all param types, seed query, collisions (18 tests) - FormFieldsEditor: empty state, add/remove fields, DnD mocks (11 tests) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../__tests__/chart-type-selector.test.tsx | 229 ++++++++++++ .../__tests__/form-fields-editor.test.tsx | 309 ++++++++++++++++ .../parameter-config-section-ui.test.tsx | 341 ++++++++++++++++++ .../parameter-config-section.test.tsx | 129 +++++++ 4 files changed, 1008 insertions(+) create mode 100644 app/src/components/widget-editor/__tests__/chart-type-selector.test.tsx create mode 100644 app/src/components/widget-editor/__tests__/form-fields-editor.test.tsx create mode 100644 app/src/components/widget-editor/__tests__/parameter-config-section-ui.test.tsx create mode 100644 app/src/components/widget-editor/__tests__/parameter-config-section.test.tsx diff --git a/app/src/components/widget-editor/__tests__/chart-type-selector.test.tsx b/app/src/components/widget-editor/__tests__/chart-type-selector.test.tsx new file mode 100644 index 00000000..7949c222 --- /dev/null +++ b/app/src/components/widget-editor/__tests__/chart-type-selector.test.tsx @@ -0,0 +1,229 @@ +import React from "react"; +import { describe, it, expect, vi } from "vitest"; +import { render, screen, fireEvent } from "@testing-library/react"; + +vi.mock("@neoboard/components", () => ({ + Label: ({ + children, + ...props + }: React.PropsWithChildren>) => ( + + ), + Combobox: ({ + value, + onChange, + options, + placeholder, + }: { + value: string; + onChange: (v: string) => void; + options: { value: string; label: string }[]; + placeholder?: string; + }) => ( + + ), +})); + +vi.mock("@/lib/plugin/chart-helpers", () => ({ + getChartConfig: (type: string) => ({ + label: type.charAt(0).toUpperCase() + type.slice(1), + }), +})); + +vi.mock("lucide-react", async (importOriginal) => { + const Icon = () => ; + const actual = await importOriginal>(); + const mocked: Record = {}; + for (const key of Object.keys(actual)) { + mocked[key] = Icon; + } + return mocked; +}); + +import { + ChartTypeSelector, + getChartTypeMeta, + chartTypeIcons, +} from "../chart-type-selector"; +import type { ChartType } from "@/lib/plugin/chart-helpers"; + +describe("getChartTypeMeta", () => { + it("returns label from chart config and icon from map", () => { + const meta = getChartTypeMeta("bar"); + expect(meta.label).toBe("Bar"); + expect(meta.Icon).toBeDefined(); + }); + + it("returns fallback icon for unknown type", () => { + const meta = getChartTypeMeta("unknown-type" as ChartType); + expect(meta.Icon).toBeDefined(); + }); +}); + +describe("chartTypeIcons", () => { + it("has entries for all 20 chart types", () => { + const expected = [ + "bar", + "line", + "pie", + "single-value", + "graph", + "map", + "table", + "json", + "parameter-select", + "form", + "markdown", + "iframe", + "gauge", + "sankey", + "sunburst", + "radar", + "treemap", + "gantt", + "circle-packing", + "choropleth", + ]; + for (const type of expected) { + expect(chartTypeIcons[type as ChartType]).toBeDefined(); + } + }); +}); + +const connections = [ + { id: "c1", name: "Neo4j Local", type: "neo4j" }, + { id: "c2", name: "Postgres Prod", type: "postgresql" }, +]; +const chartTypes: ChartType[] = ["bar", "line", "pie", "table"]; + +describe("ChartTypeSelector", () => { + it("renders chart type selector without connection when showConnection is false", () => { + render( + , + ); + expect(screen.getByText("Chart Type")).toBeInTheDocument(); + expect(screen.queryByText("Connection")).not.toBeInTheDocument(); + }); + + it("renders both connection and chart type when showConnection is true", () => { + render( + , + ); + expect(screen.getByText("Chart Type")).toBeInTheDocument(); + expect(screen.getByText("Connection")).toBeInTheDocument(); + }); + + it("calls onChartTypeChange when chart type changes", () => { + const onChartTypeChange = vi.fn(); + render( + , + ); + fireEvent.change(screen.getByTestId("Select chart type..."), { + target: { value: "line" }, + }); + expect(onChartTypeChange).toHaveBeenCalledWith("line"); + }); + + it("calls onConnectionChange when connection changes", () => { + const onConnectionChange = vi.fn(); + render( + , + ); + fireEvent.change(screen.getByTestId("Select a connection..."), { + target: { value: "c2" }, + }); + expect(onConnectionChange).toHaveBeenCalledWith("c2"); + }); + + it("renders chart type options from compatibleChartTypes", () => { + render( + , + ); + expect(screen.getByText("Bar")).toBeInTheDocument(); + expect(screen.getByText("Line")).toBeInTheDocument(); + expect(screen.getByText("Pie")).toBeInTheDocument(); + expect(screen.getByText("Table")).toBeInTheDocument(); + }); + + it("renders connection options with name and type", () => { + render( + , + ); + expect(screen.getByText("Neo4j Local (neo4j)")).toBeInTheDocument(); + expect(screen.getByText("Postgres Prod (postgresql)")).toBeInTheDocument(); + }); + + it("shows required indicator on connection label", () => { + render( + , + ); + expect(screen.getByText("*")).toBeInTheDocument(); + }); +}); diff --git a/app/src/components/widget-editor/__tests__/form-fields-editor.test.tsx b/app/src/components/widget-editor/__tests__/form-fields-editor.test.tsx new file mode 100644 index 00000000..e5aeb59f --- /dev/null +++ b/app/src/components/widget-editor/__tests__/form-fields-editor.test.tsx @@ -0,0 +1,309 @@ +import React from "react"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, screen, fireEvent } from "@testing-library/react"; +import type { FormFieldDef } from "@/lib/widget/form-field-def"; + +// Mock DnD kit — just render children without drag behavior +vi.mock("@dnd-kit/core", () => ({ + DndContext: ({ children }: React.PropsWithChildren) => <>{children}, + closestCenter: vi.fn(), + KeyboardSensor: vi.fn(), + PointerSensor: vi.fn(), + useSensor: () => ({}), + useSensors: () => [], +})); +vi.mock("@dnd-kit/sortable", () => ({ + SortableContext: ({ children }: React.PropsWithChildren) => <>{children}, + sortableKeyboardCoordinates: vi.fn(), + useSortable: () => ({ + attributes: {}, + listeners: {}, + setNodeRef: vi.fn(), + transform: null, + transition: null, + isDragging: false, + }), + verticalListSortingStrategy: vi.fn(), +})); +vi.mock("@dnd-kit/utilities", () => ({ + CSS: { Transform: { toString: () => "" } }, +})); + +vi.mock("@neoboard/components", () => ({ + Accordion: ({ + children, + }: React.PropsWithChildren<{ + value: string[]; + onValueChange: (v: string[]) => void; + }>) =>
{children}
, + AccordionContent: ({ children }: React.PropsWithChildren) => ( +
{children}
+ ), + AccordionItem: React.forwardRef< + HTMLDivElement, + React.PropsWithChildren<{ value: string; style?: React.CSSProperties }> + >(({ children, value, style }, ref) => ( +
+ {children} +
+ )), + AccordionTrigger: ({ children }: React.PropsWithChildren) => ( +
{children}
+ ), + Badge: ({ children }: React.PropsWithChildren) => {children}, + Button: ({ + children, + onClick, + ...props + }: React.PropsWithChildren>) => ( + + ), + Checkbox: ({ + id, + checked, + onCheckedChange, + }: { + id?: string; + checked?: boolean; + onCheckedChange?: (v: boolean) => void; + }) => ( + onCheckedChange?.(e.target.checked)} + data-testid={id} + /> + ), + Input: ({ + value, + onChange, + ...props + }: React.InputHTMLAttributes) => ( + + ), + Label: ({ + children, + ...props + }: React.PropsWithChildren>) => ( + + ), + Select: ({ + children, + value, + onValueChange, + }: React.PropsWithChildren<{ + value: string; + onValueChange: (v: string) => void; + }>) => ( + + ), + SelectContent: ({ children }: React.PropsWithChildren) => <>{children}, + SelectItem: ({ + children, + value, + }: React.PropsWithChildren<{ value: string }>) => ( + + ), + SelectTrigger: ({ children }: React.PropsWithChildren) => <>{children}, + SelectValue: () => null, + Textarea: ({ + value, + onChange, + ...props + }: React.TextareaHTMLAttributes) => ( +