From aa952b85e2352bcc4bfc1a8f4165ed9d5d3fd758 Mon Sep 17 00:00:00 2001 From: Tomek Zebrowski Date: Fri, 20 Mar 2026 10:42:01 +0100 Subject: [PATCH 1/3] feat: ensure all files in project are inactive when app loaded --- src/projectmanager.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/projectmanager.js b/src/projectmanager.js index f67d634..5f40d0b 100644 --- a/src/projectmanager.js +++ b/src/projectmanager.js @@ -285,7 +285,18 @@ class ProjectManager { #loadFromStorage() { const data = localStorage.getItem('current_project'); - return data ? JSON.parse(data) : null; + if (data) { + const project = JSON.parse(data); + + if (project && project.resources) { + project.resources.forEach((resource) => { + resource.isActive = false; + }); + } + + return project; + } + return null; } #saveToStorage() { From ba48e358a18b2787e1b5f984f9e8914b5a31152e Mon Sep 17 00:00:00 2001 From: Tomek Zebrowski Date: Fri, 20 Mar 2026 13:03:28 +0100 Subject: [PATCH 2/3] feat: allow user to defined whether files should be loaded during startup --- index.html | 11 +++++++++++ src/entry.js | 2 ++ src/preferences.js | 2 ++ src/projectmanager.js | 44 ++++++++++++++++++++++++++++++++----------- src/ui.js | 4 +--- 5 files changed, 49 insertions(+), 14 deletions(-) diff --git a/index.html b/index.html index 8ebd33f..1d5c35c 100644 --- a/index.html +++ b/index.html @@ -481,6 +481,17 @@

Settings & Preferences

+
+
+ Remember active files + Remember recently viewed files. +
+ +
+
Show Area Fills diff --git a/src/entry.js b/src/entry.js index 91f4128..6d68248 100644 --- a/src/entry.js +++ b/src/entry.js @@ -13,6 +13,7 @@ import { PaletteManager } from './palettemanager.js'; import { xyAnalysis } from './xyanalysis.js'; import { Histogram } from './histogram.js'; import { mathChannels } from './mathchannels.js'; +import { projectManager } from './projectmanager.js'; window.onload = async function () { await dataProcessor.loadConfiguration(); @@ -29,6 +30,7 @@ window.onload = async function () { PaletteManager.init(); xyAnalysis.init(); Histogram.init(); + projectManager.init(); const fileInput = DOM.get('fileInput'); if (fileInput) { diff --git a/src/preferences.js b/src/preferences.js index ce8a8cf..9162c96 100644 --- a/src/preferences.js +++ b/src/preferences.js @@ -19,6 +19,7 @@ export const Preferences = { 'pref-show-area-fills': 'showAreaFills', 'pref-smooth-lines': 'smoothLines', 'pref-load-map': 'loadMap', + 'pref-remember-files': 'rememberFiles', }, defaultPrefs: { @@ -29,6 +30,7 @@ export const Preferences = { showAreaFills: true, smoothLines: false, loadMap: false, + rememberFiles: true, }, get prefs() { diff --git a/src/projectmanager.js b/src/projectmanager.js index 5f40d0b..0ef8e5b 100644 --- a/src/projectmanager.js +++ b/src/projectmanager.js @@ -2,6 +2,7 @@ import { AppState, EVENTS } from './config.js'; import { mathChannels } from './mathchannels.js'; import { messenger } from './bus.js'; import { dbManager } from './dbmanager.js'; +import { Preferences } from './preferences.js'; class ProjectManager { #currentProject; @@ -13,7 +14,9 @@ class ProjectManager { this.#loadFromStorage() || this.#createEmptyProject(); this.#isReplaying = false; this.#libraryContainer = null; + } + init() { dbManager.init().then(async () => { await this.#hydrateActiveFiles(); this.renderLibrary(); @@ -185,6 +188,8 @@ class ProjectManager { messenger.emit('ui:set-loading', { message: 'Restoring Session...' }); + let actuallyLoaded = false; + for (const res of activeResources) { if (res.dbId && !AppState.files.some((f) => f.dbId === res.dbId)) { const signals = await dbManager.getFileSignals(res.dbId); @@ -202,13 +207,26 @@ class ProjectManager { size: meta.size, metadata: meta.metadata, }); + actuallyLoaded = true; + } else { + res.isActive = false; } } } - if (AppState.files.length > 0) { + if (actuallyLoaded) { messenger.emit('dataprocessor:batch-load-completed', {}); + + requestAnimationFrame(() => { + this.replayHistory(); + }); + } else { + messenger.emit('dataprocessor:batch-load-completed', {}); + messenger.emit('ui:updateDataLoadedState', { status: false }); } + messenger.emit('dataprocessor:batch-load-completed', {}); + + this.#saveToStorage(); } registerFile(file) { @@ -284,19 +302,23 @@ class ProjectManager { } #loadFromStorage() { - const data = localStorage.getItem('current_project'); - if (data) { - const project = JSON.parse(data); + if (Preferences.prefs.rememberFiles) { + const data = localStorage.getItem('current_project'); + return data ? JSON.parse(data) : null; + } else { + const data = localStorage.getItem('current_project'); + if (data) { + const project = JSON.parse(data); - if (project && project.resources) { - project.resources.forEach((resource) => { - resource.isActive = false; - }); - } + if (project && project.resources) { + project.resources.forEach((resource) => { + resource.isActive = false; + }); + } - return project; + return project; + } } - return null; } #saveToStorage() { diff --git a/src/ui.js b/src/ui.js index 6604d2e..0f37af9 100644 --- a/src/ui.js +++ b/src/ui.js @@ -39,10 +39,9 @@ export const UI = { UI.setLoading(true, event.message); }); - messenger.on('dataprocessor:batch-load-completed', (event) => { + messenger.on('dataprocessor:batch-load-completed', () => { UI.renderSignalList(); - // 1. Reveal the container first UI.updateDataLoadedState(true); UI.setLoading(false); @@ -51,7 +50,6 @@ export const UI = { fileInfo.innerText = `${AppState.files.length} logs loaded`; } - // 2. Wait for DOM reflow before rendering chart. if (AppState.files.length > 0) { requestAnimationFrame(() => { ChartManager.render(); From 58297592163bc1dc25cb9af9bfc4fe4c3026a799 Mon Sep 17 00:00:00 2001 From: Tomek Zebrowski Date: Fri, 20 Mar 2026 13:56:15 +0100 Subject: [PATCH 3/3] feat: update failing tests --- src/projectmanager.js | 6 +- tests/preferences.test.js | 17 ++-- tests/projectmanager.test.js | 171 +++++++++++++++++++---------------- 3 files changed, 103 insertions(+), 91 deletions(-) diff --git a/src/projectmanager.js b/src/projectmanager.js index 0ef8e5b..e5410b4 100644 --- a/src/projectmanager.js +++ b/src/projectmanager.js @@ -10,13 +10,15 @@ class ProjectManager { #libraryContainer; constructor() { - this.#currentProject = - this.#loadFromStorage() || this.#createEmptyProject(); this.#isReplaying = false; this.#libraryContainer = null; + this.#currentProject = this.#createEmptyProject(); } init() { + this.#currentProject = + this.#loadFromStorage() || this.#createEmptyProject(); + dbManager.init().then(async () => { await this.#hydrateActiveFiles(); this.renderLibrary(); diff --git a/tests/preferences.test.js b/tests/preferences.test.js index 227f058..79e1761 100644 --- a/tests/preferences.test.js +++ b/tests/preferences.test.js @@ -1,19 +1,23 @@ import { jest, describe, test, expect, beforeEach } from '@jest/globals'; -import { Preferences } from '../src/preferences.js'; -import { UI } from '../src/ui.js'; +await jest.unstable_mockModule('../src/ui.js', () => ({ + UI: { + setTheme: jest.fn(), + }, +})); -UI.setTheme = jest.fn(); +const { Preferences } = await import('../src/preferences.js'); +const { UI } = await import('../src/ui.js'); describe('Preferences Module', () => { beforeEach(() => { - // 2. Set up the DOM structure expected by Preferences document.body.innerHTML = `
+
`; localStorage.clear(); @@ -41,7 +45,6 @@ describe('Preferences Module', () => { }); test('init sets theme and attaches listeners', () => { - // 1. Setup localStorage so loadPreferences() sees the dark theme as active localStorage.setItem( Preferences.PREFS_KEY, JSON.stringify({ @@ -52,15 +55,11 @@ describe('Preferences Module', () => { const themeToggle = document.getElementById('pref-theme-dark'); - // 2. Run init Preferences.init(); - // Now it should be checked because loadPreferences() set it expect(themeToggle.checked).toBe(true); - // And UI.setTheme should be called with 'dark' expect(UI.setTheme).toHaveBeenCalledWith('dark'); - // 3. Test the toggle listener themeToggle.checked = false; themeToggle.dispatchEvent(new Event('change')); diff --git a/tests/projectmanager.test.js b/tests/projectmanager.test.js index bfa149c..4caea30 100644 --- a/tests/projectmanager.test.js +++ b/tests/projectmanager.test.js @@ -22,6 +22,13 @@ const mockDbManager = { const mockAppState = { files: [] }; const mockEvents = { FILE_REMOVED: 'file:removed' }; +// Mock Preferences for the new load strategy +const mockPreferences = { + prefs: { + rememberFiles: false, // Default for tests + }, +}; + // Apply mocks await jest.unstable_mockModule('../src/bus.js', () => ({ messenger: mockMessenger, @@ -36,6 +43,28 @@ await jest.unstable_mockModule('../src/config.js', () => ({ AppState: mockAppState, EVENTS: mockEvents, })); +await jest.unstable_mockModule('../src/preferences.js', () => ({ + Preferences: mockPreferences, +})); + +// Setup localStorage globally before any imports happen +const store = {}; +Object.defineProperty(global, 'localStorage', { + value: { + getItem: jest.fn((key) => store[key] || null), + setItem: jest.fn((key, val) => { + store[key] = val.toString(); + }), + removeItem: jest.fn((key) => { + delete store[key]; + }), + clear: jest.fn(() => { + for (const key in store) delete store[key]; + }), + }, + writable: true, + configurable: true, +}); // 2. Import the module const { projectManager } = await import('../src/projectmanager.js'); @@ -46,6 +75,7 @@ describe('ProjectManager Module', () => { beforeEach(() => { jest.clearAllMocks(); mockAppState.files = []; + mockPreferences.prefs.rememberFiles = false; // Reset preference // Setup generic DOM container for UI tests container = document.createElement('div'); @@ -55,24 +85,8 @@ describe('ProjectManager Module', () => { // Mock confirm dialogs to always say "Yes" global.confirm = jest.fn(() => true); - // --- FIX: Properly mock localStorage with Jest functions --- - const store = {}; - Object.defineProperty(global, 'localStorage', { - value: { - getItem: jest.fn((key) => store[key] || null), - setItem: jest.fn((key, val) => { - store[key] = val.toString(); - }), - removeItem: jest.fn((key) => { - delete store[key]; - }), - clear: jest.fn(() => { - for (const key in store) delete store[key]; - }), - }, - writable: true, - configurable: true, // Allow re-definition - }); + // Clear local storage mock store + global.localStorage.clear(); // Reset project state by creating a fresh instance or resetting via method projectManager.resetProject(); @@ -94,47 +108,79 @@ describe('ProjectManager Module', () => { // Wait for async render await new Promise(process.nextTick); - // --- FIX: Check for content more flexibly due to HTML tags --- expect(container.textContent).toContain('Library'); expect(container.textContent).toContain('(1)'); expect(container.textContent).toContain('log.json'); }); - test('constructor hydrates active files from DB on startup', async () => { - // 1. Manually seed localStorage with a project that has an ACTIVE resource + test('loadFromStorage respects rememberFiles = false (sets resources inactive)', async () => { + // Seed localStorage with an active project const savedProject = { id: 'p1', name: 'Saved Proj', resources: [ - { - fileId: 'r1', - dbId: 99, - fileName: 'old.json', - isActive: true, - addedAt: 100, - }, + { fileId: 'r1', dbId: 99, fileName: 'old.json', isActive: true }, ], history: [], }; + global.localStorage.setItem( + 'current_project', + JSON.stringify(savedProject) + ); - // Now this works because getItem is a jest.fn() - global.localStorage.getItem.mockReturnValue(JSON.stringify(savedProject)); + mockPreferences.prefs.rememberFiles = false; - // 2. Mock DB responses for hydration - mockDbManager.getAllFiles.mockResolvedValue([ - { id: 99, name: 'old.json', size: 100 }, - ]); - mockDbManager.getFileSignals.mockResolvedValue({ RPM: [] }); + // To test the private #loadFromStorage, we can isolate the module and re-import it + // so the constructor runs again with the seeded localStorage and Preferences + let isolatedManager; + await jest.isolateModulesAsync(async () => { + const module = await import('../src/projectmanager.js'); + isolatedManager = module.projectManager; + }); + + isolatedManager.init(); - // Note: Constructor logic runs on import. We can't re-run it easily in ES modules without - // complex reloading. However, we can simulate the "loadFromLibrary" effect which uses similar paths. - // For this test, we verify the mocks are set up correctly for when the logic DOES run. + const resources = isolatedManager.getResources(); + expect(resources).toHaveLength(1); + // Because rememberFiles is false, isActive should be forcefully set to false + expect(resources[0].isActive).toBe(false); + }); + + test('loadFromStorage respects rememberFiles = true (keeps resources active)', async () => { + // Seed localStorage with an active project + const savedProject = { + id: 'p1', + name: 'Saved Proj', + resources: [ + { fileId: 'r1', dbId: 99, fileName: 'old.json', isActive: true }, + ], + history: [], + }; + global.localStorage.setItem( + 'current_project', + JSON.stringify(savedProject) + ); + + mockPreferences.prefs.rememberFiles = true; + + // Re-import to trigger constructor + let isolatedManager; + await jest.isolateModulesAsync(async () => { + const module = await import('../src/projectmanager.js'); + isolatedManager = module.projectManager; + }); + + isolatedManager.init(); + + const resources = isolatedManager.getResources(); + expect(resources).toHaveLength(1); + // Because rememberFiles is true, it should leave the saved state alone + expect(resources[0].isActive).toBe(true); }); }); describe('Library Rendering (UI)', () => { beforeEach(async () => { - // Initialize UI for these tests projectManager.initLibraryUI('librarySlot'); }); @@ -146,20 +192,16 @@ describe('ProjectManager Module', () => { }); test('Renders file list with correct "Loaded" status', async () => { - // DB has 2 files const dbFiles = [ { id: 1, name: 'file1.json', addedAt: 2000, duration: 60, size: 100 }, - { id: 2, name: 'file2.json', addedAt: 1000, duration: 120, size: 200 }, // Older + { id: 2, name: 'file2.json', addedAt: 1000, duration: 120, size: 200 }, ]; mockDbManager.getAllFiles.mockResolvedValue(dbFiles); - // AppState has file1 loaded mockAppState.files = [{ dbId: 1, name: 'file1.json' }]; await projectManager.renderLibrary(); - // Check Sort Order (Newest First) - // --- FIX: Use new .pm-name selector --- const names = Array.from(container.querySelectorAll('.pm-name')).map( (el) => el.textContent.trim() ); @@ -167,9 +209,8 @@ describe('ProjectManager Module', () => { expect(names[0]).toBe('file1.json'); expect(names[1]).toBe('file2.json'); - // Check Status - expect(container.innerHTML).toContain('Loaded'); // file1 - expect(container.innerHTML).toContain('fa-plus'); // file2 (Open button icon) + expect(container.innerHTML).toContain('Loaded'); + expect(container.innerHTML).toContain('fa-plus'); }); test('Load button triggers loadFromLibrary', async () => { @@ -180,27 +221,20 @@ describe('ProjectManager Module', () => { await projectManager.renderLibrary(); - // --- FIX: Use new .pm-add-btn selector --- const loadBtn = container.querySelector('.pm-add-btn'); loadBtn.click(); - // Verify Loading started expect(mockMessenger.emit).toHaveBeenCalledWith( 'ui:set-loading', expect.any(Object) ); - // Wait for async promises await new Promise(process.nextTick); - // Verify DB fetch expect(mockDbManager.getFileSignals).toHaveBeenCalledWith(10); - - // Verify AppState update expect(mockAppState.files).toHaveLength(1); expect(mockAppState.files[0].name).toBe('click_me.json'); - // Verify Project Registry update const resources = projectManager.getResources(); expect(resources[0].fileName).toBe('click_me.json'); expect(resources[0].isActive).toBe(true); @@ -212,23 +246,15 @@ describe('ProjectManager Module', () => { ]); await projectManager.renderLibrary(); - // --- FIX: Use new .pm-del-btn selector --- const delBtn = container.querySelector('.pm-del-btn'); delBtn.click(); - // Verify Confirmation expect(global.confirm).toHaveBeenCalled(); - - // Verify DB Delete expect(mockDbManager.deleteFile).toHaveBeenCalledWith(5); - - // Verify UI Refresh - // (renderLibrary is called again inside the click handler) - expect(mockDbManager.getAllFiles).toHaveBeenCalledTimes(2); // Initial + After delete + expect(mockDbManager.getAllFiles).toHaveBeenCalledTimes(2); }); test('Purge button clears all data', async () => { - // Mock window.location.reload const originalLocation = window.location; delete window.location; window.location = { reload: jest.fn() }; @@ -241,7 +267,6 @@ describe('ProjectManager Module', () => { expect(global.confirm).toHaveBeenCalled(); expect(mockDbManager.clearAll).toHaveBeenCalled(); - // Restore window.location = originalLocation; }); }); @@ -259,24 +284,19 @@ describe('ProjectManager Module', () => { }); test('registerFile updates existing resource (re-opening file)', () => { - // 1. Add file initially projectManager.registerFile({ name: 'reuse.json', size: 500, dbId: 1 }); - // 2. Simulate closing it (isActive = false) - internal state logic - // We manually toggle it to test the reactivation logic const res = projectManager.getResources()[0]; res.isActive = false; - // 3. Re-register same file projectManager.registerFile({ name: 'reuse.json', size: 500, dbId: 1 }); const updatedRes = projectManager.getResources(); - expect(updatedRes).toHaveLength(1); // Should not add duplicate + expect(updatedRes).toHaveLength(1); expect(updatedRes[0].isActive).toBe(true); }); test('onFileRemoved marks resource inactive and archives history', () => { - // Setup: 2 files, 1 action in history for file index 0 mockAppState.files = [ { name: 'f1.json', size: 10 }, { name: 'f2.json', size: 20 }, @@ -287,16 +307,12 @@ describe('ProjectManager Module', () => { projectManager.logAction('TEST_ACTION', 'Did something', {}, 0); - // Action: Remove file at index 0 projectManager.onFileRemoved(0); const res = projectManager.getResources(); const history = projectManager.getHistory(); - // Resource check expect(res.find((r) => r.fileName === 'f1.json').isActive).toBe(false); - - // History check expect(history[0].targetFileIndex).toBe(-1); expect(history[0].description).toContain('(Archived)'); }); @@ -304,7 +320,6 @@ describe('ProjectManager Module', () => { test('renameProject updates project name', () => { projectManager.renameProject('Super Run'); expect(projectManager.getProjectName()).toBe('Super Run'); - // --- FIX: Now checks against the Jest spy --- expect(global.localStorage.setItem).toHaveBeenCalled(); }); @@ -332,10 +347,8 @@ describe('ProjectManager Module', () => { }); test('replayHistory executes MATH actions', async () => { - // Setup state for replay mockAppState.files = [{ name: 'log.json' }]; - // Inject history directly into current project projectManager.registerFile({ name: 'log.json', size: 100 }); projectManager.logAction( 'CREATE_MATH_CHANNEL', @@ -349,7 +362,6 @@ describe('ProjectManager Module', () => { 0 ); - // Execute Replay await projectManager.replayHistory(); expect(mockMathChannels.createChannel).toHaveBeenCalledWith( @@ -366,11 +378,10 @@ describe('ProjectManager Module', () => { }); test('replayHistory skips actions for closed files (index -1)', async () => { - projectManager.logAction('TEST', 'Archived Action', {}, -1); // Index -1 manually set + projectManager.logAction('TEST', 'Archived Action', {}, -1); await projectManager.replayHistory(); - // Should handle gracefully without error expect(mockMathChannels.createChannel).not.toHaveBeenCalled(); }); });