Skip to content
This repository was archived by the owner on Jun 16, 2026. It is now read-only.

Commit b3dc90d

Browse files
test: add 29 tests for StorageUtil and DOMUtil core modules
StorageUtil (19 tests): - isAvailable detection and caching behavior - get/set with string values, fallbacks, missing keys - getJSON/setJSON round-trip, corrupt JSON, circular refs, primitives - remove operations and edge cases - Graceful degradation when localStorage is unavailable - Unicode and empty string edge cases DOMUtil (10 tests): - escapeHtml XSS prevention (angle brackets, ampersands, nested tags) - Empty string, plain text passthrough, unicode handling - Element reuse across multiple calls (cached _escapeEl) - Special-character-only strings
1 parent 44e5c47 commit b3dc90d

2 files changed

Lines changed: 239 additions & 0 deletions

File tree

__tests__/dom-utils.test.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
5+
// Tests for DOMUtil — shared DOM helper utilities.
6+
7+
const fs = require('fs');
8+
const path = require('path');
9+
10+
function loadDOMUtil() {
11+
const code = fs.readFileSync(
12+
path.resolve(__dirname, '../src/modules/dom-utils.js'), 'utf8'
13+
);
14+
eval(code);
15+
return DOMUtil;
16+
}
17+
18+
describe('DOMUtil', () => {
19+
let DU;
20+
21+
beforeEach(() => {
22+
DU = loadDOMUtil();
23+
});
24+
25+
test('escapeHtml escapes angle brackets', () => {
26+
expect(DU.escapeHtml('<script>alert("xss")</script>')).toBe(
27+
'&lt;script&gt;alert("xss")&lt;/script&gt;'
28+
);
29+
});
30+
31+
test('escapeHtml escapes ampersands', () => {
32+
expect(DU.escapeHtml('a & b')).toBe('a &amp; b');
33+
});
34+
35+
test('escapeHtml escapes quotes inside HTML context', () => {
36+
const result = DU.escapeHtml('"hello" & \'world\'');
37+
expect(result).toContain('&amp;');
38+
});
39+
40+
test('escapeHtml returns empty string for empty input', () => {
41+
expect(DU.escapeHtml('')).toBe('');
42+
});
43+
44+
test('escapeHtml handles plain text unchanged', () => {
45+
expect(DU.escapeHtml('Hello World 123')).toBe('Hello World 123');
46+
});
47+
48+
test('escapeHtml handles unicode correctly', () => {
49+
expect(DU.escapeHtml('日本語 🎉')).toBe('日本語 🎉');
50+
});
51+
52+
test('escapeHtml handles nested HTML tags', () => {
53+
const input = '<div><img src=x onerror=alert(1)></div>';
54+
const result = DU.escapeHtml(input);
55+
expect(result).not.toContain('<div>');
56+
expect(result).not.toContain('<img');
57+
expect(result).toContain('&lt;');
58+
});
59+
60+
test('escapeHtml handles multiple calls (reuses element)', () => {
61+
const r1 = DU.escapeHtml('<a>');
62+
const r2 = DU.escapeHtml('<b>');
63+
expect(r1).toBe('&lt;a&gt;');
64+
expect(r2).toBe('&lt;b&gt;');
65+
});
66+
67+
test('escapeHtml handles numeric-like strings', () => {
68+
expect(DU.escapeHtml('0')).toBe('0');
69+
expect(DU.escapeHtml('3.14')).toBe('3.14');
70+
});
71+
72+
test('escapeHtml handles strings with only special chars', () => {
73+
expect(DU.escapeHtml('<<<>>>&&&')).toBe('&lt;&lt;&lt;&gt;&gt;&gt;&amp;&amp;&amp;');
74+
});
75+
});

