import { AppStateComponentNames, createKeyFromPath, deleteState, loadState, MAX_ENTRY_NB, PATH_SEPARATOR, saveState, } from "Shared/AppStatePersistenceUtility"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; jest.mock("Shared/StorageUtility", () => ({ LocalStorageUtility: { getEntryObject: jest.fn(), setEntryObject: jest.fn(), }, StorageKey: { AppState: "AppState", }, })); describe("AppStatePersistenceUtility", () => { const storePath = { componentName: AppStateComponentNames.DocumentsTab, subComponentName: "b", globalAccountName: "c", databaseName: "d", containerName: "e", }; const key = createKeyFromPath(storePath); beforeEach(() => { jest.clearAllMocks(); }); beforeEach(() => { (LocalStorageUtility.getEntryObject as jest.Mock).mockReturnValue({ key0: { schemaVersion: 1, timestamp: 0, data: {}, }, }); }); describe("saveState()", () => { const testState = { aa: 1, bb: "2", cc: [3, 4] }; it("should save state", () => { saveState(storePath, testState); expect(LocalStorageUtility.setEntryObject).toHaveBeenCalledTimes(1); expect(LocalStorageUtility.setEntryObject).toHaveBeenCalledWith(StorageKey.AppState, expect.any(Object)); const passedState = (LocalStorageUtility.setEntryObject as jest.Mock).mock.calls[0][1]; expect(passedState[key].data).toHaveProperty("aa", 1); }); it("should save state with timestamp", () => { saveState(storePath, testState); const passedState = (LocalStorageUtility.setEntryObject as jest.Mock).mock.calls[0][1]; expect(passedState[key]).toHaveProperty("timestamp"); expect(passedState[key].timestamp).toBeGreaterThan(0); }); it("should add state to existing state", () => { (LocalStorageUtility.getEntryObject as jest.Mock).mockReturnValue({ key0: { schemaVersion: 1, timestamp: 0, data: { dd: 5 }, }, }); saveState(storePath, testState); const passedState = (LocalStorageUtility.setEntryObject as jest.Mock).mock.calls[0][1]; expect(passedState["key0"].data).toHaveProperty("dd", 5); }); it("should remove the oldest entry when the number of entries exceeds the limit", () => { // Fill up storage with MAX entries const currentAppState = {}; for (let i = 0; i < MAX_ENTRY_NB; i++) { // eslint-disable-next-line @typescript-eslint/no-explicit-any (currentAppState as any)[`key${i}`] = { schemaVersion: 1, timestamp: i, data: {}, }; } (LocalStorageUtility.getEntryObject as jest.Mock).mockReturnValue(currentAppState); saveState(storePath, testState); // Verify that the new entry is saved const passedState = (LocalStorageUtility.setEntryObject as jest.Mock).mock.calls[0][1]; expect(passedState[key].data).toHaveProperty("aa", 1); // Verify that the oldest entry is removed (smallest timestamp) const passedAppState = (LocalStorageUtility.setEntryObject as jest.Mock).mock.calls[0][1]; expect(Object.keys(passedAppState).length).toBe(MAX_ENTRY_NB); expect(passedAppState).not.toHaveProperty("key0"); }); it("should not remove the oldest entry when the number of entries does not exceed the limit", () => { (LocalStorageUtility.getEntryObject as jest.Mock).mockReturnValue({ key0: { schemaVersion: 1, timestamp: 0, data: {}, }, key1: { schemaVersion: 1, timestamp: 1, data: {}, }, }); saveState(storePath, testState); const passedAppState = (LocalStorageUtility.setEntryObject as jest.Mock).mock.calls[0][1]; expect(Object.keys(passedAppState).length).toBe(3); }); }); describe("loadState()", () => { it("should load state", () => { const data = { aa: 1, bb: "2", cc: [3, 4] }; const testState = { [key]: { schemaVersion: 1, timestamp: 0, data, }, }; (LocalStorageUtility.getEntryObject as jest.Mock).mockReturnValue(testState); const state = loadState(storePath); expect(state).toEqual(data); }); it("should return undefined if the state is not found", () => { (LocalStorageUtility.getEntryObject as jest.Mock).mockReturnValue(null); const state = loadState(storePath); expect(state).toBeUndefined(); }); }); describe("deleteState()", () => { it("should delete state", () => { const key = createKeyFromPath(storePath); (LocalStorageUtility.getEntryObject as jest.Mock).mockReturnValue({ [key]: { schemaVersion: 1, timestamp: 0, data: {}, }, otherKey: { schemaVersion: 2, timestamp: 0, data: {}, }, }); deleteState(storePath); expect(LocalStorageUtility.setEntryObject).toHaveBeenCalledTimes(1); const passedAppState = (LocalStorageUtility.setEntryObject as jest.Mock).mock.calls[0][1]; expect(passedAppState).not.toHaveProperty(key); expect(passedAppState).toHaveProperty("otherKey"); }); }); describe("createKeyFromPath()", () => { it("should create path that contains all components", () => { const key = createKeyFromPath(storePath); expect(key).toContain(storePath.componentName); expect(key).toContain(storePath.subComponentName); expect(key).toContain(storePath.globalAccountName); expect(key).toContain(storePath.databaseName); expect(key).toContain(storePath.containerName); }); it("should handle components that include special characters", () => { const storePath = { componentName: AppStateComponentNames.DocumentsTab, subComponentName: 'd"e"f', globalAccountName: "g:hi{j", databaseName: "a/b/c", containerName: "https://blahblah.document.azure.com:443/", }; const key = createKeyFromPath(storePath); const segments = key.split(PATH_SEPARATOR); expect(segments.length).toEqual(6); // There should be 5 segments expect(segments[0]).toBe(""); const expectSubstringsInValue = (value: string, subStrings: string[]): boolean => subStrings.every((subString) => value.includes(subString)); expect(expectSubstringsInValue(segments[2], ["d", "e", "f"])).toBe(true); expect(expectSubstringsInValue(segments[3], ["g", "hi", "j"])).toBe(true); expect(expectSubstringsInValue(segments[4], ["a", "b", "c"])).toBe(true); expect(expectSubstringsInValue(segments[5], ["https", "blahblah", "document", "com", "443"])).toBe(true); }); }); });