Files
ARIA 9c0f09619d Fixes #11: Memory management enhancements
- Add clearDomCache() for jQuery object cache invalidation on panel rebuild
- Track all event handlers in on()/once() for complete cleanup by unregisterAllEvents()
- Migrate direct eventSource.on() calls to tracked onEvent() in index.js, injector.js, sillyTavernExpressions.js
- Add clearDebugLogs() called on CHAT_CHANGED to prevent memory accumulation
- Add trackJQueryHandler()/cleanupJQueryEvents() infrastructure for jQuery event cleanup
- Add comprehensive memory management tests (14 new tests, all passing)
2026-07-12 14:31:14 +02:00

241 lines
8.3 KiB
JavaScript

/**
* 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');
});
});