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 f67d634..e5410b4 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;
@@ -9,10 +10,14 @@ 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();
@@ -185,6 +190,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 +209,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,8 +304,23 @@ class ProjectManager {
}
#loadFromStorage() {
- const data = localStorage.getItem('current_project');
- return data ? JSON.parse(data) : null;
+ 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;
+ });
+ }
+
+ return project;
+ }
+ }
}
#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();
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();
});
});