Skip to content

Commit 0e11fd3

Browse files
authored
Create security_replicate_html.test.js
See CHANGELOG for details – too many updates to list here
1 parent bae3669 commit 0e11fd3

File tree

1 file changed

+268
-0
lines changed

1 file changed

+268
-0
lines changed
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
test('Security, HTML, and replicateHelper test placeholder', () => {
2+
expect(true).toBe(true);
3+
});
4+
import { sanitizeText, sanitizeHTML, sanitizeHTMLStrict } from '../webchat/src/security/xssProtection.js';
5+
import { stripHTML, validateNotebookEntry, validateLogEntry, checkProtocolCompliance } from '../webchat/src/logic/utils.js';
6+
import {
7+
cleanupUI,
8+
showLoadingIndicator,
9+
hideLoadingIndicator,
10+
appendMessage,
11+
createCommandMenu,
12+
toggleCommandMenu,
13+
handleNotebookClick,
14+
insertCommand,
15+
setupHelpModal,
16+
setupDarkMode
17+
} from '../webchat/src/chatbot/ui.js';
18+
19+
describe('Security: XSS Protection', () => {
20+
test('sanitizeText escapes HTML', () => {
21+
expect(sanitizeText('<b>Safe</b>')).toContain('&lt;b&gt;Safe&lt;/b&gt;');
22+
});
23+
24+
test('sanitizeHTML removes dangerous tags', () => {
25+
expect(sanitizeHTML('<script>alert(1)</script><b>Safe</b>')).toContain('<b>Safe</b>');
26+
expect(sanitizeHTML('<img src=x onerror=alert(1)>')).toContain('<img src=x>');
27+
});
28+
29+
test('sanitizeHTMLStrict only allows safe tags', () => {
30+
expect(sanitizeHTMLStrict('<script>alert(1)</script><b>Safe</b><a href="#">link</a>')).toContain('<b>Safe</b>');
31+
expect(sanitizeHTMLStrict('<img src=x onerror=alert(1)>')).not.toContain('<img');
32+
});
33+
});
34+
35+
describe('Logic Utilities', () => {
36+
test('stripHTML removes HTML tags', () => {
37+
expect(stripHTML('<b>Hello</b> World')).toBe('Hello World');
38+
expect(stripHTML('No tags')).toBe('No tags');
39+
expect(stripHTML('')).toBe('');
40+
});
41+
42+
test('validateNotebookEntry returns true for valid entry', () => {
43+
expect(validateNotebookEntry('valid_key', 'valid value')).toBe(true);
44+
});
45+
46+
test('validateNotebookEntry returns false for invalid entry', () => {
47+
expect(validateNotebookEntry('', 'valid value')).toBe(false);
48+
expect(validateNotebookEntry('valid_key', '')).toBe(false);
49+
expect(validateNotebookEntry('invalid!key', 'valid value')).toBe(false);
50+
});
51+
52+
test('validateLogEntry returns true for valid log', () => {
53+
expect(validateLogEntry('2025-08-18 - test entry - summary')).toBe(true);
54+
});
55+
56+
test('validateLogEntry returns false for invalid log', () => {
57+
expect(validateLogEntry('bad log')).toBe(false);
58+
expect(validateLogEntry('')).toBe(false);
59+
});
60+
61+
test('checkProtocolCompliance works for log and notebook', () => {
62+
expect(checkProtocolCompliance('log', '2025-08-18 - test entry - summary')).toBe(true);
63+
expect(checkProtocolCompliance('notebook', {key: 'valid_key', value: 'valid value'})).toBe(true);
64+
expect(checkProtocolCompliance('notebook', {key: '', value: 'valid value'})).toBe(false);
65+
});
66+
});
67+
68+
describe('UI Module', () => {
69+
beforeAll(() => {
70+
// Set up custom Jest matchers
71+
expect.extend({
72+
toHaveClass(received, className) {
73+
const pass = received.classList.contains(className);
74+
return {
75+
message: () =>
76+
pass
77+
? `expected element not to have class "${className}"`
78+
: `expected element to have class "${className}"`,
79+
pass,
80+
};
81+
},
82+
});
83+
});
84+
85+
beforeEach(() => {
86+
// Set up basic DOM structure for UI tests
87+
document.body.innerHTML = `
88+
<div id="chat-log"></div>
89+
<div id="user-input"></div>
90+
<div id="command-menu" style="display: none;">
91+
<div class="command-menu-header"></div>
92+
<div class="command-item" data-command="/help"></div>
93+
<div class="notebook-item"></div>
94+
</div>
95+
<div id="notebook-submenu" style="display: none;"></div>
96+
<template id="loading-indicator-template">
97+
<div class="loading-dots">
98+
<div class="dot"></div>
99+
<div class="dot"></div>
100+
<div class="dot"></div>
101+
</div>
102+
</template>
103+
<template id="code-window-template">
104+
<div class="code-window">
105+
<div class="code-window-header">
106+
<span class="code-language"></span>
107+
<button class="code-window-copy-btn">Copy</button>
108+
</div>
109+
<pre><code></code></pre>
110+
</div>
111+
</template>
112+
<div id="help-modal" class="modal">
113+
<div class="modal-header"></div>
114+
<button id="close-help">×</button>
115+
</div>
116+
<button id="helpBtn">Help</button>
117+
`;
118+
119+
// Mock clipboard API
120+
Object.assign(navigator, {
121+
clipboard: {
122+
writeText: jest.fn().mockResolvedValue(),
123+
},
124+
});
125+
126+
// Mock localStorage
127+
Storage.prototype.getItem = jest.fn();
128+
Storage.prototype.setItem = jest.fn();
129+
});
130+
131+
afterEach(() => {
132+
cleanupUI();
133+
document.body.innerHTML = '';
134+
});
135+
136+
test('showLoadingIndicator adds loading indicator to chat log', () => {
137+
showLoadingIndicator();
138+
const loadingIndicator = document.getElementById('loading-indicator');
139+
expect(loadingIndicator).toBeTruthy();
140+
expect(loadingIndicator.style.display).toBe('block');
141+
});
142+
143+
test('showLoadingIndicator throws error if chat log not found', () => {
144+
document.getElementById('chat-log').remove();
145+
expect(() => showLoadingIndicator()).toThrow('Chat log not found');
146+
});
147+
148+
test('hideLoadingIndicator removes loading indicator', () => {
149+
showLoadingIndicator();
150+
hideLoadingIndicator();
151+
const loadingIndicator = document.getElementById('loading-indicator');
152+
expect(loadingIndicator).toBeFalsy();
153+
});
154+
155+
test('appendMessage creates message elements correctly', () => {
156+
appendMessage('user', 'Hello world');
157+
const messages = document.querySelectorAll('.message');
158+
expect(messages.length).toBe(1);
159+
expect(messages[0]).toHaveClass('user-message');
160+
expect(messages[0].querySelector('.message-name').textContent).toBe('User');
161+
expect(messages[0].querySelector('.message-content').textContent).toBe('Hello world');
162+
});
163+
164+
test('appendMessage adds voice button for bot messages', () => {
165+
appendMessage('bot', 'Hello from bot');
166+
const message = document.querySelector('.bot-message');
167+
const voiceBtn = message.querySelector('.voice-btn');
168+
expect(voiceBtn).toBeTruthy();
169+
expect(voiceBtn.textContent).toBe('🔊');
170+
});
171+
172+
test('appendMessage does not add voice button for user messages', () => {
173+
appendMessage('user', 'Hello from user');
174+
const message = document.querySelector('.user-message');
175+
const voiceBtn = message.querySelector('.voice-btn');
176+
expect(voiceBtn).toBeFalsy();
177+
});
178+
179+
test('appendMessage throws error if chat log not found', () => {
180+
document.getElementById('chat-log').remove();
181+
expect(() => appendMessage('user', 'test')).toThrow('Chat log not found');
182+
});
183+
184+
test('createCommandMenu sets up command menu correctly', () => {
185+
createCommandMenu();
186+
const commandMenu = document.getElementById('command-menu');
187+
expect(commandMenu.style.display).toBe('block');
188+
});
189+
190+
test('toggleCommandMenu toggles collapsed class', () => {
191+
const menu = document.getElementById('command-menu');
192+
toggleCommandMenu();
193+
expect(menu).toHaveClass('collapsed');
194+
toggleCommandMenu();
195+
expect(menu).not.toHaveClass('collapsed');
196+
});
197+
198+
test('handleNotebookClick toggles submenu visibility', () => {
199+
const submenu = document.getElementById('notebook-submenu');
200+
const event = { stopPropagation: jest.fn() };
201+
202+
handleNotebookClick(event);
203+
expect(submenu.style.display).toBe('block');
204+
205+
handleNotebookClick(event);
206+
expect(submenu.style.display).toBe('none');
207+
208+
expect(event.stopPropagation).toHaveBeenCalledTimes(2);
209+
});
210+
211+
test('insertCommand sets input value and focuses', () => {
212+
const input = document.getElementById('user-input');
213+
input.focus = jest.fn();
214+
215+
insertCommand('/help');
216+
expect(input.value).toBe('/help');
217+
expect(input.focus).toHaveBeenCalled();
218+
});
219+
220+
test('insertCommand hides submenu after insertion', () => {
221+
const submenu = document.getElementById('notebook-submenu');
222+
submenu.style.display = 'block';
223+
224+
insertCommand('/help');
225+
expect(submenu.style.display).toBe('none');
226+
});
227+
228+
test('setupHelpModal configures modal functionality', () => {
229+
setupHelpModal();
230+
const helpBtn = document.getElementById('helpBtn');
231+
const helpModal = document.getElementById('help-modal');
232+
233+
helpBtn.click();
234+
expect(helpModal).toHaveClass('visible');
235+
236+
helpBtn.click();
237+
expect(helpModal).not.toHaveClass('visible');
238+
});
239+
240+
test('setupDarkMode applies dark mode based on preferences', () => {
241+
// Mock window.matchMedia
242+
Object.defineProperty(window, 'matchMedia', {
243+
writable: true,
244+
value: jest.fn().mockImplementation(query => ({
245+
matches: query === '(prefers-color-scheme: dark)',
246+
media: query,
247+
onchange: null,
248+
addListener: jest.fn(),
249+
removeListener: jest.fn(),
250+
addEventListener: jest.fn(),
251+
removeEventListener: jest.fn(),
252+
dispatchEvent: jest.fn(),
253+
})),
254+
});
255+
256+
Storage.prototype.getItem.mockReturnValue('1');
257+
setupDarkMode();
258+
expect(document.body).toHaveClass('dark-mode');
259+
});
260+
261+
test('cleanupUI clears event listeners and timers', () => {
262+
// Add some mock event listeners through UI functions
263+
createCommandMenu();
264+
setupHelpModal();
265+
266+
expect(() => cleanupUI()).not.toThrow();
267+
});
268+
});

0 commit comments

Comments
 (0)