Merge pull request #72 from HappyOnigiri/test/add-missing-ui-component-tests

test: add missing UI component tests
This commit is contained in:
HappyOnigiri
2026-03-04 12:43:06 +09:00
committed by GitHub
5 changed files with 846 additions and 0 deletions
@@ -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(<DeleteConfirmModal imageIds={[1, 2]} hasLineage={false} />);
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(<DeleteConfirmModal imageIds={[1, 2]} hasLineage={false} isPermanent />);
const heading = document.querySelector(".meld-modal-header h2");
expect(heading?.textContent).toContain("Permanent Deletion");
});
it("displays the number of selected items", () => {
render(<DeleteConfirmModal imageIds={[1, 2, 3]} hasLineage={false} />);
expect(screen.getByText("3")).toBeInTheDocument();
});
it("dispatches CLOSE_MODAL when Cancel button is clicked", async () => {
render(<DeleteConfirmModal imageIds={[1]} hasLineage={false} />);
await userEvent.click(screen.getByText("Cancel"));
expect(ctx.dispatch).toHaveBeenCalledWith({ type: "CLOSE_MODAL" });
});
it("shows lineage buttons when hasLineage is true", () => {
render(<DeleteConfirmModal imageIds={[1]} hasLineage />);
expect(screen.getByText("Move All Related")).toBeInTheDocument();
});
it("does not show lineage buttons when hasLineage is false", () => {
render(<DeleteConfirmModal imageIds={[1]} hasLineage={false} />);
expect(screen.queryByText("Move All Related")).not.toBeInTheDocument();
});
it("shows permanent delete labels when isPermanent is true with lineage", () => {
render(<DeleteConfirmModal imageIds={[1]} hasLineage isPermanent />);
expect(screen.getByText("Delete Permanently")).toBeInTheDocument();
expect(screen.getByText("Delete All Related")).toBeInTheDocument();
});
it("shows warning message for permanent deletion", () => {
render(<DeleteConfirmModal imageIds={[1]} hasLineage={false} isPermanent />);
expect(screen.getByText(/permanently deleted/)).toBeInTheDocument();
});
it("shows info message for trash mode", () => {
render(<DeleteConfirmModal imageIds={[1]} hasLineage={false} />);
expect(screen.getByText(/moved to the trash bin/)).toBeInTheDocument();
});
it("dispatches CLOSE_MODAL when close (X) button is clicked", async () => {
render(<DeleteConfirmModal imageIds={[1]} hasLineage={false} />);
// 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" });
});
});
@@ -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: () => <div data-testid="import-modal">ImportModal</div>,
}));
vi.mock("../../features/light-table/components/DownloadModal", () => ({
DownloadModal: () => <div data-testid="download-modal">DownloadModal</div>,
}));
vi.mock("../../features/mask-editor/components/MaskEditorModal", () => ({
MaskEditorModal: () => <div data-testid="mask-editor-modal">MaskEditorModal</div>,
}));
vi.mock("../../features/mask-editor/components/MaskSequenceModal", () => ({
MaskSequenceModal: () => <div data-testid="mask-sequence-modal">MaskSequenceModal</div>,
}));
vi.mock("../../features/settings/components/SettingsModal", () => ({
SettingsModal: () => <div data-testid="settings-modal">SettingsModal</div>,
}));
vi.mock("../../features/tags/components/TagEditModal", () => ({
TagEditModal: () => <div data-testid="tag-edit-modal">TagEditModal</div>,
}));
vi.mock("../../features/viewer/components/NoteEditModal", () => ({
NoteEditModal: () => <div data-testid="note-edit-modal">NoteEditModal</div>,
}));
vi.mock("../../features/workflows/components/NodeSelectionModal", () => ({
NodeSelectionModal: () => <div data-testid="node-selection-modal">NodeSelectionModal</div>,
}));
vi.mock("../../features/workflows/components/WorkflowSelectionModal", () => ({
WorkflowSelectionModal: () => (
<div data-testid="workflow-selection-modal">WorkflowSelectionModal</div>
),
}));
vi.mock("../../features/workflows/utils/injectImageToGraph", () => ({
injectImageToGraph: vi.fn(),
}));
vi.mock("./ConfirmModal", () => ({
ConfirmModal: ({ message }: { message: string }) => (
<div data-testid="confirm-modal">{message}</div>
),
}));
vi.mock("./DeleteConfirmModal", () => ({
DeleteConfirmModal: () => <div data-testid="delete-confirm-modal">DeleteConfirmModal</div>,
}));
vi.mock("./ErrorModal", () => ({
ErrorModal: ({ message }: { message: string }) => <div data-testid="error-modal">{message}</div>,
}));
vi.mock("./ParentSelectionModal", () => ({
ParentSelectionModal: () => <div data-testid="parent-selection-modal">ParentSelectionModal</div>,
}));
vi.mock("./Toast", () => ({
Toast: () => <div data-testid="toast">Toast</div>,
}));
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(<GalleryModals />);
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(<GalleryModals />);
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(<GalleryModals />);
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(<GalleryModals />);
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(<GalleryModals />);
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(<GalleryModals />);
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(<GalleryModals />);
expect(screen.getByTestId("confirm-modal")).toBeInTheDocument();
});
});
@@ -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: () => <div data-testid="gallery-modals" />,
}));
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: () => <div data-testid="search-bar" />,
}));
vi.mock("../../tags/components/TagManagerView", () => ({
TagManagerView: () => <div data-testid="tag-manager" />,
}));
vi.mock("../../viewer/components/ImageViewer", () => ({
ImageViewer: () => <div data-testid="image-viewer" />,
}));
vi.mock("./BulkActionBar", () => ({
BulkActionBar: () => <div data-testid="bulk-action-bar" />,
}));
vi.mock("./VirtualizedGalleryList", () => ({
VirtualizedGalleryList: () => <div data-testid="virtualized-gallery-list" />,
}));
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(<GalleryPanel />);
expect(screen.getByText("Loading images...")).toBeInTheDocument();
});
it("shows empty message when no images are found", () => {
render(<GalleryPanel />);
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(<GalleryPanel />);
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(<GalleryPanel />);
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(<GalleryPanel />);
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(<GalleryPanel />);
expect(screen.getByTestId("image-viewer")).toBeInTheDocument();
});
});
@@ -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 }) => (
<div data-testid={`image-card-${image.id}`}>{image.filename}</div>
),
}));
const defaultSettings: Settings = {
"sidebar.thumbnail_size": 100,
"gallery.view_mode": "grid_details",
} as Settings;
describe("VirtualizedGalleryList", () => {
const loadMoreRef = { current: null } as React.RefObject<HTMLDivElement>;
beforeEach(() => {
resetImageIdCounter();
});
it("shows loading indicator when isLoading is true", () => {
render(
<VirtualizedGalleryList
visibleImages={[createTestImage()]}
settings={defaultSettings}
loadMoreRef={loadMoreRef}
viewerImageId={null}
isLoading
hasMore
/>,
);
expect(screen.getByText("Loading more...")).toBeInTheDocument();
});
it("shows end of gallery when not loading and hasMore is false", () => {
const img = createTestImage();
render(
<VirtualizedGalleryList
visibleImages={[img]}
settings={defaultSettings}
loadMoreRef={loadMoreRef}
viewerImageId={null}
isLoading={false}
hasMore={false}
/>,
);
expect(screen.getByText("End of gallery")).toBeInTheDocument();
});
it("renders the scroll container", () => {
render(
<VirtualizedGalleryList
visibleImages={[]}
settings={defaultSettings}
loadMoreRef={loadMoreRef}
viewerImageId={null}
isLoading={false}
hasMore={false}
/>,
);
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(
<VirtualizedGalleryList
visibleImages={[]}
settings={settings}
loadMoreRef={loadMoreRef}
viewerImageId={null}
isLoading={false}
hasMore={false}
/>,
);
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(
<VirtualizedGalleryList
visibleImages={[]}
settings={defaultSettings}
loadMoreRef={loadMoreRef}
viewerImageId={null}
isLoading={false}
hasMore={false}
/>,
);
expect(screen.queryByText("End of gallery")).not.toBeInTheDocument();
});
});
@@ -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: () => <div data-testid="image-card-menu" />,
}));
vi.mock("./ViewerInfoPanel", () => ({
ViewerInfoPanel: () => <div data-testid="viewer-info-panel" />,
}));
vi.mock("./ViewerThumbnailStrip", () => ({
ViewerThumbnailStrip: () => <div data-testid="viewer-thumbnail-strip" />,
}));
vi.mock("./ViewerCheatSheet", () => ({
ViewerCheatSheet: () => <div data-testid="viewer-cheat-sheet" />,
}));
vi.mock("./NoteEditModal", () => ({
NoteEditModal: () => <div data-testid="note-edit-modal" />,
}));
// Mock modal components
vi.mock("../../../components/shared/DeleteConfirmModal", () => ({
DeleteConfirmModal: () => <div data-testid="delete-confirm-modal" />,
}));
vi.mock("../../../components/shared/ErrorModal", () => ({
ErrorModal: () => <div data-testid="error-modal" />,
}));
vi.mock("../../../components/shared/ParentSelectionModal", () => ({
ParentSelectionModal: () => <div data-testid="parent-selection-modal" />,
}));
vi.mock("../../importer/components/ImportModal", () => ({
ImportModal: () => <div data-testid="import-modal" />,
}));
vi.mock("../../settings/components/SettingsModal", () => ({
SettingsModal: () => <div data-testid="settings-modal" />,
}));
vi.mock("../../tags/components/TagEditModal", () => ({
TagEditModal: () => <div data-testid="tag-edit-modal" />,
}));
vi.mock("../../mask-editor/components/MaskEditorModal", () => ({
MaskEditorModal: () => <div data-testid="mask-editor-modal" />,
}));
vi.mock("../../mask-editor/components/MaskSequenceModal", () => ({
MaskSequenceModal: () => <div data-testid="mask-sequence-modal" />,
}));
vi.mock("../../workflows/components/NodeSelectionModal", () => ({
NodeSelectionModal: () => <div data-testid="node-selection-modal" />,
}));
vi.mock("../../workflows/components/WorkflowSelectionModal", () => ({
WorkflowSelectionModal: () => <div data-testid="workflow-selection-modal" />,
}));
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<typeof createTestImage> | 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(<ImageViewer />);
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(<ImageViewer />);
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(<ImageViewer />);
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(<ImageViewer />);
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(<ImageViewer />);
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(<ImageViewer />);
expect(document.querySelector(".meld-viewer-nav--prev")).toBeNull();
expect(document.querySelector(".meld-viewer-nav--next")).toBeNull();
});
});