documentation-hub 5.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +43 -0
- package/.github/workflows/build.yml +64 -0
- package/.github/workflows/ci.yml +39 -0
- package/.vscode/extensions.json +3 -0
- package/Current.md +97 -0
- package/DocHub_Image.png +0 -0
- package/README.md +666 -0
- package/USER_GUIDE.md +1173 -0
- package/Updater.md +311 -0
- package/build/256x256.png +0 -0
- package/build/512x512.png +0 -0
- package/build/app-update.yml +4 -0
- package/build/create-icon.js +208 -0
- package/build/icon.ico +0 -0
- package/build/icon.png +0 -0
- package/build/icon_1024x1024.png +0 -0
- package/dist/assets/Analytics-BpsG9895.js +1 -0
- package/dist/assets/Card-IAZin8kp.js +1 -0
- package/dist/assets/CurrentSession-B-rFkHvf.js +12 -0
- package/dist/assets/Dashboard-C_5gMb0q.js +1 -0
- package/dist/assets/Documents-CqZ25axS.js +1 -0
- package/dist/assets/Input-l89xwXBi.js +1 -0
- package/dist/assets/Reporting-DqdHJY_a.js +1 -0
- package/dist/assets/Search-XNbu5z_3.js +1 -0
- package/dist/assets/SessionManager-lH9hZfzH.js +1 -0
- package/dist/assets/Sessions-ClZOPYNc.js +1 -0
- package/dist/assets/Settings-DUEHGURa.js +11 -0
- package/dist/assets/index-8xUe8ptc.js +24 -0
- package/dist/assets/index-RYyJqF7O.css +1 -0
- package/dist/assets/path-BkOl0AGO.js +1 -0
- package/dist/assets/promises-ID_B9S-h.js +1 -0
- package/dist/assets/urlHelpers-TvgahX0r.js +1 -0
- package/dist/assets/useToast-yRSO1dkm.js +1 -0
- package/dist/assets/vendor-charts-RkGK5ROP.js +36 -0
- package/dist/assets/vendor-db-l0sNRNKZ.js +1 -0
- package/dist/assets/vendor-react-BVZ_anCF.js +4 -0
- package/dist/assets/vendor-search-Dw8P0qyA.js +1 -0
- package/dist/assets/vendor-ui-BU7NfluV.js +53 -0
- package/dist/electron/PowerAutomateApiService-LfW09ZGr.js +147 -0
- package/dist/electron/main-CXkNtyv-.js +19789 -0
- package/dist/electron/main.js +5 -0
- package/dist/electron/preload.js +1 -0
- package/dist/icon.png +0 -0
- package/dist/index.html +27 -0
- package/docs/CODEBASE_ANALYSIS_REPORT.md +309 -0
- package/docs/DEBUG_LOGGING_GUIDE.md +244 -0
- package/docs/README.md +115 -0
- package/docs/TOC_WIRING_GUIDE.md +344 -0
- package/docs/analysis/Bullet_Symbol_Bug_Analysis.md +136 -0
- package/docs/analysis/DOCXMLATER_ANALYSIS_SUMMARY.txt +169 -0
- package/docs/analysis/Document_Processing_Issues_Analysis.md +704 -0
- package/docs/analysis/FIELD_PRESERVATION_ANALYSIS.md +1200 -0
- package/docs/analysis/INDENTATION_PRESERVE_ANALYSIS.md +181 -0
- package/docs/analysis/INDENTATION_PRESERVE_IMPLEMENTATION.md +207 -0
- package/docs/analysis/List_Implementation.md +206 -0
- package/docs/analysis/List_Implementation_Accuracy_Report.md +366 -0
- package/docs/analysis/PROCESSING_OPTIONS_UI_UPDATES.md +220 -0
- package/docs/analysis/RefactorStyles.md +852 -0
- package/docs/analysis/STYLE_PARAMETER_ENHANCEMENT.md +143 -0
- package/docs/analysis/docxmlater-comparison-todo-2025-11-13.md +636 -0
- package/docs/analysis/docxmlater-implementation-analysis-2025-11-13.md +340 -0
- package/docs/analysis/docxmlater-template_ui-integration-analysis.md +263 -0
- package/docs/analysis/github-issues-to-create.md +237 -0
- package/docs/api/API_README.md +538 -0
- package/docs/api/API_REFERENCE.md +751 -0
- package/docs/api/TYPE_DEFINITIONS.md +869 -0
- package/docs/architecture/FONT_EMBEDDING_GUIDE.md +318 -0
- package/docs/architecture/docxmlater-functions-and-structure.md +726 -0
- package/docs/docxmlater-readme.md +1341 -0
- package/docs/fixes/EXECUTION_LOG_TEST_BASE.md +573 -0
- package/docs/fixes/HYPERLINK_TEXT_SANITIZATION.md +253 -0
- package/docs/fixes/README.md +37 -0
- package/docs/github-issues/issue-1-body.md +125 -0
- package/docs/github-issues/issue-10-body.md +850 -0
- package/docs/github-issues/issue-2-body.md +200 -0
- package/docs/github-issues/issue-3-body.md +270 -0
- package/docs/github-issues/issue-4-body.md +169 -0
- package/docs/github-issues/issue-5-body.md +173 -0
- package/docs/github-issues/issue-6-body.md +158 -0
- package/docs/github-issues/issue-7-body.md +171 -0
- package/docs/github-issues/issue-8-body.md +407 -0
- package/docs/github-issues/issue-9-body.md +515 -0
- package/docs/github-issues/issue-tracker.md +274 -0
- package/docs/github-issues/predictive-analysis-2025-10-18.md +2131 -0
- package/docs/implementation/List_Framework_Refactor_Plan.md +336 -0
- package/docs/implementation/PRIMARY_TEXT_COLOR_FEATURE.md +217 -0
- package/docs/implementation/RELEASE_PLAN_v2.1.0.md +362 -0
- package/docs/implementation/RefactorStyles.md +588 -0
- package/docs/implementation/implement-plan.md +489 -0
- package/docs/implementation/missing-helpers-implementation.md +391 -0
- package/docs/implementation/refactor-plan.md +520 -0
- package/docs/implementation/session-implementation-complete.md +233 -0
- package/docs/implementation/session-management-plan.md +250 -0
- package/docs/setup-checklist.md +77 -0
- package/docs/versions/changelog.md +345 -0
- package/electron/customUpdater.ts +656 -0
- package/electron/main.ts +2441 -0
- package/electron/memoryConfig.ts +187 -0
- package/electron/preload.ts +394 -0
- package/electron/proxyConfig.ts +340 -0
- package/electron/services/BackupService.ts +452 -0
- package/electron/services/DictionaryService.ts +402 -0
- package/electron/services/LocalDictionaryLookupService.ts +147 -0
- package/electron/services/PowerAutomateApiService.ts +231 -0
- package/electron/services/SharePointSyncService.ts +474 -0
- package/electron/windowsCertStore.ts +427 -0
- package/electron/zscalerConfig.ts +381 -0
- package/eslint.config.js +92 -0
- package/jest.config.js +52 -0
- package/package.json +214 -0
- package/postcss.config.mjs +6 -0
- package/public/icon.png +0 -0
- package/publish-release.ps1 +5 -0
- package/renovate.json +30 -0
- package/src/App.tsx +216 -0
- package/src/__mocks__/p-limit.js +12 -0
- package/src/__mocks__/styleMock.js +1 -0
- package/src/components/common/BugReportButton.tsx +44 -0
- package/src/components/common/BugReportDialog.tsx +193 -0
- package/src/components/common/Button.tsx +153 -0
- package/src/components/common/Card.tsx +86 -0
- package/src/components/common/ColorPickerDialog.tsx +177 -0
- package/src/components/common/ConfirmDialog.tsx +96 -0
- package/src/components/common/DebugConsole.tsx +275 -0
- package/src/components/common/EmptyState.tsx +183 -0
- package/src/components/common/ErrorBoundary.tsx +98 -0
- package/src/components/common/ErrorDetailsDialog.tsx +153 -0
- package/src/components/common/ErrorFallback.tsx +218 -0
- package/src/components/common/Input.tsx +109 -0
- package/src/components/common/Skeleton.tsx +184 -0
- package/src/components/common/SplashScreen.tsx +81 -0
- package/src/components/common/Toast.tsx +155 -0
- package/src/components/common/Tooltip.tsx +79 -0
- package/src/components/common/UpdateNotification.tsx +320 -0
- package/src/components/comparison/ComparisonWindow.tsx +374 -0
- package/src/components/comparison/SideBySideDiff.tsx +486 -0
- package/src/components/comparison/index.ts +8 -0
- package/src/components/document/DocumentUploader.tsx +288 -0
- package/src/components/document/HyperlinkPreview.tsx +430 -0
- package/src/components/document/HyperlinkService.md +1484 -0
- package/src/components/document/Hyperlink_Technical_Documentation.md +496 -0
- package/src/components/document/InlineChangesView.tsx +707 -0
- package/src/components/document/ProcessingProgress.tsx +303 -0
- package/src/components/document/ProcessingResults.tsx +256 -0
- package/src/components/document/TrackedChangesDetail.tsx +530 -0
- package/src/components/document/TrackedChangesPanel.tsx +546 -0
- package/src/components/document/VirtualDocumentList.tsx +240 -0
- package/src/components/editor/DocumentEditor.tsx +723 -0
- package/src/components/editor/DocumentEditorModal.tsx +640 -0
- package/src/components/editor/EditorQuickActions.tsx +502 -0
- package/src/components/editor/EditorToolbar.tsx +312 -0
- package/src/components/editor/TableEditor.tsx +926 -0
- package/src/components/editor/index.ts +18 -0
- package/src/components/layout/Header.tsx +190 -0
- package/src/components/layout/Sidebar.tsx +313 -0
- package/src/components/layout/TitleBar.tsx +190 -0
- package/src/components/navigation/CommandPalette.tsx +233 -0
- package/src/components/navigation/KeyboardShortcutsModal.tsx +173 -0
- package/src/components/sessions/ChangeItem.tsx +408 -0
- package/src/components/sessions/ChangeViewer.tsx +1155 -0
- package/src/components/sessions/DocumentComparisonModal.tsx +314 -0
- package/src/components/sessions/ProcessingOptions.tsx +297 -0
- package/src/components/sessions/ReplacementsTab.tsx +438 -0
- package/src/components/sessions/RevisionHandlingOptions.tsx +87 -0
- package/src/components/sessions/SessionManager.tsx +188 -0
- package/src/components/sessions/StylesEditor.tsx +1335 -0
- package/src/components/sessions/TabContainer.tsx +151 -0
- package/src/components/sessions/VirtualSessionList.tsx +157 -0
- package/src/components/sessions/sessionToProcessorManager.tsx +420 -0
- package/src/components/settings/CertificateManager.tsx +410 -0
- package/src/components/settings/SegmentedControl.tsx +88 -0
- package/src/components/settings/SettingRow.tsx +52 -0
- package/src/contexts/GlobalStatsContext.tsx +396 -0
- package/src/contexts/SessionContext.tsx +2129 -0
- package/src/contexts/ThemeContext.tsx +428 -0
- package/src/contexts/UserSettingsContext.tsx +290 -0
- package/src/contexts/__tests__/GlobalStatsContext.test.tsx +390 -0
- package/src/global.d.ts +273 -0
- package/src/hooks/useDocumentQueue.tsx +210 -0
- package/src/hooks/useToast.tsx +55 -0
- package/src/main.tsx +10 -0
- package/src/pages/Analytics.tsx +386 -0
- package/src/pages/CurrentSession.tsx +1174 -0
- package/src/pages/Dashboard.tsx +319 -0
- package/src/pages/Documents.tsx +317 -0
- package/src/pages/Projects.tsx +250 -0
- package/src/pages/Reporting.tsx +386 -0
- package/src/pages/Search.tsx +349 -0
- package/src/pages/Sessions.tsx +285 -0
- package/src/pages/Settings.tsx +2662 -0
- package/src/services/HyperlinkService.ts +1085 -0
- package/src/services/document/DocXMLaterProcessor.ts +617 -0
- package/src/services/document/DocumentProcessingComparison.ts +856 -0
- package/src/services/document/DocumentSnapshotService.ts +575 -0
- package/src/services/document/WordDocumentProcessor.ts +10509 -0
- package/src/services/document/__tests__/DocXMLaterProcessor.hyperlinks.test.md +311 -0
- package/src/services/document/__tests__/WordDocumentProcessor.integration.test.ts +515 -0
- package/src/services/document/__tests__/WordDocumentProcessor.test.ts +812 -0
- package/src/services/document/blanklines/BlankLineManager.ts +658 -0
- package/src/services/document/blanklines/__tests__/paragraphChecks.test.ts +281 -0
- package/src/services/document/blanklines/helpers/blankLineInsertion.ts +87 -0
- package/src/services/document/blanklines/helpers/blankLineSnapshot.ts +251 -0
- package/src/services/document/blanklines/helpers/clearCustom.ts +121 -0
- package/src/services/document/blanklines/helpers/contextChecks.ts +117 -0
- package/src/services/document/blanklines/helpers/imageChecks.ts +51 -0
- package/src/services/document/blanklines/helpers/paragraphChecks.ts +236 -0
- package/src/services/document/blanklines/helpers/removeBlanksBetweenListItems.ts +91 -0
- package/src/services/document/blanklines/helpers/removeTrailingBlanks.ts +35 -0
- package/src/services/document/blanklines/helpers/tableGuards.ts +21 -0
- package/src/services/document/blanklines/index.ts +67 -0
- package/src/services/document/blanklines/rules/additionRules.ts +337 -0
- package/src/services/document/blanklines/rules/indentationRules.ts +317 -0
- package/src/services/document/blanklines/rules/removalRules.ts +362 -0
- package/src/services/document/blanklines/rules/ruleTypes.ts +92 -0
- package/src/services/document/blanklines/types.ts +29 -0
- package/src/services/document/helpers/ImageBorderCropper.ts +377 -0
- package/src/services/document/helpers/__tests__/whitespace.test.ts +272 -0
- package/src/services/document/helpers/whitespace.ts +117 -0
- package/src/services/document/list/ListNormalizer.ts +947 -0
- package/src/services/document/list/index.ts +45 -0
- package/src/services/document/list/list-detection.ts +275 -0
- package/src/services/document/list/list-types.ts +162 -0
- package/src/services/document/processors/HyperlinkProcessor.ts +370 -0
- package/src/services/document/processors/ListProcessor.ts +257 -0
- package/src/services/document/processors/StructureProcessor.ts +176 -0
- package/src/services/document/processors/StyleProcessor.ts +389 -0
- package/src/services/document/processors/TableProcessor.ts +2238 -0
- package/src/services/document/processors/__tests__/HyperlinkProcessor.test.ts +314 -0
- package/src/services/document/processors/__tests__/ListProcessor.test.ts +291 -0
- package/src/services/document/processors/__tests__/StructureProcessor.test.ts +257 -0
- package/src/services/document/processors/__tests__/TableProcessor.hlp-tips-bullets.test.ts +459 -0
- package/src/services/document/processors/__tests__/TableProcessor.test.ts +1604 -0
- package/src/services/document/processors/index.ts +28 -0
- package/src/services/document/types/docx-processing.ts +310 -0
- package/src/services/editor/EditorActionHandlers.ts +901 -0
- package/src/services/editor/index.ts +13 -0
- package/src/setupTests.ts +47 -0
- package/src/styles/global.css +782 -0
- package/src/types/backup.ts +132 -0
- package/src/types/dictionary.ts +125 -0
- package/src/types/document-processing.ts +331 -0
- package/src/types/docxmlater-augments.d.ts +142 -0
- package/src/types/editor.ts +280 -0
- package/src/types/electron.ts +340 -0
- package/src/types/globalStats.ts +155 -0
- package/src/types/hyperlink.ts +471 -0
- package/src/types/operations.ts +354 -0
- package/src/types/session.ts +427 -0
- package/src/types/settings.ts +112 -0
- package/src/utils/MemoryMonitor.ts +248 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/colorConvert.ts +306 -0
- package/src/utils/diffUtils.ts +347 -0
- package/src/utils/documentUtils.ts +202 -0
- package/src/utils/electronGuard.ts +62 -0
- package/src/utils/indexedDB.ts +915 -0
- package/src/utils/logger.ts +717 -0
- package/src/utils/pathSecurity.ts +232 -0
- package/src/utils/pathValidator.ts +236 -0
- package/src/utils/processingTimeEstimator.ts +153 -0
- package/src/utils/safeJsonParse.ts +62 -0
- package/src/utils/textSanitizer.ts +162 -0
- package/src/utils/urlHelpers.ts +304 -0
- package/src/utils/urlPatterns.ts +198 -0
- package/src/utils/urlSanitizer.ts +152 -0
- package/src/vite-env.d.ts +11 -0
- package/tsconfig.electron.json +19 -0
- package/tsconfig.json +36 -0
- package/tsconfig.node.json +12 -0
- package/typedoc.json +45 -0
- package/vite.config.ts +152 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { createContext, useContext, useState, useEffect, ReactNode, useCallback, useMemo } from 'react';
|
|
2
|
+
import { UserSettings, defaultUserSettings } from '@/types/settings';
|
|
3
|
+
import { logger } from '@/utils/logger';
|
|
4
|
+
import { safeJsonParse, safeJsonStringify } from '@/utils/safeJsonParse';
|
|
5
|
+
|
|
6
|
+
interface UserSettingsContextType {
|
|
7
|
+
settings: UserSettings;
|
|
8
|
+
isLoading: boolean;
|
|
9
|
+
updateSettings: (updates: Partial<UserSettings>) => void;
|
|
10
|
+
updateProfile: (updates: Partial<UserSettings['profile']>) => void;
|
|
11
|
+
updateNotifications: (updates: Partial<UserSettings['notifications']>) => void;
|
|
12
|
+
updateApiConnections: (updates: Partial<UserSettings['apiConnections']>) => void;
|
|
13
|
+
updateUpdateSettings: (updates: Partial<UserSettings['updateSettings']>) => void;
|
|
14
|
+
updateLocalDictionary: (updates: Partial<UserSettings['localDictionary']>) => void;
|
|
15
|
+
updateBackupSettings: (updates: Partial<UserSettings['backupSettings']>) => void;
|
|
16
|
+
updateDisplaySettings: (updates: Partial<UserSettings['displaySettings']>) => void;
|
|
17
|
+
saveSettings: () => Promise<boolean>;
|
|
18
|
+
loadSettings: () => void;
|
|
19
|
+
resetSettings: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const UserSettingsContext = createContext<UserSettingsContextType | undefined>(undefined);
|
|
23
|
+
|
|
24
|
+
const STORAGE_KEY = 'userSettings';
|
|
25
|
+
|
|
26
|
+
export function UserSettingsProvider({ children }: { children: ReactNode }) {
|
|
27
|
+
const log = logger.namespace('UserSettings');
|
|
28
|
+
const [settings, setSettings] = useState<UserSettings>(defaultUserSettings);
|
|
29
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
30
|
+
|
|
31
|
+
const loadSettings = useCallback(() => {
|
|
32
|
+
setIsLoading(true);
|
|
33
|
+
try {
|
|
34
|
+
const storedSettings = localStorage.getItem(STORAGE_KEY);
|
|
35
|
+
const parsed = safeJsonParse<Partial<UserSettings>>(
|
|
36
|
+
storedSettings,
|
|
37
|
+
{},
|
|
38
|
+
'UserSettings.loadSettings'
|
|
39
|
+
);
|
|
40
|
+
setSettings({ ...defaultUserSettings, ...parsed });
|
|
41
|
+
} finally {
|
|
42
|
+
setIsLoading(false);
|
|
43
|
+
}
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
const saveSettings = useCallback(async (): Promise<boolean> => {
|
|
47
|
+
const jsonString = safeJsonStringify(settings, undefined, 'UserSettings.saveSettings');
|
|
48
|
+
if (jsonString) {
|
|
49
|
+
try {
|
|
50
|
+
localStorage.setItem(STORAGE_KEY, jsonString);
|
|
51
|
+
return true;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
log.error('Failed to save user settings to localStorage:', error);
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}, [settings, log]);
|
|
59
|
+
|
|
60
|
+
const updateSettings = useCallback((updates: Partial<UserSettings>) => {
|
|
61
|
+
setSettings((prev) => {
|
|
62
|
+
const newSettings = { ...prev, ...updates };
|
|
63
|
+
// Auto-save to localStorage
|
|
64
|
+
const jsonString = safeJsonStringify(newSettings, undefined, 'UserSettings.updateSettings');
|
|
65
|
+
if (jsonString) {
|
|
66
|
+
try {
|
|
67
|
+
localStorage.setItem(STORAGE_KEY, jsonString);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
// Silent fail - logged elsewhere
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return newSettings;
|
|
73
|
+
});
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
const updateProfile = useCallback((updates: Partial<UserSettings['profile']>) => {
|
|
77
|
+
setSettings((prev) => {
|
|
78
|
+
const newSettings = {
|
|
79
|
+
...prev,
|
|
80
|
+
profile: { ...prev.profile, ...updates },
|
|
81
|
+
};
|
|
82
|
+
// Auto-save profile settings to localStorage
|
|
83
|
+
const jsonString = safeJsonStringify(
|
|
84
|
+
newSettings,
|
|
85
|
+
undefined,
|
|
86
|
+
'UserSettings.updateProfile'
|
|
87
|
+
);
|
|
88
|
+
if (jsonString) {
|
|
89
|
+
try {
|
|
90
|
+
localStorage.setItem(STORAGE_KEY, jsonString);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
// Silent fail - logged elsewhere
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return newSettings;
|
|
96
|
+
});
|
|
97
|
+
}, []);
|
|
98
|
+
|
|
99
|
+
const updateNotifications = useCallback((updates: Partial<UserSettings['notifications']>) => {
|
|
100
|
+
setSettings((prev) => {
|
|
101
|
+
const newSettings = {
|
|
102
|
+
...prev,
|
|
103
|
+
notifications: { ...prev.notifications, ...updates },
|
|
104
|
+
};
|
|
105
|
+
// Auto-save notification settings to localStorage
|
|
106
|
+
const jsonString = safeJsonStringify(
|
|
107
|
+
newSettings,
|
|
108
|
+
undefined,
|
|
109
|
+
'UserSettings.updateNotifications'
|
|
110
|
+
);
|
|
111
|
+
if (jsonString) {
|
|
112
|
+
try {
|
|
113
|
+
localStorage.setItem(STORAGE_KEY, jsonString);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
// Silent fail - logged elsewhere
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return newSettings;
|
|
119
|
+
});
|
|
120
|
+
}, []);
|
|
121
|
+
|
|
122
|
+
const updateApiConnections = useCallback((updates: Partial<UserSettings['apiConnections']>) => {
|
|
123
|
+
setSettings((prev) => {
|
|
124
|
+
const newSettings = {
|
|
125
|
+
...prev,
|
|
126
|
+
apiConnections: { ...prev.apiConnections, ...updates },
|
|
127
|
+
};
|
|
128
|
+
// Auto-save API settings to localStorage
|
|
129
|
+
const jsonString = safeJsonStringify(
|
|
130
|
+
newSettings,
|
|
131
|
+
undefined,
|
|
132
|
+
'UserSettings.updateApiConnections'
|
|
133
|
+
);
|
|
134
|
+
if (jsonString) {
|
|
135
|
+
try {
|
|
136
|
+
localStorage.setItem(STORAGE_KEY, jsonString);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
// Silent fail - logged elsewhere
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return newSettings;
|
|
142
|
+
});
|
|
143
|
+
}, []);
|
|
144
|
+
|
|
145
|
+
const updateUpdateSettings = useCallback((updates: Partial<UserSettings['updateSettings']>) => {
|
|
146
|
+
setSettings((prev) => {
|
|
147
|
+
const newSettings = {
|
|
148
|
+
...prev,
|
|
149
|
+
updateSettings: { ...prev.updateSettings, ...updates },
|
|
150
|
+
};
|
|
151
|
+
// Auto-save update settings to localStorage
|
|
152
|
+
const jsonString = safeJsonStringify(
|
|
153
|
+
newSettings,
|
|
154
|
+
undefined,
|
|
155
|
+
'UserSettings.updateUpdateSettings'
|
|
156
|
+
);
|
|
157
|
+
if (jsonString) {
|
|
158
|
+
try {
|
|
159
|
+
localStorage.setItem(STORAGE_KEY, jsonString);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
// Silent fail - logged elsewhere
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return newSettings;
|
|
165
|
+
});
|
|
166
|
+
}, []);
|
|
167
|
+
|
|
168
|
+
const updateLocalDictionary = useCallback((updates: Partial<UserSettings['localDictionary']>) => {
|
|
169
|
+
setSettings((prev) => {
|
|
170
|
+
const newSettings = {
|
|
171
|
+
...prev,
|
|
172
|
+
localDictionary: { ...prev.localDictionary, ...updates },
|
|
173
|
+
};
|
|
174
|
+
// Auto-save local dictionary settings to localStorage
|
|
175
|
+
const jsonString = safeJsonStringify(
|
|
176
|
+
newSettings,
|
|
177
|
+
undefined,
|
|
178
|
+
'UserSettings.updateLocalDictionary'
|
|
179
|
+
);
|
|
180
|
+
if (jsonString) {
|
|
181
|
+
try {
|
|
182
|
+
localStorage.setItem(STORAGE_KEY, jsonString);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
// Silent fail - logged elsewhere
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return newSettings;
|
|
188
|
+
});
|
|
189
|
+
}, []);
|
|
190
|
+
|
|
191
|
+
const updateBackupSettings = useCallback((updates: Partial<UserSettings['backupSettings']>) => {
|
|
192
|
+
setSettings((prev) => {
|
|
193
|
+
const newSettings = {
|
|
194
|
+
...prev,
|
|
195
|
+
backupSettings: { ...prev.backupSettings, ...updates },
|
|
196
|
+
};
|
|
197
|
+
// Auto-save backup settings to localStorage
|
|
198
|
+
const jsonString = safeJsonStringify(
|
|
199
|
+
newSettings,
|
|
200
|
+
undefined,
|
|
201
|
+
'UserSettings.updateBackupSettings'
|
|
202
|
+
);
|
|
203
|
+
if (jsonString) {
|
|
204
|
+
try {
|
|
205
|
+
localStorage.setItem(STORAGE_KEY, jsonString);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
// Silent fail - logged elsewhere
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return newSettings;
|
|
211
|
+
});
|
|
212
|
+
}, []);
|
|
213
|
+
|
|
214
|
+
const updateDisplaySettings = useCallback((updates: Partial<UserSettings['displaySettings']>) => {
|
|
215
|
+
setSettings((prev) => {
|
|
216
|
+
const newSettings = {
|
|
217
|
+
...prev,
|
|
218
|
+
displaySettings: { ...prev.displaySettings, ...updates },
|
|
219
|
+
};
|
|
220
|
+
// Auto-save display settings to localStorage
|
|
221
|
+
const jsonString = safeJsonStringify(
|
|
222
|
+
newSettings,
|
|
223
|
+
undefined,
|
|
224
|
+
'UserSettings.updateDisplaySettings'
|
|
225
|
+
);
|
|
226
|
+
if (jsonString) {
|
|
227
|
+
try {
|
|
228
|
+
localStorage.setItem(STORAGE_KEY, jsonString);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
// Silent fail - logged elsewhere
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return newSettings;
|
|
234
|
+
});
|
|
235
|
+
}, []);
|
|
236
|
+
|
|
237
|
+
const resetSettings = useCallback(() => {
|
|
238
|
+
setSettings(defaultUserSettings);
|
|
239
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
240
|
+
}, []);
|
|
241
|
+
|
|
242
|
+
useEffect(() => {
|
|
243
|
+
loadSettings();
|
|
244
|
+
}, [loadSettings]);
|
|
245
|
+
|
|
246
|
+
// PERFORMANCE FIX: Memoize context value to prevent unnecessary re-renders in consumers
|
|
247
|
+
// Without this, every render creates a new object reference, causing all consumers to re-render
|
|
248
|
+
const value = useMemo<UserSettingsContextType>(
|
|
249
|
+
() => ({
|
|
250
|
+
settings,
|
|
251
|
+
isLoading,
|
|
252
|
+
updateSettings,
|
|
253
|
+
updateProfile,
|
|
254
|
+
updateNotifications,
|
|
255
|
+
updateApiConnections,
|
|
256
|
+
updateUpdateSettings,
|
|
257
|
+
updateLocalDictionary,
|
|
258
|
+
updateBackupSettings,
|
|
259
|
+
updateDisplaySettings,
|
|
260
|
+
saveSettings,
|
|
261
|
+
loadSettings,
|
|
262
|
+
resetSettings,
|
|
263
|
+
}),
|
|
264
|
+
[
|
|
265
|
+
settings,
|
|
266
|
+
isLoading,
|
|
267
|
+
updateSettings,
|
|
268
|
+
updateProfile,
|
|
269
|
+
updateNotifications,
|
|
270
|
+
updateApiConnections,
|
|
271
|
+
updateUpdateSettings,
|
|
272
|
+
updateLocalDictionary,
|
|
273
|
+
updateBackupSettings,
|
|
274
|
+
updateDisplaySettings,
|
|
275
|
+
saveSettings,
|
|
276
|
+
loadSettings,
|
|
277
|
+
resetSettings,
|
|
278
|
+
]
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
return <UserSettingsContext.Provider value={value}>{children}</UserSettingsContext.Provider>;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function useUserSettings() {
|
|
285
|
+
const context = useContext(UserSettingsContext);
|
|
286
|
+
if (!context) {
|
|
287
|
+
throw new Error('useUserSettings must be used within a UserSettingsProvider');
|
|
288
|
+
}
|
|
289
|
+
return context;
|
|
290
|
+
}
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Suite for GlobalStatsContext
|
|
3
|
+
*
|
|
4
|
+
* Tests for Issue #3: Memory Leak Prevention
|
|
5
|
+
* - Ensures connection pool is used (no duplicate connections)
|
|
6
|
+
* - Validates proper cleanup on unmount
|
|
7
|
+
* - Tests singleton pattern enforcement
|
|
8
|
+
* - Verifies connection reuse across operations
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import { render, renderHook, waitFor, act, cleanup } from '@testing-library/react';
|
|
13
|
+
import { GlobalStatsProvider, useGlobalStats } from '../GlobalStatsContext';
|
|
14
|
+
import * as indexedDB from '@/utils/indexedDB';
|
|
15
|
+
import { createDefaultGlobalStats } from '@/types/globalStats';
|
|
16
|
+
|
|
17
|
+
// Mock IndexedDB utilities
|
|
18
|
+
jest.mock('@/utils/indexedDB', () => ({
|
|
19
|
+
loadGlobalStats: jest.fn(),
|
|
20
|
+
saveGlobalStats: jest.fn(),
|
|
21
|
+
resetGlobalStats: jest.fn(),
|
|
22
|
+
getGlobalStatsConnectionPool: jest.fn(),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
// Mock logger
|
|
26
|
+
jest.mock('@/utils/logger', () => ({
|
|
27
|
+
logger: {
|
|
28
|
+
namespace: () => ({
|
|
29
|
+
info: jest.fn(),
|
|
30
|
+
warn: jest.fn(),
|
|
31
|
+
error: jest.fn(),
|
|
32
|
+
debug: jest.fn(),
|
|
33
|
+
}),
|
|
34
|
+
},
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
describe('GlobalStatsContext - Issue #3: Memory Leak Prevention', () => {
|
|
38
|
+
const mockConnectionPool = {
|
|
39
|
+
getConnection: jest.fn(),
|
|
40
|
+
close: jest.fn(),
|
|
41
|
+
getStats: jest.fn().mockReturnValue({
|
|
42
|
+
connected: true,
|
|
43
|
+
reconnectAttempts: 0,
|
|
44
|
+
lastError: null,
|
|
45
|
+
}),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
jest.clearAllMocks();
|
|
50
|
+
// Flush lingering timers from previous tests (debounced saves use 1000ms setTimeout)
|
|
51
|
+
jest.clearAllTimers();
|
|
52
|
+
const freshStats = createDefaultGlobalStats();
|
|
53
|
+
(indexedDB.loadGlobalStats as jest.Mock).mockResolvedValue(freshStats);
|
|
54
|
+
(indexedDB.saveGlobalStats as jest.Mock).mockResolvedValue(undefined);
|
|
55
|
+
(indexedDB.resetGlobalStats as jest.Mock).mockResolvedValue(undefined);
|
|
56
|
+
(indexedDB.getGlobalStatsConnectionPool as jest.Mock).mockReturnValue(mockConnectionPool);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterEach(async () => {
|
|
60
|
+
// Ensure all React trees are unmounted before clearing mocks
|
|
61
|
+
cleanup();
|
|
62
|
+
// Allow pending microtasks (async effects) to settle
|
|
63
|
+
await act(async () => {
|
|
64
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
65
|
+
});
|
|
66
|
+
jest.clearAllTimers();
|
|
67
|
+
jest.restoreAllMocks();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('Connection Pool Usage', () => {
|
|
71
|
+
it('should use connection pool singleton for all operations', async () => {
|
|
72
|
+
const { result } = renderHook(() => useGlobalStats(), {
|
|
73
|
+
wrapper: GlobalStatsProvider,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await waitFor(() => {
|
|
77
|
+
expect(result.current).toBeTruthy();
|
|
78
|
+
expect(result.current?.isLoading).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Verify loadGlobalStats was called (uses connection pool internally)
|
|
82
|
+
expect(indexedDB.loadGlobalStats).toHaveBeenCalledTimes(1);
|
|
83
|
+
|
|
84
|
+
// Update stats multiple times
|
|
85
|
+
await act(async () => {
|
|
86
|
+
await result.current?.updateStats({ documentsProcessed: 1 });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await act(async () => {
|
|
90
|
+
await result.current?.updateStats({ hyperlinksChecked: 5 });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await act(async () => {
|
|
94
|
+
await result.current?.updateStats({ timeSaved: 30 });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// All operations should use the same connection pool
|
|
98
|
+
// saveGlobalStats is called: initial load (if new) + 3 updates
|
|
99
|
+
expect(indexedDB.saveGlobalStats).toHaveBeenCalled();
|
|
100
|
+
// Connection pool reused - no new connections created
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should not create duplicate connections on multiple renders', async () => {
|
|
104
|
+
const { rerender } = render(
|
|
105
|
+
<GlobalStatsProvider>
|
|
106
|
+
<div>Test</div>
|
|
107
|
+
</GlobalStatsProvider>
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
await waitFor(() => {
|
|
111
|
+
expect(indexedDB.loadGlobalStats).toHaveBeenCalledTimes(1);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Force re-render
|
|
115
|
+
rerender(
|
|
116
|
+
<GlobalStatsProvider>
|
|
117
|
+
<div>Test Updated</div>
|
|
118
|
+
</GlobalStatsProvider>
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// Should not create new connection - still using same one
|
|
122
|
+
await waitFor(() => {
|
|
123
|
+
// loadGlobalStats might be called again, but should reuse connection
|
|
124
|
+
expect(indexedDB.loadGlobalStats).toHaveBeenCalled();
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should reuse connection across state updates', async () => {
|
|
129
|
+
const connectionCallsBefore = mockConnectionPool.getConnection.mock.calls.length;
|
|
130
|
+
|
|
131
|
+
const { result } = renderHook(() => useGlobalStats(), {
|
|
132
|
+
wrapper: GlobalStatsProvider,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
await waitFor(() => {
|
|
136
|
+
expect(result.current).toBeTruthy();
|
|
137
|
+
expect(result.current?.isLoading).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Perform 10 consecutive updates
|
|
141
|
+
for (let i = 0; i < 10; i++) {
|
|
142
|
+
await act(async () => {
|
|
143
|
+
await result.current?.updateStats({ documentsProcessed: 1 });
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Connection pool should be reused, not recreated
|
|
148
|
+
// We don't directly call getConnection in our context,
|
|
149
|
+
// but indexedDB functions use it internally
|
|
150
|
+
expect(indexedDB.saveGlobalStats).toHaveBeenCalled();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('Memory Leak Prevention', () => {
|
|
155
|
+
it('should properly cleanup on unmount', async () => {
|
|
156
|
+
const { unmount } = renderHook(() => useGlobalStats(), {
|
|
157
|
+
wrapper: GlobalStatsProvider,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
await waitFor(() => {
|
|
161
|
+
expect(indexedDB.loadGlobalStats).toHaveBeenCalled();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Unmount component
|
|
165
|
+
unmount();
|
|
166
|
+
|
|
167
|
+
// Cleanup flag should prevent state updates after unmount
|
|
168
|
+
// (verified by useEffect cleanup setting isMounted = false)
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should not create memory leak with rapid mount/unmount', async () => {
|
|
172
|
+
const mockLoadGlobalStats = indexedDB.loadGlobalStats as jest.Mock;
|
|
173
|
+
const connectionsBefore = mockLoadGlobalStats.mock.calls.length;
|
|
174
|
+
|
|
175
|
+
// Mount and unmount 20 times rapidly
|
|
176
|
+
for (let i = 0; i < 20; i++) {
|
|
177
|
+
const { unmount } = renderHook(() => useGlobalStats(), {
|
|
178
|
+
wrapper: GlobalStatsProvider,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
await waitFor(() => {
|
|
182
|
+
expect(indexedDB.loadGlobalStats).toHaveBeenCalled();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
unmount();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Should not have created 20 separate connections
|
|
189
|
+
// Connection pool should be reused
|
|
190
|
+
const connectionsAfter = mockLoadGlobalStats.mock.calls.length;
|
|
191
|
+
|
|
192
|
+
// Each mount calls loadGlobalStats once, but uses same connection pool
|
|
193
|
+
expect(connectionsAfter).toBeGreaterThanOrEqual(connectionsBefore);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should handle concurrent updates without connection leaks', async () => {
|
|
197
|
+
const { result, unmount } = renderHook(() => useGlobalStats(), {
|
|
198
|
+
wrapper: GlobalStatsProvider,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
await waitFor(() => {
|
|
202
|
+
expect(result.current).toBeTruthy();
|
|
203
|
+
expect(result.current?.isLoading).toBe(false);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Fire rapid sequential updates (tests debounce behavior)
|
|
207
|
+
for (let i = 0; i < 10; i++) {
|
|
208
|
+
await act(async () => {
|
|
209
|
+
await result.current?.updateStats({ documentsProcessed: 1 });
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// All updates should complete without errors
|
|
214
|
+
// Connection pool handles concurrency correctly
|
|
215
|
+
expect(indexedDB.saveGlobalStats).toHaveBeenCalled();
|
|
216
|
+
|
|
217
|
+
// Clean up to prevent leaking into next test
|
|
218
|
+
unmount();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('Connection Pool Singleton', () => {
|
|
223
|
+
it('should enforce singleton pattern across multiple providers', async () => {
|
|
224
|
+
// Even if we create multiple provider instances,
|
|
225
|
+
// they should all use the same connection pool
|
|
226
|
+
|
|
227
|
+
const { result: result1 } = renderHook(() => useGlobalStats(), {
|
|
228
|
+
wrapper: ({ children }) => (
|
|
229
|
+
<GlobalStatsProvider key="provider1">{children}</GlobalStatsProvider>
|
|
230
|
+
),
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const { result: result2 } = renderHook(() => useGlobalStats(), {
|
|
234
|
+
wrapper: ({ children }) => (
|
|
235
|
+
<GlobalStatsProvider key="provider2">{children}</GlobalStatsProvider>
|
|
236
|
+
),
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
await waitFor(() => {
|
|
240
|
+
expect(result1.current).toBeTruthy();
|
|
241
|
+
expect(result1.current?.isLoading).toBe(false);
|
|
242
|
+
expect(result2.current).toBeTruthy();
|
|
243
|
+
expect(result2.current?.isLoading).toBe(false);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Both should have loaded stats
|
|
247
|
+
expect(indexedDB.loadGlobalStats).toHaveBeenCalled();
|
|
248
|
+
|
|
249
|
+
// Update from both providers
|
|
250
|
+
await act(async () => {
|
|
251
|
+
await result1.current?.updateStats({ documentsProcessed: 1 });
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
await act(async () => {
|
|
255
|
+
await result2.current?.updateStats({ hyperlinksChecked: 5 });
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Both use same connection pool
|
|
259
|
+
expect(indexedDB.saveGlobalStats).toHaveBeenCalled();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('should retrieve connection pool instance correctly', () => {
|
|
263
|
+
const pool = indexedDB.getGlobalStatsConnectionPool();
|
|
264
|
+
|
|
265
|
+
expect(pool).toBeDefined();
|
|
266
|
+
expect(pool).toBe(mockConnectionPool);
|
|
267
|
+
expect(typeof pool.getConnection).toBe('function');
|
|
268
|
+
expect(typeof pool.close).toBe('function');
|
|
269
|
+
expect(typeof pool.getStats).toBe('function');
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe('Functional Tests', () => {
|
|
274
|
+
it('should initialize with default stats', async () => {
|
|
275
|
+
const { result } = renderHook(() => useGlobalStats(), {
|
|
276
|
+
wrapper: GlobalStatsProvider,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
await waitFor(() => {
|
|
280
|
+
expect(result.current).toBeTruthy();
|
|
281
|
+
expect(result.current?.isLoading).toBe(false);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
expect(result.current?.stats).toBeDefined();
|
|
285
|
+
expect(result.current?.stats.allTime).toBeDefined();
|
|
286
|
+
expect(result.current?.stats.today).toBeDefined();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should update stats correctly', async () => {
|
|
290
|
+
const { result } = renderHook(() => useGlobalStats(), {
|
|
291
|
+
wrapper: GlobalStatsProvider,
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
await waitFor(() => {
|
|
295
|
+
expect(result.current).toBeTruthy();
|
|
296
|
+
expect(result.current?.isLoading).toBe(false);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const initialDocsProcessed = result.current?.stats.allTime.documentsProcessed;
|
|
300
|
+
|
|
301
|
+
await act(async () => {
|
|
302
|
+
await result.current?.updateStats({ documentsProcessed: 5 });
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
expect(result.current?.stats.allTime.documentsProcessed).toBe(initialDocsProcessed + 5);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('should persist stats to IndexedDB on update', async () => {
|
|
309
|
+
const { result } = renderHook(() => useGlobalStats(), {
|
|
310
|
+
wrapper: GlobalStatsProvider,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
await waitFor(() => {
|
|
314
|
+
expect(result.current).toBeTruthy();
|
|
315
|
+
expect(result.current?.isLoading).toBe(false);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
await act(async () => {
|
|
319
|
+
await result.current?.updateStats({ documentsProcessed: 3 });
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// saveGlobalStats should be called with updated stats
|
|
323
|
+
expect(indexedDB.saveGlobalStats).toHaveBeenCalled();
|
|
324
|
+
const savedStats = (indexedDB.saveGlobalStats as jest.Mock).mock.calls[0][0];
|
|
325
|
+
expect(savedStats.allTime.documentsProcessed).toBeGreaterThan(0);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('should reset stats correctly', async () => {
|
|
329
|
+
const { result } = renderHook(() => useGlobalStats(), {
|
|
330
|
+
wrapper: GlobalStatsProvider,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
await waitFor(() => {
|
|
334
|
+
expect(result.current).toBeTruthy();
|
|
335
|
+
expect(result.current?.isLoading).toBe(false);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
await act(async () => {
|
|
339
|
+
await result.current?.updateStats({ documentsProcessed: 10 });
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
await act(async () => {
|
|
343
|
+
await result.current?.resetAllStats();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
expect(indexedDB.resetGlobalStats).toHaveBeenCalled();
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
describe('Error Handling', () => {
|
|
351
|
+
it('should handle load errors gracefully', async () => {
|
|
352
|
+
(indexedDB.loadGlobalStats as jest.Mock).mockRejectedValue(
|
|
353
|
+
new Error('Database connection failed')
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
const { result } = renderHook(() => useGlobalStats(), {
|
|
357
|
+
wrapper: GlobalStatsProvider,
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
await waitFor(() => {
|
|
361
|
+
expect(result.current).toBeTruthy();
|
|
362
|
+
expect(result.current?.isLoading).toBe(false);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// Should still have default stats despite error
|
|
366
|
+
expect(result.current?.stats).toBeDefined();
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('should handle save errors without crashing', async () => {
|
|
370
|
+
(indexedDB.saveGlobalStats as jest.Mock).mockRejectedValue(new Error('Quota exceeded'));
|
|
371
|
+
|
|
372
|
+
const { result } = renderHook(() => useGlobalStats(), {
|
|
373
|
+
wrapper: GlobalStatsProvider,
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
await waitFor(() => {
|
|
377
|
+
expect(result.current).toBeTruthy();
|
|
378
|
+
expect(result.current?.isLoading).toBe(false);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// Update should not crash even if save fails
|
|
382
|
+
await act(async () => {
|
|
383
|
+
await result.current?.updateStats({ documentsProcessed: 1 });
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// State should still be updated locally
|
|
387
|
+
expect(result.current?.stats.allTime.documentsProcessed).toBeGreaterThan(0);
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
});
|