diff --git a/packages/location-state-core/src/stores/storage-store.test.ts b/packages/location-state-core/src/stores/storage-store.test.ts index 83c772b1..35278693 100644 --- a/packages/location-state-core/src/stores/storage-store.test.ts +++ b/packages/location-state-core/src/stores/storage-store.test.ts @@ -127,6 +127,24 @@ test("On `load` called with serializer, if the value of the corresponding key is expect(store.get("foo")).toBe("dummy-result"); }); +test("On `load` called with invalid serializer, the value of slice remains at its initial value.", () => { + // Arrange + const navigationKey = "current_location"; + storageMock.getItem.mockReturnValueOnce( + JSON.stringify({ foo: "storage value" }), + ); + const store = new StorageStore(storage, { + serialize: JSON.stringify, + deserialize: () => { + throw new Error("deserialize error"); + }, + }); + // Act + store.load(navigationKey); + // Assert + expect(store.get("foo")).toBeUndefined(); +}); + test("On `load` called, all listener notified.", async () => { // Arrange const navigationKey = "current_location"; @@ -181,6 +199,24 @@ test("On `save` called with serializer, the state is saved in Storage with the p ); }); +test("On `save` called with invalid serializer, the state is not saved in Storage.", () => { + // Arrange + const currentLocationKey = "current_location"; + const store = new StorageStore(storage, { + serialize: () => { + throw new Error("serialize error"); + }, + deserialize: JSON.parse, + }); + store.load(currentLocationKey); + store.set("foo", "updated"); + // Act + store.save(); + // Assert + expect(store.get("foo")).toBe("updated"); + expect(storageMock.setItem).not.toBeCalled(); +}); + test("Calling `save` with empty will remove the Storage with Location key.", () => { // Arrange const currentLocationKey = "current_location"; diff --git a/packages/location-state-core/src/stores/storage-store.ts b/packages/location-state-core/src/stores/storage-store.ts index 5cd8d069..e5994c23 100644 --- a/packages/location-state-core/src/stores/storage-store.ts +++ b/packages/location-state-core/src/stores/storage-store.ts @@ -59,7 +59,11 @@ export class StorageStore implements Store { this.currentKey = locationKey; const value = this.storage?.getItem(this.createStorageKey()) ?? null; if (value !== null) { - this.state = this.serializer.deserialize(value); + try { + this.state = this.serializer.deserialize(value); + } catch (e) { + this.state = {}; + } } else { this.state = {}; } @@ -74,10 +78,13 @@ export class StorageStore implements Store { this.storage?.removeItem(this.createStorageKey()); return; } - this.storage?.setItem( - this.createStorageKey(), - this.serializer.serialize(this.state), - ); + let value: string; + try { + value = this.serializer.serialize(this.state); + } catch (e) { + return; + } + this.storage?.setItem(this.createStorageKey(), value); } private createStorageKey() { diff --git a/packages/location-state-core/src/stores/url-store.test.ts b/packages/location-state-core/src/stores/url-store.test.ts index 52b1565e..a9e5de24 100644 --- a/packages/location-state-core/src/stores/url-store.test.ts +++ b/packages/location-state-core/src/stores/url-store.test.ts @@ -39,7 +39,7 @@ test("If params is empty, the initial value is undefined.", () => { expect(slice).toBeUndefined(); }); -test("On `set` called, store's values are updated and reflected in the URL", () => { +test("On `set` called, store's values are updated and reflected in the URL.", () => { // Arrange prepareLocation({ pathname: "/", @@ -56,7 +56,7 @@ test("On `set` called, store's values are updated and reflected in the URL", () ); }); -test("On `set` called with serializer, store's values are updated and reflected in the URL", () => { +test("On `set` called with serializer, store's values are updated and reflected in the URL.", () => { // Arrange prepareLocation({ pathname: "/", @@ -78,6 +78,30 @@ test("On `set` called with serializer, store's values are updated and reflected ); }); +test("On `set` called with invalid serializer, store's values are initial value and not reflected in the URL.", () => { + // Arrange + const consoleSpy = jest.spyOn(console, "error").mockImplementation(); + prepareLocation({ + pathname: "/", + search: "?hoge=fuga", + }); + const store = new URLStore("store-key", syncerMock, { + serialize: () => { + throw new Error("serialize error"); + }, + deserialize: () => ({ + foo: "not-used-value", + }), + }); + // Act + store.set("foo", "updated"); + // Assert + expect(store.get("foo")).toBe("updated"); + expect(syncerMock.updateURL).not.toHaveBeenCalled(); + // post processing + consoleSpy.mockRestore(); +}); + test("listener is called when updating slice.", () => { // Arrange const store = new URLStore("store-key", syncerMock); @@ -168,6 +192,29 @@ test("On `load` called with serializer, the value is obtained through serialize. expect(store.get("foo")).toBe("dummy-result"); }); +test("On `load` called with invalid serializer, the value is initial value.", () => { + // Arrange + const consoleSpy = jest.spyOn(console, "error").mockImplementation(); + prepareLocation({ + pathname: "/", + search: "?store-key=%7B%22foo%22%3A%22updated%22%7D", + }); + const store = new URLStore("store-key", syncerMock, { + serialize: JSON.stringify, + deserialize: () => { + throw new Error("deserialize error"); + }, + }); + // Act + store.load(); + // Assert + expect(store.get("foo")).toBeUndefined(); + expect(syncerMock.updateURL).toHaveBeenCalledTimes(1); + expect(syncerMock.updateURL).toHaveBeenCalledWith("http://localhost/"); + // post processing + consoleSpy.mockRestore(); +}); + test("On `load` called, all listener notified.", async () => { // Arrange const store = new URLStore("store-key", syncerMock); diff --git a/packages/location-state-core/src/stores/url-store.ts b/packages/location-state-core/src/stores/url-store.ts index d3710054..7499a5f0 100644 --- a/packages/location-state-core/src/stores/url-store.ts +++ b/packages/location-state-core/src/stores/url-store.ts @@ -55,7 +55,14 @@ export class URLStore implements Store { } else { this.state[name] = value; } - this.stateJSON = this.serializer.serialize(this.state); + try { + this.stateJSON = this.serializer.serialize(this.state); + } catch (e) { + console.error(e); + this.notify(name); + // Not reflected in URL. + return; + } // save to url const url = new URL(location.href); url.searchParams.set(this.key, this.stateJSON);