Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { act, renderHook } from "@testing-library/react";
import { useCallback, useState } from "react";
import { getLocalStorage, setLocalStorage } from "@/utils/local-storage-util";

// Mock the localStorage utilities
jest.mock("@/utils/local-storage-util", () => ({
getLocalStorage: jest.fn(),
setLocalStorage: jest.fn(),
}));

// Define the function locally to avoid import issues
const getBooleanFromStorage = (key: string, defaultValue: boolean): boolean => {
const stored = getLocalStorage(key);
return stored === null ? defaultValue : stored === "true";
};

const mockGetLocalStorage = getLocalStorage as jest.MockedFunction<
typeof getLocalStorage
>;
const mockSetLocalStorage = setLocalStorage as jest.MockedFunction<
typeof setLocalStorage
>;

// Custom hook that mimics the localStorage functionality in the sidebar component
const useLocalStorageFeatures = () => {
const showBetaStorage = getBooleanFromStorage("showBeta", true);
const showLegacyStorage = getBooleanFromStorage("showLegacy", false);

const [showBeta, setShowBeta] = useState(showBetaStorage);
const [showLegacy, setShowLegacy] = useState(showLegacyStorage);

const handleSetShowBeta = useCallback((value: boolean) => {
setShowBeta(value);
setLocalStorage("showBeta", value.toString());
}, []);

const handleSetShowLegacy = useCallback((value: boolean) => {
setShowLegacy(value);
setLocalStorage("showLegacy", value.toString());
}, []);

return {
showBeta,
showLegacy,
handleSetShowBeta,
handleSetShowLegacy,
};
};

