summaryrefslogtreecommitdiffhomepage
path: root/src/adapters/local-storage/index.test.ts
blob: 57103ddfa3082e4edf09f2038ab44b62a1fd420c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
import { describe, expect, it } from "vitest";
import { createLocalStore } from "./index";

function createMemoryStorage(): Storage {
	const map = new Map<string, string>();
	return {
		get length() {
			return map.size;
		},
		clear() {
			map.clear();
		},
		getItem(key: string) {
			return map.get(key) ?? null;
		},
		key(index: number) {
			return [...map.keys()][index] ?? null;
		},
		removeItem(key: string) {
			map.delete(key);
		},
		setItem(key: string, value: string) {
			map.set(key, value);
		},
	};
}

describe("createLocalStore", () => {
	it("save then load round-trips an object", () => {
		const storage = createMemoryStorage();
		const store = createLocalStore<{ name: string; count: number }>("test", { storage });

		store.save({ name: "alice", count: 42 });
		const loaded = store.load();

		expect(loaded).toEqual({ name: "alice", count: 42 });
	});

	it("load returns null when key is absent", () => {
		const storage = createMemoryStorage();
		const store = createLocalStore<string>("missing", { storage });

		expect(store.load()).toBeNull();
	});

	it("load returns null on corrupt JSON", () => {
		const storage = createMemoryStorage();
		storage.setItem("corrupt", "{not valid json!!!");
		const store = createLocalStore<object>("corrupt", { storage });

		expect(store.load()).toBeNull();
	});

	it("clear removes the value", () => {
		const storage = createMemoryStorage();
		const store = createLocalStore<string>("key", { storage });

		store.save("hello");
		expect(store.load()).toBe("hello");

		store.clear();
		expect(store.load()).toBeNull();
	});

	it("save swallows a throwing setItem (quota) without throwing", () => {
		const storage = createMemoryStorage();
		const originalSetItem = storage.setItem.bind(storage);
		let callCount = 0;
		storage.setItem = (_key: string, _value: string) => {
			callCount++;
			if (callCount > 1) {
				throw new DOMException("QuotaExceededError", "QuotaExceededError");
			}
			originalSetItem(_key, _value);
		};

		const store = createLocalStore<number[]>("quota", { storage });

		// First save works
		store.save([1, 2, 3]);
		expect(store.load()).toEqual([1, 2, 3]);

		// Second save throws but is swallowed
		expect(() => store.save([4, 5, 6])).not.toThrow();
	});

	it("construction with undefined storage yields a safe no-op store", () => {
		const store = createLocalStore<string>("noop", { storage: undefined });

		// All operations are safe no-ops
		expect(store.load()).toBeNull();
		expect(() => store.save("hello")).not.toThrow();
		expect(() => store.clear()).not.toThrow();
	});

	it("round-trips arrays", () => {
		const storage = createMemoryStorage();
		const store = createLocalStore<number[]>("arr", { storage });

		store.save([1, 2, 3]);
		expect(store.load()).toEqual([1, 2, 3]);
	});

	it("round-trips nested objects", () => {
		const storage = createMemoryStorage();
		const store = createLocalStore<{ a: { b: string[] } }>("nested", { storage });

		store.save({ a: { b: ["x", "y"] } });
		expect(store.load()).toEqual({ a: { b: ["x", "y"] } });
	});

	it("overwrites previous value on repeated save", () => {
		const storage = createMemoryStorage();
		const store = createLocalStore<string>("key", { storage });

		store.save("first");
		store.save("second");
		expect(store.load()).toBe("second");
	});
});