diff --git a/ui/src/components/shared/DeleteConfirmModal.test.tsx b/ui/src/components/shared/DeleteConfirmModal.test.tsx new file mode 100644 index 0000000..aa466ea --- /dev/null +++ b/ui/src/components/shared/DeleteConfirmModal.test.tsx @@ -0,0 +1,109 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { + createMockGalleryContext, + type MockGalleryContext, +} from "../../test/helpers/renderWithGallery"; +import { DeleteConfirmModal } from "./DeleteConfirmModal"; + +// Mock useGallery +vi.mock("../../store/GalleryContext", () => ({ + useGallery: vi.fn(), +})); + +// Mock useEscapeToClose +vi.mock("../../hooks/useEscapeToClose", () => ({ + useEscapeToClose: vi.fn(), +})); + +// Mock useLightTableStore +vi.mock("../../features/light-table/store", () => ({ + useLightTableStore: Object.assign( + vi.fn(() => ({})), + { + getState: vi.fn(() => ({ buckets: {} })), + }, + ), +})); + +// Mock delete helpers +vi.mock("../../features/images/hooks/deleteHelpers", () => ({ + deleteImagesAndSyncLightTable: vi.fn().mockResolvedValue(undefined), +})); + +// Mock images API +vi.mock("../../features/images/api/imagesApi", () => ({ + fetchLineage: vi.fn().mockResolvedValue([]), +})); + +import { useGallery } from "../../store/GalleryContext"; + +describe("DeleteConfirmModal", () => { + let ctx: MockGalleryContext; + + beforeEach(() => { + ctx = createMockGalleryContext({ + activeModal: { type: "delete_confirm", imageIds: [1, 2], hasLineage: false }, + }); + vi.mocked(useGallery).mockReturnValue(ctx); + }); + + it("renders 'Move to Trash' header by default", () => { + render(); + const heading = document.querySelector(".meld-modal-header h2"); + expect(heading?.textContent).toContain("Move to Trash"); + }); + + it("renders 'Permanent Deletion' header when isPermanent is true", () => { + render(); + const heading = document.querySelector(".meld-modal-header h2"); + expect(heading?.textContent).toContain("Permanent Deletion"); + }); + + it("displays the number of selected items", () => { + render(); + expect(screen.getByText("3")).toBeInTheDocument(); + }); + + it("dispatches CLOSE_MODAL when Cancel button is clicked", async () => { + render(); + await userEvent.click(screen.getByText("Cancel")); + expect(ctx.dispatch).toHaveBeenCalledWith({ type: "CLOSE_MODAL" }); + }); + + it("shows lineage buttons when hasLineage is true", () => { + render(); + expect(screen.getByText("Move All Related")).toBeInTheDocument(); + }); + + it("does not show lineage buttons when hasLineage is false", () => { + render(); + expect(screen.queryByText("Move All Related")).not.toBeInTheDocument(); + }); + + it("shows permanent delete labels when isPermanent is true with lineage", () => { + render(); + expect(screen.getByText("Delete Permanently")).toBeInTheDocument(); + expect(screen.getByText("Delete All Related")).toBeInTheDocument(); + }); + + it("shows warning message for permanent deletion", () => { + render(); + expect(screen.getByText(/permanently deleted/)).toBeInTheDocument(); + }); + + it("shows info message for trash mode", () => { + render(); + expect(screen.getByText(/moved to the trash bin/)).toBeInTheDocument(); + }); + + it("dispatches CLOSE_MODAL when close (X) button is clicked", async () => { + render(); + // Close button is the one with the X icon + const closeButton = document.querySelector(".meld-modal-close"); + expect(closeButton).toBeTruthy(); + await userEvent.click(closeButton as HTMLElement); + expect(ctx.dispatch).toHaveBeenCalledWith({ type: "CLOSE_MODAL" }); + }); +}); diff --git a/ui/src/components/shared/GalleryModals.test.tsx b/ui/src/components/shared/GalleryModals.test.tsx new file mode 100644 index 0000000..648e7ef --- /dev/null +++ b/ui/src/components/shared/GalleryModals.test.tsx @@ -0,0 +1,157 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { + createMockGalleryContext, + type MockGalleryContext, +} from "../../test/helpers/renderWithGallery"; +import { GalleryModals } from "./GalleryModals"; + +// Mock useGallery +vi.mock("../../store/GalleryContext", () => ({ + useGallery: vi.fn(), +})); + +// Mock useWorkflowExecution +vi.mock("../../features/workflows/hooks/useWorkflowExecution", () => ({ + useWorkflowExecution: vi.fn(() => ({ + executeWorkflow: vi.fn(), + })), +})); + +// Mock child modals to keep tests focused on GalleryModals logic +vi.mock("../../features/importer/components/ImportModal", () => ({ + ImportModal: () =>
ImportModal
, +})); +vi.mock("../../features/light-table/components/DownloadModal", () => ({ + DownloadModal: () =>
DownloadModal
, +})); +vi.mock("../../features/mask-editor/components/MaskEditorModal", () => ({ + MaskEditorModal: () =>
MaskEditorModal
, +})); +vi.mock("../../features/mask-editor/components/MaskSequenceModal", () => ({ + MaskSequenceModal: () =>
MaskSequenceModal
, +})); +vi.mock("../../features/settings/components/SettingsModal", () => ({ + SettingsModal: () =>
SettingsModal
, +})); +vi.mock("../../features/tags/components/TagEditModal", () => ({ + TagEditModal: () =>
TagEditModal
, +})); +vi.mock("../../features/viewer/components/NoteEditModal", () => ({ + NoteEditModal: () =>
NoteEditModal
, +})); +vi.mock("../../features/workflows/components/NodeSelectionModal", () => ({ + NodeSelectionModal: () =>
NodeSelectionModal
, +})); +vi.mock("../../features/workflows/components/WorkflowSelectionModal", () => ({ + WorkflowSelectionModal: () => ( +
WorkflowSelectionModal
+ ), +})); +vi.mock("../../features/workflows/utils/injectImageToGraph", () => ({ + injectImageToGraph: vi.fn(), +})); +vi.mock("./ConfirmModal", () => ({ + ConfirmModal: ({ message }: { message: string }) => ( +
{message}
+ ), +})); +vi.mock("./DeleteConfirmModal", () => ({ + DeleteConfirmModal: () =>
DeleteConfirmModal
, +})); +vi.mock("./ErrorModal", () => ({ + ErrorModal: ({ message }: { message: string }) =>
{message}
, +})); +vi.mock("./ParentSelectionModal", () => ({ + ParentSelectionModal: () =>
ParentSelectionModal
, +})); +vi.mock("./Toast", () => ({ + Toast: () =>
Toast
, +})); + +import { useGallery } from "../../store/GalleryContext"; + +describe("GalleryModals", () => { + let ctx: MockGalleryContext; + + it("returns null when viewer is open (viewerImageId is not null)", () => { + ctx = createMockGalleryContext({ viewerImageId: 42 }); + vi.mocked(useGallery).mockReturnValue(ctx); + + const { container } = render(); + expect(container.innerHTML).toBe(""); + }); + + it("renders nothing extra when activeModal is none and no toast", () => { + ctx = createMockGalleryContext({ + viewerImageId: null, + activeModal: { type: "none" }, + confirmModal: null, + toastMessage: null, + }); + vi.mocked(useGallery).mockReturnValue(ctx); + + render(); + expect(screen.queryByTestId("confirm-modal")).not.toBeInTheDocument(); + expect(screen.queryByTestId("toast")).not.toBeInTheDocument(); + }); + + it("renders import modal when activeModal type is import", () => { + ctx = createMockGalleryContext({ + viewerImageId: null, + activeModal: { type: "import" }, + }); + vi.mocked(useGallery).mockReturnValue(ctx); + + render(); + expect(screen.getByTestId("import-modal")).toBeInTheDocument(); + }); + + it("renders settings modal when activeModal type is settings", () => { + ctx = createMockGalleryContext({ + viewerImageId: null, + activeModal: { type: "settings" }, + }); + vi.mocked(useGallery).mockReturnValue(ctx); + + render(); + expect(screen.getByTestId("settings-modal")).toBeInTheDocument(); + }); + + it("renders error modal via portal when activeModal type is error", () => { + ctx = createMockGalleryContext({ + viewerImageId: null, + activeModal: { type: "error", message: "Something went wrong" }, + }); + vi.mocked(useGallery).mockReturnValue(ctx); + + render(); + expect(screen.getByTestId("error-modal")).toBeInTheDocument(); + }); + + it("renders toast when toastMessage is present", () => { + ctx = createMockGalleryContext({ + viewerImageId: null, + toastMessage: "Success!", + }); + vi.mocked(useGallery).mockReturnValue(ctx); + + render(); + expect(screen.getByTestId("toast")).toBeInTheDocument(); + }); + + it("renders confirm modal when confirmModal state is present", () => { + ctx = createMockGalleryContext({ + viewerImageId: null, + confirmModal: { + message: "Confirm this?", + onConfirm: vi.fn(), + onCancel: vi.fn(), + }, + }); + vi.mocked(useGallery).mockReturnValue(ctx); + + render(); + expect(screen.getByTestId("confirm-modal")).toBeInTheDocument(); + }); +}); diff --git a/ui/src/features/gallery/components/GalleryPanel.test.tsx b/ui/src/features/gallery/components/GalleryPanel.test.tsx new file mode 100644 index 0000000..0579dcc --- /dev/null +++ b/ui/src/features/gallery/components/GalleryPanel.test.tsx @@ -0,0 +1,221 @@ +import { render, screen } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { createTestImage, resetImageIdCounter } from "../../../test/factories/image"; +import { GalleryPanel } from "./GalleryPanel"; + +// Mock useGalleryLogic +const mockUseGalleryLogic = vi.fn(); +vi.mock("../hooks/useGalleryLogic", () => ({ + useGalleryLogic: (...args: unknown[]) => mockUseGalleryLogic(...args), +})); + +// Mock useLightTableStore +vi.mock("../../light-table/store", () => ({ + useLightTableStore: vi.fn(() => false), +})); + +// Mock child components to keep tests focused on GalleryPanel logic +vi.mock("../../../components/shared/GalleryModals", () => ({ + GalleryModals: () =>
, +})); +vi.mock("../../importer/components/ImportProgress", () => ({ + ImportProgress: () => null, +})); +vi.mock("../../light-table/components/LightTable", () => ({ + LightTable: () => null, +})); +vi.mock("../../search/components/FavoritesContextMenu", () => ({ + FavoritesContextMenu: () => null, +})); +vi.mock("../../search/components/SearchBar", () => ({ + SearchBar: () =>
, +})); +vi.mock("../../tags/components/TagManagerView", () => ({ + TagManagerView: () =>
, +})); +vi.mock("../../viewer/components/ImageViewer", () => ({ + ImageViewer: () =>
, +})); +vi.mock("./BulkActionBar", () => ({ + BulkActionBar: () =>
, +})); +vi.mock("./VirtualizedGalleryList", () => ({ + VirtualizedGalleryList: () =>
, +})); +vi.mock("../../../styles/Gallery.css", () => ({})); +vi.mock("../../../logger", () => ({ + logger: { log: vi.fn(), error: vi.fn(), warn: vi.fn() }, +})); + +// Create default mock return value for useGalleryLogic +function createMockLogic(overrides = {}) { + const loadMoreRef = { current: null }; + return { + state: { + images: [], + isLoading: false, + error: null, + viewerImageId: null, + viewScope: "default", + searchQuery: "", + selectedIds: new Set(), + settings: { + "gallery.view_mode": "grid_details", + }, + activeModal: { type: "none" }, + pagination: { hasMore: false, limit: 50 }, + favorites: [], + toastMessage: null, + }, + dispatch: vi.fn(), + refreshImages: vi.fn(), + loadMoreImages: vi.fn(), + updateSetting: vi.fn(), + viewMode: "gallery", + setViewMode: vi.fn(), + lastSearchQuery: "", + setLastSearchQuery: vi.fn(), + displayedImages: [], + visibleImages: [], + isSearchActive: false, + loadMoreRef, + ...overrides, + }; +} + +describe("GalleryPanel", () => { + beforeEach(() => { + resetImageIdCounter(); + mockUseGalleryLogic.mockReturnValue(createMockLogic()); + }); + + it("shows loading message when loading with no images", () => { + mockUseGalleryLogic.mockReturnValue( + createMockLogic({ + state: { + images: [], + isLoading: true, + error: null, + viewerImageId: null, + viewScope: "default", + searchQuery: "", + selectedIds: new Set(), + settings: { "gallery.view_mode": "grid_details" }, + activeModal: { type: "none" }, + pagination: { hasMore: false, limit: 50 }, + favorites: [], + toastMessage: null, + }, + displayedImages: [], + visibleImages: [], + }), + ); + + render(); + expect(screen.getByText("Loading images...")).toBeInTheDocument(); + }); + + it("shows empty message when no images are found", () => { + render(); + expect(screen.getByText("No images found.")).toBeInTheDocument(); + }); + + it("renders VirtualizedGalleryList when images are present", () => { + const img = createTestImage(); + mockUseGalleryLogic.mockReturnValue( + createMockLogic({ + state: { + images: [img], + isLoading: false, + error: null, + viewerImageId: null, + viewScope: "default", + searchQuery: "", + selectedIds: new Set(), + settings: { "gallery.view_mode": "grid_details" }, + activeModal: { type: "none" }, + pagination: { hasMore: false, limit: 50 }, + favorites: [], + toastMessage: null, + }, + displayedImages: [img], + visibleImages: [img], + }), + ); + + render(); + expect(screen.getByTestId("virtualized-gallery-list")).toBeInTheDocument(); + }); + + it("shows trash indicator when viewScope is trash", () => { + mockUseGalleryLogic.mockReturnValue( + createMockLogic({ + state: { + images: [], + isLoading: false, + error: null, + viewerImageId: null, + viewScope: "trash", + searchQuery: "", + selectedIds: new Set(), + settings: { "gallery.view_mode": "grid_details" }, + activeModal: { type: "none" }, + pagination: { hasMore: false, limit: 50 }, + favorites: [], + toastMessage: null, + }, + }), + ); + + render(); + expect(screen.getByText("Trash Bin")).toBeInTheDocument(); + }); + + it("shows error message when error is present", () => { + mockUseGalleryLogic.mockReturnValue( + createMockLogic({ + state: { + images: [], + isLoading: false, + error: "Something went wrong", + viewerImageId: null, + viewScope: "default", + searchQuery: "", + selectedIds: new Set(), + settings: { "gallery.view_mode": "grid_details" }, + activeModal: { type: "none" }, + pagination: { hasMore: false, limit: 50 }, + favorites: [], + toastMessage: null, + }, + }), + ); + + render(); + expect(screen.getByText("Something went wrong")).toBeInTheDocument(); + }); + + it("renders ImageViewer when viewerImageId is set", () => { + mockUseGalleryLogic.mockReturnValue( + createMockLogic({ + state: { + images: [], + isLoading: false, + error: null, + viewerImageId: 1, + viewScope: "default", + searchQuery: "", + selectedIds: new Set(), + settings: { "gallery.view_mode": "grid_details" }, + activeModal: { type: "none" }, + pagination: { hasMore: false, limit: 50 }, + favorites: [], + toastMessage: null, + }, + }), + ); + + render(); + expect(screen.getByTestId("image-viewer")).toBeInTheDocument(); + }); +}); diff --git a/ui/src/features/gallery/components/VirtualizedGalleryList.test.tsx b/ui/src/features/gallery/components/VirtualizedGalleryList.test.tsx new file mode 100644 index 0000000..f9b98ec --- /dev/null +++ b/ui/src/features/gallery/components/VirtualizedGalleryList.test.tsx @@ -0,0 +1,118 @@ +import { render, screen } from "@testing-library/react"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { createTestImage, resetImageIdCounter } from "../../../test/factories/image"; +import type { MeldImage, Settings } from "../../../types"; +import { VirtualizedGalleryList } from "./VirtualizedGalleryList"; + +// jsdom does not provide ResizeObserver +beforeAll(() => { + globalThis.ResizeObserver = class { + observe() {} + unobserve() {} + disconnect() {} + } as unknown as typeof ResizeObserver; +}); + +// Mock @tanstack/react-virtual +vi.mock("@tanstack/react-virtual", () => ({ + useVirtualizer: vi.fn(() => ({ + getVirtualItems: () => [], + getTotalSize: () => 0, + scrollToIndex: vi.fn(), + measureElement: vi.fn(), + })), +})); + +// Mock ImageCard +vi.mock("../../../components/shared/ImageCard", () => ({ + ImageCard: ({ image }: { image: MeldImage }) => ( +
{image.filename}
+ ), +})); + +const defaultSettings: Settings = { + "sidebar.thumbnail_size": 100, + "gallery.view_mode": "grid_details", +} as Settings; + +describe("VirtualizedGalleryList", () => { + const loadMoreRef = { current: null } as React.RefObject; + + beforeEach(() => { + resetImageIdCounter(); + }); + + it("shows loading indicator when isLoading is true", () => { + render( + , + ); + expect(screen.getByText("Loading more...")).toBeInTheDocument(); + }); + + it("shows end of gallery when not loading and hasMore is false", () => { + const img = createTestImage(); + render( + , + ); + expect(screen.getByText("End of gallery")).toBeInTheDocument(); + }); + + it("renders the scroll container", () => { + render( + , + ); + const scrollEl = document.querySelector(".meld-gallery__list-scroll"); + expect(scrollEl).toBeTruthy(); + }); + + it("sets thumbnail size CSS variable", () => { + const settings = { ...defaultSettings, "sidebar.thumbnail_size": 150 } as Settings; + render( + , + ); + const scrollEl = document.querySelector(".meld-gallery__list-scroll") as HTMLElement; + expect(scrollEl?.style.getPropertyValue("--meld-thumbnail-size")).toBe("150px"); + }); + + it("does not show end message when images list is empty", () => { + render( + , + ); + expect(screen.queryByText("End of gallery")).not.toBeInTheDocument(); + }); +}); diff --git a/ui/src/features/viewer/components/ImageViewer.test.tsx b/ui/src/features/viewer/components/ImageViewer.test.tsx new file mode 100644 index 0000000..64a6235 --- /dev/null +++ b/ui/src/features/viewer/components/ImageViewer.test.tsx @@ -0,0 +1,241 @@ +import { render, screen } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { createTestImage, resetImageIdCounter } from "../../../test/factories/image"; +import { + createMockGalleryContext, + type MockGalleryContext, +} from "../../../test/helpers/renderWithGallery"; +import type { Settings } from "../../../types"; +import { ImageViewer } from "./ImageViewer"; + +// Mock useGallery +vi.mock("../../../store/GalleryContext", () => ({ + useGallery: vi.fn(), +})); + +// Mock useImageViewerLogic +const mockUseImageViewerLogic = vi.fn(); +vi.mock("../hooks/useImageViewerLogic", () => ({ + useImageViewerLogic: (...args: unknown[]) => mockUseImageViewerLogic(...args), +})); + +// Mock useWorkflowExecution +vi.mock("../../workflows/hooks/useWorkflowExecution", () => ({ + useWorkflowExecution: vi.fn(() => ({ + executeWorkflow: vi.fn(), + })), +})); + +// Mock child components +vi.mock("./ImageCardMenu", () => ({ + ImageCardMenu: () =>
, +})); +vi.mock("./ViewerInfoPanel", () => ({ + ViewerInfoPanel: () =>
, +})); +vi.mock("./ViewerThumbnailStrip", () => ({ + ViewerThumbnailStrip: () =>
, +})); +vi.mock("./ViewerCheatSheet", () => ({ + ViewerCheatSheet: () =>
, +})); +vi.mock("./NoteEditModal", () => ({ + NoteEditModal: () =>
, +})); + +// Mock modal components +vi.mock("../../../components/shared/DeleteConfirmModal", () => ({ + DeleteConfirmModal: () =>
, +})); +vi.mock("../../../components/shared/ErrorModal", () => ({ + ErrorModal: () =>
, +})); +vi.mock("../../../components/shared/ParentSelectionModal", () => ({ + ParentSelectionModal: () =>
, +})); +vi.mock("../../importer/components/ImportModal", () => ({ + ImportModal: () =>
, +})); +vi.mock("../../settings/components/SettingsModal", () => ({ + SettingsModal: () =>
, +})); +vi.mock("../../tags/components/TagEditModal", () => ({ + TagEditModal: () =>
, +})); +vi.mock("../../mask-editor/components/MaskEditorModal", () => ({ + MaskEditorModal: () =>
, +})); +vi.mock("../../mask-editor/components/MaskSequenceModal", () => ({ + MaskSequenceModal: () =>
, +})); +vi.mock("../../workflows/components/NodeSelectionModal", () => ({ + NodeSelectionModal: () =>
, +})); +vi.mock("../../workflows/components/WorkflowSelectionModal", () => ({ + WorkflowSelectionModal: () =>
, +})); +vi.mock("../../workflows/utils/injectImageToGraph", () => ({ + injectImageToGraph: vi.fn(), +})); +vi.mock("../../../utils/url", () => ({ + getImageViewUrl: (img: { filename: string }) => `/api/view/${img.filename}`, +})); + +import { useGallery } from "../../../store/GalleryContext"; +import { initialState } from "../../../store/galleryReducer"; + +function createMockViewerLogic(image: ReturnType | null, overrides = {}) { + return { + isFullscreen: false, + showDetails: false, + setShowDetails: vi.fn(), + showThumbnails: false, + setShowThumbnailsOverride: vi.fn(), + isLoadingLineage: false, + isJumping: false, + isMenuOpen: false, + setIsMenuOpen: vi.fn(), + activeShortcutKey: null, + setLastDeletedImages: vi.fn(), + overlayRef: { current: null }, + handleNext: vi.fn(), + handlePrevious: vi.fn(), + handleTagEdit: vi.fn(), + handleEditNotes: vi.fn(), + handleRestore: vi.fn(), + handleRestoreWorkflow: vi.fn(), + handleAddUnifiedLoader: vi.fn(), + handleSendToWorkflow: vi.fn(), + handleRunWithWorkflow: vi.fn(), + handleRunWithMask: vi.fn(), + handleEditSource: vi.fn(), + handleDelete: vi.fn(), + toggleFullscreen: vi.fn(), + image, + windowedThumbnails: [], + parentChain: [], + ...overrides, + }; +} + +// Settings with icons visible +const iconsVisibleSettings: Settings = { + ...initialState.settings, + "viewer.show_icons": true, + "viewer.small_image_mode": "fit", + "viewer.thumbnail_window_size": 5, +}; + +// Settings with icons hidden +const iconsHiddenSettings: Settings = { + ...initialState.settings, + "viewer.show_icons": false, + "viewer.small_image_mode": "fit", + "viewer.thumbnail_window_size": 5, +}; + +describe("ImageViewer", () => { + let ctx: MockGalleryContext; + + beforeEach(() => { + resetImageIdCounter(); + }); + + it("renders nothing when image is null", () => { + ctx = createMockGalleryContext({ + viewerImageId: 1, + activeModal: { type: "none" }, + }); + vi.mocked(useGallery).mockReturnValue(ctx); + mockUseImageViewerLogic.mockReturnValue(createMockViewerLogic(null)); + + const { container } = render(); + expect(container.innerHTML).toBe(""); + }); + + it("renders the viewer overlay when image is present", () => { + const img = createTestImage(); + ctx = createMockGalleryContext({ + viewerImageId: img.id, + viewerMode: "gallery", + activeModal: { type: "none" }, + viewScope: "default", + settings: iconsVisibleSettings, + }); + vi.mocked(useGallery).mockReturnValue(ctx); + mockUseImageViewerLogic.mockReturnValue(createMockViewerLogic(img)); + + render(); + const overlay = document.querySelector(".meld-viewer-overlay"); + expect(overlay).toBeTruthy(); + }); + + it("renders the image element with correct src", () => { + const img = createTestImage(); + ctx = createMockGalleryContext({ + viewerImageId: img.id, + viewerMode: "gallery", + activeModal: { type: "none" }, + viewScope: "default", + settings: iconsVisibleSettings, + }); + vi.mocked(useGallery).mockReturnValue(ctx); + mockUseImageViewerLogic.mockReturnValue(createMockViewerLogic(img)); + + render(); + const imgEl = document.querySelector(".meld-viewer-image") as HTMLImageElement; + expect(imgEl).toBeTruthy(); + expect(imgEl.src).toContain(img.filename); + }); + + it("renders navigation buttons when icons are shown", () => { + const img = createTestImage(); + ctx = createMockGalleryContext({ + viewerImageId: img.id, + viewerMode: "gallery", + activeModal: { type: "none" }, + viewScope: "default", + settings: iconsVisibleSettings, + }); + vi.mocked(useGallery).mockReturnValue(ctx); + mockUseImageViewerLogic.mockReturnValue(createMockViewerLogic(img)); + + render(); + expect(document.querySelector(".meld-viewer-nav--prev")).toBeTruthy(); + expect(document.querySelector(".meld-viewer-nav--next")).toBeTruthy(); + }); + + it("renders close button with correct title", () => { + const img = createTestImage(); + ctx = createMockGalleryContext({ + viewerImageId: img.id, + viewerMode: "gallery", + activeModal: { type: "none" }, + viewScope: "default", + settings: iconsVisibleSettings, + }); + vi.mocked(useGallery).mockReturnValue(ctx); + mockUseImageViewerLogic.mockReturnValue(createMockViewerLogic(img)); + + render(); + const closeBtn = screen.getByTitle("Close (Esc)"); + expect(closeBtn).toBeInTheDocument(); + }); + + it("does not render navigation buttons when icons are hidden", () => { + const img = createTestImage(); + ctx = createMockGalleryContext({ + viewerImageId: img.id, + viewerMode: "gallery", + activeModal: { type: "none" }, + viewScope: "default", + settings: iconsHiddenSettings, + }); + vi.mocked(useGallery).mockReturnValue(ctx); + mockUseImageViewerLogic.mockReturnValue(createMockViewerLogic(img)); + + render(); + expect(document.querySelector(".meld-viewer-nav--prev")).toBeNull(); + expect(document.querySelector(".meld-viewer-nav--next")).toBeNull(); + }); +});