201 lines
6.6 KiB
TypeScript
201 lines
6.6 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { emptyFilterGroup, type FilterGroup, type SavedView } from "@/lib/object-filters";
|
|
import { resolveActiveViewSyncDecision } from "./object-view-active-view";
|
|
|
|
function statusFilter(value: string): FilterGroup {
|
|
return {
|
|
id: "root",
|
|
conjunction: "and",
|
|
rules: [
|
|
{ id: "rule-status", field: "Status", operator: "is", value },
|
|
],
|
|
};
|
|
}
|
|
|
|
describe("resolveActiveViewSyncDecision", () => {
|
|
const importantView: SavedView = {
|
|
name: "Important",
|
|
filters: statusFilter("Important"),
|
|
columns: ["Name", "Status", "Owner"],
|
|
};
|
|
|
|
it("applies active view on initial load even when active view name already matches (prevents visible-but-unapplied bug)", () => {
|
|
const decision = resolveActiveViewSyncDecision({
|
|
savedViews: [importantView],
|
|
activeView: "Important",
|
|
currentActiveViewName: "Important",
|
|
currentFilters: emptyFilterGroup(),
|
|
currentViewColumns: undefined,
|
|
});
|
|
|
|
expect(decision).not.toBeNull();
|
|
expect(decision?.shouldApply).toBe(true);
|
|
expect(decision?.nextFilters).toEqual(statusFilter("Important"));
|
|
expect(decision?.nextColumns).toEqual(["Name", "Status", "Owner"]);
|
|
});
|
|
|
|
it("does not re-apply when name, filters, and columns are already aligned", () => {
|
|
const decision = resolveActiveViewSyncDecision({
|
|
savedViews: [importantView],
|
|
activeView: "Important",
|
|
currentActiveViewName: "Important",
|
|
currentFilters: statusFilter("Important"),
|
|
currentViewColumns: ["Name", "Status", "Owner"],
|
|
});
|
|
|
|
expect(decision).not.toBeNull();
|
|
expect(decision?.shouldApply).toBe(false);
|
|
});
|
|
|
|
it("re-applies when active view changes during refresh (keeps label and table state in sync)", () => {
|
|
const backlogView: SavedView = {
|
|
name: "Backlog",
|
|
filters: statusFilter("Backlog"),
|
|
columns: ["Name", "Status"],
|
|
};
|
|
|
|
const decision = resolveActiveViewSyncDecision({
|
|
savedViews: [importantView, backlogView],
|
|
activeView: "Backlog",
|
|
currentActiveViewName: "Important",
|
|
currentFilters: statusFilter("Important"),
|
|
currentViewColumns: ["Name", "Status", "Owner"],
|
|
});
|
|
|
|
expect(decision).not.toBeNull();
|
|
expect(decision?.shouldApply).toBe(true);
|
|
expect(decision?.nextActiveViewName).toBe("Backlog");
|
|
expect(decision?.nextFilters).toEqual(statusFilter("Backlog"));
|
|
expect(decision?.nextColumns).toEqual(["Name", "Status"]);
|
|
});
|
|
|
|
it("returns null when active view is missing or cannot be resolved", () => {
|
|
const missingActive = resolveActiveViewSyncDecision({
|
|
savedViews: [importantView],
|
|
activeView: undefined,
|
|
currentActiveViewName: "Important",
|
|
currentFilters: statusFilter("Important"),
|
|
currentViewColumns: ["Name", "Status", "Owner"],
|
|
});
|
|
|
|
const unknownActive = resolveActiveViewSyncDecision({
|
|
savedViews: [importantView],
|
|
activeView: "Unknown",
|
|
currentActiveViewName: "Important",
|
|
currentFilters: statusFilter("Important"),
|
|
currentViewColumns: ["Name", "Status", "Owner"],
|
|
});
|
|
|
|
expect(missingActive).toBeNull();
|
|
expect(unknownActive).toBeNull();
|
|
});
|
|
|
|
// --- New tests for viewType and settings ---
|
|
|
|
it("detects viewType change and triggers re-apply (prevents stale view mode after external edit)", () => {
|
|
const kanbanView: SavedView = {
|
|
name: "Board",
|
|
view_type: "kanban",
|
|
settings: { kanbanField: "Status" },
|
|
};
|
|
|
|
const decision = resolveActiveViewSyncDecision({
|
|
savedViews: [kanbanView],
|
|
activeView: "Board",
|
|
currentActiveViewName: "Board",
|
|
currentFilters: emptyFilterGroup(),
|
|
currentViewColumns: undefined,
|
|
currentViewType: "table",
|
|
currentSettings: undefined,
|
|
});
|
|
|
|
expect(decision).not.toBeNull();
|
|
expect(decision?.shouldApply).toBe(true);
|
|
expect(decision?.nextViewType).toBe("kanban");
|
|
expect(decision?.nextSettings).toEqual({ kanbanField: "Status" });
|
|
});
|
|
|
|
it("detects settings change while name and filters stay the same (calendar mode changed externally)", () => {
|
|
const calView: SavedView = {
|
|
name: "Monthly",
|
|
view_type: "calendar",
|
|
settings: { calendarDateField: "Due Date", calendarMode: "month" },
|
|
};
|
|
|
|
const decision = resolveActiveViewSyncDecision({
|
|
savedViews: [calView],
|
|
activeView: "Monthly",
|
|
currentActiveViewName: "Monthly",
|
|
currentFilters: emptyFilterGroup(),
|
|
currentViewColumns: undefined,
|
|
currentViewType: "calendar",
|
|
currentSettings: { calendarDateField: "Due Date", calendarMode: "week" },
|
|
});
|
|
|
|
expect(decision?.shouldApply).toBe(true);
|
|
expect(decision?.nextSettings?.calendarMode).toBe("month");
|
|
});
|
|
|
|
it("does not re-apply when viewType and settings are already aligned", () => {
|
|
const view: SavedView = {
|
|
name: "Timeline",
|
|
view_type: "timeline",
|
|
settings: { timelineStartField: "Start", timelineEndField: "End" },
|
|
};
|
|
|
|
const decision = resolveActiveViewSyncDecision({
|
|
savedViews: [view],
|
|
activeView: "Timeline",
|
|
currentActiveViewName: "Timeline",
|
|
currentFilters: emptyFilterGroup(),
|
|
currentViewColumns: undefined,
|
|
currentViewType: "timeline",
|
|
currentSettings: { timelineStartField: "Start", timelineEndField: "End" },
|
|
});
|
|
|
|
expect(decision?.shouldApply).toBe(false);
|
|
});
|
|
|
|
it("detects viewType-only change when settings are identical (prevents stuck view mode)", () => {
|
|
const view: SavedView = {
|
|
name: "Gallery",
|
|
view_type: "gallery",
|
|
settings: { galleryTitleField: "Name" },
|
|
};
|
|
|
|
const decision = resolveActiveViewSyncDecision({
|
|
savedViews: [view],
|
|
activeView: "Gallery",
|
|
currentActiveViewName: "Gallery",
|
|
currentFilters: emptyFilterGroup(),
|
|
currentViewColumns: undefined,
|
|
currentViewType: "table",
|
|
currentSettings: { galleryTitleField: "Name" },
|
|
});
|
|
|
|
expect(decision?.shouldApply).toBe(true);
|
|
expect(decision?.nextViewType).toBe("gallery");
|
|
});
|
|
|
|
it("propagates undefined viewType without crashing (backwards compat with old saved views)", () => {
|
|
const legacyView: SavedView = {
|
|
name: "Legacy",
|
|
filters: statusFilter("Active"),
|
|
};
|
|
|
|
const decision = resolveActiveViewSyncDecision({
|
|
savedViews: [legacyView],
|
|
activeView: "Legacy",
|
|
currentActiveViewName: undefined,
|
|
currentFilters: emptyFilterGroup(),
|
|
currentViewColumns: undefined,
|
|
currentViewType: "table",
|
|
currentSettings: {},
|
|
});
|
|
|
|
expect(decision?.shouldApply).toBe(true);
|
|
expect(decision?.nextViewType).toBeUndefined();
|
|
expect(decision?.nextSettings).toBeUndefined();
|
|
});
|
|
});
|