import { fireEvent, render, screen } from "@testing-library/react" import userEvent from "@testing-library/user-event" import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" import { MediaToolbar, type MediaToolbarProps, type ViewMode } from "../../components/media-toolbar" // Mock react-i18next vi.mock("react-i18next", () => ({ useTranslation: () => ({ t: (key: string) => { const translations: Record = { "common.search": "Search", "common.import": "Import", "common.importing": "Importing...", "browser.media.addMedia": "Add Media", "browser.media.addFolder": "Add Folder", "browser.media.favorites": "Favorites", "browser.toolbar.list": "List", "browser.toolbar.thumbnails": "Thumbnails", "browser.toolbar.sort": "Sort", "browser.toolbar.filter": "Filter", "browser.toolbar.group": "Group", "browser.toolbar.zoomOut": "Zoom Out", "browser.toolbar.zoomIn": "Zoom In", "browser.toolbar.sortOrder.asc": "Ascending", "browser.toolbar.sortOrder.desc": "Descending", "browser.toolbar.filterBy.all": "All Files", "sort.name": "Name", "sort.date": "Date", "sort.size": "Size", "group.none": "None", "group.type": "Type", "filter.video": "Video", "filter.audio": "Audio", } return translations[key] || key }, }), })) // Mock UI components vi.mock("@/components/ui/button", () => ({ Button: ({ children, className, onClick, disabled, ...props }: any) => ( ), })) vi.mock("@/components/ui/input", () => ({ Input: ({ value, onChange, className, placeholder, ...props }: any) => ( ), })) vi.mock("@/components/ui/dropdown-menu", () => ({ DropdownMenu: ({ children }: any) =>
{children}
, DropdownMenuTrigger: ({ children, asChild }: any) => (asChild ? children :
{children}
), DropdownMenuContent: ({ children, align }: any) => (
{children}
), DropdownMenuItem: ({ children, onClick }: any) => (
{children}
), DropdownMenuSeparator: () =>
, })) vi.mock("@/components/ui/tooltip", () => ({ Tooltip: ({ children }: any) =>
{children}
, TooltipProvider: ({ children }: any) =>
{children}
, TooltipTrigger: ({ children, asChild }: any) => (asChild ? children :
{children}
), TooltipContent: ({ children }: any) =>
{children}
, })) // Mock lucide-react icons vi.mock("lucide-react", () => ({ ArrowDownUp: ({ size }: any) =>
, ArrowUpDown: ({ size }: any) =>
, Check: ({ className }: any) =>
, File: ({ size, className }: any) =>
, Filter: ({ size }: any) =>
, Folder: ({ size, className }: any) =>
, Grid2x2: ({ size }: any) =>
, List: ({ size }: any) =>
, ListFilterPlus: ({ size }: any) =>
, SortDesc: ({ size }: any) =>
, Star: ({ size, className }: any) =>
, ZoomIn: ({ size }: any) =>
, ZoomOut: ({ size }: any) =>
, })) // Mock utils vi.mock("@/lib/utils", () => ({ cn: (...args: any[]) => args.filter(Boolean).join(" "), })) describe("MediaToolbar", () => { const mockProps: MediaToolbarProps = { // State searchQuery: "", sortBy: "name", sortOrder: "asc", filterType: "all", viewMode: "list" as ViewMode, groupBy: "none", availableExtensions: ["mp4", "mp3", "jpg"], showFavoritesOnly: false, // Options sortOptions: [ { value: "name", label: "sort.name" }, { value: "date", label: "sort.date" }, { value: "size", label: "sort.size" }, ], groupOptions: [ { value: "none", label: "group.none" }, { value: "type", label: "group.type" }, ], // Callbacks onSearch: vi.fn(), onSort: vi.fn(), onFilter: vi.fn(), onChangeOrder: vi.fn(), onChangeViewMode: vi.fn(), onChangeGroupBy: vi.fn(), onToggleFavorites: vi.fn(), } beforeEach(() => { vi.clearAllMocks() }) afterEach(() => { vi.resetAllMocks() }) describe("Basic rendering", () => { it("should render without crashing", () => { render() expect(screen.getByPlaceholderText("Search")).toBeInTheDocument() }) it("should render search input with placeholder", () => { render() const searchInput = screen.getByPlaceholderText("Search") expect(searchInput).toBeInTheDocument() expect(searchInput).toHaveValue("") }) it("should render with custom className", () => { const { container } = render() const toolbar = container.firstChild as HTMLElement expect(toolbar).toHaveClass("custom-toolbar") }) it("should render default view mode buttons", () => { render() expect(screen.getByTestId("list-view-button")).toBeInTheDocument() expect(screen.getByTestId("thumbnails-view-button")).toBeInTheDocument() }) }) describe("Search functionality", () => { it("should call onSearch when typing in search input", async () => { const user = userEvent.setup() render() const searchInput = screen.getByPlaceholderText("Search") await user.clear(searchInput) await user.type(searchInput, "test") // user.type() calls onChange for each character individually expect(mockProps.onSearch).toHaveBeenCalledTimes(4) // The last call should be with the complete value expect(mockProps.onSearch).toHaveBeenLastCalledWith("t") }) it("should display current search query", () => { render() const searchInput = screen.getByDisplayValue("current search") expect(searchInput).toBeInTheDocument() }) it("should handle search input change", () => { render() const searchInput = screen.getByPlaceholderText("Search") fireEvent.change(searchInput, { target: { value: "new search" } }) expect(mockProps.onSearch).toHaveBeenCalledWith("new search") }) }) describe("Import functionality", () => { it("should render import buttons when showImport is true", () => { render() expect(screen.getByTestId("add-media-button")).toBeInTheDocument() expect(screen.getByTestId("add-folder-button")).toBeInTheDocument() }) it("should not render import buttons when showImport is false", () => { render() expect(screen.queryByTestId("add-media-button")).not.toBeInTheDocument() expect(screen.queryByTestId("add-folder-button")).not.toBeInTheDocument() }) it("should call onImportFile when file import button is clicked", async () => { const onImportFile = vi.fn() const user = userEvent.setup() render() const fileButton = screen.getByTestId("add-media-button") await user.click(fileButton) expect(onImportFile).toHaveBeenCalled() }) it("should call onImportFolder when folder import button is clicked", async () => { const onImportFolder = vi.fn() const user = userEvent.setup() render() const folderButton = screen.getByTestId("add-folder-button") await user.click(folderButton) expect(onImportFolder).toHaveBeenCalled() }) it("should show importing state", () => { render() expect(screen.getByText("Importing...")).toBeInTheDocument() // Check for pulse animation on icon const fileIcon = screen.getByTestId("file-icon") expect(fileIcon).toHaveClass("animate-pulse") }) it("should disable import buttons when importing", () => { render() const importButton = screen.getByText("Importing...").closest("button") expect(importButton).toBeDisabled() }) it("should prevent event propagation on icon clicks", async () => { const onImportFile = vi.fn() const user = userEvent.setup() render() const fileIcon = screen.getByTestId("add-media-button") await user.click(fileIcon) expect(onImportFile).toHaveBeenCalled() }) }) describe("Favorites functionality", () => { it("should render favorites button", () => { render() const favoritesButton = screen.getByTestId("star-icon") expect(favoritesButton).toBeInTheDocument() }) it("should call onToggleFavorites when favorites button is clicked", async () => { const user = userEvent.setup() render() const favoritesButton = screen.getByTestId("star-icon").closest("button")! await user.click(favoritesButton) expect(mockProps.onToggleFavorites).toHaveBeenCalled() }) it("should show active state when showFavoritesOnly is true", () => { render() const favoritesButton = screen.getByTestId("star-icon").closest("button")! expect(favoritesButton).toHaveClass("bg-[#dddbdd]") const starIcon = screen.getByTestId("star-icon") expect(starIcon).toHaveClass("fill-current") }) it("should show inactive state when showFavoritesOnly is false", () => { render() const starIcon = screen.getByTestId("star-icon") expect(starIcon).not.toHaveClass("fill-current") }) }) describe("View mode functionality", () => { it("should render view mode buttons when multiple modes available", () => { render() expect(screen.getByTestId("list-view-button")).toBeInTheDocument() expect(screen.getByTestId("thumbnails-view-button")).toBeInTheDocument() }) it("should not render view mode buttons when only one mode available", () => { const singleViewMode = [{ value: "list" as ViewMode, icon: () =>
, label: "browser.toolbar.list" }] render() expect(screen.queryByTestId("list-view-button")).not.toBeInTheDocument() }) it("should call onChangeViewMode when view mode button is clicked", async () => { const user = userEvent.setup() render() const thumbnailsButton = screen.getByTestId("thumbnails-view-button") await user.click(thumbnailsButton) expect(mockProps.onChangeViewMode).toHaveBeenCalledWith("thumbnails") }) it("should show active state for current view mode", () => { render() const thumbnailsButton = screen.getByTestId("thumbnails-view-button") expect(thumbnailsButton).toHaveClass("bg-[#dddbdd]") }) it("should use custom view modes when provided", () => { const customViewModes = [ { value: "grid" as ViewMode, icon: () =>
, label: "Custom Grid", testId: "custom-grid-button", }, { value: "list" as ViewMode, icon: () =>
, label: "Custom List", testId: "custom-list-button", }, ] render() expect(screen.getByTestId("custom-grid-button")).toBeInTheDocument() expect(screen.getByTestId("custom-list-button")).toBeInTheDocument() expect(screen.queryByTestId("list-view-button")).not.toBeInTheDocument() }) }) describe("Sort functionality", () => { it("should render sort dropdown", () => { render() expect(screen.getByTestId("sort-desc")).toBeInTheDocument() }) it("should show active state when sort is not default", () => { render() const sortButton = screen.getByTestId("sort-desc").closest("button")! expect(sortButton).toHaveClass("bg-[#dddbdd]") }) it("should show inactive state when sort is default", () => { render() const sortButton = screen.getByTestId("sort-desc").closest("button")! expect(sortButton).not.toHaveClass("bg-[#dddbdd]") }) it("should render sort options in dropdown", () => { render() const sortItems = screen.getAllByTestId("dropdown-item") expect(sortItems.length).toBeGreaterThan(0) }) it("should call onSort when sort option is clicked", async () => { const user = userEvent.setup() render() const sortItems = screen.getAllByTestId("dropdown-item") await user.click(sortItems[1]) // Click second sort option expect(mockProps.onSort).toHaveBeenCalled() }) }) describe("Filter functionality", () => { it("should render filter dropdown", () => { render() expect(screen.getByTestId("filter-icon")).toBeInTheDocument() }) it("should show active state when filter is not 'all'", () => { render() const filterButton = screen.getByTestId("filter-icon").closest("button")! expect(filterButton).toHaveClass("bg-[#dddbdd]") }) it("should show inactive state when filter is 'all'", () => { render() const filterButton = screen.getByTestId("filter-icon").closest("button")! expect(filterButton).not.toHaveClass("bg-[#dddbdd]") }) it("should render default extension filters when no custom filters provided", () => { render() // Should render extensions from availableExtensions const filterItems = screen.getAllByTestId("dropdown-item") expect(filterItems.length).toBeGreaterThan(mockProps.availableExtensions.length) }) it("should render custom filter options when provided", () => { const filterOptions = [ { value: "video", label: "filter.video" }, { value: "audio", label: "filter.audio" }, ] render() const filterItems = screen.getAllByTestId("dropdown-item") expect(filterItems.length).toBeGreaterThan(0) }) it("should call onFilter when filter option is clicked", async () => { const user = userEvent.setup() render() // Find filter dropdown items specifically const filterDropdown = screen.getAllByTestId("dropdown-menu")[1] // Second dropdown is filter const filterItems = filterDropdown.querySelectorAll('[data-testid="dropdown-item"]') await user.click(filterItems[0]) // Click first filter option expect(mockProps.onFilter).toHaveBeenCalled() }) }) describe("Group functionality", () => { it("should render group dropdown when showGroupBy is true", () => { render() expect(screen.getByTestId("list-filter-plus")).toBeInTheDocument() }) it("should not render group dropdown when showGroupBy is false", () => { render() expect(screen.queryByTestId("list-filter-plus")).not.toBeInTheDocument() }) it("should show active state when group is not default", () => { render() const groupButton = screen.getByTestId("list-filter-plus").closest("button")! expect(groupButton).toHaveClass("bg-[#dddbdd]") }) it("should call onChangeGroupBy when group option is clicked", async () => { const user = userEvent.setup() render() // Find group dropdown items specifically const groupDropdown = screen.getAllByTestId("dropdown-menu")[2] // Third dropdown is group const groupItems = groupDropdown.querySelectorAll('[data-testid="dropdown-item"]') await user.click(groupItems[0]) expect(mockProps.onChangeGroupBy).toHaveBeenCalled() }) }) describe("Zoom functionality", () => { it("should render zoom buttons when showZoom is true", () => { render() expect(screen.getByTestId("zoom-out")).toBeInTheDocument() expect(screen.getByTestId("zoom-in")).toBeInTheDocument() }) it("should not render zoom buttons when showZoom is false", () => { render() expect(screen.queryByTestId("zoom-out")).not.toBeInTheDocument() expect(screen.queryByTestId("zoom-in")).not.toBeInTheDocument() }) it("should call onZoomIn when zoom in button is clicked", async () => { const onZoomIn = vi.fn() const user = userEvent.setup() render() const zoomInButton = screen.getByTestId("zoom-in").closest("button")! await user.click(zoomInButton) expect(onZoomIn).toHaveBeenCalled() }) it("should call onZoomOut when zoom out button is clicked", async () => { const onZoomOut = vi.fn() const user = userEvent.setup() render() const zoomOutButton = screen.getByTestId("zoom-out").closest("button")! await user.click(zoomOutButton) expect(onZoomOut).toHaveBeenCalled() }) it("should disable zoom buttons when cannot zoom", () => { render( , ) const zoomInButton = screen.getByTestId("zoom-in").closest("button")! const zoomOutButton = screen.getByTestId("zoom-out").closest("button")! expect(zoomInButton).toBeDisabled() expect(zoomOutButton).toBeDisabled() expect(zoomInButton).toHaveClass("cursor-not-allowed") expect(zoomOutButton).toHaveClass("cursor-not-allowed") }) }) describe("Sort order functionality", () => { it("should render sort order button", () => { render() expect(screen.getByTestId("arrow-down-up")).toBeInTheDocument() }) it("should show descending icon when sort order is asc", () => { render() expect(screen.getByTestId("arrow-down-up")).toBeInTheDocument() expect(screen.queryByTestId("arrow-up-down")).not.toBeInTheDocument() }) it("should show ascending icon when sort order is desc", () => { render() expect(screen.getByTestId("arrow-up-down")).toBeInTheDocument() expect(screen.queryByTestId("arrow-down-up")).not.toBeInTheDocument() }) it("should call onChangeOrder when sort order button is clicked", async () => { const user = userEvent.setup() render() const sortOrderButton = screen.getByTestId("arrow-down-up").closest("button")! await user.click(sortOrderButton) expect(mockProps.onChangeOrder).toHaveBeenCalled() }) }) describe("Extra buttons", () => { it("should render extra buttons when provided", () => { const extraButtons = render() expect(screen.getByTestId("extra-button")).toBeInTheDocument() }) it("should not render extra buttons when not provided", () => { render() expect(screen.queryByTestId("extra-button")).not.toBeInTheDocument() }) }) describe("Edge cases and error handling", () => { it("should handle empty sort options", () => { render() expect(screen.getByTestId("sort-desc")).toBeInTheDocument() }) it("should handle empty group options", () => { render() expect(screen.getByTestId("list-filter-plus")).toBeInTheDocument() }) it("should handle empty available extensions", () => { render() expect(screen.getByTestId("filter-icon")).toBeInTheDocument() }) it("should handle missing onImportFile callback", () => { render() expect(screen.queryByTestId("add-media-button")).not.toBeInTheDocument() }) it("should handle missing onImportFolder callback", () => { render() expect(screen.queryByTestId("add-folder-button")).not.toBeInTheDocument() }) it("should handle missing zoom callbacks", () => { render() expect(screen.queryByTestId("zoom-in")).not.toBeInTheDocument() expect(screen.queryByTestId("zoom-out")).not.toBeInTheDocument() }) }) describe("Keyboard interactions", () => { it("should handle Enter key on search input", async () => { const user = userEvent.setup() render() const searchInput = screen.getByPlaceholderText("Search") await user.clear(searchInput) await user.type(searchInput, "test{enter}") // user.type() calls onChange for each character individually expect(mockProps.onSearch).toHaveBeenCalledTimes(4) expect(mockProps.onSearch).toHaveBeenLastCalledWith("t") }) it("should handle keyboard navigation on buttons", async () => { const user = userEvent.setup() render() const searchInput = screen.getByPlaceholderText("Search") const favoritesButton = screen.getByTestId("star-icon").closest("button")! await user.tab() // Should be able to focus buttons expect(document.activeElement).toBe(searchInput || favoritesButton) }) }) describe("Accessibility", () => { it("should have proper aria labels and roles", () => { render() const searchInput = screen.getByPlaceholderText("Search") expect(searchInput).toHaveAttribute("type", "search") }) it("should have tooltip content for all buttons", () => { render() const tooltipContents = screen.getAllByTestId("tooltip-content") expect(tooltipContents.length).toBeGreaterThan(0) }) }) })