diff --git a/index-node.cjs b/index-node.cjs index c39c8ff3..21e1b178 100644 --- a/index-node.cjs +++ b/index-node.cjs @@ -4,5 +4,6 @@ module.exports = aa.default; module.exports.createInsightsClient = aa.createInsightsClient; module.exports.getRequesterForNode = aa.getRequesterForNode; module.exports.AlgoliaAnalytics = aa.AlgoliaAnalytics; +module.exports.LocalStorage = aa.LocalStorage; module.exports.getFunctionalInterface = aa.getFunctionalInterface; module.exports.processQueue = aa.processQueue; diff --git a/lib/entry-browser.ts b/lib/entry-browser.ts index f2451881..eea574e9 100644 --- a/lib/entry-browser.ts +++ b/lib/entry-browser.ts @@ -3,11 +3,13 @@ import { getFunctionalInterface } from "./_getFunctionalInterface"; import { processQueue } from "./_processQueue"; import AlgoliaAnalytics from "./insights"; import { getRequesterForBrowser } from "./utils/getRequesterForBrowser"; +import { LocalStorage } from "./utils/localStorage"; export { createInsightsClient, getRequesterForBrowser, AlgoliaAnalytics, + LocalStorage, getFunctionalInterface, processQueue }; diff --git a/lib/entry-node.ts b/lib/entry-node.ts index 4e38d436..66f46211 100644 --- a/lib/entry-node.ts +++ b/lib/entry-node.ts @@ -3,11 +3,13 @@ import { getFunctionalInterface } from "./_getFunctionalInterface"; import { processQueue } from "./_processQueue"; import AlgoliaAnalytics from "./insights"; import { getRequesterForNode } from "./utils/getRequesterForNode"; +import { LocalStorage } from "./utils/localStorage"; export { createInsightsClient, getRequesterForNode, AlgoliaAnalytics, + LocalStorage, getFunctionalInterface, processQueue }; diff --git a/lib/utils/__tests__/localStorage.test.ts b/lib/utils/__tests__/localStorage.test.ts index 99c26ca8..03747e4e 100644 --- a/lib/utils/__tests__/localStorage.test.ts +++ b/lib/utils/__tests__/localStorage.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-classes-per-file */ import { LocalStorage } from "../localStorage"; const setItemMock = jest.spyOn(Object.getPrototypeOf(localStorage), "setItem"); @@ -7,71 +8,161 @@ const consoleErrorSpy = jest describe("LocalStorage", () => { beforeEach(() => { - localStorage.clear(); + jest.clearAllMocks(); }); - it("gets a value from localStorage", () => { - const key = "testKey"; - const value = "testValue"; - localStorage.setItem(key, JSON.stringify(value)); + describe("when localStorage is defined", () => { + beforeEach(() => { + localStorage.clear(); + }); - const result = LocalStorage.get(key); + it("gets a value from localStorage", () => { + const key = "testKey"; + const value = "testValue"; + localStorage.setItem(key, JSON.stringify(value)); - expect(result).toEqual(value); - }); + const result = LocalStorage.get(key); - it("returns null if the key is not found", () => { - const key = "nonExistentKey"; + expect(result).toEqual(value); + }); - const result = LocalStorage.get(key); + it("returns null if the key is not found", () => { + const key = "nonExistentKey"; - expect(result).toBeNull(); - }); + const result = LocalStorage.get(key); - it("returns null if the value cannot be parsed as JSON", () => { - const key = "testKey"; - const value = "invalidJSON"; - localStorage.setItem(key, value); + expect(result).toBeNull(); + }); - const result = LocalStorage.get(key); + it("returns null if the value cannot be parsed as JSON", () => { + const key = "testKey"; + const value = "invalidJSON"; + localStorage.setItem(key, value); - expect(result).toBeNull(); - }); + const result = LocalStorage.get(key); - it("sets a value in localStorage", () => { - const key = "testKey"; - const value = "testValue"; + expect(result).toBeNull(); + }); - LocalStorage.set(key, value); + it("sets a value in localStorage", () => { + const key = "testKey"; + const value = "testValue"; - const result = localStorage.getItem(key); - expect(result).toEqual(JSON.stringify(value)); - }); + LocalStorage.set(key, value); - it("catches the error and logs an error if the storage is full", () => { - const key = "testKey"; - const value = "testValue"; + const result = localStorage.getItem(key); + expect(result).toEqual(JSON.stringify(value)); + }); + + it("catches the error and logs an error if the storage is full", () => { + const key = "testKey"; + const value = "testValue"; + + // Emulate filling up the storage + setItemMock.mockImplementationOnce(() => { + throw new Error("pretend QuotaExceededError"); + }); + + LocalStorage.set(key, value); - // Emulate filling up the storage - setItemMock.mockImplementationOnce(() => { - throw new Error("pretend QuotaExceededError"); + const result = localStorage.getItem(key); + expect(result).toBeNull(); + expect(consoleErrorSpy).toHaveBeenCalledTimes(1); }); - LocalStorage.set(key, value); + it("removes a value from localStorage", () => { + const key = "testKey"; + const value = "testValue"; + localStorage.setItem(key, JSON.stringify(value)); + + LocalStorage.remove(key); + + const result = localStorage.getItem(key); + expect(result).toBeNull(); + }); + }); - const result = localStorage.getItem(key); - expect(result).toBeNull(); - expect(consoleErrorSpy).toHaveBeenCalledTimes(1); + describe("when localStorage is not defined", () => { + class LocalStorageWithoutStore extends LocalStorage {} + LocalStorageWithoutStore.store = undefined; + + it("doesn't throw errors", () => { + expect(LocalStorageWithoutStore.get("testKey")).toBeNull(); + expect(() => + LocalStorageWithoutStore.set("testKey", "testValue") + ).not.toThrow(); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + expect(() => LocalStorageWithoutStore.remove("testKey")).not.toThrow(); + }); }); - it("removes a value from localStorage", () => { - const key = "testKey"; - const value = "testValue"; - localStorage.setItem(key, JSON.stringify(value)); + describe("when localStorage is replaced", () => { + const store = { + getItem: jest.fn() as jest.MockedFn, + setItem: jest.fn() as jest.MockedFn, + removeItem: jest.fn() as jest.MockedFn, + clear: jest.fn() as jest.MockedFn, + key: jest.fn() as jest.MockedFn, + length: 50 + }; + class LocalStorageWithReplacedStore extends LocalStorage {} + LocalStorageWithReplacedStore.store = store; + + it("gets a value from the localStorage replacement", () => { + const key = "testKey"; + const value = "testValue"; + store.getItem.mockReturnValue(JSON.stringify(value)); + + expect(LocalStorageWithReplacedStore.get(key)).toEqual(value); + expect( + LocalStorageWithReplacedStore.store?.getItem + ).toHaveBeenCalledTimes(1); + expect(LocalStorageWithReplacedStore.store?.getItem).toHaveBeenCalledWith( + key + ); + }); + + it("returns null if the key is not found in the localStorage replacement", () => { + const key = "nonExistentKey"; + store.getItem.mockReturnValue(null); - LocalStorage.remove(key); + expect(LocalStorageWithReplacedStore.get(key)).toBeNull(); + }); + + it("returns null if the value from the localStorage replacement cannot be parsed as JSON", () => { + const key = "testKey"; + const value = "invalidJSON"; + store.getItem.mockReturnValue(value); + + expect(LocalStorageWithReplacedStore.get(key)).toBeNull(); + }); - const result = localStorage.getItem(key); - expect(result).toBeNull(); + it("sets a value in the localStorage replacement", () => { + const key = "testKey"; + const value = "testValue"; + + LocalStorageWithReplacedStore.set(key, value); + + expect( + LocalStorageWithReplacedStore.store?.setItem + ).toHaveBeenCalledTimes(1); + expect(LocalStorageWithReplacedStore.store?.setItem).toHaveBeenCalledWith( + key, + JSON.stringify(value) + ); + }); + + it("removes a value from the localStorage replacement", () => { + const key = "testKey"; + + LocalStorageWithReplacedStore.remove(key); + + expect( + LocalStorageWithReplacedStore.store?.removeItem + ).toHaveBeenCalledTimes(1); + expect( + LocalStorageWithReplacedStore.store?.removeItem + ).toHaveBeenCalledWith(key); + }); }); }); diff --git a/lib/utils/localStorage.ts b/lib/utils/localStorage.ts index 16a91300..9b06ced4 100644 --- a/lib/utils/localStorage.ts +++ b/lib/utils/localStorage.ts @@ -2,7 +2,7 @@ * A utility class for safely interacting with localStorage. */ export class LocalStorage { - static readonly THRESHOLD = 0.9; + static store: Storage | undefined = globalThis.localStorage; /** * Safely get a value from localStorage. @@ -12,14 +12,14 @@ export class LocalStorage { * @returns Null if the key is not found or unable to be parsed, the value otherwise. */ static get(key: string): T | null { - const val = localStorage.getItem(key); + const val = this.store?.getItem(key); if (!val) { return null; } try { return JSON.parse(val) as T; - } catch (e) { + } catch { return null; } } @@ -33,7 +33,7 @@ export class LocalStorage { */ static set(key: string, value: any): void { try { - localStorage.setItem(key, JSON.stringify(value)); + this.store?.setItem(key, JSON.stringify(value)); } catch { // eslint-disable-next-line no-console console.error( @@ -48,6 +48,6 @@ export class LocalStorage { * @param key - String value of the key. */ static remove(key: string): void { - localStorage.removeItem(key); + this.store?.removeItem(key); } }