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.
Files changed (271) hide show
  1. package/.eslintrc.json +43 -0
  2. package/.github/workflows/build.yml +64 -0
  3. package/.github/workflows/ci.yml +39 -0
  4. package/.vscode/extensions.json +3 -0
  5. package/Current.md +97 -0
  6. package/DocHub_Image.png +0 -0
  7. package/README.md +666 -0
  8. package/USER_GUIDE.md +1173 -0
  9. package/Updater.md +311 -0
  10. package/build/256x256.png +0 -0
  11. package/build/512x512.png +0 -0
  12. package/build/app-update.yml +4 -0
  13. package/build/create-icon.js +208 -0
  14. package/build/icon.ico +0 -0
  15. package/build/icon.png +0 -0
  16. package/build/icon_1024x1024.png +0 -0
  17. package/dist/assets/Analytics-BpsG9895.js +1 -0
  18. package/dist/assets/Card-IAZin8kp.js +1 -0
  19. package/dist/assets/CurrentSession-B-rFkHvf.js +12 -0
  20. package/dist/assets/Dashboard-C_5gMb0q.js +1 -0
  21. package/dist/assets/Documents-CqZ25axS.js +1 -0
  22. package/dist/assets/Input-l89xwXBi.js +1 -0
  23. package/dist/assets/Reporting-DqdHJY_a.js +1 -0
  24. package/dist/assets/Search-XNbu5z_3.js +1 -0
  25. package/dist/assets/SessionManager-lH9hZfzH.js +1 -0
  26. package/dist/assets/Sessions-ClZOPYNc.js +1 -0
  27. package/dist/assets/Settings-DUEHGURa.js +11 -0
  28. package/dist/assets/index-8xUe8ptc.js +24 -0
  29. package/dist/assets/index-RYyJqF7O.css +1 -0
  30. package/dist/assets/path-BkOl0AGO.js +1 -0
  31. package/dist/assets/promises-ID_B9S-h.js +1 -0
  32. package/dist/assets/urlHelpers-TvgahX0r.js +1 -0
  33. package/dist/assets/useToast-yRSO1dkm.js +1 -0
  34. package/dist/assets/vendor-charts-RkGK5ROP.js +36 -0
  35. package/dist/assets/vendor-db-l0sNRNKZ.js +1 -0
  36. package/dist/assets/vendor-react-BVZ_anCF.js +4 -0
  37. package/dist/assets/vendor-search-Dw8P0qyA.js +1 -0
  38. package/dist/assets/vendor-ui-BU7NfluV.js +53 -0
  39. package/dist/electron/PowerAutomateApiService-LfW09ZGr.js +147 -0
  40. package/dist/electron/main-CXkNtyv-.js +19789 -0
  41. package/dist/electron/main.js +5 -0
  42. package/dist/electron/preload.js +1 -0
  43. package/dist/icon.png +0 -0
  44. package/dist/index.html +27 -0
  45. package/docs/CODEBASE_ANALYSIS_REPORT.md +309 -0
  46. package/docs/DEBUG_LOGGING_GUIDE.md +244 -0
  47. package/docs/README.md +115 -0
  48. package/docs/TOC_WIRING_GUIDE.md +344 -0
  49. package/docs/analysis/Bullet_Symbol_Bug_Analysis.md +136 -0
  50. package/docs/analysis/DOCXMLATER_ANALYSIS_SUMMARY.txt +169 -0
  51. package/docs/analysis/Document_Processing_Issues_Analysis.md +704 -0
  52. package/docs/analysis/FIELD_PRESERVATION_ANALYSIS.md +1200 -0
  53. package/docs/analysis/INDENTATION_PRESERVE_ANALYSIS.md +181 -0
  54. package/docs/analysis/INDENTATION_PRESERVE_IMPLEMENTATION.md +207 -0
  55. package/docs/analysis/List_Implementation.md +206 -0
  56. package/docs/analysis/List_Implementation_Accuracy_Report.md +366 -0
  57. package/docs/analysis/PROCESSING_OPTIONS_UI_UPDATES.md +220 -0
  58. package/docs/analysis/RefactorStyles.md +852 -0
  59. package/docs/analysis/STYLE_PARAMETER_ENHANCEMENT.md +143 -0
  60. package/docs/analysis/docxmlater-comparison-todo-2025-11-13.md +636 -0
  61. package/docs/analysis/docxmlater-implementation-analysis-2025-11-13.md +340 -0
  62. package/docs/analysis/docxmlater-template_ui-integration-analysis.md +263 -0
  63. package/docs/analysis/github-issues-to-create.md +237 -0
  64. package/docs/api/API_README.md +538 -0
  65. package/docs/api/API_REFERENCE.md +751 -0
  66. package/docs/api/TYPE_DEFINITIONS.md +869 -0
  67. package/docs/architecture/FONT_EMBEDDING_GUIDE.md +318 -0
  68. package/docs/architecture/docxmlater-functions-and-structure.md +726 -0
  69. package/docs/docxmlater-readme.md +1341 -0
  70. package/docs/fixes/EXECUTION_LOG_TEST_BASE.md +573 -0
  71. package/docs/fixes/HYPERLINK_TEXT_SANITIZATION.md +253 -0
  72. package/docs/fixes/README.md +37 -0
  73. package/docs/github-issues/issue-1-body.md +125 -0
  74. package/docs/github-issues/issue-10-body.md +850 -0
  75. package/docs/github-issues/issue-2-body.md +200 -0
  76. package/docs/github-issues/issue-3-body.md +270 -0
  77. package/docs/github-issues/issue-4-body.md +169 -0
  78. package/docs/github-issues/issue-5-body.md +173 -0
  79. package/docs/github-issues/issue-6-body.md +158 -0
  80. package/docs/github-issues/issue-7-body.md +171 -0
  81. package/docs/github-issues/issue-8-body.md +407 -0
  82. package/docs/github-issues/issue-9-body.md +515 -0
  83. package/docs/github-issues/issue-tracker.md +274 -0
  84. package/docs/github-issues/predictive-analysis-2025-10-18.md +2131 -0
  85. package/docs/implementation/List_Framework_Refactor_Plan.md +336 -0
  86. package/docs/implementation/PRIMARY_TEXT_COLOR_FEATURE.md +217 -0
  87. package/docs/implementation/RELEASE_PLAN_v2.1.0.md +362 -0
  88. package/docs/implementation/RefactorStyles.md +588 -0
  89. package/docs/implementation/implement-plan.md +489 -0
  90. package/docs/implementation/missing-helpers-implementation.md +391 -0
  91. package/docs/implementation/refactor-plan.md +520 -0
  92. package/docs/implementation/session-implementation-complete.md +233 -0
  93. package/docs/implementation/session-management-plan.md +250 -0
  94. package/docs/setup-checklist.md +77 -0
  95. package/docs/versions/changelog.md +345 -0
  96. package/electron/customUpdater.ts +656 -0
  97. package/electron/main.ts +2441 -0
  98. package/electron/memoryConfig.ts +187 -0
  99. package/electron/preload.ts +394 -0
  100. package/electron/proxyConfig.ts +340 -0
  101. package/electron/services/BackupService.ts +452 -0
  102. package/electron/services/DictionaryService.ts +402 -0
  103. package/electron/services/LocalDictionaryLookupService.ts +147 -0
  104. package/electron/services/PowerAutomateApiService.ts +231 -0
  105. package/electron/services/SharePointSyncService.ts +474 -0
  106. package/electron/windowsCertStore.ts +427 -0
  107. package/electron/zscalerConfig.ts +381 -0
  108. package/eslint.config.js +92 -0
  109. package/jest.config.js +52 -0
  110. package/package.json +214 -0
  111. package/postcss.config.mjs +6 -0
  112. package/public/icon.png +0 -0
  113. package/publish-release.ps1 +5 -0
  114. package/renovate.json +30 -0
  115. package/src/App.tsx +216 -0
  116. package/src/__mocks__/p-limit.js +12 -0
  117. package/src/__mocks__/styleMock.js +1 -0
  118. package/src/components/common/BugReportButton.tsx +44 -0
  119. package/src/components/common/BugReportDialog.tsx +193 -0
  120. package/src/components/common/Button.tsx +153 -0
  121. package/src/components/common/Card.tsx +86 -0
  122. package/src/components/common/ColorPickerDialog.tsx +177 -0
  123. package/src/components/common/ConfirmDialog.tsx +96 -0
  124. package/src/components/common/DebugConsole.tsx +275 -0
  125. package/src/components/common/EmptyState.tsx +183 -0
  126. package/src/components/common/ErrorBoundary.tsx +98 -0
  127. package/src/components/common/ErrorDetailsDialog.tsx +153 -0
  128. package/src/components/common/ErrorFallback.tsx +218 -0
  129. package/src/components/common/Input.tsx +109 -0
  130. package/src/components/common/Skeleton.tsx +184 -0
  131. package/src/components/common/SplashScreen.tsx +81 -0
  132. package/src/components/common/Toast.tsx +155 -0
  133. package/src/components/common/Tooltip.tsx +79 -0
  134. package/src/components/common/UpdateNotification.tsx +320 -0
  135. package/src/components/comparison/ComparisonWindow.tsx +374 -0
  136. package/src/components/comparison/SideBySideDiff.tsx +486 -0
  137. package/src/components/comparison/index.ts +8 -0
  138. package/src/components/document/DocumentUploader.tsx +288 -0
  139. package/src/components/document/HyperlinkPreview.tsx +430 -0
  140. package/src/components/document/HyperlinkService.md +1484 -0
  141. package/src/components/document/Hyperlink_Technical_Documentation.md +496 -0
  142. package/src/components/document/InlineChangesView.tsx +707 -0
  143. package/src/components/document/ProcessingProgress.tsx +303 -0
  144. package/src/components/document/ProcessingResults.tsx +256 -0
  145. package/src/components/document/TrackedChangesDetail.tsx +530 -0
  146. package/src/components/document/TrackedChangesPanel.tsx +546 -0
  147. package/src/components/document/VirtualDocumentList.tsx +240 -0
  148. package/src/components/editor/DocumentEditor.tsx +723 -0
  149. package/src/components/editor/DocumentEditorModal.tsx +640 -0
  150. package/src/components/editor/EditorQuickActions.tsx +502 -0
  151. package/src/components/editor/EditorToolbar.tsx +312 -0
  152. package/src/components/editor/TableEditor.tsx +926 -0
  153. package/src/components/editor/index.ts +18 -0
  154. package/src/components/layout/Header.tsx +190 -0
  155. package/src/components/layout/Sidebar.tsx +313 -0
  156. package/src/components/layout/TitleBar.tsx +190 -0
  157. package/src/components/navigation/CommandPalette.tsx +233 -0
  158. package/src/components/navigation/KeyboardShortcutsModal.tsx +173 -0
  159. package/src/components/sessions/ChangeItem.tsx +408 -0
  160. package/src/components/sessions/ChangeViewer.tsx +1155 -0
  161. package/src/components/sessions/DocumentComparisonModal.tsx +314 -0
  162. package/src/components/sessions/ProcessingOptions.tsx +297 -0
  163. package/src/components/sessions/ReplacementsTab.tsx +438 -0
  164. package/src/components/sessions/RevisionHandlingOptions.tsx +87 -0
  165. package/src/components/sessions/SessionManager.tsx +188 -0
  166. package/src/components/sessions/StylesEditor.tsx +1335 -0
  167. package/src/components/sessions/TabContainer.tsx +151 -0
  168. package/src/components/sessions/VirtualSessionList.tsx +157 -0
  169. package/src/components/sessions/sessionToProcessorManager.tsx +420 -0
  170. package/src/components/settings/CertificateManager.tsx +410 -0
  171. package/src/components/settings/SegmentedControl.tsx +88 -0
  172. package/src/components/settings/SettingRow.tsx +52 -0
  173. package/src/contexts/GlobalStatsContext.tsx +396 -0
  174. package/src/contexts/SessionContext.tsx +2129 -0
  175. package/src/contexts/ThemeContext.tsx +428 -0
  176. package/src/contexts/UserSettingsContext.tsx +290 -0
  177. package/src/contexts/__tests__/GlobalStatsContext.test.tsx +390 -0
  178. package/src/global.d.ts +273 -0
  179. package/src/hooks/useDocumentQueue.tsx +210 -0
  180. package/src/hooks/useToast.tsx +55 -0
  181. package/src/main.tsx +10 -0
  182. package/src/pages/Analytics.tsx +386 -0
  183. package/src/pages/CurrentSession.tsx +1174 -0
  184. package/src/pages/Dashboard.tsx +319 -0
  185. package/src/pages/Documents.tsx +317 -0
  186. package/src/pages/Projects.tsx +250 -0
  187. package/src/pages/Reporting.tsx +386 -0
  188. package/src/pages/Search.tsx +349 -0
  189. package/src/pages/Sessions.tsx +285 -0
  190. package/src/pages/Settings.tsx +2662 -0
  191. package/src/services/HyperlinkService.ts +1085 -0
  192. package/src/services/document/DocXMLaterProcessor.ts +617 -0
  193. package/src/services/document/DocumentProcessingComparison.ts +856 -0
  194. package/src/services/document/DocumentSnapshotService.ts +575 -0
  195. package/src/services/document/WordDocumentProcessor.ts +10509 -0
  196. package/src/services/document/__tests__/DocXMLaterProcessor.hyperlinks.test.md +311 -0
  197. package/src/services/document/__tests__/WordDocumentProcessor.integration.test.ts +515 -0
  198. package/src/services/document/__tests__/WordDocumentProcessor.test.ts +812 -0
  199. package/src/services/document/blanklines/BlankLineManager.ts +658 -0
  200. package/src/services/document/blanklines/__tests__/paragraphChecks.test.ts +281 -0
  201. package/src/services/document/blanklines/helpers/blankLineInsertion.ts +87 -0
  202. package/src/services/document/blanklines/helpers/blankLineSnapshot.ts +251 -0
  203. package/src/services/document/blanklines/helpers/clearCustom.ts +121 -0
  204. package/src/services/document/blanklines/helpers/contextChecks.ts +117 -0
  205. package/src/services/document/blanklines/helpers/imageChecks.ts +51 -0
  206. package/src/services/document/blanklines/helpers/paragraphChecks.ts +236 -0
  207. package/src/services/document/blanklines/helpers/removeBlanksBetweenListItems.ts +91 -0
  208. package/src/services/document/blanklines/helpers/removeTrailingBlanks.ts +35 -0
  209. package/src/services/document/blanklines/helpers/tableGuards.ts +21 -0
  210. package/src/services/document/blanklines/index.ts +67 -0
  211. package/src/services/document/blanklines/rules/additionRules.ts +337 -0
  212. package/src/services/document/blanklines/rules/indentationRules.ts +317 -0
  213. package/src/services/document/blanklines/rules/removalRules.ts +362 -0
  214. package/src/services/document/blanklines/rules/ruleTypes.ts +92 -0
  215. package/src/services/document/blanklines/types.ts +29 -0
  216. package/src/services/document/helpers/ImageBorderCropper.ts +377 -0
  217. package/src/services/document/helpers/__tests__/whitespace.test.ts +272 -0
  218. package/src/services/document/helpers/whitespace.ts +117 -0
  219. package/src/services/document/list/ListNormalizer.ts +947 -0
  220. package/src/services/document/list/index.ts +45 -0
  221. package/src/services/document/list/list-detection.ts +275 -0
  222. package/src/services/document/list/list-types.ts +162 -0
  223. package/src/services/document/processors/HyperlinkProcessor.ts +370 -0
  224. package/src/services/document/processors/ListProcessor.ts +257 -0
  225. package/src/services/document/processors/StructureProcessor.ts +176 -0
  226. package/src/services/document/processors/StyleProcessor.ts +389 -0
  227. package/src/services/document/processors/TableProcessor.ts +2238 -0
  228. package/src/services/document/processors/__tests__/HyperlinkProcessor.test.ts +314 -0
  229. package/src/services/document/processors/__tests__/ListProcessor.test.ts +291 -0
  230. package/src/services/document/processors/__tests__/StructureProcessor.test.ts +257 -0
  231. package/src/services/document/processors/__tests__/TableProcessor.hlp-tips-bullets.test.ts +459 -0
  232. package/src/services/document/processors/__tests__/TableProcessor.test.ts +1604 -0
  233. package/src/services/document/processors/index.ts +28 -0
  234. package/src/services/document/types/docx-processing.ts +310 -0
  235. package/src/services/editor/EditorActionHandlers.ts +901 -0
  236. package/src/services/editor/index.ts +13 -0
  237. package/src/setupTests.ts +47 -0
  238. package/src/styles/global.css +782 -0
  239. package/src/types/backup.ts +132 -0
  240. package/src/types/dictionary.ts +125 -0
  241. package/src/types/document-processing.ts +331 -0
  242. package/src/types/docxmlater-augments.d.ts +142 -0
  243. package/src/types/editor.ts +280 -0
  244. package/src/types/electron.ts +340 -0
  245. package/src/types/globalStats.ts +155 -0
  246. package/src/types/hyperlink.ts +471 -0
  247. package/src/types/operations.ts +354 -0
  248. package/src/types/session.ts +427 -0
  249. package/src/types/settings.ts +112 -0
  250. package/src/utils/MemoryMonitor.ts +248 -0
  251. package/src/utils/cn.ts +6 -0
  252. package/src/utils/colorConvert.ts +306 -0
  253. package/src/utils/diffUtils.ts +347 -0
  254. package/src/utils/documentUtils.ts +202 -0
  255. package/src/utils/electronGuard.ts +62 -0
  256. package/src/utils/indexedDB.ts +915 -0
  257. package/src/utils/logger.ts +717 -0
  258. package/src/utils/pathSecurity.ts +232 -0
  259. package/src/utils/pathValidator.ts +236 -0
  260. package/src/utils/processingTimeEstimator.ts +153 -0
  261. package/src/utils/safeJsonParse.ts +62 -0
  262. package/src/utils/textSanitizer.ts +162 -0
  263. package/src/utils/urlHelpers.ts +304 -0
  264. package/src/utils/urlPatterns.ts +198 -0
  265. package/src/utils/urlSanitizer.ts +152 -0
  266. package/src/vite-env.d.ts +11 -0
  267. package/tsconfig.electron.json +19 -0
  268. package/tsconfig.json +36 -0
  269. package/tsconfig.node.json +12 -0
  270. package/typedoc.json +45 -0
  271. package/vite.config.ts +152 -0