describe("Sidebar localStorage Feature", () => {
beforeEach(() => {
jest.clearAllMocks();
});

describe("Default behavior (first time users)", () => {
beforeEach(() => {
mockGetLocalStorage.mockReturnValue(null);
});

it("should initialize showBeta as true by default", () => {
const { result } = renderHook(() => useLocalStorageFeatures());
expect(result.current.showBeta).toBe(true);
});

it("should initialize showLegacy as false by default", () => {
const { result } = renderHook(() => useLocalStorageFeatures());
expect(result.current.showLegacy).toBe(false);
});
});

describe("Persisted preferences (returning users)", () => {
it("should respect stored showBeta=false preference", () => {
mockGetLocalStorage
.mockReturnValueOnce("false") // showBeta
.mockReturnValueOnce(null); // showLegacy

const { result } = renderHook(() => useLocalStorageFeatures());
expect(result.current.showBeta).toBe(false);
expect(result.current.showLegacy).toBe(false);
});

it("should respect stored showLegacy=true preference", () => {
mockGetLocalStorage
.mockReturnValueOnce(null) // showBeta
.mockReturnValueOnce("true"); // showLegacy

const { result } = renderHook(() => useLocalStorageFeatures());
expect(result.current.showBeta).toBe(true);
expect(result.current.showLegacy).toBe(true);
});
});

describe("Updating preferences", () => {
beforeEach(() => {
mockGetLocalStorage.mockReturnValue(null);
});

it("should update showBeta and save to localStorage", () => {
const { result } = renderHook(() => useLocalStorageFeatures());

act(() => {
result.current.handleSetShowBeta(false);
});

expect(result.current.showBeta).toBe(false);
expect(mockSetLocalStorage).toHaveBeenCalledWith("showBeta", "false");
});

it("should update showLegacy and save to localStorage", () => {
const { result } = renderHook(() => useLocalStorageFeatures());

act(() => {
result.current.handleSetShowLegacy(true);
});

expect(result.current.showLegacy).toBe(true);
expect(mockSetLocalStorage).toHaveBeenCalledWith("showLegacy", "true");
});
});

describe("Multiple updates", () => {
beforeEach(() => {
mockGetLocalStorage.mockReturnValue(null);
});

it("should handle toggling showBeta multiple times", () => {
const { result } = renderHook(() => useLocalStorageFeatures());

// Toggle to false
act(() => {
result.current.handleSetShowBeta(false);
});
expect(result.current.showBeta).toBe(false);

// Toggle back to true
act(() => {
result.current.handleSetShowBeta(true);
});
expect(result.current.showBeta).toBe(true);

expect(mockSetLocalStorage).toHaveBeenCalledTimes(2);
expect(mockSetLocalStorage).toHaveBeenCalledWith("showBeta", "false");
expect(mockSetLocalStorage).toHaveBeenCalledWith("showBeta", "true");
});
});

describe("Real-world scenarios", () => {
it("should simulate complete user journey", () => {
// First visit - defaults
mockGetLocalStorage.mockReturnValue(null);
const { result } = renderHook(() => useLocalStorageFeatures());

expect(result.current.showBeta).toBe(true);
expect(result.current.showLegacy).toBe(false);

// User changes preferences
act(() => {
result.current.handleSetShowBeta(false);
result.current.handleSetShowLegacy(true);
});

expect(result.current.showBeta).toBe(false);
expect(result.current.showLegacy).toBe(true);
expect(mockSetLocalStorage).toHaveBeenCalledWith("showBeta", "false");
expect(mockSetLocalStorage).toHaveBeenCalledWith("showLegacy", "true");
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ import { useGetMCPServers } from "@/controllers/API/queries/mcp/use-get-mcp-serv
import { ENABLE_NEW_SIDEBAR } from "@/customization/feature-flags";
import { useAddComponent } from "@/hooks/use-add-component";
import { useShortcutsStore } from "@/stores/shortcuts";
import { getLocalStorage, setLocalStorage } from "@/utils/local-storage-util";
import {
nodeColors,
SIDEBAR_BUNDLES,
SIDEBAR_CATEGORIES,
} from "@/utils/styleUtils";
import { cn } from "@/utils/utils";
import { cn, getBooleanFromStorage } from "@/utils/utils";
import useFlowStore from "../../../../stores/flowStore";
import { useTypesStore } from "../../../../stores/typesStore";
import type { APIClassType } from "../../../../types/api";
Expand Down Expand Up @@ -182,12 +183,26 @@ export function FlowSidebarComponent({ isLoading }: FlowSidebarComponentProps) {
handleInputChange = () => {},
} = context;

const showBetaStorage = getBooleanFromStorage("showBeta", true);
const showLegacyStorage = getBooleanFromStorage("showLegacy", false);

// State
const [fuse, setFuse] = useState<Fuse<any> | null>(null);
const [openCategories, setOpenCategories] = useState<string[]>([]);
const [showConfig, setShowConfig] = useState(false);
const [showBeta, setShowBeta] = useState(true);
const [showLegacy, setShowLegacy] = useState(false);
const [showBeta, setShowBeta] = useState(showBetaStorage);
const [showLegacy, setShowLegacy] = useState(showLegacyStorage);

// Functions to handle state changes with localStorage persistence
const handleSetShowBeta = useCallback((value: boolean) => {
setShowBeta(value);
setLocalStorage("showBeta", value.toString());
}, []);

const handleSetShowLegacy = useCallback((value: boolean) => {
setShowLegacy(value);
setLocalStorage("showLegacy", value.toString());
}, []);
const [mcpSearchData, setMcpSearchData] = useState<any[]>([]);

// Create base data that includes MCP category when available
Expand Down Expand Up @@ -501,9 +516,9 @@ export function FlowSidebarComponent({ isLoading }: FlowSidebarComponentProps) {
showConfig={showConfig}
setShowConfig={setShowConfig}
showBeta={showBeta}
setShowBeta={setShowBeta}
setShowBeta={handleSetShowBeta}
showLegacy={showLegacy}
setShowLegacy={setShowLegacy}
setShowLegacy={handleSetShowLegacy}
searchInputRef={searchInputRef}
isInputFocused={isSearchFocused}
search={search}
Expand Down
109 changes: 109 additions & 0 deletions src/frontend/src/utils/__tests__/getBooleanFromStorage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { getLocalStorage } from "../local-storage-util";

// Mock the localStorage utility
jest.mock("../local-storage-util", () => ({
getLocalStorage: jest.fn(),
}));

const mockGetLocalStorage = getLocalStorage as jest.MockedFunction<
typeof getLocalStorage
>;

// The function we're testing (copied directly from utils.ts to avoid import issues)
const getBooleanFromStorage = (key: string, defaultValue: boolean): boolean => {
const stored = getLocalStorage(key);
return stored === null ? defaultValue : stored === "true";
};

describe("getBooleanFromStorage", () => {
beforeEach(() => {
jest.clearAllMocks();
});

describe("when localStorage contains no value (null)", () => {
beforeEach(() => {
mockGetLocalStorage.mockReturnValue(null);
});

it("should return true when defaultValue is true", () => {
const result = getBooleanFromStorage("testKey", true);
expect(result).toBe(true);
expect(mockGetLocalStorage).toHaveBeenCalledWith("testKey");
});

it("should return false when defaultValue is false", () => {
const result = getBooleanFromStorage("testKey", false);
expect(result).toBe(false);
expect(mockGetLocalStorage).toHaveBeenCalledWith("testKey");
});
});

describe("when localStorage contains 'true' string", () => {
beforeEach(() => {
mockGetLocalStorage.mockReturnValue("true");
});

it("should return true regardless of defaultValue", () => {
const resultWithTrueDefault = getBooleanFromStorage("testKey", true);
const resultWithFalseDefault = getBooleanFromStorage("testKey", false);

expect(resultWithTrueDefault).toBe(true);
expect(resultWithFalseDefault).toBe(true);
expect(mockGetLocalStorage).toHaveBeenCalledTimes(2);
});
});

describe("when localStorage contains 'false' string", () => {
beforeEach(() => {
mockGetLocalStorage.mockReturnValue("false");
});

it("should return false regardless of defaultValue", () => {
const resultWithTrueDefault = getBooleanFromStorage("testKey", true);
const resultWithFalseDefault = getBooleanFromStorage("testKey", false);

expect(resultWithTrueDefault).toBe(false);
expect(resultWithFalseDefault).toBe(false);
expect(mockGetLocalStorage).toHaveBeenCalledTimes(2);
});
});

describe("when localStorage contains other string values", () => {
it.each(["1", "yes", "TRUE", "False", "anything else", ""])(
"should return false for non-'true' string: '%s'",
(value) => {
mockGetLocalStorage.mockReturnValue(value);

const result = getBooleanFromStorage("testKey", true);
expect(result).toBe(false);
},
);
});

describe("real-world scenarios", () => {
it("should work correctly for showBeta with default true", () => {
mockGetLocalStorage.mockReturnValue(null);

const result = getBooleanFromStorage("showBeta", true);
expect(result).toBe(true);
});

it("should work correctly for showLegacy with default false", () => {
mockGetLocalStorage.mockReturnValue(null);

const result = getBooleanFromStorage("showLegacy", false);
expect(result).toBe(false);
});

it("should preserve user preferences when stored", () => {
mockGetLocalStorage.mockReturnValue("false");

const betaResult = getBooleanFromStorage("showBeta", true);
expect(betaResult).toBe(false); // User chose to disable beta

mockGetLocalStorage.mockReturnValue("true");
const legacyResult = getBooleanFromStorage("showLegacy", false);
expect(legacyResult).toBe(true); // User chose to enable legacy
});
});
});
9 changes: 9 additions & 0 deletions src/frontend/src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type {
import type { AllNodeType, NodeDataType } from "../types/flow";
import type { FlowState } from "../types/tabs";
import { isErrorLog } from "../types/utils/typeCheckingUtils";
import { getLocalStorage } from "./local-storage-util";
import { parseString } from "./stringManipulation";

export function classNames(...classes: Array<string>): string {
Expand Down Expand Up @@ -1022,3 +1023,11 @@ export const setAuthCookie = (
sameSite: "strict",
});
};

export const getBooleanFromStorage = (
key: string,
defaultValue: boolean,
): boolean => {
const stored = getLocalStorage(key);
return stored === null ? defaultValue : stored === "true";
};
Loading