- 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)
241 lines
8.3 KiB
JavaScript
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');
|
|
});
|
|
});
|