From 600b94ce32527022ee4cdbebb2e7fbe82d3bc7a9 Mon Sep 17 00:00:00 2001 From: hugo Date: Mon, 16 Feb 2026 07:06:47 +0100 Subject: [PATCH] fix: final refactoring pass --- REFACTOR_DUPLICATION.md | 1 + src/renderer/App.tsx | 23 ++------- .../ProjectSelector/ProjectSelector.tsx | 26 +--------- src/renderer/utils/index.ts | 1 + src/renderer/utils/tabPersistence.ts | 23 +++++++++ tests/renderer/utils/tabPersistence.test.ts | 50 +++++++++++++++++++ 6 files changed, 80 insertions(+), 44 deletions(-) create mode 100644 src/renderer/utils/tabPersistence.ts create mode 100644 tests/renderer/utils/tabPersistence.test.ts diff --git a/REFACTOR_DUPLICATION.md b/REFACTOR_DUPLICATION.md index 7803288..45b4ad8 100644 --- a/REFACTOR_DUPLICATION.md +++ b/REFACTOR_DUPLICATION.md @@ -398,6 +398,7 @@ Reduce repeated local code with tiny helpers while preserving readability. ### Progress Check - Completed: consolidated repeated suggestion item rendering in `AISuggestionsModal` into one shared rendering path. - Completed: added focused component tests to guard selection/apply and empty-suggestions behavior. +- Completed: extracted shared tab-state persistence utility and replaced duplicate local-storage logic in `App` and `ProjectSelector`. ### Coverage & Test Quality (fresh run: `npm run test:coverage`) - `src/renderer/components/ProjectSelector/ProjectSelector.tsx`: 0% statements/functions/branches. diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 80418ac..8635188 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1,22 +1,9 @@ import React, { useEffect } from 'react'; import { ActivityBar, Sidebar, Editor, StatusBar, Panel, TabBar, ToastContainer, showToast, ResizablePanel } from './components'; -import { useAppStore, PostData, MediaData, TaskProgress, TabState } from './store'; +import { useAppStore, PostData, MediaData, TaskProgress } from './store'; +import { loadTabsForProject, saveTabsForProject } from './utils'; import './App.css'; -// Helper to load tabs for a project from localStorage -const TAB_STATE_PREFIX = 'bds-tabs-'; -const loadTabsForProject = (projectId: string): TabState | null => { - try { - const stored = localStorage.getItem(`${TAB_STATE_PREFIX}${projectId}`); - if (stored) { - return JSON.parse(stored) as TabState; - } - } catch (error) { - console.error('Failed to load tab state:', error); - } - return null; -}; - const App: React.FC = () => { const { setPosts, @@ -93,11 +80,7 @@ const App: React.FC = () => { const projectId = state.activeProject?.id; if (projectId) { const tabState = state.getTabState(); - try { - localStorage.setItem(`${TAB_STATE_PREFIX}${projectId}`, JSON.stringify(tabState)); - } catch (error) { - console.error('Failed to save tab state on unload:', error); - } + saveTabsForProject(projectId, tabState); } }; diff --git a/src/renderer/components/ProjectSelector/ProjectSelector.tsx b/src/renderer/components/ProjectSelector/ProjectSelector.tsx index b6f69d9..028b8a1 100644 --- a/src/renderer/components/ProjectSelector/ProjectSelector.tsx +++ b/src/renderer/components/ProjectSelector/ProjectSelector.tsx @@ -1,31 +1,9 @@ import React, { useState, useRef, useEffect } from 'react'; -import { useAppStore, ProjectData, PostData, MediaData, TabState } from '../../store'; +import { useAppStore, ProjectData, PostData, MediaData } from '../../store'; +import { loadTabsForProject, saveTabsForProject } from '../../utils'; import { showToast } from '../Toast'; import './ProjectSelector.css'; -// Helper functions for project-specific tab persistence -const TAB_STATE_PREFIX = 'bds-tabs-'; - -const saveTabsForProject = (projectId: string, tabState: TabState): void => { - try { - localStorage.setItem(`${TAB_STATE_PREFIX}${projectId}`, JSON.stringify(tabState)); - } catch (error) { - console.error('Failed to save tab state:', error); - } -}; - -const loadTabsForProject = (projectId: string): TabState | null => { - try { - const stored = localStorage.getItem(`${TAB_STATE_PREFIX}${projectId}`); - if (stored) { - return JSON.parse(stored) as TabState; - } - } catch (error) { - console.error('Failed to load tab state:', error); - } - return null; -}; - export const ProjectSelector: React.FC = () => { const { projects, activeProject, setProjects, setActiveProject, setPosts, setMedia, setSelectedPost, setSelectedMedia, removeProject, getTabState, restoreTabState, clearTabs } = useAppStore(); const [isOpen, setIsOpen] = useState(false); diff --git a/src/renderer/utils/index.ts b/src/renderer/utils/index.ts index b232a49..f45b463 100644 --- a/src/renderer/utils/index.ts +++ b/src/renderer/utils/index.ts @@ -2,3 +2,4 @@ export { AutoSaveManager, type AutoSaveConfig } from './autoSave'; export { getContrastColor } from './color'; export { unescapeMacroSyntax } from './markdownEscape'; export { groupPostsByStatus, type GroupedPosts, type PostStatus } from './postGrouping'; +export { loadTabsForProject, saveTabsForProject } from './tabPersistence'; diff --git a/src/renderer/utils/tabPersistence.ts b/src/renderer/utils/tabPersistence.ts new file mode 100644 index 0000000..5460f65 --- /dev/null +++ b/src/renderer/utils/tabPersistence.ts @@ -0,0 +1,23 @@ +import type { TabState } from '../store'; + +const TAB_STATE_PREFIX = 'bds-tabs-'; + +export const saveTabsForProject = (projectId: string, tabState: TabState): void => { + try { + localStorage.setItem(`${TAB_STATE_PREFIX}${projectId}`, JSON.stringify(tabState)); + } catch (error) { + console.error('Failed to save tab state:', error); + } +}; + +export const loadTabsForProject = (projectId: string): TabState | null => { + try { + const stored = localStorage.getItem(`${TAB_STATE_PREFIX}${projectId}`); + if (stored) { + return JSON.parse(stored) as TabState; + } + } catch (error) { + console.error('Failed to load tab state:', error); + } + return null; +}; \ No newline at end of file diff --git a/tests/renderer/utils/tabPersistence.test.ts b/tests/renderer/utils/tabPersistence.test.ts new file mode 100644 index 0000000..d7cd060 --- /dev/null +++ b/tests/renderer/utils/tabPersistence.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import type { TabState } from '../../../src/renderer/store'; +import { loadTabsForProject, saveTabsForProject } from '../../../src/renderer/utils/tabPersistence'; + +const projectId = 'project-1'; + +const sampleTabState: TabState = { + tabs: [ + { + id: 'post-1', + type: 'post', + title: 'Hello World', + dirty: false, + }, + ], + activeTabId: 'post-1', +}; + +describe('tabPersistence', () => { + beforeEach(() => { + localStorage.clear(); + vi.restoreAllMocks(); + }); + + it('saves and loads tab state for a project', () => { + saveTabsForProject(projectId, sampleTabState); + + const loaded = loadTabsForProject(projectId); + + expect(loaded).toEqual(sampleTabState); + }); + + it('returns null when no tab state is stored', () => { + expect(loadTabsForProject(projectId)).toBeNull(); + }); + + it('returns null when stored tab state is invalid JSON', () => { + localStorage.setItem('bds-tabs-project-1', '{invalid-json'); + + expect(loadTabsForProject(projectId)).toBeNull(); + }); + + it('does not throw when localStorage.setItem fails', () => { + vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { + throw new Error('quota exceeded'); + }); + + expect(() => saveTabsForProject(projectId, sampleTabState)).not.toThrow(); + }); +});