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,1604 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Suite for TableProcessor
|
|
3
|
+
*
|
|
4
|
+
* Tests table formatting, uniformity, and Header2 handling.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { vi, describe, it, expect, beforeEach, type Mocked } from 'vitest';
|
|
8
|
+
import { TableProcessor } from '../TableProcessor';
|
|
9
|
+
import type { HLPTableAnalysis } from '../TableProcessor';
|
|
10
|
+
import { Document, Table, Paragraph, Run, Hyperlink, PreservedElement, TableCell, TableRow, inchesToTwips } from 'docxmlater';
|
|
11
|
+
|
|
12
|
+
// Mock docxmlater
|
|
13
|
+
vi.mock('docxmlater');
|
|
14
|
+
|
|
15
|
+
describe('TableProcessor', () => {
|
|
16
|
+
let processor: TableProcessor;
|
|
17
|
+
let mockDoc: Mocked<Document>;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
vi.clearAllMocks();
|
|
21
|
+
processor = new TableProcessor();
|
|
22
|
+
|
|
23
|
+
mockDoc = {
|
|
24
|
+
getTables: vi.fn().mockReturnValue([]),
|
|
25
|
+
getBodyElements: vi.fn().mockReturnValue([]),
|
|
26
|
+
} as unknown as Mocked<Document>;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('applyTableUniformity', () => {
|
|
30
|
+
it('should process tables and recolor cells', async () => {
|
|
31
|
+
const mockCell = createMockCell('CCCCCC');
|
|
32
|
+
const mockRow = createMockRow([mockCell]);
|
|
33
|
+
const mockTable = createMockTable([mockRow]);
|
|
34
|
+
|
|
35
|
+
mockDoc.getTables.mockReturnValue([mockTable]);
|
|
36
|
+
|
|
37
|
+
const result = await processor.applyTableUniformity(mockDoc);
|
|
38
|
+
|
|
39
|
+
expect(result.tablesProcessed).toBe(1);
|
|
40
|
+
expect(result.cellsRecolored).toBeGreaterThanOrEqual(0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should apply Header2 shading to 1x1 tables', async () => {
|
|
44
|
+
// Cell has Heading2 style paragraph — triggers Header2 shading
|
|
45
|
+
const mockH2Paragraph = createMockParagraph('Heading2');
|
|
46
|
+
const mockCell = {
|
|
47
|
+
getShading: vi.fn().mockReturnValue('FFFFFF'),
|
|
48
|
+
setShading: vi.fn(),
|
|
49
|
+
getParagraphs: vi.fn().mockReturnValue([mockH2Paragraph]),
|
|
50
|
+
getFormatting: vi.fn().mockReturnValue({ shading: { fill: 'FFFFFF' } }),
|
|
51
|
+
setAllRunsFont: vi.fn(),
|
|
52
|
+
setAllRunsSize: vi.fn(),
|
|
53
|
+
getText: vi.fn().mockReturnValue('Section Header'),
|
|
54
|
+
hasNestedTables: vi.fn().mockReturnValue(false),
|
|
55
|
+
} as unknown as Mocked<TableCell>;
|
|
56
|
+
const mockRow = createMockRow([mockCell]);
|
|
57
|
+
const mockTable = createMockTable([mockRow]); // 1x1 table
|
|
58
|
+
|
|
59
|
+
mockDoc.getTables.mockReturnValue([mockTable]);
|
|
60
|
+
|
|
61
|
+
const result = await processor.applyTableUniformity(mockDoc, {
|
|
62
|
+
header2Shading: 'BFBFBF',
|
|
63
|
+
otherShading: 'DFDFDF',
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(result.tablesProcessed).toBe(1);
|
|
67
|
+
expect(mockCell.setShading).toHaveBeenCalledWith({ fill: 'BFBFBF', pattern: 'clear', color: 'auto' });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should preserve cells without shading', async () => {
|
|
71
|
+
const mockCell = createMockCell('FFFFFF'); // White/no shading
|
|
72
|
+
const mockRow = createMockRow([mockCell, createMockCell('FFFFFF')]);
|
|
73
|
+
const mockTable = createMockTable([mockRow, mockRow]); // 2x2 table
|
|
74
|
+
|
|
75
|
+
mockDoc.getTables.mockReturnValue([mockTable]);
|
|
76
|
+
|
|
77
|
+
const result = await processor.applyTableUniformity(mockDoc);
|
|
78
|
+
|
|
79
|
+
expect(result.tablesProcessed).toBe(1);
|
|
80
|
+
// White cells should not be recolored
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should handle tables with no rows', async () => {
|
|
84
|
+
const mockTable = createMockTable([]);
|
|
85
|
+
|
|
86
|
+
mockDoc.getTables.mockReturnValue([mockTable]);
|
|
87
|
+
|
|
88
|
+
const result = await processor.applyTableUniformity(mockDoc);
|
|
89
|
+
|
|
90
|
+
expect(result.tablesProcessed).toBe(0);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should handle empty document', async () => {
|
|
94
|
+
mockDoc.getTables.mockReturnValue([]);
|
|
95
|
+
|
|
96
|
+
const result = await processor.applyTableUniformity(mockDoc);
|
|
97
|
+
|
|
98
|
+
expect(result.tablesProcessed).toBe(0);
|
|
99
|
+
expect(result.cellsRecolored).toBe(0);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('detect1x1Tables', () => {
|
|
104
|
+
it('should detect 1x1 tables', () => {
|
|
105
|
+
const mockCell = createMockCell('BFBFBF');
|
|
106
|
+
const mockRow = createMockRow([mockCell]);
|
|
107
|
+
const mockTable = createMockTable([mockRow]);
|
|
108
|
+
|
|
109
|
+
mockDoc.getTables.mockReturnValue([mockTable, createMockTable([mockRow, mockRow])]);
|
|
110
|
+
|
|
111
|
+
const tables = processor.detect1x1Tables(mockDoc);
|
|
112
|
+
|
|
113
|
+
expect(tables).toHaveLength(1);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should return empty array for no 1x1 tables', () => {
|
|
117
|
+
const mockCell = createMockCell('BFBFBF');
|
|
118
|
+
const mockRow = createMockRow([mockCell, mockCell]);
|
|
119
|
+
const mockTable = createMockTable([mockRow, mockRow]);
|
|
120
|
+
|
|
121
|
+
mockDoc.getTables.mockReturnValue([mockTable]);
|
|
122
|
+
|
|
123
|
+
const tables = processor.detect1x1Tables(mockDoc);
|
|
124
|
+
|
|
125
|
+
expect(tables).toHaveLength(0);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('tableHasHeader2Content', () => {
|
|
130
|
+
it('should detect Header2 styled content in table', () => {
|
|
131
|
+
const mockParagraph = createMockParagraph('Heading2');
|
|
132
|
+
const mockCell = createMockCellWithParagraphs([mockParagraph]);
|
|
133
|
+
const mockRow = createMockRow([mockCell]);
|
|
134
|
+
const mockTable = createMockTable([mockRow]);
|
|
135
|
+
|
|
136
|
+
const hasHeader2 = processor.tableHasHeader2Content(mockTable);
|
|
137
|
+
|
|
138
|
+
expect(hasHeader2).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should return false for tables without Header2', () => {
|
|
142
|
+
const mockParagraph = createMockParagraph('Normal');
|
|
143
|
+
const mockCell = createMockCellWithParagraphs([mockParagraph]);
|
|
144
|
+
const mockRow = createMockRow([mockCell]);
|
|
145
|
+
const mockTable = createMockTable([mockRow]);
|
|
146
|
+
|
|
147
|
+
const hasHeader2 = processor.tableHasHeader2Content(mockTable);
|
|
148
|
+
|
|
149
|
+
expect(hasHeader2).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('applySmartTableFormatting', () => {
|
|
154
|
+
it('should detect and format header rows', async () => {
|
|
155
|
+
const boldRun = createMockRun(true);
|
|
156
|
+
const mockParagraph = createMockParagraphWithRuns([boldRun]);
|
|
157
|
+
const headerCell = createMockCellWithParagraphs([mockParagraph]);
|
|
158
|
+
const headerRow = createMockRow([headerCell]);
|
|
159
|
+
const dataRow = createMockRow([createMockCell('FFFFFF')]);
|
|
160
|
+
const mockTable = createMockTable([headerRow, dataRow]);
|
|
161
|
+
|
|
162
|
+
mockDoc.getTables.mockReturnValue([mockTable]);
|
|
163
|
+
|
|
164
|
+
const count = await processor.applySmartTableFormatting(mockDoc);
|
|
165
|
+
|
|
166
|
+
expect(count).toBe(1);
|
|
167
|
+
expect(headerCell.setShading).toHaveBeenCalledWith({ fill: 'BFBFBF', pattern: 'clear', color: 'auto' });
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should handle tables without header rows', async () => {
|
|
171
|
+
const normalRun = createMockRun(false);
|
|
172
|
+
const mockParagraph = createMockParagraphWithRuns([normalRun]);
|
|
173
|
+
const normalCell = createMockCellWithParagraphs([mockParagraph]);
|
|
174
|
+
const normalRow = createMockRow([normalCell]);
|
|
175
|
+
const mockTable = createMockTable([normalRow, normalRow]);
|
|
176
|
+
|
|
177
|
+
mockDoc.getTables.mockReturnValue([mockTable]);
|
|
178
|
+
|
|
179
|
+
const count = await processor.applySmartTableFormatting(mockDoc);
|
|
180
|
+
|
|
181
|
+
expect(count).toBe(1);
|
|
182
|
+
// No header shading should be applied
|
|
183
|
+
expect(normalCell.setShading).not.toHaveBeenCalled();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Helper functions
|
|
189
|
+
|
|
190
|
+
function createMockCell(shading: string): Mocked<TableCell> {
|
|
191
|
+
const mockParagraph = createMockParagraph('Normal');
|
|
192
|
+
return {
|
|
193
|
+
getShading: vi.fn().mockReturnValue(shading),
|
|
194
|
+
setShading: vi.fn(),
|
|
195
|
+
getParagraphs: vi.fn().mockReturnValue([mockParagraph]),
|
|
196
|
+
getFormatting: vi.fn().mockReturnValue({ shading: { fill: shading } }),
|
|
197
|
+
setAllRunsFont: vi.fn(),
|
|
198
|
+
setAllRunsSize: vi.fn(),
|
|
199
|
+
getText: vi.fn().mockReturnValue('Sample text'),
|
|
200
|
+
hasNestedTables: vi.fn().mockReturnValue(false),
|
|
201
|
+
} as unknown as Mocked<TableCell>;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function createMockCellWithParagraphs(paragraphs: any[]): Mocked<TableCell> {
|
|
205
|
+
return {
|
|
206
|
+
getShading: vi.fn().mockReturnValue('FFFFFF'),
|
|
207
|
+
setShading: vi.fn(),
|
|
208
|
+
getParagraphs: vi.fn().mockReturnValue(paragraphs),
|
|
209
|
+
getFormatting: vi.fn().mockReturnValue({}),
|
|
210
|
+
setAllRunsFont: vi.fn(),
|
|
211
|
+
setAllRunsSize: vi.fn(),
|
|
212
|
+
getText: vi.fn().mockReturnValue('Sample text'),
|
|
213
|
+
hasNestedTables: vi.fn().mockReturnValue(false),
|
|
214
|
+
} as unknown as Mocked<TableCell>;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function createMockRow(cells: any[]): Mocked<TableRow> {
|
|
218
|
+
return {
|
|
219
|
+
getCells: vi.fn().mockReturnValue(cells),
|
|
220
|
+
} as unknown as Mocked<TableRow>;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function createMockTable(rows: any[]): Mocked<Table> {
|
|
224
|
+
return {
|
|
225
|
+
getRows: vi.fn().mockReturnValue(rows),
|
|
226
|
+
isFloating: vi.fn().mockReturnValue(false),
|
|
227
|
+
} as unknown as Mocked<Table>;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function createMockParagraph(style: string): Mocked<Paragraph> {
|
|
231
|
+
return {
|
|
232
|
+
getStyle: vi.fn().mockReturnValue(style),
|
|
233
|
+
setStyle: vi.fn(),
|
|
234
|
+
getText: vi.fn().mockReturnValue('Sample text'),
|
|
235
|
+
getRuns: vi.fn().mockReturnValue([]),
|
|
236
|
+
getNumbering: vi.fn().mockReturnValue(null),
|
|
237
|
+
getContent: vi.fn().mockReturnValue([]),
|
|
238
|
+
setAlignment: vi.fn(),
|
|
239
|
+
setSpaceBefore: vi.fn(),
|
|
240
|
+
setSpaceAfter: vi.fn(),
|
|
241
|
+
setLineSpacing: vi.fn(),
|
|
242
|
+
setNumbering: vi.fn(),
|
|
243
|
+
getLeftIndent: vi.fn().mockReturnValue(undefined),
|
|
244
|
+
setLeftIndent: vi.fn(),
|
|
245
|
+
setFirstLineIndent: vi.fn(),
|
|
246
|
+
} as unknown as Mocked<Paragraph>;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function createMockParagraphWithRuns(runs: any[], style = 'Normal'): Mocked<Paragraph> {
|
|
250
|
+
return {
|
|
251
|
+
getStyle: vi.fn().mockReturnValue(style),
|
|
252
|
+
setStyle: vi.fn(),
|
|
253
|
+
getText: vi.fn().mockReturnValue('Sample text'),
|
|
254
|
+
getRuns: vi.fn().mockReturnValue(runs),
|
|
255
|
+
getNumbering: vi.fn().mockReturnValue(null),
|
|
256
|
+
getContent: vi.fn().mockReturnValue(runs),
|
|
257
|
+
setAlignment: vi.fn(),
|
|
258
|
+
setSpaceBefore: vi.fn(),
|
|
259
|
+
setSpaceAfter: vi.fn(),
|
|
260
|
+
setLineSpacing: vi.fn(),
|
|
261
|
+
setNumbering: vi.fn(),
|
|
262
|
+
getLeftIndent: vi.fn().mockReturnValue(undefined),
|
|
263
|
+
setLeftIndent: vi.fn(),
|
|
264
|
+
setFirstLineIndent: vi.fn(),
|
|
265
|
+
} as unknown as Mocked<Paragraph>;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function createMockRun(bold: boolean, characterStyle?: string): Mocked<Run> {
|
|
269
|
+
return {
|
|
270
|
+
getText: vi.fn().mockReturnValue('Text'),
|
|
271
|
+
getFormatting: vi.fn().mockReturnValue({ bold, characterStyle }),
|
|
272
|
+
setFont: vi.fn(),
|
|
273
|
+
setSize: vi.fn(),
|
|
274
|
+
setBold: vi.fn(),
|
|
275
|
+
setColor: vi.fn(),
|
|
276
|
+
setUnderline: vi.fn(),
|
|
277
|
+
setCharacterStyle: vi.fn(),
|
|
278
|
+
} as unknown as Mocked<Run>;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ═══════════════════════════════════════════════════════════
|
|
282
|
+
// HLP Table Test Helpers
|
|
283
|
+
// ═══════════════════════════════════════════════════════════
|
|
284
|
+
|
|
285
|
+
function createMockHyperlink(): Mocked<Hyperlink> {
|
|
286
|
+
const mockHyperlink = {
|
|
287
|
+
setFormatting: vi.fn(),
|
|
288
|
+
getRun: vi.fn().mockReturnValue(createMockRun(true)),
|
|
289
|
+
getText: vi.fn().mockReturnValue('Link text'),
|
|
290
|
+
getUrl: vi.fn().mockReturnValue('https://example.com'),
|
|
291
|
+
// Mark as Hyperlink instance for isHyperlink() type guard
|
|
292
|
+
constructor: { name: 'Hyperlink' },
|
|
293
|
+
} as unknown as Mocked<Hyperlink>;
|
|
294
|
+
// Make instanceof check work with mock
|
|
295
|
+
Object.setPrototypeOf(mockHyperlink, Hyperlink.prototype);
|
|
296
|
+
return mockHyperlink;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function createHLPHeaderCell(text = 'High Level Process'): Mocked<TableCell> {
|
|
300
|
+
const mockParagraph = createMockParagraph('Heading2');
|
|
301
|
+
mockParagraph.getText.mockReturnValue(text);
|
|
302
|
+
return {
|
|
303
|
+
getShading: vi.fn().mockReturnValue('FFC000'),
|
|
304
|
+
setShading: vi.fn(),
|
|
305
|
+
setBorders: vi.fn(),
|
|
306
|
+
setMargins: vi.fn(),
|
|
307
|
+
getParagraphs: vi.fn().mockReturnValue([mockParagraph]),
|
|
308
|
+
getFormatting: vi.fn().mockReturnValue({ shading: { fill: 'FFC000' } }),
|
|
309
|
+
getColumnSpan: vi.fn().mockReturnValue(1),
|
|
310
|
+
setAllRunsFont: vi.fn(),
|
|
311
|
+
setAllRunsSize: vi.fn(),
|
|
312
|
+
getText: vi.fn().mockReturnValue(text),
|
|
313
|
+
hasNestedTables: vi.fn().mockReturnValue(false),
|
|
314
|
+
} as unknown as Mocked<TableCell>;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function createHLPDataCell(shading?: string): Mocked<TableCell> {
|
|
318
|
+
const numberedRun = createMockRun(false);
|
|
319
|
+
const mainRun = createMockRun(true);
|
|
320
|
+
|
|
321
|
+
const mainPara = createMockParagraphWithRuns([mainRun]);
|
|
322
|
+
// Level-0 items in HLP tables are ListParagraph without explicit numbering.
|
|
323
|
+
// The code converts them to Normal + explicit numbering via setNumbering().
|
|
324
|
+
mainPara.getStyle.mockReturnValue('ListParagraph');
|
|
325
|
+
let mainParaNumbering: { numId: number; level: number } | null = null;
|
|
326
|
+
mainPara.getNumbering.mockImplementation(() => mainParaNumbering);
|
|
327
|
+
mainPara.setNumbering.mockImplementation((numId: number, level: number) => {
|
|
328
|
+
mainParaNumbering = { numId, level };
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const numberedPara = createMockParagraphWithRuns([numberedRun]);
|
|
332
|
+
numberedPara.getNumbering.mockReturnValue({ numId: 33, level: 1 });
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
getShading: vi.fn().mockReturnValue(shading || 'FFFFFF'),
|
|
336
|
+
setShading: vi.fn(),
|
|
337
|
+
setBorders: vi.fn(),
|
|
338
|
+
setMargins: vi.fn(),
|
|
339
|
+
getParagraphs: vi.fn().mockReturnValue([mainPara, numberedPara]),
|
|
340
|
+
getFormatting: vi.fn().mockReturnValue({ shading: shading ? { fill: shading } : undefined }),
|
|
341
|
+
getColumnSpan: vi.fn().mockReturnValue(1),
|
|
342
|
+
setAllRunsFont: vi.fn(),
|
|
343
|
+
setAllRunsSize: vi.fn(),
|
|
344
|
+
getText: vi.fn().mockReturnValue('Item1\nA1'),
|
|
345
|
+
hasNestedTables: vi.fn().mockReturnValue(false),
|
|
346
|
+
} as unknown as Mocked<TableCell>;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function createHLPTipsCell(): Mocked<TableCell> {
|
|
350
|
+
const tipRun = createMockRun(false);
|
|
351
|
+
const tipPara = createMockParagraphWithRuns([tipRun]);
|
|
352
|
+
tipPara.getText.mockReturnValue('[TIP] Some tip text');
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
getShading: vi.fn().mockReturnValue('FFF2CC'),
|
|
356
|
+
setShading: vi.fn(),
|
|
357
|
+
setBorders: vi.fn(),
|
|
358
|
+
setMargins: vi.fn(),
|
|
359
|
+
getParagraphs: vi.fn().mockReturnValue([tipPara]),
|
|
360
|
+
getFormatting: vi.fn().mockReturnValue({ shading: { fill: 'FFF2CC' } }),
|
|
361
|
+
getColumnSpan: vi.fn().mockReturnValue(1),
|
|
362
|
+
setAllRunsFont: vi.fn(),
|
|
363
|
+
setAllRunsSize: vi.fn(),
|
|
364
|
+
getText: vi.fn().mockReturnValue('[TIP] Some tip text'),
|
|
365
|
+
hasNestedTables: vi.fn().mockReturnValue(false),
|
|
366
|
+
} as unknown as Mocked<TableCell>;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function createMockHLPTable(variant: 'single-column' | 'two-column'): Mocked<Table> {
|
|
370
|
+
const headerCell = createHLPHeaderCell();
|
|
371
|
+
if (variant === 'two-column') {
|
|
372
|
+
headerCell.getColumnSpan.mockReturnValue(2);
|
|
373
|
+
}
|
|
374
|
+
const headerRow = createMockRow([headerCell]);
|
|
375
|
+
|
|
376
|
+
const dataRows: Mocked<TableRow>[] = [];
|
|
377
|
+
for (let i = 0; i < 3; i++) {
|
|
378
|
+
if (variant === 'single-column') {
|
|
379
|
+
dataRows.push(createMockRow([createHLPDataCell()]));
|
|
380
|
+
} else {
|
|
381
|
+
dataRows.push(createMockRow([createHLPDataCell(), createHLPTipsCell()]));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const allRows = [headerRow, ...dataRows];
|
|
386
|
+
return {
|
|
387
|
+
getRows: vi.fn().mockReturnValue(allRows),
|
|
388
|
+
getColumnCount: vi.fn().mockReturnValue(variant === 'single-column' ? 1 : 2),
|
|
389
|
+
isFloating: vi.fn().mockReturnValue(false),
|
|
390
|
+
setBorders: vi.fn(),
|
|
391
|
+
setShading: vi.fn(),
|
|
392
|
+
} as unknown as Mocked<Table>;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ═══════════════════════════════════════════════════════════
|
|
396
|
+
// HLP Table Tests
|
|
397
|
+
// ═══════════════════════════════════════════════════════════
|
|
398
|
+
|
|
399
|
+
describe('HLP Table Processing', () => {
|
|
400
|
+
let processor: TableProcessor;
|
|
401
|
+
let mockDoc: Mocked<Document>;
|
|
402
|
+
|
|
403
|
+
beforeEach(() => {
|
|
404
|
+
vi.clearAllMocks();
|
|
405
|
+
processor = new TableProcessor();
|
|
406
|
+
mockDoc = {
|
|
407
|
+
getTables: vi.fn().mockReturnValue([]),
|
|
408
|
+
// getBodyElements returns same tables — fixPostHLPTableNumbering
|
|
409
|
+
// is a no-op when there are no post-table paragraphs in the array
|
|
410
|
+
getBodyElements: vi.fn().mockReturnValue([]),
|
|
411
|
+
// getNumberingManager — convertHLPBulletsToLettered gracefully handles null
|
|
412
|
+
getNumberingManager: vi.fn().mockReturnValue(null),
|
|
413
|
+
} as unknown as Mocked<Document>;
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
describe('analyzeHLPTable', () => {
|
|
417
|
+
it('should identify a single-column HLP table', () => {
|
|
418
|
+
const table = createMockHLPTable('single-column');
|
|
419
|
+
const analysis = processor.analyzeHLPTable(table);
|
|
420
|
+
|
|
421
|
+
expect(analysis.isHLP).toBe(true);
|
|
422
|
+
expect(analysis.variant).toBe('single-column');
|
|
423
|
+
expect(analysis.columnCount).toBe(1);
|
|
424
|
+
expect(analysis.rowCount).toBe(4); // 1 header + 3 data
|
|
425
|
+
expect(analysis.hasTipsColumn).toBe(false);
|
|
426
|
+
expect(analysis.headerText).toBe('High Level Process');
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('should identify a two-column HLP table with tips', () => {
|
|
430
|
+
const table = createMockHLPTable('two-column');
|
|
431
|
+
const analysis = processor.analyzeHLPTable(table);
|
|
432
|
+
|
|
433
|
+
expect(analysis.isHLP).toBe(true);
|
|
434
|
+
expect(analysis.variant).toBe('two-column');
|
|
435
|
+
expect(analysis.columnCount).toBe(2);
|
|
436
|
+
expect(analysis.hasTipsColumn).toBe(true);
|
|
437
|
+
expect(analysis.headerCellSpan).toBe(2);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('should reject tables without FFC000 shading', () => {
|
|
441
|
+
const normalCell = createMockCell('BFBFBF');
|
|
442
|
+
const row1 = createMockRow([normalCell]);
|
|
443
|
+
const row2 = createMockRow([createMockCell('FFFFFF')]);
|
|
444
|
+
const table = createMockTable([row1, row2]);
|
|
445
|
+
|
|
446
|
+
const analysis = processor.analyzeHLPTable(table);
|
|
447
|
+
|
|
448
|
+
expect(analysis.isHLP).toBe(false);
|
|
449
|
+
expect(analysis.variant).toBeNull();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('should reject tables with only 1 row (header only)', () => {
|
|
453
|
+
const headerCell = createHLPHeaderCell();
|
|
454
|
+
const headerRow = createMockRow([headerCell]);
|
|
455
|
+
const table = createMockTable([headerRow]);
|
|
456
|
+
|
|
457
|
+
const analysis = processor.analyzeHLPTable(table);
|
|
458
|
+
|
|
459
|
+
expect(analysis.isHLP).toBe(false);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('should reject empty tables', () => {
|
|
463
|
+
const table = createMockTable([]);
|
|
464
|
+
|
|
465
|
+
const analysis = processor.analyzeHLPTable(table);
|
|
466
|
+
|
|
467
|
+
expect(analysis.isHLP).toBe(false);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('should reject tables with FFC000 shading but without "High Level Process" text', () => {
|
|
471
|
+
// A non-HLP table that happens to use FFC000 in its header cell
|
|
472
|
+
const headerCell = createHLPHeaderCell('Project Schedule');
|
|
473
|
+
const headerRow = createMockRow([headerCell]);
|
|
474
|
+
const dataRow = createMockRow([createMockCell('FFFFFF')]);
|
|
475
|
+
const table = createMockTable([headerRow, dataRow]);
|
|
476
|
+
|
|
477
|
+
const analysis = processor.analyzeHLPTable(table);
|
|
478
|
+
|
|
479
|
+
expect(analysis.isHLP).toBe(false);
|
|
480
|
+
expect(analysis.variant).toBeNull();
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
describe('isHLPTable (backward compatibility)', () => {
|
|
485
|
+
it('should return true for HLP tables', () => {
|
|
486
|
+
const table = createMockHLPTable('single-column');
|
|
487
|
+
expect(processor.isHLPTable(table)).toBe(true);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('should return false for non-HLP tables', () => {
|
|
491
|
+
const normalCell = createMockCell('BFBFBF');
|
|
492
|
+
const row1 = createMockRow([normalCell]);
|
|
493
|
+
const row2 = createMockRow([createMockCell('FFFFFF')]);
|
|
494
|
+
const table = createMockTable([row1, row2]);
|
|
495
|
+
expect(processor.isHLPTable(table)).toBe(false);
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
describe('processHLPTables', () => {
|
|
500
|
+
it('should apply table-level borders for single-column variant', async () => {
|
|
501
|
+
const table = createMockHLPTable('single-column');
|
|
502
|
+
mockDoc.getTables.mockReturnValue([table]);
|
|
503
|
+
|
|
504
|
+
const result = await processor.processHLPTables(mockDoc);
|
|
505
|
+
|
|
506
|
+
expect(result.tablesFound).toBe(1);
|
|
507
|
+
expect(result.singleColumnTables).toBe(1);
|
|
508
|
+
expect(result.twoColumnTables).toBe(0);
|
|
509
|
+
// Table-level borders should be set
|
|
510
|
+
expect(table.setBorders).toHaveBeenCalledWith(
|
|
511
|
+
expect.objectContaining({
|
|
512
|
+
top: expect.objectContaining({ style: 'single', color: 'FFC000' }),
|
|
513
|
+
bottom: expect.objectContaining({ style: 'single', color: 'FFC000' }),
|
|
514
|
+
})
|
|
515
|
+
);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it('should clear table-level borders for two-column variant', async () => {
|
|
519
|
+
const table = createMockHLPTable('two-column');
|
|
520
|
+
mockDoc.getTables.mockReturnValue([table]);
|
|
521
|
+
|
|
522
|
+
const result = await processor.processHLPTables(mockDoc);
|
|
523
|
+
|
|
524
|
+
expect(result.tablesFound).toBe(1);
|
|
525
|
+
expect(result.twoColumnTables).toBe(1);
|
|
526
|
+
expect(result.singleColumnTables).toBe(0);
|
|
527
|
+
// Table-level borders should be cleared (none)
|
|
528
|
+
expect(table.setBorders).toHaveBeenCalledWith(
|
|
529
|
+
expect.objectContaining({
|
|
530
|
+
top: expect.objectContaining({ style: 'none' }),
|
|
531
|
+
bottom: expect.objectContaining({ style: 'none' }),
|
|
532
|
+
})
|
|
533
|
+
);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it('should apply Heading2 style to header row', async () => {
|
|
537
|
+
const table = createMockHLPTable('single-column');
|
|
538
|
+
mockDoc.getTables.mockReturnValue([table]);
|
|
539
|
+
|
|
540
|
+
const result = await processor.processHLPTables(mockDoc);
|
|
541
|
+
|
|
542
|
+
expect(result.headersStyled).toBe(1);
|
|
543
|
+
// Header cell paragraph should have Heading2 style set
|
|
544
|
+
const headerCell = table.getRows()[0].getCells()[0];
|
|
545
|
+
const headerPara = headerCell.getParagraphs()[0];
|
|
546
|
+
expect(headerPara.setStyle).toHaveBeenCalledWith('Heading2');
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it('should NOT apply bold to numbered sub-item runs', async () => {
|
|
550
|
+
const table = createMockHLPTable('single-column');
|
|
551
|
+
mockDoc.getTables.mockReturnValue([table]);
|
|
552
|
+
|
|
553
|
+
await processor.processHLPTables(mockDoc, {
|
|
554
|
+
header2Shading: 'BFBFBF',
|
|
555
|
+
otherShading: 'DFDFDF',
|
|
556
|
+
normalFontFamily: 'Arial',
|
|
557
|
+
normalFontSize: 11,
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// Get data row, content cell, numbered paragraph's run
|
|
561
|
+
const dataRow = table.getRows()[1];
|
|
562
|
+
const dataCell = dataRow.getCells()[0];
|
|
563
|
+
const paras = dataCell.getParagraphs();
|
|
564
|
+
// P[1] is the numbered sub-item paragraph
|
|
565
|
+
const numberedPara = paras[1];
|
|
566
|
+
const numberedRuns = numberedPara.getRuns();
|
|
567
|
+
|
|
568
|
+
// Sub-items should NOT be bold (only P[0] main action items are bold)
|
|
569
|
+
expect(numberedRuns[0].setBold).not.toHaveBeenCalled();
|
|
570
|
+
expect(numberedRuns[0].setFont).toHaveBeenCalledWith('Arial');
|
|
571
|
+
expect(numberedRuns[0].setSize).toHaveBeenCalledWith(11);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it('should apply heading2 font/size to main action item (P[0])', async () => {
|
|
575
|
+
const table = createMockHLPTable('single-column');
|
|
576
|
+
mockDoc.getTables.mockReturnValue([table]);
|
|
577
|
+
|
|
578
|
+
await processor.processHLPTables(mockDoc, {
|
|
579
|
+
header2Shading: 'BFBFBF',
|
|
580
|
+
otherShading: 'DFDFDF',
|
|
581
|
+
heading2FontFamily: 'Georgia',
|
|
582
|
+
heading2FontSize: 16,
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
const dataRow = table.getRows()[1];
|
|
586
|
+
const dataCell = dataRow.getCells()[0];
|
|
587
|
+
const mainPara = dataCell.getParagraphs()[0]; // P[0] = main action item
|
|
588
|
+
const mainRuns = mainPara.getRuns();
|
|
589
|
+
|
|
590
|
+
// Main action items use normalFont/normalSize (not heading2 font/size)
|
|
591
|
+
// heading2 settings only affect the header row, not content runs
|
|
592
|
+
expect(mainRuns[0].setFont).toHaveBeenCalledWith('Verdana');
|
|
593
|
+
expect(mainRuns[0].setSize).toHaveBeenCalledWith(12);
|
|
594
|
+
expect(mainRuns[0].setBold).toHaveBeenCalledWith(true);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it('should clear Hyperlink character style and restore blue on all hyperlink runs', async () => {
|
|
598
|
+
// Any run with characterStyle=Hyperlink is a real hyperlink.
|
|
599
|
+
// Even internal bookmark hyperlinks with color=auto should get blue restored.
|
|
600
|
+
const hyperlinkRun = createMockRun(true, 'Hyperlink');
|
|
601
|
+
hyperlinkRun.getFormatting.mockReturnValue({
|
|
602
|
+
bold: true,
|
|
603
|
+
characterStyle: 'Hyperlink',
|
|
604
|
+
color: undefined,
|
|
605
|
+
underline: 'none',
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
const mainPara = createMockParagraphWithRuns([hyperlinkRun], 'ListParagraph');
|
|
609
|
+
mainPara.getNumbering.mockReturnValue(null);
|
|
610
|
+
|
|
611
|
+
const dataCell = {
|
|
612
|
+
getShading: vi.fn().mockReturnValue('FFFFFF'),
|
|
613
|
+
setShading: vi.fn(),
|
|
614
|
+
setBorders: vi.fn(),
|
|
615
|
+
setMargins: vi.fn(),
|
|
616
|
+
getParagraphs: vi.fn().mockReturnValue([mainPara]),
|
|
617
|
+
getFormatting: vi.fn().mockReturnValue({ shading: undefined }),
|
|
618
|
+
getColumnSpan: vi.fn().mockReturnValue(1),
|
|
619
|
+
setAllRunsFont: vi.fn(),
|
|
620
|
+
setAllRunsSize: vi.fn(),
|
|
621
|
+
getText: vi.fn().mockReturnValue('Item with Hyperlink style'),
|
|
622
|
+
hasNestedTables: vi.fn().mockReturnValue(false),
|
|
623
|
+
} as unknown as Mocked<TableCell>;
|
|
624
|
+
|
|
625
|
+
const headerCell = createHLPHeaderCell();
|
|
626
|
+
const headerRow = createMockRow([headerCell]);
|
|
627
|
+
const dataRow = createMockRow([dataCell]);
|
|
628
|
+
const table = {
|
|
629
|
+
getRows: vi.fn().mockReturnValue([headerRow, dataRow]),
|
|
630
|
+
getColumnCount: vi.fn().mockReturnValue(1),
|
|
631
|
+
isFloating: vi.fn().mockReturnValue(false),
|
|
632
|
+
setBorders: vi.fn(),
|
|
633
|
+
} as unknown as Mocked<Table>;
|
|
634
|
+
|
|
635
|
+
mockDoc.getTables.mockReturnValue([table]);
|
|
636
|
+
await processor.processHLPTables(mockDoc);
|
|
637
|
+
|
|
638
|
+
// Should clear the Hyperlink charStyle to prevent font/size conflicts
|
|
639
|
+
expect(hyperlinkRun.setCharacterStyle).toHaveBeenCalledWith(undefined);
|
|
640
|
+
// After clearing charStyle, setFont/setSize can drop run properties,
|
|
641
|
+
// so restoreHyperlink re-applies blue color and underline
|
|
642
|
+
expect(hyperlinkRun.setColor).toHaveBeenCalledWith('0000FF');
|
|
643
|
+
expect(hyperlinkRun.setUnderline).toHaveBeenCalledWith('single');
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it('should restore blue+underline on internal bookmark hyperlinks (color=auto)', async () => {
|
|
647
|
+
// Internal bookmark hyperlinks have characterStyle=Hyperlink with color=auto.
|
|
648
|
+
// Previously these were treated as "placeholders" and NOT restored.
|
|
649
|
+
// Now all Hyperlink-styled runs get blue+underline restored.
|
|
650
|
+
const bookmarkRun = createMockRun(true, 'Hyperlink');
|
|
651
|
+
bookmarkRun.getFormatting.mockReturnValue({
|
|
652
|
+
bold: true,
|
|
653
|
+
characterStyle: 'Hyperlink',
|
|
654
|
+
color: 'auto',
|
|
655
|
+
underline: 'single',
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
const mainPara = createMockParagraphWithRuns([bookmarkRun], 'ListParagraph');
|
|
659
|
+
mainPara.getNumbering.mockReturnValue(null);
|
|
660
|
+
|
|
661
|
+
const dataCell = {
|
|
662
|
+
getShading: vi.fn().mockReturnValue('FFFFFF'),
|
|
663
|
+
setShading: vi.fn(),
|
|
664
|
+
setBorders: vi.fn(),
|
|
665
|
+
setMargins: vi.fn(),
|
|
666
|
+
getParagraphs: vi.fn().mockReturnValue([mainPara]),
|
|
667
|
+
getFormatting: vi.fn().mockReturnValue({ shading: undefined }),
|
|
668
|
+
getColumnSpan: vi.fn().mockReturnValue(1),
|
|
669
|
+
setAllRunsFont: vi.fn(),
|
|
670
|
+
setAllRunsSize: vi.fn(),
|
|
671
|
+
getText: vi.fn().mockReturnValue('Determine something'),
|
|
672
|
+
hasNestedTables: vi.fn().mockReturnValue(false),
|
|
673
|
+
} as unknown as Mocked<TableCell>;
|
|
674
|
+
|
|
675
|
+
const headerCell = createHLPHeaderCell();
|
|
676
|
+
const headerRow = createMockRow([headerCell]);
|
|
677
|
+
const dataRow = createMockRow([dataCell]);
|
|
678
|
+
const table = {
|
|
679
|
+
getRows: vi.fn().mockReturnValue([headerRow, dataRow]),
|
|
680
|
+
getColumnCount: vi.fn().mockReturnValue(1),
|
|
681
|
+
isFloating: vi.fn().mockReturnValue(false),
|
|
682
|
+
setBorders: vi.fn(),
|
|
683
|
+
} as unknown as Mocked<Table>;
|
|
684
|
+
|
|
685
|
+
mockDoc.getTables.mockReturnValue([table]);
|
|
686
|
+
await processor.processHLPTables(mockDoc);
|
|
687
|
+
|
|
688
|
+
// Even though color was 'auto', it should still get blue+underline restored
|
|
689
|
+
expect(bookmarkRun.setCharacterStyle).toHaveBeenCalledWith(undefined);
|
|
690
|
+
expect(bookmarkRun.setColor).toHaveBeenCalledWith('0000FF');
|
|
691
|
+
expect(bookmarkRun.setUnderline).toHaveBeenCalledWith('single');
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
it('should detect hyperlinks via paragraph content structure when characterStyle and color are missing', async () => {
|
|
695
|
+
// Simulate a hyperlink run whose characterStyle was cleared by
|
|
696
|
+
// standardizeHyperlinkFormatting() and whose blue color was dropped
|
|
697
|
+
// by assignStylesToDocument() calling setFont()/setSize().
|
|
698
|
+
// The run has no Hyperlink characterStyle and no blue color — only
|
|
699
|
+
// its structural position inside a Hyperlink container identifies it.
|
|
700
|
+
const orphanedHyperlinkRun = createMockRun(false);
|
|
701
|
+
orphanedHyperlinkRun.getFormatting.mockReturnValue({
|
|
702
|
+
bold: false,
|
|
703
|
+
characterStyle: undefined,
|
|
704
|
+
color: '000000', // Black — not blue
|
|
705
|
+
underline: 'none',
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
// A direct paragraph run (not inside any container)
|
|
709
|
+
// Must pass instanceof Run check so it's added to directRuns set
|
|
710
|
+
const directRun = createMockRun(false);
|
|
711
|
+
Object.setPrototypeOf(directRun, Run.prototype);
|
|
712
|
+
directRun.getFormatting.mockReturnValue({
|
|
713
|
+
bold: false,
|
|
714
|
+
characterStyle: undefined,
|
|
715
|
+
color: '000000',
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
// Create a mock Hyperlink container that holds the orphanedHyperlinkRun
|
|
719
|
+
const mockHyperlink = {} as any;
|
|
720
|
+
Object.setPrototypeOf(mockHyperlink, Hyperlink.prototype);
|
|
721
|
+
|
|
722
|
+
// Paragraph content: directRun (direct child) + Hyperlink container
|
|
723
|
+
// para.getRuns() returns both runs (direct + hyperlink children)
|
|
724
|
+
const mainPara = createMockParagraph('ListParagraph');
|
|
725
|
+
mainPara.getContent.mockReturnValue([directRun, mockHyperlink]);
|
|
726
|
+
mainPara.getRuns.mockReturnValue([directRun, orphanedHyperlinkRun]);
|
|
727
|
+
mainPara.getNumbering.mockReturnValue(null);
|
|
728
|
+
|
|
729
|
+
const dataCell = {
|
|
730
|
+
getShading: vi.fn().mockReturnValue('FFFFFF'),
|
|
731
|
+
setShading: vi.fn(),
|
|
732
|
+
setBorders: vi.fn(),
|
|
733
|
+
setMargins: vi.fn(),
|
|
734
|
+
getParagraphs: vi.fn().mockReturnValue([mainPara]),
|
|
735
|
+
getFormatting: vi.fn().mockReturnValue({ shading: undefined }),
|
|
736
|
+
getColumnSpan: vi.fn().mockReturnValue(1),
|
|
737
|
+
setAllRunsFont: vi.fn(),
|
|
738
|
+
setAllRunsSize: vi.fn(),
|
|
739
|
+
getText: vi.fn().mockReturnValue('Text with link'),
|
|
740
|
+
hasNestedTables: vi.fn().mockReturnValue(false),
|
|
741
|
+
} as unknown as Mocked<TableCell>;
|
|
742
|
+
|
|
743
|
+
const headerCell = createHLPHeaderCell();
|
|
744
|
+
const headerRow = createMockRow([headerCell]);
|
|
745
|
+
const dataRow = createMockRow([dataCell]);
|
|
746
|
+
const table = {
|
|
747
|
+
getRows: vi.fn().mockReturnValue([headerRow, dataRow]),
|
|
748
|
+
getColumnCount: vi.fn().mockReturnValue(1),
|
|
749
|
+
isFloating: vi.fn().mockReturnValue(false),
|
|
750
|
+
setBorders: vi.fn(),
|
|
751
|
+
} as unknown as Mocked<Table>;
|
|
752
|
+
|
|
753
|
+
mockDoc.getTables.mockReturnValue([table]);
|
|
754
|
+
await processor.processHLPTables(mockDoc);
|
|
755
|
+
|
|
756
|
+
// The orphaned hyperlink run (not in directRuns, hasHyperlinkContainer=true)
|
|
757
|
+
// should be detected structurally and get blue+underline restored
|
|
758
|
+
expect(orphanedHyperlinkRun.setColor).toHaveBeenCalledWith('0000FF');
|
|
759
|
+
expect(orphanedHyperlinkRun.setUnderline).toHaveBeenCalledWith('single');
|
|
760
|
+
|
|
761
|
+
// The direct run should NOT get hyperlink formatting
|
|
762
|
+
expect(directRun.setColor).not.toHaveBeenCalledWith('0000FF');
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it('should change ListParagraph to Normal for non-numbered paragraphs', async () => {
|
|
766
|
+
// ListParagraph paragraphs without numbering would inherit the style's
|
|
767
|
+
// numId=33 because docxmlater strips numId=0 on save. Fix by switching
|
|
768
|
+
// to Normal style and preserving indentation.
|
|
769
|
+
const noteRun = createMockRun(true);
|
|
770
|
+
const notePara = createMockParagraphWithRuns([noteRun], 'ListParagraph');
|
|
771
|
+
notePara.getNumbering.mockReturnValue(null); // No numbering (had numId=0)
|
|
772
|
+
notePara.getLeftIndent.mockReturnValue(1440);
|
|
773
|
+
notePara.getText.mockReturnValue('Note: some text');
|
|
774
|
+
|
|
775
|
+
const mainRun = createMockRun(true);
|
|
776
|
+
const mainPara = createMockParagraphWithRuns([mainRun], 'ListParagraph');
|
|
777
|
+
mainPara.getNumbering.mockReturnValue(null);
|
|
778
|
+
mainPara.getLeftIndent.mockReturnValue(720);
|
|
779
|
+
|
|
780
|
+
const dataCell = {
|
|
781
|
+
getShading: vi.fn().mockReturnValue('FFFFFF'),
|
|
782
|
+
setShading: vi.fn(),
|
|
783
|
+
setBorders: vi.fn(),
|
|
784
|
+
setMargins: vi.fn(),
|
|
785
|
+
getParagraphs: vi.fn().mockReturnValue([mainPara, notePara]),
|
|
786
|
+
getFormatting: vi.fn().mockReturnValue({ shading: undefined }),
|
|
787
|
+
getColumnSpan: vi.fn().mockReturnValue(1),
|
|
788
|
+
setAllRunsFont: vi.fn(),
|
|
789
|
+
setAllRunsSize: vi.fn(),
|
|
790
|
+
getText: vi.fn().mockReturnValue('Item\nNote: text'),
|
|
791
|
+
hasNestedTables: vi.fn().mockReturnValue(false),
|
|
792
|
+
} as unknown as Mocked<TableCell>;
|
|
793
|
+
|
|
794
|
+
const headerCell = createHLPHeaderCell();
|
|
795
|
+
const headerRow = createMockRow([headerCell]);
|
|
796
|
+
const dataRow = createMockRow([dataCell]);
|
|
797
|
+
const table = {
|
|
798
|
+
getRows: vi.fn().mockReturnValue([headerRow, dataRow]),
|
|
799
|
+
getColumnCount: vi.fn().mockReturnValue(1),
|
|
800
|
+
isFloating: vi.fn().mockReturnValue(false),
|
|
801
|
+
setBorders: vi.fn(),
|
|
802
|
+
} as unknown as Mocked<Table>;
|
|
803
|
+
|
|
804
|
+
mockDoc.getTables.mockReturnValue([table]);
|
|
805
|
+
await processor.processHLPTables(mockDoc);
|
|
806
|
+
|
|
807
|
+
// Both ListParagraph paragraphs without numbering get changed to Normal
|
|
808
|
+
expect(notePara.setStyle).toHaveBeenCalledWith('Normal');
|
|
809
|
+
expect(notePara.setLeftIndent).toHaveBeenCalledWith(1440);
|
|
810
|
+
expect(mainPara.setStyle).toHaveBeenCalledWith('Normal');
|
|
811
|
+
expect(mainPara.setLeftIndent).toHaveBeenCalledWith(720);
|
|
812
|
+
// No discoveredNumId in this table (no sub-items with numbering),
|
|
813
|
+
// so P[0] does NOT get explicit numbering set
|
|
814
|
+
expect(mainPara.setNumbering).not.toHaveBeenCalled();
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
it('should ensure tips column has FFF2CC shading', async () => {
|
|
818
|
+
const table = createMockHLPTable('two-column');
|
|
819
|
+
mockDoc.getTables.mockReturnValue([table]);
|
|
820
|
+
|
|
821
|
+
await processor.processHLPTables(mockDoc);
|
|
822
|
+
|
|
823
|
+
// Tips cells should have setShading called with FFF2CC
|
|
824
|
+
const dataRow = table.getRows()[1];
|
|
825
|
+
const tipsCell = dataRow.getCells()[1];
|
|
826
|
+
expect(tipsCell.setShading).toHaveBeenCalledWith({ fill: 'FFF2CC', pattern: 'clear', color: 'auto' });
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
it('should discover numId from sub-items and set explicit numbering on P[0]', async () => {
|
|
830
|
+
// P[0] is ListParagraph with no explicit numbering (inherits from style).
|
|
831
|
+
// P[1] is a numbered sub-item with numId=33, level=1.
|
|
832
|
+
// The processor should discover numId=33 from P[1] and set it on P[0] at level 0.
|
|
833
|
+
const mainRun = createMockRun(true);
|
|
834
|
+
const mainPara = createMockParagraphWithRuns([mainRun], 'ListParagraph');
|
|
835
|
+
mainPara.getNumbering.mockReturnValue(null);
|
|
836
|
+
|
|
837
|
+
const subRun = createMockRun(false);
|
|
838
|
+
const subPara = createMockParagraphWithRuns([subRun], 'ListParagraph');
|
|
839
|
+
subPara.getNumbering.mockReturnValue({ numId: 33, level: 1 });
|
|
840
|
+
|
|
841
|
+
const dataCell = {
|
|
842
|
+
getShading: vi.fn().mockReturnValue('FFFFFF'),
|
|
843
|
+
setShading: vi.fn(),
|
|
844
|
+
setBorders: vi.fn(),
|
|
845
|
+
setMargins: vi.fn(),
|
|
846
|
+
getParagraphs: vi.fn().mockReturnValue([mainPara, subPara]),
|
|
847
|
+
getFormatting: vi.fn().mockReturnValue({ shading: undefined }),
|
|
848
|
+
getColumnSpan: vi.fn().mockReturnValue(1),
|
|
849
|
+
setAllRunsFont: vi.fn(),
|
|
850
|
+
setAllRunsSize: vi.fn(),
|
|
851
|
+
getText: vi.fn().mockReturnValue('Item\na. Sub'),
|
|
852
|
+
hasNestedTables: vi.fn().mockReturnValue(false),
|
|
853
|
+
} as unknown as Mocked<TableCell>;
|
|
854
|
+
|
|
855
|
+
const headerCell = createHLPHeaderCell();
|
|
856
|
+
const headerRow = createMockRow([headerCell]);
|
|
857
|
+
const dataRow = createMockRow([dataCell]);
|
|
858
|
+
const table = {
|
|
859
|
+
getRows: vi.fn().mockReturnValue([headerRow, dataRow]),
|
|
860
|
+
getColumnCount: vi.fn().mockReturnValue(1),
|
|
861
|
+
isFloating: vi.fn().mockReturnValue(false),
|
|
862
|
+
setBorders: vi.fn(),
|
|
863
|
+
} as unknown as Mocked<Table>;
|
|
864
|
+
|
|
865
|
+
mockDoc.getTables.mockReturnValue([table]);
|
|
866
|
+
await processor.processHLPTables(mockDoc);
|
|
867
|
+
|
|
868
|
+
// P[0] should be converted to Normal and given explicit numbering
|
|
869
|
+
expect(mainPara.setStyle).toHaveBeenCalledWith('Normal');
|
|
870
|
+
expect(mainPara.setNumbering).toHaveBeenCalledWith(33, 0);
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
it('should apply list indentation levels to numbered paragraphs', async () => {
|
|
874
|
+
// Provide real implementation for inchesToTwips (auto-mocked by vi.mock)
|
|
875
|
+
vi.mocked(inchesToTwips).mockImplementation((inches: number) => Math.round(inches * 1440));
|
|
876
|
+
|
|
877
|
+
const table = createMockHLPTable('single-column');
|
|
878
|
+
mockDoc.getTables.mockReturnValue([table]);
|
|
879
|
+
|
|
880
|
+
await processor.processHLPTables(mockDoc, {
|
|
881
|
+
header2Shading: 'BFBFBF',
|
|
882
|
+
otherShading: 'DFDFDF',
|
|
883
|
+
listIndentationLevels: [
|
|
884
|
+
{ level: 0, symbolIndent: 0.25, textIndent: 0.50 },
|
|
885
|
+
{ level: 1, symbolIndent: 0.75, textIndent: 1.00 },
|
|
886
|
+
],
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
const dataRow = table.getRows()[1];
|
|
890
|
+
const dataCell = dataRow.getCells()[0];
|
|
891
|
+
const paras = dataCell.getParagraphs();
|
|
892
|
+
|
|
893
|
+
// P[1] sub-item (numId=33, level=1) should get level-1 indentation
|
|
894
|
+
// textIndent=1.00" = 1440 twips, hanging = (1.00 - 0.75) * 1440 = 360 twips
|
|
895
|
+
const subPara = paras[1];
|
|
896
|
+
expect(subPara.setLeftIndent).toHaveBeenCalledWith(1440);
|
|
897
|
+
expect(subPara.setFirstLineIndent).toHaveBeenCalledWith(-360);
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
it('should process floating HLP tables (not skip them)', async () => {
|
|
901
|
+
const table = createMockHLPTable('single-column');
|
|
902
|
+
table.isFloating.mockReturnValue(true);
|
|
903
|
+
mockDoc.getTables.mockReturnValue([table]);
|
|
904
|
+
|
|
905
|
+
const result = await processor.processHLPTables(mockDoc);
|
|
906
|
+
|
|
907
|
+
// Floating HLP tables should still be detected and processed —
|
|
908
|
+
// shouldSkipTable() is NOT applied to HLP detection/processing
|
|
909
|
+
expect(result.tablesFound).toBe(1);
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
it('should handle multiple HLP tables of different variants', async () => {
|
|
913
|
+
const singleColTable = createMockHLPTable('single-column');
|
|
914
|
+
const twoColTable = createMockHLPTable('two-column');
|
|
915
|
+
mockDoc.getTables.mockReturnValue([singleColTable, twoColTable]);
|
|
916
|
+
|
|
917
|
+
const result = await processor.processHLPTables(mockDoc);
|
|
918
|
+
|
|
919
|
+
expect(result.tablesFound).toBe(2);
|
|
920
|
+
expect(result.singleColumnTables).toBe(1);
|
|
921
|
+
expect(result.twoColumnTables).toBe(1);
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
it('should use default font settings when no settings provided', async () => {
|
|
925
|
+
const table = createMockHLPTable('single-column');
|
|
926
|
+
mockDoc.getTables.mockReturnValue([table]);
|
|
927
|
+
|
|
928
|
+
await processor.processHLPTables(mockDoc); // No settings
|
|
929
|
+
|
|
930
|
+
const dataRow = table.getRows()[1];
|
|
931
|
+
const dataCell = dataRow.getCells()[0];
|
|
932
|
+
const mainPara = dataCell.getParagraphs()[0];
|
|
933
|
+
const mainRuns = mainPara.getRuns();
|
|
934
|
+
|
|
935
|
+
// Should use defaults: Verdana 12 for main item (normalFont/normalSize)
|
|
936
|
+
expect(mainRuns[0].setFont).toHaveBeenCalledWith('Verdana');
|
|
937
|
+
expect(mainRuns[0].setSize).toHaveBeenCalledWith(12);
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
it('should fix post-table ListParagraph paragraphs to prevent phantom numbering', async () => {
|
|
941
|
+
const table = createMockHLPTable('single-column');
|
|
942
|
+
// Create a mock paragraph after the table with ListParagraph style and no numbering
|
|
943
|
+
const postTablePara = createMockParagraph('ListParagraph');
|
|
944
|
+
postTablePara.getNumbering.mockReturnValue(null);
|
|
945
|
+
postTablePara.getRuns.mockReturnValue([]);
|
|
946
|
+
|
|
947
|
+
mockDoc.getTables.mockReturnValue([table]);
|
|
948
|
+
// Body elements: table followed by the ListParagraph paragraph
|
|
949
|
+
mockDoc.getBodyElements.mockReturnValue([table, postTablePara]);
|
|
950
|
+
|
|
951
|
+
await processor.processHLPTables(mockDoc);
|
|
952
|
+
|
|
953
|
+
// The post-table paragraph should be converted to Normal
|
|
954
|
+
expect(postTablePara.setStyle).toHaveBeenCalledWith('Normal');
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
it('should NOT convert post-table paragraph if it has explicit numbering', async () => {
|
|
958
|
+
const table = createMockHLPTable('single-column');
|
|
959
|
+
const postTablePara = createMockParagraph('ListParagraph');
|
|
960
|
+
postTablePara.getNumbering.mockReturnValue({ numId: 5, level: 0 });
|
|
961
|
+
postTablePara.getRuns.mockReturnValue([]);
|
|
962
|
+
|
|
963
|
+
mockDoc.getTables.mockReturnValue([table]);
|
|
964
|
+
mockDoc.getBodyElements.mockReturnValue([table, postTablePara]);
|
|
965
|
+
|
|
966
|
+
await processor.processHLPTables(mockDoc);
|
|
967
|
+
|
|
968
|
+
// Should NOT be converted — it has its own numbering
|
|
969
|
+
expect(postTablePara.setStyle).not.toHaveBeenCalledWith('Normal');
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
it('should set setBold(false) on converted sub-item numbering levels', async () => {
|
|
973
|
+
// Create a mock numbering manager with bullet abstractNums
|
|
974
|
+
const mockLevel0 = {
|
|
975
|
+
getFormat: vi.fn().mockReturnValue('bullet'),
|
|
976
|
+
setFormat: vi.fn(),
|
|
977
|
+
setText: vi.fn(),
|
|
978
|
+
setFont: vi.fn(),
|
|
979
|
+
setFontSize: vi.fn(),
|
|
980
|
+
setColor: vi.fn(),
|
|
981
|
+
setBold: vi.fn(),
|
|
982
|
+
setLeftIndent: vi.fn(),
|
|
983
|
+
setHangingIndent: vi.fn(),
|
|
984
|
+
toXML: vi.fn().mockReturnValue({ name: 'w:lvl', children: [{ name: 'w:rPr', children: [] }] }),
|
|
985
|
+
};
|
|
986
|
+
const mockLevel1 = {
|
|
987
|
+
getFormat: vi.fn().mockReturnValue('bullet'),
|
|
988
|
+
setFormat: vi.fn(),
|
|
989
|
+
setText: vi.fn(),
|
|
990
|
+
setFont: vi.fn(),
|
|
991
|
+
setFontSize: vi.fn(),
|
|
992
|
+
setColor: vi.fn(),
|
|
993
|
+
setBold: vi.fn(),
|
|
994
|
+
setLeftIndent: vi.fn(),
|
|
995
|
+
setHangingIndent: vi.fn(),
|
|
996
|
+
toXML: vi.fn().mockReturnValue({ name: 'w:lvl', children: [{ name: 'w:rPr', children: [] }] }),
|
|
997
|
+
};
|
|
998
|
+
const mockLevel2 = {
|
|
999
|
+
getFormat: vi.fn().mockReturnValue('bullet'),
|
|
1000
|
+
setFormat: vi.fn(),
|
|
1001
|
+
setText: vi.fn(),
|
|
1002
|
+
setFont: vi.fn(),
|
|
1003
|
+
setFontSize: vi.fn(),
|
|
1004
|
+
setColor: vi.fn(),
|
|
1005
|
+
setBold: vi.fn(),
|
|
1006
|
+
setLeftIndent: vi.fn(),
|
|
1007
|
+
setHangingIndent: vi.fn(),
|
|
1008
|
+
toXML: vi.fn().mockReturnValue({ name: 'w:lvl', children: [{ name: 'w:rPr', children: [] }] }),
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
const mockAbstractNum = {
|
|
1012
|
+
getLevel: vi.fn().mockImplementation((level: number) => {
|
|
1013
|
+
if (level === 0) return mockLevel0;
|
|
1014
|
+
if (level === 1) return mockLevel1;
|
|
1015
|
+
if (level === 2) return mockLevel2;
|
|
1016
|
+
return null;
|
|
1017
|
+
}),
|
|
1018
|
+
};
|
|
1019
|
+
|
|
1020
|
+
const mockInstance = {
|
|
1021
|
+
getAbstractNumId: vi.fn().mockReturnValue(42),
|
|
1022
|
+
};
|
|
1023
|
+
|
|
1024
|
+
const mockManager = {
|
|
1025
|
+
getInstance: vi.fn().mockReturnValue(mockInstance),
|
|
1026
|
+
getAbstractNumbering: vi.fn().mockReturnValue(mockAbstractNum),
|
|
1027
|
+
createCustomList: vi.fn().mockReturnValue(0),
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
(mockDoc as any).getNumberingManager = vi.fn().mockReturnValue(mockManager);
|
|
1031
|
+
|
|
1032
|
+
// Build a table with a sub-item paragraph that has numbering
|
|
1033
|
+
const subRun = createMockRun(false);
|
|
1034
|
+
const subPara = createMockParagraphWithRuns([subRun], 'ListParagraph');
|
|
1035
|
+
subPara.getNumbering.mockReturnValue({ numId: 7, level: 0 });
|
|
1036
|
+
|
|
1037
|
+
const dataCell = {
|
|
1038
|
+
getShading: vi.fn().mockReturnValue('FFFFFF'),
|
|
1039
|
+
setShading: vi.fn(),
|
|
1040
|
+
setBorders: vi.fn(),
|
|
1041
|
+
setMargins: vi.fn(),
|
|
1042
|
+
getParagraphs: vi.fn().mockReturnValue([subPara]),
|
|
1043
|
+
getFormatting: vi.fn().mockReturnValue({ shading: undefined }),
|
|
1044
|
+
getColumnSpan: vi.fn().mockReturnValue(1),
|
|
1045
|
+
setAllRunsFont: vi.fn(),
|
|
1046
|
+
setAllRunsSize: vi.fn(),
|
|
1047
|
+
getText: vi.fn().mockReturnValue('a. Sub-item'),
|
|
1048
|
+
hasNestedTables: vi.fn().mockReturnValue(false),
|
|
1049
|
+
} as unknown as Mocked<TableCell>;
|
|
1050
|
+
|
|
1051
|
+
const headerCell = createHLPHeaderCell();
|
|
1052
|
+
const headerRow = createMockRow([headerCell]);
|
|
1053
|
+
const dataRow = createMockRow([dataCell]);
|
|
1054
|
+
const table = {
|
|
1055
|
+
getRows: vi.fn().mockReturnValue([headerRow, dataRow]),
|
|
1056
|
+
getColumnCount: vi.fn().mockReturnValue(1),
|
|
1057
|
+
isFloating: vi.fn().mockReturnValue(false),
|
|
1058
|
+
setBorders: vi.fn(),
|
|
1059
|
+
} as unknown as Mocked<Table>;
|
|
1060
|
+
|
|
1061
|
+
mockDoc.getTables.mockReturnValue([table]);
|
|
1062
|
+
await processor.processHLPTables(mockDoc);
|
|
1063
|
+
|
|
1064
|
+
// All three levels should have setBold(false) called
|
|
1065
|
+
expect(mockLevel0.setBold).toHaveBeenCalledWith(false);
|
|
1066
|
+
expect(mockLevel1.setBold).toHaveBeenCalledWith(false);
|
|
1067
|
+
expect(mockLevel2.setBold).toHaveBeenCalledWith(false);
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
it('should patch converted level toXML to include <w:b w:val="0"/>', async () => {
|
|
1071
|
+
// Create a mock level with a toXML() that returns an rPr child
|
|
1072
|
+
const mockLevel0 = {
|
|
1073
|
+
getFormat: vi.fn().mockReturnValue('bullet'),
|
|
1074
|
+
setFormat: vi.fn(),
|
|
1075
|
+
setText: vi.fn(),
|
|
1076
|
+
setFont: vi.fn(),
|
|
1077
|
+
setFontSize: vi.fn(),
|
|
1078
|
+
setColor: vi.fn(),
|
|
1079
|
+
setBold: vi.fn(),
|
|
1080
|
+
setLeftIndent: vi.fn(),
|
|
1081
|
+
setHangingIndent: vi.fn(),
|
|
1082
|
+
toXML: vi.fn().mockReturnValue({
|
|
1083
|
+
name: 'w:lvl',
|
|
1084
|
+
children: [
|
|
1085
|
+
{ name: 'w:rPr', children: [{ name: 'w:rFonts', attributes: { 'w:ascii': 'Verdana' } }] },
|
|
1086
|
+
],
|
|
1087
|
+
}),
|
|
1088
|
+
};
|
|
1089
|
+
const mockLevel1 = {
|
|
1090
|
+
getFormat: vi.fn().mockReturnValue('bullet'),
|
|
1091
|
+
setFormat: vi.fn(),
|
|
1092
|
+
setText: vi.fn(),
|
|
1093
|
+
setFont: vi.fn(),
|
|
1094
|
+
setFontSize: vi.fn(),
|
|
1095
|
+
setColor: vi.fn(),
|
|
1096
|
+
setBold: vi.fn(),
|
|
1097
|
+
setLeftIndent: vi.fn(),
|
|
1098
|
+
setHangingIndent: vi.fn(),
|
|
1099
|
+
toXML: vi.fn().mockReturnValue({
|
|
1100
|
+
name: 'w:lvl',
|
|
1101
|
+
children: [
|
|
1102
|
+
{ name: 'w:rPr', children: [] },
|
|
1103
|
+
],
|
|
1104
|
+
}),
|
|
1105
|
+
};
|
|
1106
|
+
const mockLevel2 = {
|
|
1107
|
+
getFormat: vi.fn().mockReturnValue('bullet'),
|
|
1108
|
+
setFormat: vi.fn(),
|
|
1109
|
+
setText: vi.fn(),
|
|
1110
|
+
setFont: vi.fn(),
|
|
1111
|
+
setFontSize: vi.fn(),
|
|
1112
|
+
setColor: vi.fn(),
|
|
1113
|
+
setBold: vi.fn(),
|
|
1114
|
+
setLeftIndent: vi.fn(),
|
|
1115
|
+
setHangingIndent: vi.fn(),
|
|
1116
|
+
toXML: vi.fn().mockReturnValue({
|
|
1117
|
+
name: 'w:lvl',
|
|
1118
|
+
children: [
|
|
1119
|
+
{ name: 'w:rPr', children: [] },
|
|
1120
|
+
],
|
|
1121
|
+
}),
|
|
1122
|
+
};
|
|
1123
|
+
|
|
1124
|
+
const mockAbstractNum = {
|
|
1125
|
+
getLevel: vi.fn().mockImplementation((level: number) => {
|
|
1126
|
+
if (level === 0) return mockLevel0;
|
|
1127
|
+
if (level === 1) return mockLevel1;
|
|
1128
|
+
if (level === 2) return mockLevel2;
|
|
1129
|
+
return null;
|
|
1130
|
+
}),
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1133
|
+
const mockInstance = {
|
|
1134
|
+
getAbstractNumId: vi.fn().mockReturnValue(99),
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
const mockManager = {
|
|
1138
|
+
getInstance: vi.fn().mockReturnValue(mockInstance),
|
|
1139
|
+
getAbstractNumbering: vi.fn().mockReturnValue(mockAbstractNum),
|
|
1140
|
+
createCustomList: vi.fn().mockReturnValue(0),
|
|
1141
|
+
};
|
|
1142
|
+
|
|
1143
|
+
(mockDoc as any).getNumberingManager = vi.fn().mockReturnValue(mockManager);
|
|
1144
|
+
|
|
1145
|
+
// Build a table with a sub-item paragraph that triggers bullet conversion
|
|
1146
|
+
const subRun = createMockRun(false);
|
|
1147
|
+
const subPara = createMockParagraphWithRuns([subRun], 'ListParagraph');
|
|
1148
|
+
subPara.getNumbering.mockReturnValue({ numId: 7, level: 0 });
|
|
1149
|
+
|
|
1150
|
+
const dataCell = {
|
|
1151
|
+
getShading: vi.fn().mockReturnValue('FFFFFF'),
|
|
1152
|
+
setShading: vi.fn(),
|
|
1153
|
+
setBorders: vi.fn(),
|
|
1154
|
+
setMargins: vi.fn(),
|
|
1155
|
+
getParagraphs: vi.fn().mockReturnValue([subPara]),
|
|
1156
|
+
getFormatting: vi.fn().mockReturnValue({ shading: undefined }),
|
|
1157
|
+
getColumnSpan: vi.fn().mockReturnValue(1),
|
|
1158
|
+
setAllRunsFont: vi.fn(),
|
|
1159
|
+
setAllRunsSize: vi.fn(),
|
|
1160
|
+
getText: vi.fn().mockReturnValue('a. Sub-item'),
|
|
1161
|
+
hasNestedTables: vi.fn().mockReturnValue(false),
|
|
1162
|
+
} as unknown as Mocked<TableCell>;
|
|
1163
|
+
|
|
1164
|
+
const headerCell = createHLPHeaderCell();
|
|
1165
|
+
const headerRow = createMockRow([headerCell]);
|
|
1166
|
+
const dataRow = createMockRow([dataCell]);
|
|
1167
|
+
const table = {
|
|
1168
|
+
getRows: vi.fn().mockReturnValue([headerRow, dataRow]),
|
|
1169
|
+
getColumnCount: vi.fn().mockReturnValue(1),
|
|
1170
|
+
isFloating: vi.fn().mockReturnValue(false),
|
|
1171
|
+
setBorders: vi.fn(),
|
|
1172
|
+
} as unknown as Mocked<Table>;
|
|
1173
|
+
|
|
1174
|
+
mockDoc.getTables.mockReturnValue([table]);
|
|
1175
|
+
await processor.processHLPTables(mockDoc);
|
|
1176
|
+
|
|
1177
|
+
// After processing, level0's toXML should be monkey-patched.
|
|
1178
|
+
// Call it and verify the rPr now contains <w:b w:val="0"/> and <w:bCs w:val="0"/>
|
|
1179
|
+
const xml0 = mockLevel0.toXML();
|
|
1180
|
+
const rPr0 = xml0.children.find((c: any) => c.name === 'w:rPr');
|
|
1181
|
+
expect(rPr0).toBeDefined();
|
|
1182
|
+
const bElement = rPr0.children.find((c: any) => c.name === 'w:b');
|
|
1183
|
+
const bCsElement = rPr0.children.find((c: any) => c.name === 'w:bCs');
|
|
1184
|
+
expect(bElement).toEqual({ name: 'w:b', attributes: { 'w:val': '0' } });
|
|
1185
|
+
expect(bCsElement).toEqual({ name: 'w:bCs', attributes: { 'w:val': '0' } });
|
|
1186
|
+
|
|
1187
|
+
// Verify level1 also got patched
|
|
1188
|
+
const xml1 = mockLevel1.toXML();
|
|
1189
|
+
const rPr1 = xml1.children.find((c: any) => c.name === 'w:rPr');
|
|
1190
|
+
expect(rPr1.children.find((c: any) => c.name === 'w:b')).toEqual({ name: 'w:b', attributes: { 'w:val': '0' } });
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
it('should NOT insert blank line when previous paragraph is already blank', async () => {
|
|
1194
|
+
// Simulate: blank paragraph already exists between two level-0 items.
|
|
1195
|
+
// insertHLPBlankLines should skip insertion to avoid double blank lines.
|
|
1196
|
+
const mainRun1 = createMockRun(true);
|
|
1197
|
+
const para1 = createMockParagraphWithRuns([mainRun1], 'Normal');
|
|
1198
|
+
let para1Numbering: { numId: number; level: number } | null = null;
|
|
1199
|
+
para1.getNumbering.mockImplementation(() => para1Numbering);
|
|
1200
|
+
para1.setNumbering.mockImplementation((numId: number, level: number) => {
|
|
1201
|
+
para1Numbering = { numId, level };
|
|
1202
|
+
});
|
|
1203
|
+
para1.getStyle.mockReturnValue('ListParagraph');
|
|
1204
|
+
|
|
1205
|
+
// Blank paragraph between items (already converted to Normal in step 6)
|
|
1206
|
+
const blankPara = createMockParagraph('Normal');
|
|
1207
|
+
blankPara.getText.mockReturnValue('');
|
|
1208
|
+
blankPara.getNumbering.mockReturnValue(null);
|
|
1209
|
+
|
|
1210
|
+
const mainRun2 = createMockRun(true);
|
|
1211
|
+
const para2 = createMockParagraphWithRuns([mainRun2], 'Normal');
|
|
1212
|
+
let para2Numbering: { numId: number; level: number } | null = null;
|
|
1213
|
+
para2.getNumbering.mockImplementation(() => para2Numbering);
|
|
1214
|
+
para2.setNumbering.mockImplementation((numId: number, level: number) => {
|
|
1215
|
+
para2Numbering = { numId, level };
|
|
1216
|
+
});
|
|
1217
|
+
para2.getStyle.mockReturnValue('ListParagraph');
|
|
1218
|
+
|
|
1219
|
+
// The cell has addParagraphAt to track insertions
|
|
1220
|
+
const addParagraphAtCalls: any[] = [];
|
|
1221
|
+
const allParas = [para1, blankPara, para2];
|
|
1222
|
+
|
|
1223
|
+
const dataCell = {
|
|
1224
|
+
getShading: vi.fn().mockReturnValue('FFFFFF'),
|
|
1225
|
+
setShading: vi.fn(),
|
|
1226
|
+
setBorders: vi.fn(),
|
|
1227
|
+
setMargins: vi.fn(),
|
|
1228
|
+
getParagraphs: vi.fn().mockReturnValue(allParas),
|
|
1229
|
+
getFormatting: vi.fn().mockReturnValue({ shading: undefined }),
|
|
1230
|
+
getColumnSpan: vi.fn().mockReturnValue(1),
|
|
1231
|
+
setAllRunsFont: vi.fn(),
|
|
1232
|
+
setAllRunsSize: vi.fn(),
|
|
1233
|
+
getText: vi.fn().mockReturnValue('Item1\n\nItem2'),
|
|
1234
|
+
hasNestedTables: vi.fn().mockReturnValue(false),
|
|
1235
|
+
addParagraphAt: vi.fn().mockImplementation((idx: number, p: any) => {
|
|
1236
|
+
addParagraphAtCalls.push({ idx, p });
|
|
1237
|
+
}),
|
|
1238
|
+
} as unknown as Mocked<TableCell>;
|
|
1239
|
+
|
|
1240
|
+
// Need a sub-item so discoverHLPMainNumId finds numId=33
|
|
1241
|
+
const subRun = createMockRun(false);
|
|
1242
|
+
const subPara = createMockParagraphWithRuns([subRun]);
|
|
1243
|
+
subPara.getNumbering.mockReturnValue({ numId: 33, level: 1 });
|
|
1244
|
+
|
|
1245
|
+
const dataCell2 = {
|
|
1246
|
+
getShading: vi.fn().mockReturnValue('FFFFFF'),
|
|
1247
|
+
setShading: vi.fn(),
|
|
1248
|
+
setBorders: vi.fn(),
|
|
1249
|
+
setMargins: vi.fn(),
|
|
1250
|
+
getParagraphs: vi.fn().mockReturnValue([subPara]),
|
|
1251
|
+
getFormatting: vi.fn().mockReturnValue({ shading: undefined }),
|
|
1252
|
+
getColumnSpan: vi.fn().mockReturnValue(1),
|
|
1253
|
+
setAllRunsFont: vi.fn(),
|
|
1254
|
+
setAllRunsSize: vi.fn(),
|
|
1255
|
+
getText: vi.fn().mockReturnValue('a. sub'),
|
|
1256
|
+
hasNestedTables: vi.fn().mockReturnValue(false),
|
|
1257
|
+
addParagraphAt: vi.fn(),
|
|
1258
|
+
} as unknown as Mocked<TableCell>;
|
|
1259
|
+
|
|
1260
|
+
const headerCell = createHLPHeaderCell();
|
|
1261
|
+
const headerRow = createMockRow([headerCell]);
|
|
1262
|
+
const dataRow1 = createMockRow([dataCell]);
|
|
1263
|
+
const dataRow2 = createMockRow([dataCell2]);
|
|
1264
|
+
const table = {
|
|
1265
|
+
getRows: vi.fn().mockReturnValue([headerRow, dataRow1, dataRow2]),
|
|
1266
|
+
getColumnCount: vi.fn().mockReturnValue(1),
|
|
1267
|
+
isFloating: vi.fn().mockReturnValue(false),
|
|
1268
|
+
setBorders: vi.fn(),
|
|
1269
|
+
} as unknown as Mocked<Table>;
|
|
1270
|
+
|
|
1271
|
+
mockDoc.getTables.mockReturnValue([table]);
|
|
1272
|
+
await processor.processHLPTables(mockDoc);
|
|
1273
|
+
|
|
1274
|
+
// No blank paragraphs should be inserted because one already exists
|
|
1275
|
+
expect(dataCell.addParagraphAt).not.toHaveBeenCalled();
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
// ═══════════════════════════════════════════════════════════
|
|
1279
|
+
// savedNumbering restoration tests (step 5.1)
|
|
1280
|
+
// ═══════════════════════════════════════════════════════════
|
|
1281
|
+
|
|
1282
|
+
it('should restore corrupted numbering from savedNumbering map', async () => {
|
|
1283
|
+
const subRun = createMockRun(false);
|
|
1284
|
+
const subPara = createMockParagraphWithRuns([subRun], 'ListParagraph');
|
|
1285
|
+
// Current numbering is corrupted (ilvl changed from 1 to 0)
|
|
1286
|
+
subPara.getNumbering.mockReturnValue({ numId: 33, level: 0 });
|
|
1287
|
+
|
|
1288
|
+
const dataCell = {
|
|
1289
|
+
getShading: vi.fn().mockReturnValue('FFFFFF'),
|
|
1290
|
+
setShading: vi.fn(),
|
|
1291
|
+
setBorders: vi.fn(),
|
|
1292
|
+
setMargins: vi.fn(),
|
|
1293
|
+
getParagraphs: vi.fn().mockReturnValue([subPara]),
|
|
1294
|
+
getFormatting: vi.fn().mockReturnValue({ shading: undefined }),
|
|
1295
|
+
getColumnSpan: vi.fn().mockReturnValue(1),
|
|
1296
|
+
setAllRunsFont: vi.fn(),
|
|
1297
|
+
setAllRunsSize: vi.fn(),
|
|
1298
|
+
getText: vi.fn().mockReturnValue('a. Sub-item'),
|
|
1299
|
+
hasNestedTables: vi.fn().mockReturnValue(false),
|
|
1300
|
+
} as unknown as Mocked<TableCell>;
|
|
1301
|
+
|
|
1302
|
+
const headerCell = createHLPHeaderCell();
|
|
1303
|
+
const headerRow = createMockRow([headerCell]);
|
|
1304
|
+
const dataRow = createMockRow([dataCell]);
|
|
1305
|
+
const table = {
|
|
1306
|
+
getRows: vi.fn().mockReturnValue([headerRow, dataRow]),
|
|
1307
|
+
getColumnCount: vi.fn().mockReturnValue(1),
|
|
1308
|
+
isFloating: vi.fn().mockReturnValue(false),
|
|
1309
|
+
setBorders: vi.fn(),
|
|
1310
|
+
} as unknown as Mocked<Table>;
|
|
1311
|
+
|
|
1312
|
+
mockDoc.getTables.mockReturnValue([table]);
|
|
1313
|
+
|
|
1314
|
+
// Build savedNumbering map with original values
|
|
1315
|
+
const savedNumbering = new Map<Paragraph, { numId: number; level: number }>();
|
|
1316
|
+
savedNumbering.set(subPara as unknown as Paragraph, { numId: 33, level: 1 });
|
|
1317
|
+
|
|
1318
|
+
await processor.processHLPTables(mockDoc, undefined, savedNumbering);
|
|
1319
|
+
|
|
1320
|
+
// Restoration should call setNumbering with the saved values
|
|
1321
|
+
expect(subPara.setNumbering).toHaveBeenCalledWith(33, 1);
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
it('should skip restoration when current numbering already matches saved values', async () => {
|
|
1325
|
+
const subRun = createMockRun(false);
|
|
1326
|
+
const subPara = createMockParagraphWithRuns([subRun], 'ListParagraph');
|
|
1327
|
+
// Current numbering already matches saved (no corruption occurred)
|
|
1328
|
+
subPara.getNumbering.mockReturnValue({ numId: 33, level: 1 });
|
|
1329
|
+
|
|
1330
|
+
const dataCell = {
|
|
1331
|
+
getShading: vi.fn().mockReturnValue('FFFFFF'),
|
|
1332
|
+
setShading: vi.fn(),
|
|
1333
|
+
setBorders: vi.fn(),
|
|
1334
|
+
setMargins: vi.fn(),
|
|
1335
|
+
getParagraphs: vi.fn().mockReturnValue([subPara]),
|
|
1336
|
+
getFormatting: vi.fn().mockReturnValue({ shading: undefined }),
|
|
1337
|
+
getColumnSpan: vi.fn().mockReturnValue(1),
|
|
1338
|
+
setAllRunsFont: vi.fn(),
|
|
1339
|
+
setAllRunsSize: vi.fn(),
|
|
1340
|
+
getText: vi.fn().mockReturnValue('a. Sub-item'),
|
|
1341
|
+
hasNestedTables: vi.fn().mockReturnValue(false),
|
|
1342
|
+
} as unknown as Mocked<TableCell>;
|
|
1343
|
+
|
|
1344
|
+
const headerCell = createHLPHeaderCell();
|
|
1345
|
+
const headerRow = createMockRow([headerCell]);
|
|
1346
|
+
const dataRow = createMockRow([dataCell]);
|
|
1347
|
+
const table = {
|
|
1348
|
+
getRows: vi.fn().mockReturnValue([headerRow, dataRow]),
|
|
1349
|
+
getColumnCount: vi.fn().mockReturnValue(1),
|
|
1350
|
+
isFloating: vi.fn().mockReturnValue(false),
|
|
1351
|
+
setBorders: vi.fn(),
|
|
1352
|
+
} as unknown as Mocked<Table>;
|
|
1353
|
+
|
|
1354
|
+
mockDoc.getTables.mockReturnValue([table]);
|
|
1355
|
+
|
|
1356
|
+
const savedNumbering = new Map<Paragraph, { numId: number; level: number }>();
|
|
1357
|
+
savedNumbering.set(subPara as unknown as Paragraph, { numId: 33, level: 1 });
|
|
1358
|
+
|
|
1359
|
+
await processor.processHLPTables(mockDoc, undefined, savedNumbering);
|
|
1360
|
+
|
|
1361
|
+
// setNumbering should NOT be called since values already match
|
|
1362
|
+
expect(subPara.setNumbering).not.toHaveBeenCalled();
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
it('should skip restoration entirely when savedNumbering is undefined', async () => {
|
|
1366
|
+
const subRun = createMockRun(false);
|
|
1367
|
+
const subPara = createMockParagraphWithRuns([subRun], 'ListParagraph');
|
|
1368
|
+
subPara.getNumbering.mockReturnValue({ numId: 33, level: 0 });
|
|
1369
|
+
|
|
1370
|
+
const dataCell = {
|
|
1371
|
+
getShading: vi.fn().mockReturnValue('FFFFFF'),
|
|
1372
|
+
setShading: vi.fn(),
|
|
1373
|
+
setBorders: vi.fn(),
|
|
1374
|
+
setMargins: vi.fn(),
|
|
1375
|
+
getParagraphs: vi.fn().mockReturnValue([subPara]),
|
|
1376
|
+
getFormatting: vi.fn().mockReturnValue({ shading: undefined }),
|
|
1377
|
+
getColumnSpan: vi.fn().mockReturnValue(1),
|
|
1378
|
+
setAllRunsFont: vi.fn(),
|
|
1379
|
+
setAllRunsSize: vi.fn(),
|
|
1380
|
+
getText: vi.fn().mockReturnValue('a. Sub-item'),
|
|
1381
|
+
hasNestedTables: vi.fn().mockReturnValue(false),
|
|
1382
|
+
} as unknown as Mocked<TableCell>;
|
|
1383
|
+
|
|
1384
|
+
const headerCell = createHLPHeaderCell();
|
|
1385
|
+
const headerRow = createMockRow([headerCell]);
|
|
1386
|
+
const dataRow = createMockRow([dataCell]);
|
|
1387
|
+
const table = {
|
|
1388
|
+
getRows: vi.fn().mockReturnValue([headerRow, dataRow]),
|
|
1389
|
+
getColumnCount: vi.fn().mockReturnValue(1),
|
|
1390
|
+
isFloating: vi.fn().mockReturnValue(false),
|
|
1391
|
+
setBorders: vi.fn(),
|
|
1392
|
+
} as unknown as Mocked<Table>;
|
|
1393
|
+
|
|
1394
|
+
mockDoc.getTables.mockReturnValue([table]);
|
|
1395
|
+
|
|
1396
|
+
// Pass undefined (no savedNumbering) — should not attempt restoration
|
|
1397
|
+
await processor.processHLPTables(mockDoc, undefined, undefined);
|
|
1398
|
+
|
|
1399
|
+
// setNumbering not called for restoration (may be called by content formatting)
|
|
1400
|
+
// The key assertion: no restoration-specific setNumbering(33, 0→1) call
|
|
1401
|
+
const setNumberingCalls = subPara.setNumbering.mock.calls;
|
|
1402
|
+
const restorationCall = setNumberingCalls.find(
|
|
1403
|
+
(args: any[]) => args[0] === 33 && args[1] === 1,
|
|
1404
|
+
);
|
|
1405
|
+
expect(restorationCall).toBeUndefined();
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1408
|
+
it('should only restore data row paragraphs (skip header row ri=0)', async () => {
|
|
1409
|
+
const headerParaRun = createMockRun(true);
|
|
1410
|
+
const headerPara = createMockParagraphWithRuns([headerParaRun], 'Heading2');
|
|
1411
|
+
headerPara.getText.mockReturnValue('High Level Process');
|
|
1412
|
+
|
|
1413
|
+
const headerCell = {
|
|
1414
|
+
getShading: vi.fn().mockReturnValue('FFC000'),
|
|
1415
|
+
setShading: vi.fn(),
|
|
1416
|
+
setBorders: vi.fn(),
|
|
1417
|
+
setMargins: vi.fn(),
|
|
1418
|
+
getParagraphs: vi.fn().mockReturnValue([headerPara]),
|
|
1419
|
+
getFormatting: vi.fn().mockReturnValue({ shading: { fill: 'FFC000' } }),
|
|
1420
|
+
getColumnSpan: vi.fn().mockReturnValue(1),
|
|
1421
|
+
setAllRunsFont: vi.fn(),
|
|
1422
|
+
setAllRunsSize: vi.fn(),
|
|
1423
|
+
getText: vi.fn().mockReturnValue('High Level Process'),
|
|
1424
|
+
hasNestedTables: vi.fn().mockReturnValue(false),
|
|
1425
|
+
} as unknown as Mocked<TableCell>;
|
|
1426
|
+
|
|
1427
|
+
const dataRun = createMockRun(false);
|
|
1428
|
+
const dataPara = createMockParagraphWithRuns([dataRun], 'ListParagraph');
|
|
1429
|
+
dataPara.getNumbering.mockReturnValue({ numId: 33, level: 0 });
|
|
1430
|
+
|
|
1431
|
+
const dataCell = {
|
|
1432
|
+
getShading: vi.fn().mockReturnValue('FFFFFF'),
|
|
1433
|
+
setShading: vi.fn(),
|
|
1434
|
+
setBorders: vi.fn(),
|
|
1435
|
+
setMargins: vi.fn(),
|
|
1436
|
+
getParagraphs: vi.fn().mockReturnValue([dataPara]),
|
|
1437
|
+
getFormatting: vi.fn().mockReturnValue({ shading: undefined }),
|
|
1438
|
+
getColumnSpan: vi.fn().mockReturnValue(1),
|
|
1439
|
+
setAllRunsFont: vi.fn(),
|
|
1440
|
+
setAllRunsSize: vi.fn(),
|
|
1441
|
+
getText: vi.fn().mockReturnValue('Item'),
|
|
1442
|
+
hasNestedTables: vi.fn().mockReturnValue(false),
|
|
1443
|
+
} as unknown as Mocked<TableCell>;
|
|
1444
|
+
|
|
1445
|
+
const headerRow = createMockRow([headerCell]);
|
|
1446
|
+
const dataRow = createMockRow([dataCell]);
|
|
1447
|
+
const table = {
|
|
1448
|
+
getRows: vi.fn().mockReturnValue([headerRow, dataRow]),
|
|
1449
|
+
getColumnCount: vi.fn().mockReturnValue(1),
|
|
1450
|
+
isFloating: vi.fn().mockReturnValue(false),
|
|
1451
|
+
setBorders: vi.fn(),
|
|
1452
|
+
} as unknown as Mocked<Table>;
|
|
1453
|
+
|
|
1454
|
+
mockDoc.getTables.mockReturnValue([table]);
|
|
1455
|
+
|
|
1456
|
+
// Map includes BOTH header paragraph and data paragraph
|
|
1457
|
+
const savedNumbering = new Map<Paragraph, { numId: number; level: number }>();
|
|
1458
|
+
savedNumbering.set(headerPara as unknown as Paragraph, { numId: 33, level: 1 });
|
|
1459
|
+
savedNumbering.set(dataPara as unknown as Paragraph, { numId: 33, level: 1 });
|
|
1460
|
+
|
|
1461
|
+
await processor.processHLPTables(mockDoc, undefined, savedNumbering);
|
|
1462
|
+
|
|
1463
|
+
// Header paragraph should NOT have restoration applied (ri=0 is skipped)
|
|
1464
|
+
// Only check that headerPara.setNumbering was NOT called with the saved values
|
|
1465
|
+
const headerCalls = headerPara.setNumbering.mock.calls.filter(
|
|
1466
|
+
(args: any[]) => args[0] === 33 && args[1] === 1,
|
|
1467
|
+
);
|
|
1468
|
+
expect(headerCalls).toHaveLength(0);
|
|
1469
|
+
|
|
1470
|
+
// Data paragraph SHOULD have restoration applied
|
|
1471
|
+
expect(dataPara.setNumbering).toHaveBeenCalledWith(33, 1);
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
// ═══════════════════════════════════════════════════════════
|
|
1475
|
+
// discoverHLPMainNumId Pass 2 tests (all-sub-items scenario)
|
|
1476
|
+
// ═══════════════════════════════════════════════════════════
|
|
1477
|
+
|
|
1478
|
+
it('should discover numId via Pass 2 when all paragraphs are sub-items (ilvl=1+)', async () => {
|
|
1479
|
+
// All content paragraphs are sub-items at level 1 — no level-0 decimal exists.
|
|
1480
|
+
// Pass 2 should return the most common numId among them.
|
|
1481
|
+
const mockLevel0 = {
|
|
1482
|
+
getFormat: vi.fn().mockReturnValue('decimal'),
|
|
1483
|
+
setFormat: vi.fn(), setText: vi.fn(), setFont: vi.fn(),
|
|
1484
|
+
setFontSize: vi.fn(), setColor: vi.fn(), setBold: vi.fn(),
|
|
1485
|
+
setLeftIndent: vi.fn(), setHangingIndent: vi.fn(),
|
|
1486
|
+
toXML: vi.fn().mockReturnValue({ name: 'w:lvl', children: [] }),
|
|
1487
|
+
};
|
|
1488
|
+
const mockAbstractNum = {
|
|
1489
|
+
getLevel: vi.fn().mockImplementation((level: number) => level === 0 ? mockLevel0 : null),
|
|
1490
|
+
};
|
|
1491
|
+
const mockInstance = { getAbstractNumId: vi.fn().mockReturnValue(10) };
|
|
1492
|
+
const mockManager = {
|
|
1493
|
+
getInstance: vi.fn().mockReturnValue(mockInstance),
|
|
1494
|
+
getAbstractNumbering: vi.fn().mockReturnValue(mockAbstractNum),
|
|
1495
|
+
createCustomList: vi.fn().mockReturnValue(0),
|
|
1496
|
+
};
|
|
1497
|
+
(mockDoc as any).getNumberingManager = vi.fn().mockReturnValue(mockManager);
|
|
1498
|
+
|
|
1499
|
+
// Three sub-item paragraphs all at numId=33, level=1
|
|
1500
|
+
const paras = [1, 2, 3].map(() => {
|
|
1501
|
+
const run = createMockRun(false);
|
|
1502
|
+
const p = createMockParagraphWithRuns([run], 'ListParagraph');
|
|
1503
|
+
p.getNumbering.mockReturnValue({ numId: 33, level: 1 });
|
|
1504
|
+
return p;
|
|
1505
|
+
});
|
|
1506
|
+
|
|
1507
|
+
const dataCell = {
|
|
1508
|
+
getShading: vi.fn().mockReturnValue('FFFFFF'),
|
|
1509
|
+
setShading: vi.fn(),
|
|
1510
|
+
setBorders: vi.fn(),
|
|
1511
|
+
setMargins: vi.fn(),
|
|
1512
|
+
getParagraphs: vi.fn().mockReturnValue(paras),
|
|
1513
|
+
getFormatting: vi.fn().mockReturnValue({ shading: undefined }),
|
|
1514
|
+
getColumnSpan: vi.fn().mockReturnValue(1),
|
|
1515
|
+
setAllRunsFont: vi.fn(),
|
|
1516
|
+
setAllRunsSize: vi.fn(),
|
|
1517
|
+
getText: vi.fn().mockReturnValue('a. Sub1\nb. Sub2\nc. Sub3'),
|
|
1518
|
+
hasNestedTables: vi.fn().mockReturnValue(false),
|
|
1519
|
+
} as unknown as Mocked<TableCell>;
|
|
1520
|
+
|
|
1521
|
+
const headerCell = createHLPHeaderCell();
|
|
1522
|
+
const headerRow = createMockRow([headerCell]);
|
|
1523
|
+
const dataRow = createMockRow([dataCell]);
|
|
1524
|
+
const table = {
|
|
1525
|
+
getRows: vi.fn().mockReturnValue([headerRow, dataRow]),
|
|
1526
|
+
getColumnCount: vi.fn().mockReturnValue(1),
|
|
1527
|
+
isFloating: vi.fn().mockReturnValue(false),
|
|
1528
|
+
setBorders: vi.fn(),
|
|
1529
|
+
} as unknown as Mocked<Table>;
|
|
1530
|
+
|
|
1531
|
+
mockDoc.getTables.mockReturnValue([table]);
|
|
1532
|
+
const result = await processor.processHLPTables(mockDoc);
|
|
1533
|
+
|
|
1534
|
+
// Should find the table and process it (numId=33 discovered via Pass 2)
|
|
1535
|
+
expect(result.tablesFound).toBe(1);
|
|
1536
|
+
// The paragraphs should get formatting applied (font/size on runs)
|
|
1537
|
+
expect(paras[0].getRuns()[0].setFont).toHaveBeenCalled();
|
|
1538
|
+
});
|
|
1539
|
+
|
|
1540
|
+
it('should discover the highest-count numId via Pass 2 when mixed numIds exist', async () => {
|
|
1541
|
+
// Two paragraphs with numId=33 and one with numId=44 — should pick 33
|
|
1542
|
+
const mockLevel0 = {
|
|
1543
|
+
getFormat: vi.fn().mockReturnValue('lowerLetter'), // NOT decimal, so Pass 1 skips
|
|
1544
|
+
setFormat: vi.fn(), setText: vi.fn(), setFont: vi.fn(),
|
|
1545
|
+
setFontSize: vi.fn(), setColor: vi.fn(), setBold: vi.fn(),
|
|
1546
|
+
setLeftIndent: vi.fn(), setHangingIndent: vi.fn(),
|
|
1547
|
+
toXML: vi.fn().mockReturnValue({ name: 'w:lvl', children: [] }),
|
|
1548
|
+
};
|
|
1549
|
+
const mockAbstractNum = {
|
|
1550
|
+
getLevel: vi.fn().mockImplementation((level: number) => level === 0 ? mockLevel0 : null),
|
|
1551
|
+
};
|
|
1552
|
+
const mockInstance = { getAbstractNumId: vi.fn().mockReturnValue(10) };
|
|
1553
|
+
const mockManager = {
|
|
1554
|
+
getInstance: vi.fn().mockReturnValue(mockInstance),
|
|
1555
|
+
getAbstractNumbering: vi.fn().mockReturnValue(mockAbstractNum),
|
|
1556
|
+
createCustomList: vi.fn().mockReturnValue(0),
|
|
1557
|
+
};
|
|
1558
|
+
(mockDoc as any).getNumberingManager = vi.fn().mockReturnValue(mockManager);
|
|
1559
|
+
|
|
1560
|
+
// Row 1: two paragraphs with numId=33
|
|
1561
|
+
const para1 = createMockParagraphWithRuns([createMockRun(false)], 'ListParagraph');
|
|
1562
|
+
para1.getNumbering.mockReturnValue({ numId: 33, level: 1 });
|
|
1563
|
+
const para2 = createMockParagraphWithRuns([createMockRun(false)], 'ListParagraph');
|
|
1564
|
+
para2.getNumbering.mockReturnValue({ numId: 33, level: 1 });
|
|
1565
|
+
|
|
1566
|
+
// Row 2: one paragraph with numId=44
|
|
1567
|
+
const para3 = createMockParagraphWithRuns([createMockRun(false)], 'ListParagraph');
|
|
1568
|
+
para3.getNumbering.mockReturnValue({ numId: 44, level: 1 });
|
|
1569
|
+
|
|
1570
|
+
const makeCell = (paras: any[]) => ({
|
|
1571
|
+
getShading: vi.fn().mockReturnValue('FFFFFF'),
|
|
1572
|
+
setShading: vi.fn(),
|
|
1573
|
+
setBorders: vi.fn(),
|
|
1574
|
+
setMargins: vi.fn(),
|
|
1575
|
+
getParagraphs: vi.fn().mockReturnValue(paras),
|
|
1576
|
+
getFormatting: vi.fn().mockReturnValue({ shading: undefined }),
|
|
1577
|
+
getColumnSpan: vi.fn().mockReturnValue(1),
|
|
1578
|
+
setAllRunsFont: vi.fn(),
|
|
1579
|
+
setAllRunsSize: vi.fn(),
|
|
1580
|
+
getText: vi.fn().mockReturnValue('sub items'),
|
|
1581
|
+
hasNestedTables: vi.fn().mockReturnValue(false),
|
|
1582
|
+
}) as unknown as Mocked<TableCell>;
|
|
1583
|
+
|
|
1584
|
+
const headerCell = createHLPHeaderCell();
|
|
1585
|
+
const headerRow = createMockRow([headerCell]);
|
|
1586
|
+
const dataRow1 = createMockRow([makeCell([para1, para2])]);
|
|
1587
|
+
const dataRow2 = createMockRow([makeCell([para3])]);
|
|
1588
|
+
const table = {
|
|
1589
|
+
getRows: vi.fn().mockReturnValue([headerRow, dataRow1, dataRow2]),
|
|
1590
|
+
getColumnCount: vi.fn().mockReturnValue(1),
|
|
1591
|
+
isFloating: vi.fn().mockReturnValue(false),
|
|
1592
|
+
setBorders: vi.fn(),
|
|
1593
|
+
} as unknown as Mocked<Table>;
|
|
1594
|
+
|
|
1595
|
+
mockDoc.getTables.mockReturnValue([table]);
|
|
1596
|
+
const result = await processor.processHLPTables(mockDoc);
|
|
1597
|
+
|
|
1598
|
+
// Should successfully process (no fallback list created)
|
|
1599
|
+
expect(result.tablesFound).toBe(1);
|
|
1600
|
+
// No createCustomList call since Pass 2 found numId=33
|
|
1601
|
+
expect(mockManager.createCustomList).not.toHaveBeenCalled();
|
|
1602
|
+
});
|
|
1603
|
+
});
|
|
1604
|
+
});
|