@sparkleideas/browser 3.0.0-alpha.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,277 @@
1
+ /**
2
+ * @sparkleideas/browser - Memory Integration Tests
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach } from 'vitest';
6
+ import {
7
+ ClaudeFlowMemoryAdapter,
8
+ BrowserMemoryManager,
9
+ createMemoryManager,
10
+ getMemoryAdapter,
11
+ type BrowserMemoryEntry,
12
+ } from '../src/infrastructure/memory-integration.js';
13
+ import type { BrowserTrajectory, Snapshot } from '../src/domain/types.js';
14
+
15
+ describe('ClaudeFlowMemoryAdapter', () => {
16
+ let adapter: ClaudeFlowMemoryAdapter;
17
+
18
+ beforeEach(() => {
19
+ adapter = new ClaudeFlowMemoryAdapter('test-browser');
20
+ });
21
+
22
+ describe('store and retrieve', () => {
23
+ it('should store and retrieve a memory entry', async () => {
24
+ const entry: BrowserMemoryEntry = {
25
+ id: 'test-1',
26
+ type: 'trajectory',
27
+ key: 'test-1',
28
+ value: { goal: 'Login to app', steps: [] },
29
+ metadata: {
30
+ sessionId: 'session-1',
31
+ goal: 'Login to app',
32
+ success: true,
33
+ timestamp: new Date().toISOString(),
34
+ },
35
+ };
36
+
37
+ await adapter.store(entry);
38
+ const retrieved = await adapter.retrieve('test-browser:trajectory:test-1');
39
+
40
+ expect(retrieved).toBeDefined();
41
+ expect(retrieved?.id).toBe('test-1');
42
+ expect(retrieved?.metadata.goal).toBe('Login to app');
43
+ });
44
+
45
+ it('should return null for non-existent key', async () => {
46
+ const result = await adapter.retrieve('non-existent');
47
+ expect(result).toBeNull();
48
+ });
49
+ });
50
+
51
+ describe('search', () => {
52
+ beforeEach(async () => {
53
+ const entries: BrowserMemoryEntry[] = [
54
+ {
55
+ id: 'login-1',
56
+ type: 'trajectory',
57
+ key: 'login-1',
58
+ value: { goal: 'Login to dashboard' },
59
+ metadata: { sessionId: 's1', goal: 'Login to dashboard', timestamp: new Date().toISOString() },
60
+ },
61
+ {
62
+ id: 'login-2',
63
+ type: 'trajectory',
64
+ key: 'login-2',
65
+ value: { goal: 'Login to admin panel' },
66
+ metadata: { sessionId: 's1', goal: 'Login to admin panel', timestamp: new Date().toISOString() },
67
+ },
68
+ {
69
+ id: 'scrape-1',
70
+ type: 'pattern',
71
+ key: 'scrape-1',
72
+ value: { goal: 'Scrape product data' },
73
+ metadata: { sessionId: 's2', goal: 'Scrape product data', timestamp: new Date().toISOString() },
74
+ },
75
+ ];
76
+
77
+ for (const entry of entries) {
78
+ await adapter.store(entry);
79
+ }
80
+ });
81
+
82
+ it('should search by keyword', async () => {
83
+ const results = await adapter.search('login');
84
+ expect(results.length).toBe(2);
85
+ expect(results.every(r => r.entry.metadata.goal?.toLowerCase().includes('login'))).toBe(true);
86
+ });
87
+
88
+ it('should filter by type', async () => {
89
+ const results = await adapter.search('login', { type: 'trajectory' });
90
+ expect(results.every(r => r.entry.type === 'trajectory')).toBe(true);
91
+ });
92
+
93
+ it('should limit results with topK', async () => {
94
+ const results = await adapter.search('login', { topK: 1 });
95
+ expect(results.length).toBe(1);
96
+ });
97
+
98
+ it('should filter by minimum score', async () => {
99
+ const results = await adapter.search('login', { minScore: 0.5 });
100
+ expect(results.every(r => r.score >= 0.5)).toBe(true);
101
+ });
102
+ });
103
+
104
+ describe('delete', () => {
105
+ it('should delete an entry', async () => {
106
+ const entry: BrowserMemoryEntry = {
107
+ id: 'delete-test',
108
+ type: 'snapshot',
109
+ key: 'delete-test',
110
+ value: { url: 'https://example.com' },
111
+ metadata: { sessionId: 's1', timestamp: new Date().toISOString() },
112
+ };
113
+
114
+ await adapter.store(entry);
115
+ const deleted = await adapter.delete('test-browser:snapshot:delete-test');
116
+ expect(deleted).toBe(true);
117
+
118
+ const retrieved = await adapter.retrieve('test-browser:snapshot:delete-test');
119
+ expect(retrieved).toBeNull();
120
+ });
121
+ });
122
+
123
+ describe('list', () => {
124
+ it('should list entries with filters', async () => {
125
+ await adapter.store({
126
+ id: 'list-1',
127
+ type: 'trajectory',
128
+ key: 'list-1',
129
+ value: {},
130
+ metadata: { sessionId: 'list-session', success: true, timestamp: new Date().toISOString() },
131
+ });
132
+
133
+ await adapter.store({
134
+ id: 'list-2',
135
+ type: 'error',
136
+ key: 'list-2',
137
+ value: {},
138
+ metadata: { sessionId: 'list-session', success: false, timestamp: new Date().toISOString() },
139
+ });
140
+
141
+ const trajectories = await adapter.list({ type: 'trajectory' });
142
+ expect(trajectories.some(e => e.id === 'list-1')).toBe(true);
143
+
144
+ const failures = await adapter.list({ success: false });
145
+ expect(failures.some(e => e.id === 'list-2')).toBe(true);
146
+ });
147
+ });
148
+
149
+ describe('getStats', () => {
150
+ it('should return memory statistics', async () => {
151
+ await adapter.store({
152
+ id: 'stats-1',
153
+ type: 'trajectory',
154
+ key: 'stats-1',
155
+ value: {},
156
+ metadata: { sessionId: 's1', timestamp: new Date().toISOString() },
157
+ });
158
+
159
+ const stats = await adapter.getStats();
160
+ expect(stats.totalEntries).toBeGreaterThan(0);
161
+ expect(stats.byType).toBeDefined();
162
+ expect(stats.bySession).toBeDefined();
163
+ });
164
+ });
165
+ });
166
+
167
+ describe('BrowserMemoryManager', () => {
168
+ let manager: BrowserMemoryManager;
169
+
170
+ beforeEach(() => {
171
+ manager = createMemoryManager('test-session');
172
+ });
173
+
174
+ describe('storeTrajectory', () => {
175
+ it('should store a completed trajectory', async () => {
176
+ const trajectory: BrowserTrajectory = {
177
+ id: 'traj-1',
178
+ sessionId: 'test-session',
179
+ goal: 'Complete checkout flow',
180
+ steps: [
181
+ {
182
+ action: 'click',
183
+ input: { target: '#checkout' },
184
+ result: { success: true },
185
+ timestamp: new Date().toISOString(),
186
+ },
187
+ ],
188
+ startedAt: new Date().toISOString(),
189
+ completedAt: new Date().toISOString(),
190
+ success: true,
191
+ };
192
+
193
+ await manager.storeTrajectory(trajectory);
194
+ // No error means success
195
+ });
196
+ });
197
+
198
+ describe('storePattern', () => {
199
+ it('should store a learned pattern', async () => {
200
+ await manager.storePattern(
201
+ 'pattern-1',
202
+ 'Login to app',
203
+ [
204
+ { action: 'fill', selector: '#username', value: 'test' },
205
+ { action: 'fill', selector: '#password', value: 'pass' },
206
+ { action: 'click', selector: '#submit' },
207
+ ],
208
+ true
209
+ );
210
+ // No error means success
211
+ });
212
+ });
213
+
214
+ describe('storeSnapshot', () => {
215
+ it('should store a snapshot', async () => {
216
+ const snapshot: Snapshot = {
217
+ tree: { role: 'document', children: [] },
218
+ refs: {},
219
+ url: 'https://example.com',
220
+ title: 'Example',
221
+ timestamp: new Date().toISOString(),
222
+ };
223
+
224
+ await manager.storeSnapshot('snap-1', snapshot);
225
+ // No error means success
226
+ });
227
+ });
228
+
229
+ describe('storeError', () => {
230
+ it('should store an error', async () => {
231
+ const error = new Error('Element not found');
232
+ await manager.storeError('error-1', error, {
233
+ action: 'click',
234
+ selector: '#non-existent',
235
+ url: 'https://example.com',
236
+ });
237
+ // No error means success
238
+ });
239
+ });
240
+
241
+ describe('findSimilarTrajectories', () => {
242
+ it('should find trajectories similar to a goal', async () => {
243
+ const trajectory: BrowserTrajectory = {
244
+ id: 'traj-search-1',
245
+ sessionId: 'test-session',
246
+ goal: 'Login to dashboard',
247
+ steps: [],
248
+ startedAt: new Date().toISOString(),
249
+ success: true,
250
+ };
251
+
252
+ await manager.storeTrajectory(trajectory);
253
+ const similar = await manager.findSimilarTrajectories('Login');
254
+ // Should return results (may be empty depending on scoring)
255
+ expect(Array.isArray(similar)).toBe(true);
256
+ });
257
+ });
258
+
259
+ describe('getSessionStats', () => {
260
+ it('should return session statistics', async () => {
261
+ const stats = await manager.getSessionStats();
262
+ expect(stats.trajectories).toBeDefined();
263
+ expect(stats.patterns).toBeDefined();
264
+ expect(stats.snapshots).toBeDefined();
265
+ expect(stats.errors).toBeDefined();
266
+ expect(typeof stats.successRate).toBe('number');
267
+ });
268
+ });
269
+ });
270
+
271
+ describe('factory functions', () => {
272
+ it('getMemoryAdapter should return singleton', () => {
273
+ const adapter1 = getMemoryAdapter();
274
+ const adapter2 = getMemoryAdapter();
275
+ expect(adapter1).toBe(adapter2);
276
+ });
277
+ });
@@ -0,0 +1,219 @@
1
+ /**
2
+ * @sparkleideas/browser - ReasoningBank Adapter Tests
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach } from 'vitest';
6
+ import { ReasoningBankAdapter, getReasoningBank } from '../src/infrastructure/reasoningbank-adapter.js';
7
+ import type { BrowserTrajectory } from '../src/domain/types.js';
8
+
9
+ describe('ReasoningBankAdapter', () => {
10
+ let adapter: ReasoningBankAdapter;
11
+
12
+ beforeEach(() => {
13
+ adapter = new ReasoningBankAdapter();
14
+ });
15
+
16
+ describe('getReasoningBank singleton', () => {
17
+ it('should return singleton instance', () => {
18
+ const instance1 = getReasoningBank();
19
+ const instance2 = getReasoningBank();
20
+ expect(instance1).toBe(instance2);
21
+ });
22
+ });
23
+
24
+ describe('trajectory storage', () => {
25
+ const mockTrajectory: BrowserTrajectory = {
26
+ id: 'traj-1',
27
+ goal: 'Login to dashboard',
28
+ startUrl: 'https://example.com/login',
29
+ startTime: new Date().toISOString(),
30
+ endTime: new Date().toISOString(),
31
+ steps: [
32
+ {
33
+ action: 'open',
34
+ input: { url: 'https://example.com/login' },
35
+ result: { success: true, duration: 100 },
36
+ timestamp: new Date().toISOString(),
37
+ },
38
+ {
39
+ action: 'fill',
40
+ input: { target: '@e1', value: 'user@example.com' },
41
+ result: { success: true, duration: 50 },
42
+ timestamp: new Date().toISOString(),
43
+ },
44
+ {
45
+ action: 'fill',
46
+ input: { target: '@e2', value: 'password' },
47
+ result: { success: true, duration: 50 },
48
+ timestamp: new Date().toISOString(),
49
+ },
50
+ {
51
+ action: 'click',
52
+ input: { target: '@e3' },
53
+ result: { success: true, duration: 200 },
54
+ timestamp: new Date().toISOString(),
55
+ },
56
+ ],
57
+ success: true,
58
+ verdict: 'Login successful',
59
+ };
60
+
61
+ it('should store successful trajectory and extract pattern', async () => {
62
+ await adapter.storeTrajectory(mockTrajectory);
63
+
64
+ const stats = adapter.getStats();
65
+ expect(stats.totalPatterns).toBeGreaterThan(0);
66
+ });
67
+
68
+ it('should not create pattern from single-step trajectory', async () => {
69
+ const singleStepTrajectory: BrowserTrajectory = {
70
+ ...mockTrajectory,
71
+ id: 'traj-single',
72
+ steps: [mockTrajectory.steps[0]],
73
+ };
74
+
75
+ await adapter.storeTrajectory(singleStepTrajectory);
76
+
77
+ // Pattern extraction requires at least 2 steps
78
+ const patterns = adapter.exportPatterns();
79
+ const singlePattern = patterns.find(p => p.goal === singleStepTrajectory.goal && p.steps.length === 1);
80
+ expect(singlePattern).toBeUndefined();
81
+ });
82
+
83
+ it('should update existing pattern on repeated success', async () => {
84
+ await adapter.storeTrajectory(mockTrajectory);
85
+ await adapter.storeTrajectory(mockTrajectory);
86
+
87
+ const patterns = adapter.exportPatterns();
88
+ const pattern = patterns.find(p => p.goal === mockTrajectory.goal);
89
+
90
+ expect(pattern).toBeDefined();
91
+ expect(pattern?.usageCount).toBeGreaterThan(1);
92
+ });
93
+ });
94
+
95
+ describe('pattern matching', () => {
96
+ const mockTrajectory: BrowserTrajectory = {
97
+ id: 'traj-2',
98
+ goal: 'Fill contact form',
99
+ startUrl: 'https://example.com/contact',
100
+ startTime: new Date().toISOString(),
101
+ endTime: new Date().toISOString(),
102
+ steps: [
103
+ { action: 'fill', input: { target: '@e1', value: 'Name' }, result: { success: true, duration: 50 }, timestamp: new Date().toISOString() },
104
+ { action: 'fill', input: { target: '@e2', value: 'email' }, result: { success: true, duration: 50 }, timestamp: new Date().toISOString() },
105
+ { action: 'click', input: { target: '@e3' }, result: { success: true, duration: 100 }, timestamp: new Date().toISOString() },
106
+ ],
107
+ success: true,
108
+ };
109
+
110
+ it('should find similar patterns by goal', async () => {
111
+ await adapter.storeTrajectory(mockTrajectory);
112
+
113
+ const patterns = await adapter.findSimilarPatterns('contact form');
114
+
115
+ expect(Array.isArray(patterns)).toBe(true);
116
+ });
117
+
118
+ it('should return empty array for no matches', async () => {
119
+ const patterns = await adapter.findSimilarPatterns('completely unrelated xyz');
120
+
121
+ expect(patterns).toEqual([]);
122
+ });
123
+
124
+ it('should get recommended steps for similar goal', async () => {
125
+ await adapter.storeTrajectory(mockTrajectory);
126
+
127
+ const steps = await adapter.getRecommendedSteps('Fill a form');
128
+
129
+ expect(Array.isArray(steps)).toBe(true);
130
+ });
131
+ });
132
+
133
+ describe('verdict recording', () => {
134
+ it('should record verdict for trajectory', async () => {
135
+ const mockTrajectory: BrowserTrajectory = {
136
+ id: 'traj-3',
137
+ goal: 'Test pattern',
138
+ startUrl: 'https://example.com',
139
+ startTime: new Date().toISOString(),
140
+ endTime: new Date().toISOString(),
141
+ steps: [
142
+ { action: 'click', input: { target: '@e1' }, result: { success: true, duration: 100 }, timestamp: new Date().toISOString() },
143
+ { action: 'click', input: { target: '@e2' }, result: { success: true, duration: 100 }, timestamp: new Date().toISOString() },
144
+ ],
145
+ success: true,
146
+ };
147
+
148
+ await adapter.storeTrajectory(mockTrajectory);
149
+ await adapter.recordVerdict('traj-3', true, 'Works great');
150
+
151
+ // Just verify it doesn't throw
152
+ expect(true).toBe(true);
153
+ });
154
+ });
155
+
156
+ describe('stats', () => {
157
+ it('should return stats with correct structure', () => {
158
+ const stats = adapter.getStats();
159
+
160
+ expect(stats).toHaveProperty('totalPatterns');
161
+ expect(stats).toHaveProperty('avgSuccessRate');
162
+ expect(stats).toHaveProperty('bufferedTrajectories');
163
+ expect(typeof stats.totalPatterns).toBe('number');
164
+ expect(typeof stats.avgSuccessRate).toBe('number');
165
+ expect(typeof stats.bufferedTrajectories).toBe('number');
166
+ });
167
+
168
+ it('should return zero patterns initially', () => {
169
+ const stats = adapter.getStats();
170
+
171
+ expect(stats.totalPatterns).toBe(0);
172
+ expect(stats.avgSuccessRate).toBe(0);
173
+ });
174
+ });
175
+
176
+ describe('pattern export/import', () => {
177
+ it('should export patterns as array', async () => {
178
+ const mockTrajectory: BrowserTrajectory = {
179
+ id: 'traj-4',
180
+ goal: 'Export test',
181
+ startUrl: 'https://example.com',
182
+ startTime: new Date().toISOString(),
183
+ endTime: new Date().toISOString(),
184
+ steps: [
185
+ { action: 'open', input: { url: 'https://example.com' }, result: { success: true, duration: 100 }, timestamp: new Date().toISOString() },
186
+ { action: 'click', input: { target: '@e1' }, result: { success: true, duration: 50 }, timestamp: new Date().toISOString() },
187
+ ],
188
+ success: true,
189
+ };
190
+
191
+ await adapter.storeTrajectory(mockTrajectory);
192
+
193
+ const patterns = adapter.exportPatterns();
194
+
195
+ expect(Array.isArray(patterns)).toBe(true);
196
+ expect(patterns.length).toBeGreaterThan(0);
197
+ });
198
+
199
+ it('should import patterns', () => {
200
+ const patternsToImport = [
201
+ {
202
+ id: 'imported-1',
203
+ type: 'navigation' as const,
204
+ goal: 'Imported pattern',
205
+ steps: [{ action: 'open' }],
206
+ successRate: 0.9,
207
+ avgDuration: 100,
208
+ lastUsed: new Date().toISOString(),
209
+ usageCount: 5,
210
+ },
211
+ ];
212
+
213
+ adapter.importPatterns(patternsToImport);
214
+
215
+ const stats = adapter.getStats();
216
+ expect(stats.totalPatterns).toBe(1);
217
+ });
218
+ });
219
+ });
@@ -0,0 +1,194 @@
1
+ /**
2
+ * @sparkleideas/browser - Security Integration Tests
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach } from 'vitest';
6
+ import {
7
+ BrowserSecurityScanner,
8
+ getSecurityScanner,
9
+ isUrlSafe,
10
+ containsPII,
11
+ type SecurityConfig,
12
+ } from '../src/infrastructure/security-integration.js';
13
+
14
+ describe('BrowserSecurityScanner', () => {
15
+ let scanner: BrowserSecurityScanner;
16
+
17
+ beforeEach(() => {
18
+ scanner = new BrowserSecurityScanner();
19
+ });
20
+
21
+ describe('scanUrl', () => {
22
+ it('should pass safe HTTPS URLs', async () => {
23
+ const result = await scanner.scanUrl('https://example.com');
24
+ expect(result.safe).toBe(true);
25
+ expect(result.score).toBeGreaterThan(0.7);
26
+ });
27
+
28
+ it('should warn about HTTP URLs when requireHttps is enabled', async () => {
29
+ const strictScanner = new BrowserSecurityScanner({ requireHttps: true });
30
+ const result = await strictScanner.scanUrl('http://example.com');
31
+ expect(result.threats.some(t => t.type === 'insecure-protocol')).toBe(true);
32
+ });
33
+
34
+ it('should detect blocked domains', async () => {
35
+ const result = await scanner.scanUrl('https://bit.ly/abcd123');
36
+ expect(result.threats.some(t => t.type === 'blocked-domain')).toBe(true);
37
+ });
38
+
39
+ it('should detect suspicious TLDs', async () => {
40
+ const result = await scanner.scanUrl('https://suspicious-site.xyz');
41
+ expect(result.threats.some(t => t.type === 'phishing')).toBe(true);
42
+ });
43
+
44
+ it('should detect IP address URLs', async () => {
45
+ const result = await scanner.scanUrl('https://192.168.1.1/login');
46
+ expect(result.threats.some(t => t.type === 'suspicious-redirect')).toBe(true);
47
+ });
48
+
49
+ it('should detect phishing indicators in URL', async () => {
50
+ const result = await scanner.scanUrl('https://secure-login-verify.example.com');
51
+ expect(result.threats.some(t => t.type === 'phishing')).toBe(true);
52
+ });
53
+
54
+ it('should allow domains in allowedDomains list', async () => {
55
+ const allowedScanner = new BrowserSecurityScanner({
56
+ allowedDomains: ['bit.ly'],
57
+ });
58
+ const result = await allowedScanner.scanUrl('https://bit.ly/safe');
59
+ expect(result.threats.some(t => t.type === 'blocked-domain')).toBe(false);
60
+ });
61
+ });
62
+
63
+ describe('scanContent', () => {
64
+ it('should detect email addresses', () => {
65
+ const result = scanner.scanContent('Contact us at test@example.com for help');
66
+ expect(result.pii.some(p => p.type === 'email')).toBe(true);
67
+ });
68
+
69
+ it('should detect phone numbers', () => {
70
+ const result = scanner.scanContent('Call us at 555-123-4567');
71
+ expect(result.pii.some(p => p.type === 'phone')).toBe(true);
72
+ });
73
+
74
+ it('should detect SSNs', () => {
75
+ const result = scanner.scanContent('SSN: 123-45-6789');
76
+ expect(result.pii.some(p => p.type === 'ssn')).toBe(true);
77
+ });
78
+
79
+ it('should detect credit card numbers', () => {
80
+ const result = scanner.scanContent('Card: 4111-1111-1111-1111');
81
+ expect(result.pii.some(p => p.type === 'credit-card')).toBe(true);
82
+ });
83
+
84
+ it('should detect API keys', () => {
85
+ // API key pattern: sk- or sk_ followed by 20+ alphanumeric chars
86
+ const result = scanner.scanContent('API Key: sk_abcdefghij1234567890abc');
87
+ expect(result.pii.some(p => p.type === 'api-key')).toBe(true);
88
+ });
89
+
90
+ it('should add threat for sensitive PII', () => {
91
+ const result = scanner.scanContent('SSN: 123-45-6789, Card: 4111-1111-1111-1111');
92
+ expect(result.threats.some(t => t.type === 'data-exfiltration')).toBe(true);
93
+ });
94
+
95
+ it('should return safe for content without PII', () => {
96
+ const result = scanner.scanContent('This is just regular text content');
97
+ expect(result.pii.length).toBe(0);
98
+ expect(result.safe).toBe(true);
99
+ });
100
+ });
101
+
102
+ describe('validateInput', () => {
103
+ it('should detect SQL injection patterns', () => {
104
+ const result = scanner.validateInput("'; DROP TABLE users; --", 'username');
105
+ expect(result.threats.some(t => t.type === 'injection')).toBe(true);
106
+ });
107
+
108
+ it('should detect XSS patterns', () => {
109
+ const result = scanner.validateInput('<script>alert("xss")</script>', 'comment');
110
+ expect(result.threats.some(t => t.type === 'xss')).toBe(true);
111
+ });
112
+
113
+ it('should detect event handler XSS', () => {
114
+ const result = scanner.validateInput('<img src="x" onerror="alert(1)">', 'bio');
115
+ expect(result.threats.some(t => t.type === 'xss')).toBe(true);
116
+ });
117
+
118
+ it('should pass safe input', () => {
119
+ const result = scanner.validateInput('Hello, World!', 'message');
120
+ expect(result.safe).toBe(true);
121
+ });
122
+ });
123
+
124
+ describe('sanitizeInput', () => {
125
+ it('should escape HTML entities', () => {
126
+ const result = scanner.sanitizeInput('<script>alert("xss")</script>');
127
+ expect(result).not.toContain('<script>');
128
+ expect(result).toContain('&lt;');
129
+ expect(result).toContain('&gt;');
130
+ });
131
+
132
+ it('should remove script tags', () => {
133
+ const result = scanner.sanitizeInput('Hello <script>evil()</script> World');
134
+ expect(result).not.toContain('<script>');
135
+ });
136
+
137
+ it('should escape HTML entities', () => {
138
+ const result = scanner.sanitizeInput('<div onclick="evil()">click me</div>');
139
+ // The sanitizer escapes HTML entities (which also neutralizes event handlers)
140
+ expect(result).toContain('&lt;div');
141
+ expect(result).toContain('&gt;');
142
+ });
143
+ });
144
+
145
+ describe('maskPII', () => {
146
+ it('should mask email addresses', () => {
147
+ const result = scanner.maskPII('test@example.com', 'email');
148
+ expect(result).toMatch(/t\*+@example\.com/);
149
+ });
150
+
151
+ it('should mask phone numbers', () => {
152
+ const result = scanner.maskPII('5551234567', 'phone');
153
+ // The phone masking leaves the last 4 digits visible
154
+ expect(result.endsWith('4567')).toBe(true);
155
+ });
156
+
157
+ it('should mask SSNs', () => {
158
+ const result = scanner.maskPII('123-45-6789', 'ssn');
159
+ expect(result).toBe('***-**-6789');
160
+ });
161
+
162
+ it('should mask credit card numbers', () => {
163
+ const result = scanner.maskPII('4111111111111111', 'credit-card');
164
+ expect(result).toMatch(/\*+ 1111$/);
165
+ });
166
+
167
+ it('should mask API keys', () => {
168
+ const result = scanner.maskPII('sk_live_1234567890abcdefghij', 'api-key');
169
+ expect(result).toMatch(/^sk_live_\*+/);
170
+ });
171
+
172
+ it('should completely mask passwords', () => {
173
+ const result = scanner.maskPII('secretpassword', 'password');
174
+ expect(result).toBe('********');
175
+ });
176
+ });
177
+ });
178
+
179
+ describe('factory functions', () => {
180
+ it('getSecurityScanner should return scanner', () => {
181
+ const scanner = getSecurityScanner();
182
+ expect(scanner).toBeInstanceOf(BrowserSecurityScanner);
183
+ });
184
+
185
+ it('isUrlSafe should return boolean', async () => {
186
+ const result = await isUrlSafe('https://example.com');
187
+ expect(typeof result).toBe('boolean');
188
+ });
189
+
190
+ it('containsPII should detect PII', () => {
191
+ expect(containsPII('My email is test@example.com')).toBe(true);
192
+ expect(containsPII('Hello World')).toBe(false);
193
+ });
194
+ });