/** * Memory Management Tests * Tests DOM cache invalidation, event listener cleanup, and debug log management */ describe('DOM Cache Invalidation', () => { test('clearDomCache sets all jQuery references to null', () => { // Simulate the clearDomCache logic const domCache = { $panelContainer: { isPanel: true }, $userStatsContainer: { isStats: true }, $infoBoxContainer: { isInfoBox: true }, $thoughtsContainer: { isThoughts: true }, $inventoryContainer: { isInventory: true }, $questsContainer: { isQuests: true }, $musicPlayerContainer: { isMusic: true }, $equipmentContainer: { isEquipment: true } }; // Simulate clearDomCache domCache.$panelContainer = null; domCache.$userStatsContainer = null; domCache.$infoBoxContainer = null; domCache.$thoughtsContainer = null; domCache.$inventoryContainer = null; domCache.$questsContainer = null; domCache.$musicPlayerContainer = null; domCache.$equipmentContainer = null; // Verify all are null expect(domCache.$panelContainer).toBeNull(); expect(domCache.$userStatsContainer).toBeNull(); expect(domCache.$infoBoxContainer).toBeNull(); expect(domCache.$thoughtsContainer).toBeNull(); expect(domCache.$inventoryContainer).toBeNull(); expect(domCache.$questsContainer).toBeNull(); expect(domCache.$musicPlayerContainer).toBeNull(); expect(domCache.$equipmentContainer).toBeNull(); }); test('clearDomCache is idempotent (safe to call multiple times)', () => { const cache = { $panelContainer: null }; // Call clear twice - should not throw cache.$panelContainer = null; cache.$panelContainer = null; expect(cache.$panelContainer).toBeNull(); }); }); describe('Debug Log Memory Management', () => { test('debugLogs array is capped at 100 entries', () => { const debugLogs = []; const MAX_LOGS = 100; // Add 150 entries for (let i = 0; i < 150; i++) { debugLogs.push({ timestamp: new Date().toISOString(), message: `Log ${i}` }); if (debugLogs.length > MAX_LOGS) { debugLogs.shift(); } } expect(debugLogs.length).toBe(100); expect(debugLogs[0].message).toBe('Log 50'); // First 50 were shifted out expect(debugLogs[99].message).toBe('Log 149'); }); test('clearDebugLogs empties the array', () => { const debugLogs = [ { timestamp: '2024-01-01T00:00:00Z', message: 'Log 1' }, { timestamp: '2024-01-01T00:00:01Z', message: 'Log 2' }, { timestamp: '2024-01-01T00:00:02Z', message: 'Log 3' } ]; // Simulate clearDebugLogs: debugLogs.length = 0 debugLogs.length = 0; expect(debugLogs.length).toBe(0); }); test('debugLogs cleared on chat change prevents memory accumulation', () => { const debugLogs = []; const MAX_LOGS = 100; // Simulate multiple chat sessions for (let chat = 0; chat < 5; chat++) { // Clear on chat change debugLogs.length = 0; // Add logs during this chat session for (let i = 0; i < 30; i++) { debugLogs.push({ timestamp: new Date().toISOString(), message: `Chat ${chat} Log ${i}` }); if (debugLogs.length > MAX_LOGS) { debugLogs.shift(); } } } // After 5 chats with clearing, we should only have logs from the last chat expect(debugLogs.length).toBe(30); expect(debugLogs[0].message).toBe('Chat 4 Log 0'); }); }); describe('Event Listener Cleanup', () => { test('registered handlers are tracked for cleanup', () => { // Simulate the registeredHandlers Map from events.js const registeredHandlers = new Map(); function trackHandler(eventType, handler) { if (!registeredHandlers.has(eventType)) { registeredHandlers.set(eventType, []); } registeredHandlers.get(eventType).push(handler); } const handler1 = () => {}; const handler2 = () => {}; trackHandler('MESSAGE_SENT', handler1); trackHandler('MESSAGE_SENT', handler2); trackHandler('CHAT_CHANGED', handler1); expect(registeredHandlers.get('MESSAGE_SENT').length).toBe(2); expect(registeredHandlers.get('CHAT_CHANGED').length).toBe(1); }); test('unregisterAllEvents clears all tracked handlers', () => { const registeredHandlers = new Map(); function trackHandler(eventType, handler) { if (!registeredHandlers.has(eventType)) { registeredHandlers.set(eventType, []); } registeredHandlers.get(eventType).push(handler); } trackHandler('MESSAGE_SENT', () => {}); trackHandler('CHAT_CHANGED', () => {}); // Simulate unregisterAllEvents registeredHandlers.clear(); expect(registeredHandlers.size).toBe(0); }); test('on() function tracks handlers for cleanup', () => { const registeredHandlers = new Map(); // Simulate the on() function with tracking function on(eventType, handler) { // eventSource.on(eventType, handler); // Would call real event source if (!registeredHandlers.has(eventType)) { registeredHandlers.set(eventType, []); } registeredHandlers.get(eventType).push(handler); } const handler = () => {}; on('GENERATE_BEFORE_COMBINE_PROMPTS', handler); expect(registeredHandlers.has('GENERATE_BEFORE_COMBINE_PROMPTS')).toBe(true); expect(registeredHandlers.get('GENERATE_BEFORE_COMBINE_PROMPTS').includes(handler)).toBe(true); }); test('once() function tracks handlers for cleanup', () => { const registeredHandlers = new Map(); // Simulate the once() function with tracking function once(eventType, handler) { // eventSource.once(eventType, handler); // Would call real event source if (!registeredHandlers.has(eventType)) { registeredHandlers.set(eventType, []); } registeredHandlers.get(eventType).push(handler); } const handler = () => {}; once('TEXT_COMPLETION_SETTINGS_READY', handler); expect(registeredHandlers.has('TEXT_COMPLETION_SETTINGS_READY')).toBe(true); expect(registeredHandlers.get('TEXT_COMPLETION_SETTINGS_READY').includes(handler)).toBe(true); }); test('multiple handlers for same event type are all tracked', () => { const registeredHandlers = new Map(); function on(eventType, handler) { if (!registeredHandlers.has(eventType)) { registeredHandlers.set(eventType, []); } registeredHandlers.get(eventType).push(handler); } const handlers = [() => {}, () => {}, () => {}]; handlers.forEach(h => on('CHAT_CHANGED', h)); expect(registeredHandlers.get('CHAT_CHANGED').length).toBe(3); }); }); describe('jQuery Event Handler Tracking', () => { test('tracked jQuery handlers can be cleaned up', () => { const jqueryEventHandlers = []; function trackJQueryHandler(event, selector, handler) { jqueryEventHandlers.push({ event, selector, handler }); return handler; } const handler1 = () => {}; const handler2 = () => {}; trackJQueryHandler('click', '.rpg-item-remove', handler1); trackJQueryHandler('click', '.rpg-equip-btn', handler2); expect(jqueryEventHandlers.length).toBe(2); // Simulate cleanup jqueryEventHandlers.length = 0; expect(jqueryEventHandlers.length).toBe(0); }); test('trackJQueryHandler returns the handler for chaining', () => { const jqueryEventHandlers = []; function trackJQueryHandler(event, selector, handler) { jqueryEventHandlers.push({ event, selector, handler }); return handler; } const handler = () => 'test'; const returned = trackJQueryHandler('click', '.selector', handler); expect(returned).toBe(handler); expect(returned()).toBe('test'); }); });