import { beforeEach, describe, expect, it, vi } from "vitest"; const bridgeState = vi.hoisted(() => ({ values: { scope: "output", customRootId: "", currentFolderRelativePath: "", kindFilter: "", workflowOnly: false, minRating: 0, minSizeMB: 0, maxSizeMB: 0, resolutionCompare: "gte", minWidth: 0, minHeight: 0, maxWidth: 0, maxHeight: 0, workflowType: "", dateRangeFilter: "", dateExactFilter: "", sort: "mtime_desc", activeAssetId: "", selectedAssetIds: [], collectionId: "", collectionName: "", viewScope: "", similarResults: [], similarTitle: "", similarSourceAssetId: "", lastGridCount: 0, lastGridTotal: 0, }, })); vi.mock("../stores/panelStateBridge.js", () => ({ createPanelStateBridge: () => ({ read: (key, fallback = "") => Object.prototype.hasOwnProperty.call(bridgeState.values, key) ? bridgeState.values[key] : fallback, write: vi.fn((key, value) => { bridgeState.values[key] = value; }), }), })); vi.mock("../api/client.js", () => ({ vectorSearch: vi.fn(), hybridSearch: vi.fn(), vectorStats: vi.fn(), })); vi.mock("../app/toast.js", () => ({ comfyToast: vi.fn(), })); vi.mock("../app/i18n.js", () => ({ t: (_key, fallback) => fallback, })); vi.mock("../app/settings.js", () => ({ loadMajoorSettings: () => ({ ai: { vectorSearchEnabled: true } }), })); vi.mock("../app/config.js", () => ({ APP_CONFIG: { EXECUTION_GROUPING_ENABLED: true }, })); function createGridContainer() { return { dataset: { mjrViewScope: "", mjrScope: "output" }, dispatchEvent: vi.fn(), }; } describe("gridController scope switch cache behavior", () => { beforeEach(() => { vi.clearAllMocks(); Object.assign(bridgeState.values, { scope: "output", customRootId: "", currentFolderRelativePath: "", kindFilter: "", workflowOnly: false, minRating: 0, minSizeMB: 0, maxSizeMB: 0, resolutionCompare: "gte", minWidth: 0, minHeight: 0, maxWidth: 0, maxHeight: 0, workflowType: "", dateRangeFilter: "", dateExactFilter: "", sort: "mtime_desc", activeAssetId: "", selectedAssetIds: [], collectionId: "", collectionName: "", viewScope: "", similarResults: [], similarTitle: "", similarSourceAssetId: "", lastGridCount: 0, lastGridTotal: 0, }); globalThis.window = {}; globalThis.CustomEvent = class { constructor(type, init = {}) { this.type = type; this.detail = init.detail; } }; }); it("writes mjrCollectionId to the dataset during reload", async () => { const { createGridController } = await import("../features/panel/controllers/gridController.js"); bridgeState.values.collectionId = "col-123"; bridgeState.values.collectionName = "Test Collection"; const gridContainer = createGridContainer(); const loadAssets = vi.fn(async () => ({ ok: true, count: 5, total: 5 })); const getCollectionAssets = vi.fn(async () => ({ ok: true, data: { assets: [{ id: 1 }, { id: 2 }] }, })); const loadAssetsFromList = vi.fn(async () => ({ ok: true, count: 2, total: 2 })); const controller = createGridController({ gridContainer, loadAssets, loadAssetsFromList, getCollectionAssets, disposeGrid: vi.fn(), getQuery: () => "*", searchInputEl: { dataset: { mjrSemanticMode: "0" } }, state: {}, }); await controller.reloadGrid(); expect(gridContainer.dataset.mjrCollectionId).toBe("col-123"); }); it("loads collection assets when collectionId is set", async () => { const { createGridController } = await import("../features/panel/controllers/gridController.js"); bridgeState.values.collectionId = "col-456"; bridgeState.values.collectionName = "My Collection"; const gridContainer = createGridContainer(); const loadAssets = vi.fn(async () => ({ ok: true, count: 5, total: 5 })); const collectionAssets = [ { id: 10, filename: "a.png" }, { id: 11, filename: "b.png" }, ]; const getCollectionAssets = vi.fn(async () => ({ ok: true, data: { assets: collectionAssets }, })); const loadAssetsFromList = vi.fn(async () => ({ ok: true, count: 2, total: 2 })); const controller = createGridController({ gridContainer, loadAssets, loadAssetsFromList, getCollectionAssets, disposeGrid: vi.fn(), getQuery: () => "*", searchInputEl: { dataset: { mjrSemanticMode: "0" } }, state: {}, }); await controller.reloadGrid(); expect(getCollectionAssets).toHaveBeenCalledWith("col-456"); expect(loadAssetsFromList).toHaveBeenCalledWith( gridContainer, collectionAssets, expect.objectContaining({ title: "Collection: My Collection" }), ); // Normal loadAssets should NOT be called when collection is active expect(loadAssets).not.toHaveBeenCalled(); }); it("falls back to normal load when collection fetch fails", async () => { const { createGridController } = await import("../features/panel/controllers/gridController.js"); bridgeState.values.collectionId = "col-bad"; const gridContainer = createGridContainer(); const loadAssets = vi.fn(async () => ({ ok: true, count: 50, total: 50 })); const getCollectionAssets = vi.fn(async () => ({ ok: false, error: "Not found" })); const loadAssetsFromList = vi.fn(); const controller = createGridController({ gridContainer, loadAssets, loadAssetsFromList, getCollectionAssets, disposeGrid: vi.fn(), getQuery: () => "*", searchInputEl: { dataset: { mjrSemanticMode: "0" } }, state: {}, }); await controller.reloadGrid(); // Should fall back to normal loadAssets expect(loadAssets).toHaveBeenCalled(); // collectionId should be cleared expect(bridgeState.values.collectionId).toBe(""); }); it("switches from output to input scope and reloads normally", async () => { const { createGridController } = await import("../features/panel/controllers/gridController.js"); const gridContainer = createGridContainer(); const loadAssets = vi.fn(async () => ({ ok: true, count: 100, total: 100 })); const controller = createGridController({ gridContainer, loadAssets, loadAssetsFromList: vi.fn(async () => ({ ok: true })), getCollectionAssets: vi.fn(), disposeGrid: vi.fn(), getQuery: () => "*", searchInputEl: { dataset: { mjrSemanticMode: "0" } }, state: {}, }); // First load in output scope await controller.reloadGrid(); expect(gridContainer.dataset.mjrScope).toBe("output"); // Switch to input scope bridgeState.values.scope = "input"; loadAssets.mockClear(); await controller.reloadGrid(); expect(gridContainer.dataset.mjrScope).toBe("input"); expect(loadAssets).toHaveBeenCalled(); }); it("does not dispose the live grid when entering browser root scope", async () => { const { createGridController } = await import("../features/panel/controllers/gridController.js"); Object.assign(bridgeState.values, { scope: "custom", customRootId: "", currentFolderRelativePath: "", }); const gridContainer = createGridContainer(); const loadAssets = vi.fn(async () => ({ ok: true, count: 4, total: 4 })); const disposeGrid = vi.fn(); const controller = createGridController({ gridContainer, loadAssets, loadAssetsFromList: vi.fn(async () => ({ ok: true })), getCollectionAssets: vi.fn(), disposeGrid, getQuery: () => "*", searchInputEl: { dataset: { mjrSemanticMode: "0" } }, state: {}, }); await controller.reloadGrid(); expect(gridContainer.dataset.mjrScope).toBe("custom"); expect(gridContainer.dataset.mjrCustomRootId).toBe(""); expect(loadAssets).toHaveBeenCalled(); expect(disposeGrid).not.toHaveBeenCalled(); }); });