@@ -0,0 +1,515 @@
1
+ /**
2
+ * Integration Tests for WordDocumentProcessor
3
+ *
4
+ * IMPORTANT: These tests use REAL docxmlater library, NOT mocks!
5
+ * Tests work with actual DOCX files in ./fixtures/ directory.
6
+ *
7
+ * Coverage target: 90%+ of WordDocumentProcessor.ts
8
+ */
9
+
10
+ import { vi, describe, it, expect, beforeEach, type Mocked } from 'vitest';
11
+ import {
12
+ WordDocumentProcessor,
13
+ WordProcessingOptions,
14
+ WordProcessingResult,
15
+ } from '../WordDocumentProcessor';
16
+ import { Document } from 'docxmlater';
17
+ import { promises as fs } from 'fs';
18
+ import * as path from 'path';
19
+ import { hyperlinkService } from '../../HyperlinkService';
20
+
21
+ // Mock ONLY external dependencies, NOT docxmlater
22
+ vi.mock('../../HyperlinkService');
23
+ vi.mock('@/utils/MemoryMonitor', () => ({
24
+ MemoryMonitor: {
25
+ checkMemory: vi.fn(),
26
+ forceGarbageCollection: vi.fn(),
27
+ logMemoryUsage: vi.fn(),
28
+ compareCheckpoints: vi.fn(),
29
+ getMemoryStats: vi.fn().mockReturnValue({
30
+ heapUsed: 100,
31
+ heapTotal: 200,
32
+ rss: 300,
33
+ }),
34
+ },
35
+ }));
36
+ vi.mock('@/utils/logger', () => ({
37
+ logger: {
38
+ namespace: vi.fn().mockReturnValue({
39
+ info: vi.fn(),
40
+ debug: vi.fn(),
41
+ warn: vi.fn(),
42
+ error: vi.fn(),
43
+ }),
44
+ },
45
+ startTimer: vi.fn().mockReturnValue({
46
+ end: vi.fn().mockReturnValue(0),
47
+ elapsed: vi.fn().mockReturnValue(0),
48
+ }),
49
+ debugModes: {
50
+ DOCUMENT_PROCESSING: 'debug:documentProcessing',
51
+ SESSION_STATE: 'debug:sessionState',
52
+ IPC_CALLS: 'debug:ipcCalls',
53
+ DATABASE: 'debug:database',
54
+ HYPERLINKS: 'debug:hyperlinks',
55
+ BACKUPS: 'debug:backups',
56
+ LIST_PROCESSING: 'debug:listProcessing',
57
+ },
58
+ isDebugEnabled: vi.fn().mockReturnValue(false),
59
+ }));
60
+
61
+ const fixturesDir = path.join(__dirname, 'fixtures');
62
+
63
+ describe('WordDocumentProcessor - Integration Tests', () => {
64
+ let processor: WordDocumentProcessor;
65
+
66
+ beforeEach(() => {
67
+ vi.clearAllMocks();
68
+ processor = new WordDocumentProcessor();
69
+ });
70
+
71
+ describe('Document Loading & Validation', () => {
72
+ it('should successfully load and process a valid DOCX file', async () => {
73
+ const filePath = path.join(fixturesDir, 'sample.docx');
74
+
75
+ const result = await processor.processDocument(filePath);
76
+
77
+ // Show actual errors in assertion diff if processing fails
78
+ expect(result.errorMessages).toEqual([]);
79
+ expect(result.success).toBe(true);
80
+ expect(result.errorCount).toBe(0);
81
+ expect(result).toMatchObject({
82
+ success: true,
83
+ totalHyperlinks: expect.any(Number),
84
+ processedHyperlinks: expect.any(Number),
85
+ skippedHyperlinks: expect.any(Number),
86
+ processedLinks: expect.any(Array),
87
+ });
88
+ });
89
+
90
+ it('should reject files exceeding size limit', async () => {
91
+ const filePath = path.join(fixturesDir, 'sample.docx');
92
+
93
+ vi.spyOn(fs, 'stat').mockResolvedValue({
94
+ size: 200 * 1024 * 1024, // 200MB
95
+ } as any);
96
+
97
+ const result = await processor.processDocument(filePath, {
98
+ maxFileSizeMB: 100,
99
+ });
100
+
101
+ expect(result.success).toBe(false);
102
+ expect(result.errorMessages.length).toBeGreaterThan(0);
103
+ expect(result.errorMessages[0]).toContain('File too large');
104
+ expect(result.errorMessages[0]).toContain('200.00MB');
105
+
106
+ // Restore fs.stat
107
+ vi.restoreAllMocks();
108
+ });
109
+
110
+ it('should handle corrupt DOCX files gracefully', async () => {
111
+ const filePath = path.join(fixturesDir, 'corrupt.docx');
112
+
113
+ const result = await processor.processDocument(filePath);
114
+
115
+ expect(result.success).toBe(false);
116
+ expect(result.errorMessages.length).toBeGreaterThan(0);
117
+ // Error message should indicate loading failure
118
+ expect(result.errorMessages[0]).toMatch(/load|invalid|corrupt|format/i);
119
+ });
120
+
121
+ it('should create backup before processing when requested', async () => {
122
+ const filePath = path.join(fixturesDir, 'sample.docx');
123
+
124
+ const copyFileSpy = vi.spyOn(fs, 'copyFile').mockResolvedValue(undefined);
125
+
126
+ const result = await processor.processDocument(filePath, {
127
+ createBackup: true,
128
+ });
129
+
130
+ expect(copyFileSpy).toHaveBeenCalled();
131
+ const [sourcePath, backupPath] = copyFileSpy.mock.calls[0];
132
+ expect(sourcePath).toBe(filePath);
133
+ expect(backupPath).toContain('Backup');
134
+ expect(result.backupPath).toBeDefined();
135
+
136
+ vi.restoreAllMocks();
137
+ });
138
+
139
+ it('should handle file not found errors', async () => {
140
+ const result = await processor.processDocument('/nonexistent/path/fake.docx');
141
+
142
+ expect(result.success).toBe(false);
143
+ expect(result.errorMessages.length).toBeGreaterThan(0);
144
+ });
145
+ });
146
+
147
+ describe('Hyperlink Extraction', () => {
148
+ it('should extract all hyperlinks from document', async () => {
149
+ const filePath = path.join(fixturesDir, 'hyperlinks.docx');
150
+
151
+ const result = await processor.processDocument(filePath);
152
+
153
+ expect(result.success).toBe(true);
154
+ expect(result.totalHyperlinks).toBeGreaterThanOrEqual(0);
155
+
156
+ // Verify hyperlink structure in processedLinks
157
+ expect(result.processedLinks).toBeInstanceOf(Array);
158
+
159
+ if (result.processedLinks.length > 0) {
160
+ const link = result.processedLinks[0];
161
+ expect(link).toHaveProperty('id');
162
+ expect(link).toHaveProperty('url');
163
+ expect(link).toHaveProperty('displayText');
164
+ expect(link).toHaveProperty('type');
165
+ expect(link).toHaveProperty('status');
166
+ }
167
+ });
168
+
169
+ it('should extract hyperlinks with correct URLs and text', async () => {
170
+ const filePath = path.join(fixturesDir, 'hyperlinks.docx');
171
+
172
+ // Load document directly to verify
173
+ const doc = await Document.load(filePath);
174
+ const hyperlinksData = doc.getHyperlinks();
175
+
176
+ // Fixture may have zero hyperlinks — verify structure if any exist
177
+ expect(hyperlinksData).toBeInstanceOf(Array);
178
+
179
+ for (const { hyperlink, paragraph } of hyperlinksData) {
180
+ expect(hyperlink.getUrl()).toBeTruthy();
181
+ expect(hyperlink.getText()).toBeTruthy();
182
+ expect(paragraph.getText()).toBeTruthy();
183
+ }
184
+
185
+ // Clean up
186
+ doc.dispose();
187
+ });
188
+
189
+ it('should handle documents with no hyperlinks', async () => {
190
+ const filePath = path.join(fixturesDir, 'sample.docx');
191
+
192
+ const result = await processor.processDocument(filePath);
193
+
194
+ expect(result.success).toBe(true);
195
+ expect(result.totalHyperlinks).toBe(0);
196
+ expect(result.processedHyperlinks).toBe(0);
197
+ expect(result.processedLinks).toHaveLength(0);
198
+ });
199
+ });
200
+
201
+ describe('Content ID Appending', () => {
202
+ it('should append content ID to theSource URLs', async () => {
203
+ const filePath = path.join(fixturesDir, 'theSource.docx');
204
+
205
+ const result = await processor.processDocument(filePath, {
206
+ operations: { fixContentIds: true },
207
+ contentId: '#test-content',
208
+ });
209
+
210
+ expect(result.success).toBe(true);
211
+
212
+ // Should have modified some URLs
213
+ if (result.appendedContentIds !== undefined) {
214
+ expect(result.appendedContentIds).toBeGreaterThanOrEqual(0);
215
+ }
216
+
217
+ // Check processedLinks for modifications
218
+ const modifiedLinks = result.processedLinks.filter(
219
+ (l) => l.status === 'processed' && l.after
220
+ );
221
+
222
+ // If content IDs were appended, verify they contain the ID
223
+ if (result.appendedContentIds && result.appendedContentIds > 0) {
224
+ expect(modifiedLinks.length).toBeGreaterThan(0);
225
+
226
+ for (const link of modifiedLinks) {
227
+ if (link.after && link.after.includes('thesource')) {
228
+ expect(link.after).toContain('#test-content');
229
+ }
230
+ }
231
+ }
232
+ });
233
+
234
+ it('should skip URLs that already have content IDs', async () => {
235
+ const filePath = path.join(fixturesDir, 'theSource-with-ids.docx');
236
+
237
+ const result = await processor.processDocument(filePath, {
238
+ operations: { fixContentIds: true },
239
+ contentId: '#test-content',
240
+ });
241
+
242
+ expect(result.success).toBe(true);
243
+
244
+ // These URLs already have content IDs, should be skipped
245
+ expect(result.skippedHyperlinks).toBeGreaterThanOrEqual(0);
246
+
247
+ // Should NOT append to URLs that already have IDs
248
+ if (result.appendedContentIds !== undefined) {
249
+ expect(result.appendedContentIds).toBe(0);
250
+ }
251
+ });
252
+
253
+ it('should handle edge case URLs gracefully', async () => {
254
+ const filePath = path.join(fixturesDir, 'theSource-malformed.docx');
255
+
256
+ const result = await processor.processDocument(filePath, {
257
+ operations: { fixContentIds: true },
258
+ contentId: '#test',
259
+ });
260
+
261
+ // Should not crash on edge cases
262
+ expect(result.success).toBe(true);
263
+ expect(result.errorMessages).not.toContain(/crash|exception/i);
264
+ });
265
+ });
266
+
267
+ describe('Custom Replacements', () => {
268
+ it('should apply custom URL replacements', async () => {
269
+ const filePath = path.join(fixturesDir, 'hyperlinks.docx');
270
+
271
+ const result = await processor.processDocument(filePath, {
272
+ customReplacements: [
273
+ {
274
+ find: 'example.com',
275
+ replace: 'new-example.com',
276
+ matchType: 'contains',
277
+ applyTo: 'url',
278
+ },
279
+ ],
280
+ });
281
+
282
+ expect(result.success).toBe(true);
283
+
284
+ // Check if replacements were attempted
285
+ if (result.updatedUrls !== undefined) {
286
+ expect(result.updatedUrls).toBeGreaterThanOrEqual(0);
287
+ }
288
+
289
+ // Check processedLinks for URL modifications
290
+ const urlReplacements = result.processedLinks.filter(
291
+ (l) => l.before && l.after && l.before !== l.after && l.after.includes('new-example.com')
292
+ );
293
+
294
+ if (result.updatedUrls && result.updatedUrls > 0) {
295
+ expect(urlReplacements.length).toBeGreaterThan(0);
296
+ }
297
+ });
298
+
299
+ it('should apply custom text replacements', async () => {
300
+ const filePath = path.join(fixturesDir, 'hyperlinks.docx');
301
+
302
+ const result = await processor.processDocument(filePath, {
303
+ customReplacements: [
304
+ {
305
+ find: 'GitHub',
306
+ replace: 'GitLab',
307
+ matchType: 'exact',
308
+ applyTo: 'text',
309
+ },
310
+ ],
311
+ });
312
+
313
+ expect(result.success).toBe(true);
314
+
315
+ // Text replacements should be tracked
316
+ if (result.updatedDisplayTexts !== undefined) {
317
+ expect(result.updatedDisplayTexts).toBeGreaterThanOrEqual(0);
318
+ }
319
+ });
320
+ });
321
+
322
+ describe('Batch Processing', () => {
323
+ it('should process multiple documents concurrently', async () => {
324
+ const files = [
325
+ path.join(fixturesDir, 'sample.docx'),
326
+ path.join(fixturesDir, 'hyperlinks.docx'),
327
+ path.join(fixturesDir, 'theSource.docx'),
328
+ ];
329
+
330
+ const batchResult = await processor.batchProcess(files, {}, 2);
331
+
332
+ expect(batchResult.totalFiles).toBe(3);
333
+ expect(batchResult.successfulFiles).toBe(3);
334
+ expect(batchResult.failedFiles).toBe(0);
335
+ expect(batchResult.results).toHaveLength(3);
336
+
337
+ // Each result should be valid
338
+ for (const item of batchResult.results) {
339
+ expect(item.result.success).toBe(true);
340
+ }
341
+ });
342
+
343
+ it('should handle individual file failures in batch', async () => {
344
+ const files = [
345
+ path.join(fixturesDir, 'sample.docx'),
346
+ path.join(fixturesDir, 'corrupt.docx'), // This will fail
347
+ path.join(fixturesDir, 'hyperlinks.docx'),
348
+ ];
349
+
350
+ const batchResult = await processor.batchProcess(files, {}, 2);
351
+
352
+ expect(batchResult.totalFiles).toBe(3);
353
+ expect(batchResult.successfulFiles).toBe(2);
354
+ expect(batchResult.failedFiles).toBe(1);
355
+ });
356
+
357
+ it('should call progress callback for each file', async () => {
358
+ const files = [
359
+ path.join(fixturesDir, 'sample.docx'),
360
+ path.join(fixturesDir, 'hyperlinks.docx'),
361
+ ];
362
+
363
+ const progressCallback = vi.fn();
364
+
365
+ await processor.batchProcess(files, {}, 1, progressCallback);
366
+
367
+ expect(progressCallback).toHaveBeenCalledTimes(2);
368
+ expect(progressCallback).toHaveBeenCalledWith(
369
+ files[0],
370
+ 1,
371
+ 2,
372
+ expect.objectContaining({ success: true })
373
+ );
374
+ });
375
+
376
+ it('should respect concurrency limit', async () => {
377
+ const files = Array(5).fill(path.join(fixturesDir, 'sample.docx'));
378
+
379
+ const startTime = Date.now();
380
+ await processor.batchProcess(files, {}, 2); // Max 2 concurrent
381
+ const duration = Date.now() - startTime;
382
+
383
+ // With concurrency of 2, should take roughly 3 batches (2+2+1)
384
+ // Just verify it completes without errors
385
+ expect(duration).toBeGreaterThan(0);
386
+ });
387
+ });
388
+
389
+ describe('Memory Management', () => {
390
+ it('should trigger garbage collection periodically in batch processing', async () => {
391
+ const files = Array(15).fill(path.join(fixturesDir, 'sample.docx'));
392
+
393
+ // Mock global.gc
394
+ global.gc = vi.fn();
395
+
396
+ await processor.batchProcess(files, {}, 3);
397
+
398
+ // Should trigger GC at least once (every 10 documents)
399
+ if (global.gc) {
400
+ expect(global.gc).toHaveBeenCalled();
401
+ }
402
+ });
403
+
404
+ it('should clean up resources after processing', async () => {
405
+ const filePath = path.join(fixturesDir, 'sample.docx');
406
+
407
+ const result = await processor.processDocument(filePath);
408
+
409
+ expect(result.success).toBe(true);
410
+ expect(result.processingTimeMs).toBeDefined();
411
+ expect(result.processingTimeMs).toBeGreaterThan(0);
412
+ });
413
+ });
414
+
415
+ describe('PowerAutomate API Integration', () => {
416
+ it('should process hyperlinks with PowerAutomate API', async () => {
417
+ const filePath = path.join(fixturesDir, 'theSource.docx');
418
+
419
+ // Mock API response
420
+ const mockApiResponse = {
421
+ success: true,
422
+ body: {
423
+ results: [
424
+ {
425
+ contentId: 'TSRC-ABC-123456',
426
+ documentId: 'uuid-123',
427
+ title: 'Test Document',
428
+ status: 'active',
429
+ },
430
+ ],
431
+ },
432
+ };
433
+
434
+ (hyperlinkService.processHyperlinksWithApi as ReturnType<typeof vi.fn>).mockResolvedValue(mockApiResponse);
435
+
436
+ const result = await processor.processDocument(filePath, {
437
+ apiEndpoint: 'https://api.example.com',
438
+ operations: { updateTitles: true },
439
+ });
440
+
441
+ // If document has theSource hyperlinks, API should be called
442
+ // If not, processing still succeeds but API is not invoked
443
+ if (result.totalHyperlinks > 0) {
444
+ expect(hyperlinkService.processHyperlinksWithApi).toHaveBeenCalled();
445
+ }
446
+ expect(result.success).toBe(true);
447
+ });
448
+
449
+ it('should handle API failures gracefully', async () => {
450
+ const filePath = path.join(fixturesDir, 'theSource.docx');
451
+
452
+ (hyperlinkService.processHyperlinksWithApi as ReturnType<typeof vi.fn>).mockResolvedValue({
453
+ success: false,
454
+ error: 'API timeout',
455
+ });
456
+
457
+ const result = await processor.processDocument(filePath, {
458
+ apiEndpoint: 'https://api.example.com',
459
+ operations: { updateTitles: true },
460
+ });
461
+
462
+ // If document has theSource hyperlinks, API failure causes processing failure
463
+ // If no hyperlinks, processing succeeds without API call
464
+ if (result.totalHyperlinks > 0) {
465
+ expect(result.success).toBe(false);
466
+ expect(result.errorMessages.some((msg) => msg.includes('PowerAutomate'))).toBe(true);
467
+ } else {
468
+ expect(result.success).toBe(true);
469
+ }
470
+ });
471
+
472
+ it('should fail when API endpoint not configured', async () => {
473
+ const filePath = path.join(fixturesDir, 'theSource.docx');
474
+
475
+ const result = await processor.processDocument(filePath, {
476
+ operations: { updateTitles: true },
477
+ // No apiEndpoint provided
478
+ });
479
+
480
+ // If document has theSource hyperlinks, missing API endpoint causes failure
481
+ // If no hyperlinks, processing succeeds
482
+ if (result.totalHyperlinks > 0) {
483
+ expect(result.success).toBe(false);
484
+ expect(result.errorMessages[0]).toContain('API endpoint not configured');
485
+ } else {
486
+ expect(result.success).toBe(true);
487
+ }
488
+ });
489
+ });
490
+
491
+ describe('Error Handling', () => {
492
+ it('should handle processing errors gracefully', async () => {
493
+ const result = await processor.processDocument('');
494
+
495
+ expect(result.success).toBe(false);
496
+ expect(result.errorMessages.length).toBeGreaterThan(0);
497
+ });
498
+
499
+ it('should return proper error structure on failure', async () => {
500
+ const result = await processor.processDocument('/invalid/path.docx');
501
+
502
+ expect(result).toMatchObject({
503
+ success: false,
504
+ errorCount: expect.any(Number),
505
+ errorMessages: expect.any(Array),
506
+ totalHyperlinks: expect.any(Number),
507
+ processedHyperlinks: expect.any(Number),
508
+ processedLinks: expect.any(Array),
509
+ });
510
+
511
+ expect(result.errorCount).toBeGreaterThan(0);
512
+ expect(result.errorMessages.length).toBeGreaterThan(0);
513
+ });
514
+ });
515
+ });