openclaw/apps/web/app/workspace/object-view-active-view.test.ts

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();
});
});