diff --git a/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts b/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts index 22ce8d4564..a5681d65c0 100644 --- a/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts +++ b/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts @@ -8,6 +8,23 @@ import { import { fromChromeEvent } from "../../browser/from-chrome-event"; +export const serializationIndicator = "__json__"; + +export const objToStore = (obj: any) => { + if (obj == null) { + return null; + } + + if (obj instanceof Set) { + obj = Array.from(obj); + } + + return { + [serializationIndicator]: true, + value: JSON.stringify(obj), + }; +}; + export default abstract class AbstractChromeStorageService implements AbstractStorageService, ObservableStorageService { @@ -44,7 +61,11 @@ export default abstract class AbstractChromeStorageService return new Promise((resolve) => { this.chromeStorageApi.get(key, (obj: any) => { if (obj != null && obj[key] != null) { - resolve(obj[key] as T); + let value = obj[key]; + if (value[serializationIndicator] && typeof value.value === "string") { + value = JSON.parse(value.value); + } + resolve(value as T); return; } resolve(null); @@ -57,14 +78,7 @@ export default abstract class AbstractChromeStorageService } async save(key: string, obj: any): Promise { - if (obj == null) { - // Fix safari not liking null in set - return this.remove(key); - } - - if (obj instanceof Set) { - obj = Array.from(obj); - } + obj = objToStore(obj); const keyedObj = { [key]: obj }; return new Promise((resolve) => { diff --git a/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts b/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts new file mode 100644 index 0000000000..bb89dc8a6a --- /dev/null +++ b/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts @@ -0,0 +1,99 @@ +import AbstractChromeStorageService, { + objToStore, + serializationIndicator, +} from "./abstract-chrome-storage-api.service"; + +class TestChromeStorageApiService extends AbstractChromeStorageService {} + +describe("objectToStore", () => { + it("converts an object to a tagged string", () => { + const obj = { key: "value" }; + const result = objToStore(obj); + expect(result).toEqual({ + [serializationIndicator]: true, + value: JSON.stringify(obj), + }); + }); + + it("converts a set to an array prior to serialization", () => { + const obj = new Set(["value"]); + const result = objToStore(obj); + expect(result).toEqual({ + [serializationIndicator]: true, + value: JSON.stringify(Array.from(obj)), + }); + }); + + it("does nothing to null", () => { + expect(objToStore(null)).toEqual(null); + }); +}); + +describe("ChromeStorageApiService", () => { + let service: TestChromeStorageApiService; + let store: Record; + + beforeEach(() => { + store = {}; + + service = new TestChromeStorageApiService(chrome.storage.local); + }); + + describe("save", () => { + let setMock: jest.Mock; + + beforeEach(() => { + // setup save + setMock = chrome.storage.local.set as jest.Mock; + setMock.mockImplementation((data, callback) => { + Object.assign(store, data); + callback(); + }); + }); + + it("uses `objToStore` to prepare a value for set", async () => { + const key = "key"; + const value = { key: "value" }; + await service.save(key, value); + expect(setMock).toHaveBeenCalledWith( + { + [key]: objToStore(value), + }, + expect.any(Function), + ); + }); + }); + + describe("get", () => { + let getMock: jest.Mock; + + beforeEach(() => { + // setup get + getMock = chrome.storage.local.get as jest.Mock; + getMock.mockImplementation((key, callback) => { + callback({ [key]: store[key] }); + }); + }); + + it("returns a stored value when it is serialized", async () => { + const key = "key"; + const value = { key: "value" }; + store[key] = objToStore(value); + const result = await service.get(key); + expect(result).toEqual(value); + }); + + it("returns a stored value when it is not serialized", async () => { + const key = "key"; + const value = "value"; + store[key] = value; + const result = await service.get(key); + expect(result).toEqual(value); + }); + + it("returns null when the key does not exist", async () => { + const result = await service.get("key"); + expect(result).toBeNull(); + }); + }); +});