__tests__/storage.test.js

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
5+
// Tests for StorageUtil — the shared localStorage wrapper used by all modules.
6+
7+
const fs = require('fs');
8+
const path = require('path');
9+
10+
function loadStorageUtil() {
11+
const code = fs.readFileSync(
12+
path.resolve(__dirname, '../src/modules/storage.js'), 'utf8'
13+
);
14+
eval(code);
15+
return StorageUtil;
16+
}
17+
18+
describe('StorageUtil', () => {
19+
let SU;
20+
21+
beforeEach(() => {
22+
localStorage.clear();
23+
SU = loadStorageUtil();
24+
});
25+
26+
// --- isAvailable ---
27+
28+
test('isAvailable returns true when localStorage works', () => {
29+
expect(SU.isAvailable()).toBe(true);
30+
});
31+
32+
test('isAvailable returns false when localStorage throws', () => {
33+
const orig = Storage.prototype.setItem;
34+
Storage.prototype.setItem = () => { throw new Error('quota'); };
35+
try {
36+
const Broken = loadStorageUtil();
37+
expect(Broken.isAvailable()).toBe(false);
38+
} finally {
39+
Storage.prototype.setItem = orig;
40+
}
41+
});
42+
43+
test('isAvailable caches result on repeated calls', () => {
44+
SU.isAvailable();
45+
const spy = jest.spyOn(Storage.prototype, 'setItem');
46+
SU.isAvailable(); // second call should use cache
47+
const probeCalls = spy.mock.calls.filter(c => c[0] === '__agentbox_storage_probe__');
48+
expect(probeCalls.length).toBe(0); // cached, no new probe
49+
spy.mockRestore();
50+
});
51+
52+
// --- get / set ---
53+
54+
test('set stores and get retrieves a string value', () => {
55+
expect(SU.set('key1', 'hello')).toBe(true);
56+
expect(SU.get('key1')).toBe('hello');
57+
});
58+
59+
test('get returns empty string for missing key with no fallback', () => {
60+
expect(SU.get('nonexistent')).toBe('');
61+
});
62+
63+
test('get returns fallback for missing key', () => {
64+
expect(SU.get('nonexistent', 'default')).toBe('default');
65+
});
66+
67+
test('set returns false when localStorage is unavailable', () => {
68+
const orig = Storage.prototype.setItem;
69+
Storage.prototype.setItem = () => { throw new Error('quota'); };
70+
try {
71+
const Broken = loadStorageUtil();
72+
expect(Broken.set('x', 'y')).toBe(false);
73+
expect(Broken.get('x', 'fb')).toBe('fb');
74+
} finally {
75+
Storage.prototype.setItem = orig;
76+
}
77+
});
78+
79+
test('get handles localStorage.getItem throwing', () => {
80+
const orig = Storage.prototype.getItem;
81+
Storage.prototype.getItem = () => { throw new Error('fail'); };
82+
try {
83+
expect(SU.get('any', 'safe')).toBe('safe');
84+
} finally {
85+
Storage.prototype.getItem = orig;
86+
}
87+
});
88+
89+
// --- getJSON / setJSON ---
90+
91+
test('setJSON and getJSON round-trip an object', () => {
92+
const data = { theme: 'dark', count: 42, nested: [1, 2] };
93+
expect(SU.setJSON('prefs', data)).toBe(true);
94+
expect(SU.getJSON('prefs', null)).toEqual(data);
95+
});
96+
97+
test('getJSON returns fallback for missing key', () => {
98+
expect(SU.getJSON('nope', { x: 1 })).toEqual({ x: 1 });
99+
});
100+
101+
test('getJSON returns fallback for corrupt JSON', () => {
102+
localStorage.setItem('bad', '{not json!!!');
103+
expect(SU.getJSON('bad', 'fallback')).toBe('fallback');
104+
});
105+
106+
test('setJSON returns false for non-serializable value (circular ref)', () => {
107+
const obj = {};
108+
obj.self = obj;
109+
expect(SU.setJSON('circ', obj)).toBe(false);
110+
});
111+
112+
test('setJSON handles primitive values', () => {
113+
SU.setJSON('num', 42);
114+
expect(SU.getJSON('num', 0)).toBe(42);
115+
SU.setJSON('str', 'hello');
116+
expect(SU.getJSON('str', '')).toBe('hello');
117+
SU.setJSON('bool', true);
118+
expect(SU.getJSON('bool', false)).toBe(true);
119+
SU.setJSON('nil', null);
120+
expect(SU.getJSON('nil', 'x')).toBe(null);
121+
});
122+
123+
// --- remove ---
124+
125+
test('remove deletes a stored key', () => {
126+
SU.set('temp', 'value');
127+
expect(SU.get('temp')).toBe('value');
128+
SU.remove('temp');
129+
expect(SU.get('temp')).toBe('');
130+
});
131+
132+
test('remove is safe on nonexistent key', () => {
133+
expect(() => SU.remove('ghost')).not.toThrow();
134+
});
135+
136+
test('remove is no-op when localStorage unavailable', () => {
137+
const orig = Storage.prototype.setItem;
138+
Storage.prototype.setItem = () => { throw new Error('quota'); };
139+
try {
140+
const Broken = loadStorageUtil();
141+
expect(() => Broken.remove('anything')).not.toThrow();
142+
} finally {
143+
Storage.prototype.setItem = orig;
144+
}
145+
});
146+
147+
// --- edge cases ---
148+
149+
test('set handles empty string key and value', () => {
150+
expect(SU.set('', '')).toBe(true);
151+
expect(SU.get('')).toBe('');
152+
});
153+
154+
test('set and get handle unicode strings', () => {
155+
SU.set('emoji', '🚀✨🎉');
156+
expect(SU.get('emoji')).toBe('🚀✨🎉');
157+
});
158+
159+
test('set overwrites existing key', () => {
160+
SU.set('k', 'v1');
161+
SU.set('k', 'v2');
162+
expect(SU.get('k')).toBe('v2');
163+
});
164+
});

0 commit comments

Comments
 (0)