import { fireEvent, render, screen, waitFor } from "@testing-library/react" import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" import { MidiDevice, MidiMapping } from "../../../services/midi/midi-engine" import { MidiSetup } from "../midi-setup" import { resetSelectStates } from "./test-utils/mocks" // Mock i18n vi.mock("react-i18next", () => ({ useTranslation: () => ({ t: (key: string) => key, }), })) // Mock Radix UI Tabs with our custom mock vi.mock("@/components/ui/tabs", async () => { const tabsMock = await import("./test-utils/tabs-mock") return { Tabs: tabsMock.Tabs, TabsContent: tabsMock.TabsContent, TabsList: tabsMock.TabsList, TabsTrigger: tabsMock.TabsTrigger, } }) // Mock other UI components vi.mock("@/components/ui/button", () => ({ Button: ({ children, onClick, disabled, variant, size, className }: any) => ( ), })) vi.mock("@/components/ui/card", () => ({ Card: ({ children, className }: any) =>
{children}
, CardContent: ({ children, className }: any) =>
{children}
, CardHeader: ({ children, className }: any) =>
{children}
, CardTitle: ({ children, className }: any) =>

{children}

, })) vi.mock("@/components/ui/label", () => ({ Label: ({ children, htmlFor, className }: any) => ( ), })) vi.mock("@/components/ui/select", () => ({ Select: ({ children }: any) =>
{children}
, SelectContent: ({ children }: any) =>
{children}
, SelectItem: ({ children }: any) =>
{children}
, SelectTrigger: ({ children }: any) =>
{children}
, SelectValue: ({ placeholder }: any) => {placeholder}, })) // Mock useModal hook const mockOpenModal = vi.fn() vi.mock("@/features/modals/services", () => ({ useModal: () => ({ openModal: mockOpenModal }), })) vi.mock("../midi-mapping-editor", () => ({ MidiMappingEditor: ({ mapping, onSave }: any) => ( ), })) vi.mock("../midi-router-view", () => ({ MidiRouterView: () =>
MidiRouterView
, })) // Mock MIDI hook const mockDevices: MidiDevice[] = [ { id: "input1", name: "MIDI Input 1", type: "input", manufacturer: "Test Manufacturer", state: "connected" }, { id: "input2", name: "MIDI Input 2", type: "input", manufacturer: "Test Manufacturer", state: "connected" }, { id: "output1", name: "MIDI Output 1", type: "output", manufacturer: "Test Manufacturer", state: "connected" }, { id: "output2", name: "MIDI Output 2", type: "output", manufacturer: "Test Manufacturer", state: "disconnected" }, ] const mockMappings: MidiMapping[] = [ { id: "mapping1", deviceId: "input1", messageType: "cc", channel: 1, controller: 7, targetParameter: "channel.1.volume", min: 0, max: 1, curve: "linear", }, { id: "mapping2", deviceId: "input2", messageType: "noteon", channel: 2, targetParameter: "channel.2.mute", min: 0, max: 1, curve: "linear", }, ] const mockUseMidi = { devices: mockDevices, inputDevices: mockDevices.filter((d) => d.type === "input"), outputDevices: mockDevices.filter((d) => d.type === "output"), mappings: mockMappings, isInitialized: true, error: null, addMapping: vi.fn(), removeMapping: vi.fn(), updateMapping: vi.fn(), } vi.mock("../../../hooks/use-midi", () => ({ useMidi: () => mockUseMidi, })) describe("MidiSetup", () => { beforeEach(() => { vi.clearAllMocks() mockOpenModal.mockClear() resetSelectStates() mockUseMidi.isInitialized = true mockUseMidi.error = null mockUseMidi.mappings = [...mockMappings] mockUseMidi.devices = [...mockDevices] mockUseMidi.inputDevices = mockDevices.filter((d) => d.type === "input") mockUseMidi.outputDevices = mockDevices.filter((d) => d.type === "output") }) afterEach(() => { vi.restoreAllMocks() resetSelectStates() }) describe("Rendering", () => { it("should render tabs", () => { render() expect(screen.getByText("fairlightAudio.midi.setup.tabs.devices")).toBeInTheDocument() expect(screen.getByText("fairlightAudio.midi.setup.tabs.mappings")).toBeInTheDocument() expect(screen.getByText("fairlightAudio.midi.setup.tabs.router")).toBeInTheDocument() }) it("should show loading state", () => { mockUseMidi.isInitialized = false render() expect(screen.getByTestId("loader2-icon")).toBeInTheDocument() expect(screen.getByText("fairlightAudio.midi.setup.initializing")).toBeInTheDocument() }) it("should show error state", () => { mockUseMidi.error = "Failed to initialize MIDI" as any // Mock error for test render() expect(screen.getByTestId("info-icon")).toBeInTheDocument() expect(screen.getByText("fairlightAudio.midi.setup.error")).toBeInTheDocument() expect(screen.getByText("Failed to initialize MIDI")).toBeInTheDocument() }) }) describe("Devices Tab", () => { it("should display input devices", () => { render() expect(screen.getByText("fairlightAudio.midi.setup.devices.inputDevices")).toBeInTheDocument() // Multiple elements with same text exist (device list and select options) const input1Elements = screen.getAllByText("MIDI Input 1") const input2Elements = screen.getAllByText("MIDI Input 2") expect(input1Elements.length).toBeGreaterThan(0) expect(input2Elements.length).toBeGreaterThan(0) }) it("should display output devices", () => { render() expect(screen.getByText("fairlightAudio.midi.setup.devices.outputDevices")).toBeInTheDocument() // Multiple elements with same text exist (device list and select options) const output1Elements = screen.getAllByText("MIDI Output 1") const output2Elements = screen.getAllByText("MIDI Output 2") expect(output1Elements.length).toBeGreaterThan(0) expect(output2Elements.length).toBeGreaterThan(0) }) it("should show no devices message", () => { mockUseMidi.inputDevices = [] mockUseMidi.outputDevices = [] render() expect(screen.getByText("fairlightAudio.midi.setup.devices.noInputDevices")).toBeInTheDocument() expect(screen.getByText("fairlightAudio.midi.setup.devices.noOutputDevices")).toBeInTheDocument() }) it("should display device manufacturer", () => { render() // Verify devices are displayed with manufacturer info const inputDevices = screen.getAllByText("MIDI Input 1") expect(inputDevices.length).toBeGreaterThan(0) // Find device item that shows manufacturer const deviceItem = inputDevices[0].closest(".flex.items-center.justify-between") expect(deviceItem?.textContent).toContain("Test Manufacturer") }) it("should show device connection status", () => { const { container } = render() // Find all device items const deviceItems = container.querySelectorAll(".flex.items-center.justify-between.p-2") expect(deviceItems.length).toBeGreaterThan(0) // Check for connection indicators const indicators = container.querySelectorAll(".rounded-full") expect(indicators.length).toBeGreaterThan(0) // We should have both connected (green) and disconnected (red) devices const greenIndicators = Array.from(indicators).filter((ind) => ind.className.includes("bg-green-500")) const redIndicators = Array.from(indicators).filter((ind) => ind.className.includes("bg-red-500")) expect(greenIndicators.length).toBeGreaterThan(0) // 3 connected devices expect(redIndicators.length).toBeGreaterThan(0) // 1 disconnected device }) it("should render default device selectors", () => { render() expect(screen.getByText("fairlightAudio.midi.setup.devices.defaultDevices")).toBeInTheDocument() expect(screen.getByText("fairlightAudio.midi.setup.devices.defaultInput")).toBeInTheDocument() expect(screen.getByText("fairlightAudio.midi.setup.devices.defaultOutput")).toBeInTheDocument() }) }) describe("Mappings Tab", () => { it("should display mappings", async () => { render() // Click on mappings tab const mappingsTab = screen.getByRole("tab", { name: "fairlightAudio.midi.setup.tabs.mappings" }) fireEvent.click(mappingsTab) // Debug: check what's rendered after click await waitFor(() => { // The tab should become selected expect(mappingsTab).toHaveAttribute("aria-selected", "true") }) // Debug: print current DOM // screen.debug() // Now check for content - wait for it to appear await waitFor(() => { expect(screen.getByText("channel.1.volume")).toBeInTheDocument() }) expect(screen.getByText("channel.2.mute")).toBeInTheDocument() }) it("should show mapping details", async () => { render() // Click on mappings tab const mappingsTab = screen.getByRole("tab", { name: "fairlightAudio.midi.setup.tabs.mappings" }) fireEvent.click(mappingsTab) // Wait for mappings title to appear await waitFor(() => { expect(screen.getByText("fairlightAudio.midi.setup.mappings.title")).toBeInTheDocument() }) // Check for parameter names expect(screen.getByText("channel.1.volume")).toBeInTheDocument() // Check for device and message details text which includes bullet separator const detailText1 = screen.getByText((content, element) => { return ( element?.tagName === "P" && element?.className?.includes("text-xs") && content.includes("MIDI Input 1") && content.includes("•") && content.includes("CC") && content.includes("CC7") && content.includes("CH1") ) }) expect(detailText1).toBeInTheDocument() }) it("should show no mappings message", async () => { mockUseMidi.mappings = [] render() // Click on mappings tab const mappingsTab = screen.getByRole("tab", { name: "fairlightAudio.midi.setup.tabs.mappings" }) fireEvent.click(mappingsTab) // Wait for mappings title to appear await waitFor(() => { expect(screen.getByText("fairlightAudio.midi.setup.mappings.title")).toBeInTheDocument() }) // Now check for empty state content expect(screen.getByTestId("music-icon")).toBeInTheDocument() expect(screen.getByText("fairlightAudio.midi.setup.mappings.noMappings")).toBeInTheDocument() expect(screen.getByText("fairlightAudio.midi.setup.mappings.addFirstMapping")).toBeInTheDocument() }) it("should render add mapping button", async () => { render() // Click on mappings tab const mappingsTab = screen.getByRole("tab", { name: "fairlightAudio.midi.setup.tabs.mappings" }) fireEvent.click(mappingsTab) // Wait for mappings title to appear await waitFor(() => { expect(screen.getByText("fairlightAudio.midi.setup.mappings.title")).toBeInTheDocument() }) // Check for add mapping button expect(screen.getByText("fairlightAudio.midi.setup.mappings.addMapping")).toBeInTheDocument() }) it("should handle adding mapping", async () => { // Setup mock to immediately call onComplete mockOpenModal.mockImplementation((modalId, props) => { if (modalId === "midi-learn" && props.onComplete) { // Simulate completing MIDI learn props.onComplete( { id: "device1", name: "MIDI Device 1", type: "input", manufacturer: "Test", state: "connected" }, { type: "cc", channel: 1, data: { controller: 7 } }, "test.param", ) } }) render() // Click on mappings tab const mappingsTab = screen.getByRole("tab", { name: "fairlightAudio.midi.setup.tabs.mappings" }) fireEvent.click(mappingsTab) // Wait for mappings title to appear await waitFor(() => { expect(screen.getByText("fairlightAudio.midi.setup.mappings.title")).toBeInTheDocument() }) const addButton = screen.getByText("fairlightAudio.midi.setup.mappings.addMapping") fireEvent.click(addButton) expect(mockOpenModal).toHaveBeenCalledWith("midi-learn", expect.any(Object)) expect(mockUseMidi.addMapping).toHaveBeenCalledWith({ deviceId: "device1", messageType: "cc", channel: 1, controller: 7, targetParameter: "test.param", min: 0, max: 1, curve: "linear", }) }) it("should handle updating mapping", async () => { render() // Click on mappings tab const mappingsTab = screen.getByRole("tab", { name: "fairlightAudio.midi.setup.tabs.mappings" }) fireEvent.click(mappingsTab) // Wait for mappings title to appear await waitFor(() => { expect(screen.getByText("fairlightAudio.midi.setup.mappings.title")).toBeInTheDocument() }) const editButton = screen.getByText("Edit channel.1.volume") fireEvent.click(editButton) expect(mockUseMidi.updateMapping).toHaveBeenCalledWith("mapping1", { min: 0.5, max: 1 }) }) it("should handle removing mapping", async () => { render() // Click on mappings tab const mappingsTab = screen.getByRole("tab", { name: "fairlightAudio.midi.setup.tabs.mappings" }) fireEvent.click(mappingsTab) // Wait for mappings title to appear await waitFor(() => { expect(screen.getByText("fairlightAudio.midi.setup.mappings.title")).toBeInTheDocument() }) const deleteButtons = screen.getAllByTestId("trash2-icon") expect(deleteButtons).toHaveLength(2) fireEvent.click(deleteButtons[0].parentElement!) expect(mockUseMidi.removeMapping).toHaveBeenCalledWith("mapping1") }) it("should handle unknown device in mapping", async () => { mockUseMidi.mappings = [ { ...mockMappings[0], deviceId: "unknown-device", }, ] render() // Click on mappings tab const mappingsTab = screen.getByRole("tab", { name: "fairlightAudio.midi.setup.tabs.mappings" }) fireEvent.click(mappingsTab) // Wait for mappings title to appear await waitFor(() => { expect(screen.getByText("fairlightAudio.midi.setup.mappings.title")).toBeInTheDocument() }) // Check for unknown device text in mapping details const detailText = screen.getByText((content, element) => { return ( element?.tagName === "P" && element?.className?.includes("text-xs") && content.includes("fairlightAudio.midi.setup.devices.unknownDevice") ) }) expect(detailText).toBeInTheDocument() }) }) describe("Router Tab", () => { it("should render router view", async () => { render() // Click on router tab const routerTab = screen.getByRole("tab", { name: "fairlightAudio.midi.setup.tabs.router" }) fireEvent.click(routerTab) // Wait for router view to appear await waitFor(() => { expect(screen.getByText("MidiRouterView")).toBeInTheDocument() }) }) }) describe("Tab Content", () => { it("should show devices tab by default", () => { render() // Devices tab content should be visible expect(screen.getByText("fairlightAudio.midi.setup.devices.inputDevices")).toBeInTheDocument() // Check that devices tab is active const devicesTab = screen.getByRole("tab", { name: "fairlightAudio.midi.setup.tabs.devices" }) expect(devicesTab).toHaveAttribute("aria-selected", "true") // Other tabs content should not be rendered with our mock expect(screen.queryByText("fairlightAudio.midi.setup.mappings.title")).not.toBeInTheDocument() }) it("should switch between tabs", async () => { render() // All content is rendered, just check tab selection state const devicesTab = screen.getByRole("tab", { name: "fairlightAudio.midi.setup.tabs.devices" }) const mappingsTab = screen.getByRole("tab", { name: "fairlightAudio.midi.setup.tabs.mappings" }) const routerTab = screen.getByRole("tab", { name: "fairlightAudio.midi.setup.tabs.router" }) // Initially devices tab is active expect(devicesTab).toHaveAttribute("aria-selected", "true") expect(mappingsTab).toHaveAttribute("aria-selected", "false") expect(routerTab).toHaveAttribute("aria-selected", "false") // Click mappings tab fireEvent.click(mappingsTab) await waitFor(() => { expect(mappingsTab).toHaveAttribute("aria-selected", "true") }) expect(devicesTab).toHaveAttribute("aria-selected", "false") // Click router tab fireEvent.click(routerTab) await waitFor(() => { expect(routerTab).toHaveAttribute("aria-selected", "true") }) expect(mappingsTab).toHaveAttribute("aria-selected", "false") }) }) })