- Added deduplication logic to prevent multiple simultaneous requests for the same asset metadata. - Introduced a pending requests cache to manage in-flight promises. - Updated `getAssetMetadata` and `vectorFindSimilar` functions to utilize the deduplication feature. - Enhanced error handling to clear pending entries on request failures. feat: add MediaBlobCache for media asset error tracking - Created MediaBlobCache to manage blob URLs and track loading errors for media assets. - Implemented methods to mark URLs as errored and acquire blob URLs with ref-counting. - Integrated MediaBlobCache into the Card component to handle broken image URLs. feat: reconcile selection IDs in grid management - Added `reconcileSelectionIds` function to synchronize selected asset IDs with visible assets. - Updated grid selection management to prune selections based on visibility. - Enhanced the AssetsManagerPanel to trigger selection reconciliation on filter changes. style: improve loading overlay and skeleton loaders - Refined loading overlay styles for better user experience. - Added CSS for skeleton loaders to provide visual feedback during asset loading. test: add unit tests for API client deduplication and grid selection - Implemented tests for deduplication logic in the API client. - Added tests for the new `reconcileSelectionIds` function in the grid selection manager.
119 lines
3.6 KiB
JavaScript
119 lines
3.6 KiB
JavaScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
getSelectedIdSet,
|
|
reconcileSelectionIds,
|
|
safeEscapeId,
|
|
setSelectionIds,
|
|
syncSelectionClasses,
|
|
} from "../features/grid/GridSelectionManager.js";
|
|
|
|
function makeCard(id) {
|
|
const classSet = new Set();
|
|
const attrs = new Map();
|
|
return {
|
|
dataset: { mjrAssetId: String(id) },
|
|
classList: {
|
|
add(v) {
|
|
classSet.add(v);
|
|
},
|
|
remove(v) {
|
|
classSet.delete(v);
|
|
},
|
|
has(v) {
|
|
return classSet.has(v);
|
|
},
|
|
},
|
|
setAttribute(k, v) {
|
|
attrs.set(k, v);
|
|
},
|
|
getAttribute(k) {
|
|
return attrs.get(k);
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("GridSelectionManager", () => {
|
|
it("supports single and multi selection dataset keys", () => {
|
|
const g1 = { dataset: { mjrSelectedAssetIds: JSON.stringify([1, "2"]) } };
|
|
expect(Array.from(getSelectedIdSet(g1)).sort()).toEqual(["1", "2"]);
|
|
|
|
const g2 = { dataset: { mjrSelectedAssetId: "9" } };
|
|
expect(Array.from(getSelectedIdSet(g2))).toEqual(["9"]);
|
|
});
|
|
|
|
it("updates cards and emits event when selection changes", () => {
|
|
const cards = [makeCard(1), makeCard(2), makeCard(3)];
|
|
const events = [];
|
|
globalThis.CustomEvent = class {
|
|
constructor(type, init) {
|
|
this.type = type;
|
|
this.detail = init?.detail;
|
|
}
|
|
};
|
|
globalThis.window = {
|
|
dispatchEvent(evt) {
|
|
events.push(evt);
|
|
},
|
|
};
|
|
const grid = {
|
|
dataset: {},
|
|
dispatchEvent(evt) {
|
|
events.push(evt);
|
|
},
|
|
};
|
|
|
|
syncSelectionClasses(grid, new Set(["2"]), () => cards);
|
|
expect(cards[1].classList.has("is-selected")).toBe(true);
|
|
expect(cards[0].getAttribute("aria-selected")).toBe("false");
|
|
|
|
const selected = setSelectionIds(grid, ["1", 3, "3"], { activeId: "3" }, () => cards);
|
|
expect(selected.sort()).toEqual(["1", "3"]);
|
|
expect(grid.dataset.mjrSelectedAssetId).toBe("3");
|
|
expect(events.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it("falls back when CSS.escape is unavailable", () => {
|
|
globalThis.CSS = undefined;
|
|
expect(safeEscapeId('a"b\\c')).toBe('a\\"b\\\\c');
|
|
});
|
|
|
|
it("reconciles selection against visible assets and reports pruned items", () => {
|
|
const cards = [makeCard(1), makeCard(2), makeCard(3)];
|
|
const events = [];
|
|
globalThis.CustomEvent = class {
|
|
constructor(type, init) {
|
|
this.type = type;
|
|
this.detail = init?.detail;
|
|
}
|
|
};
|
|
globalThis.window = {
|
|
dispatchEvent(evt) {
|
|
events.push(evt);
|
|
},
|
|
};
|
|
const grid = {
|
|
dataset: {
|
|
mjrSelectedAssetIds: JSON.stringify(["1", "2", "9"]),
|
|
mjrSelectedAssetId: "2",
|
|
},
|
|
dispatchEvent(evt) {
|
|
events.push(evt);
|
|
},
|
|
};
|
|
|
|
const result = reconcileSelectionIds(
|
|
grid,
|
|
["2", "3"],
|
|
{ activeId: "2" },
|
|
() => cards,
|
|
);
|
|
|
|
expect(result.pruned).toBe(2);
|
|
expect(result.selectedIds).toEqual(["2"]);
|
|
expect(grid.dataset.mjrSelectedAssetIds).toBe(JSON.stringify(["2"]));
|
|
expect(grid.dataset.mjrSelectedAssetId).toBe("2");
|
|
expect(events.some((evt) => evt.type === "mjr:selection-changed")).toBe(true);
|
|
});
|
|
});
|