md-redline 0.1.0
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/LICENSE +21 -0
- package/README.md +169 -0
- package/bin/md-redline +255 -0
- package/bin/test-windows.ps1 +70 -0
- package/dist/assets/_baseFor-Ck08IaSF.js +1 -0
- package/dist/assets/arc-DI2g9LXK.js +1 -0
- package/dist/assets/architecture-YZFGNWBL-BDgMfc-b.js +1 -0
- package/dist/assets/architectureDiagram-Q4EWVU46-Dg1hcUEa.js +36 -0
- package/dist/assets/array-DOVTz2Mq.js +1 -0
- package/dist/assets/blockDiagram-DXYQGD6D-BAXkTCAk.js +132 -0
- package/dist/assets/c4Diagram-AHTNJAMY-BIkgwQSx.js +10 -0
- package/dist/assets/channel-DPCihw7y.js +1 -0
- package/dist/assets/chunk-2KRD3SAO-Dc_tBGsw.js +1 -0
- package/dist/assets/chunk-336JU56O-Dhi-ID9Y.js +2 -0
- package/dist/assets/chunk-426QAEUC-DnFdrNMW.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-Z63FkGov.js +1 -0
- package/dist/assets/chunk-4TB4RGXK-BAiBlfyy.js +206 -0
- package/dist/assets/chunk-55IACEB6-BXDWXbxy.js +1 -0
- package/dist/assets/chunk-5FUZZQ4R-C72e1c_O.js +62 -0
- package/dist/assets/chunk-5PVQY5BW-BBHW_uCu.js +2 -0
- package/dist/assets/chunk-67CJDMHE-3Cf_D9m6.js +1 -0
- package/dist/assets/chunk-7N4EOEYR-DAXUXJ2c.js +1 -0
- package/dist/assets/chunk-AA7GKIK3-Dr7fOryc.js +1 -0
- package/dist/assets/chunk-BSJP7CBP-BmsSs1Nt.js +1 -0
- package/dist/assets/chunk-CIAEETIT-QDzV-X_Y.js +1 -0
- package/dist/assets/chunk-EDXVE4YY-C25WFHxY.js +1 -0
- package/dist/assets/chunk-ENJZ2VHE-_OzxcZOU.js +10 -0
- package/dist/assets/chunk-FMBD7UC4-CjsTKY4u.js +15 -0
- package/dist/assets/chunk-FOC6F5B3-g-xaH5nc.js +1 -0
- package/dist/assets/chunk-ICPOFSXX-iKiUSjDK.js +121 -0
- package/dist/assets/chunk-K5T4RW27-CKR-lPBN.js +94 -0
- package/dist/assets/chunk-KGLVRYIC-DRccT-B_.js +1 -0
- package/dist/assets/chunk-LIHQZDEY-DTbMwMXj.js +1 -0
- package/dist/assets/chunk-ORNJ4GCN-DlerdcWX.js +1 -0
- package/dist/assets/chunk-OYMX7WX6-Dekv1on2.js +231 -0
- package/dist/assets/chunk-QZHKN3VN-BHu0RdKl.js +1 -0
- package/dist/assets/chunk-U2HBQHQK-BvtlVHAg.js +70 -0
- package/dist/assets/chunk-X2U36JSP-BI_g8mub.js +1 -0
- package/dist/assets/chunk-XPW4576I-B39JkmSE.js +32 -0
- package/dist/assets/chunk-YZCP3GAM-BfPcXRm2.js +1 -0
- package/dist/assets/chunk-ZZ45TVLE-Bg4q68wZ.js +1 -0
- package/dist/assets/classDiagram-6PBFFD2Q-p73p727_.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-C4Ftpivp.js +1 -0
- package/dist/assets/clone-CI9aUwHe.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-7BpAeDh5.js +1 -0
- package/dist/assets/cytoscape.esm-DoTFyJaN.js +321 -0
- package/dist/assets/dagre-CilMRazv.js +1 -0
- package/dist/assets/dagre-KV5264BT-DDMqpjkB.js +4 -0
- package/dist/assets/defaultLocale-Ck2Xxk-C.js +1 -0
- package/dist/assets/diagram-5BDNPKRD-BFeyfnCx.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-DoqT-PtF.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-BPV6KADk.js +43 -0
- package/dist/assets/diagram-TYMM5635-okvcTBtl.js +24 -0
- package/dist/assets/dist-C_eddq6m.js +1 -0
- package/dist/assets/erDiagram-SMLLAGMA-Dl-Ixy8n.js +85 -0
- package/dist/assets/flatten-B8XIuT0x.js +1 -0
- package/dist/assets/flowDiagram-DWJPFMVM-CsqWAx5r.js +162 -0
- package/dist/assets/ganttDiagram-T4ZO3ILL-mIt6zVeF.js +292 -0
- package/dist/assets/gitGraph-7Q5UKJZL-COXHGMvj.js +1 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-syVqZJX_.js +106 -0
- package/dist/assets/graphlib-Bpd0q3yO.js +1 -0
- package/dist/assets/index-BoggyWS0.css +2 -0
- package/dist/assets/index-aLvjHQW4.js +104 -0
- package/dist/assets/info-OMHHGYJF-B-0wfxwL.js +1 -0
- package/dist/assets/infoDiagram-42DDH7IO-C0_uqsVa.js +2 -0
- package/dist/assets/init-Bft5Ffpj.js +1 -0
- package/dist/assets/isEmpty-BrFi5AqV.js +1 -0
- package/dist/assets/ishikawaDiagram-UXIWVN3A-CTjFbDBV.js +70 -0
- package/dist/assets/journeyDiagram-VCZTEJTY-BDBcej1q.js +139 -0
- package/dist/assets/kanban-definition-6JOO6SKY-Ylgzakw7.js +89 -0
- package/dist/assets/katex-Uj9wLT16.js +265 -0
- package/dist/assets/line-CRxEwpOv.js +1 -0
- package/dist/assets/linear-PDPfFByd.js +1 -0
- package/dist/assets/mermaid-parser.core-CY-XNOOy.js +4 -0
- package/dist/assets/mermaid.core-BPlTADIX.js +11 -0
- package/dist/assets/mindmap-definition-QFDTVHPH-TefzJnBM.js +96 -0
- package/dist/assets/ordinal-DIg8h6NI.js +1 -0
- package/dist/assets/packet-4T2RLAQJ-BW1T_A-C.js +1 -0
- package/dist/assets/path-DfRbCp9y.js +1 -0
- package/dist/assets/pie-ZZUOXDRM-DkKU-SFu.js +1 -0
- package/dist/assets/pieDiagram-DEJITSTG-BCXuaeEy.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-VSBAicWL.js +7 -0
- package/dist/assets/radar-PYXPWWZC-CYvTacKJ.js +1 -0
- package/dist/assets/reduce-CV2X8n1a.js +1 -0
- package/dist/assets/requirementDiagram-MS252O5E-4NeL9Z6J.js +84 -0
- package/dist/assets/rough.esm-Bbn_-PMU.js +1 -0
- package/dist/assets/sankeyDiagram-XADWPNL6-DMBSDnrH.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-DVpzcZUi.js +157 -0
- package/dist/assets/src-PKe5NtkK.js +1 -0
- package/dist/assets/stateDiagram-FHFEXIEX-BkHTlCjL.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-nMeWu9fP.js +1 -0
- package/dist/assets/timeline-definition-GMOUNBTQ-CyLt92nf.js +120 -0
- package/dist/assets/treeView-SZITEDCU-BUgcJ4eR.js +1 -0
- package/dist/assets/treemap-W4RFUUIX-BIWGQ4Pw.js +1 -0
- package/dist/assets/vennDiagram-DHZGUBPP-BCK0xB_m.js +34 -0
- package/dist/assets/wardley-RL74JXVD-DMZZRlby.js +1 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-BisBgfsF.js +20 -0
- package/dist/assets/xychartDiagram-5P7HB3ND-D_REDciv.js +7 -0
- package/dist/favicon.svg +15 -0
- package/dist/index.html +14 -0
- package/dist/screenshot.png +0 -0
- package/index.html +13 -0
- package/package.json +105 -0
- package/public/favicon.svg +15 -0
- package/public/screenshot.png +0 -0
- package/server/index.test.ts +814 -0
- package/server/index.ts +736 -0
- package/server/preferences.test.ts +126 -0
- package/server/preferences.ts +76 -0
- package/src/App.tsx +1620 -0
- package/src/components/ActionButton.tsx +41 -0
- package/src/components/CommandPalette.tsx +191 -0
- package/src/components/CommentCard.tsx +556 -0
- package/src/components/CommentForm.tsx +285 -0
- package/src/components/CommentSidebar.tsx +428 -0
- package/src/components/ConfirmDialog.tsx +64 -0
- package/src/components/ContextMenu.tsx +220 -0
- package/src/components/DragHandles.tsx +48 -0
- package/src/components/FileExplorer.tsx +251 -0
- package/src/components/FileOpener.tsx +304 -0
- package/src/components/IconButton.tsx +32 -0
- package/src/components/KeyboardShortcutsPanel.tsx +136 -0
- package/src/components/MarkdownViewer.tsx +682 -0
- package/src/components/RawView.tsx +798 -0
- package/src/components/SearchBar.tsx +129 -0
- package/src/components/Separator.tsx +7 -0
- package/src/components/SettingsPanel.tsx +813 -0
- package/src/components/SplitIconButton.tsx +133 -0
- package/src/components/TabBar.tsx +594 -0
- package/src/components/TableOfContents.tsx +70 -0
- package/src/components/ThemeSelector.tsx +159 -0
- package/src/components/Toast.tsx +99 -0
- package/src/components/Toolbar.tsx +161 -0
- package/src/components/iconButtonVariants.ts +19 -0
- package/src/components/rawView.test.ts +291 -0
- package/src/contexts/SettingsContext.tsx +120 -0
- package/src/hooks/useAuthor.test.ts +58 -0
- package/src/hooks/useAuthor.ts +69 -0
- package/src/hooks/useAutoResize.ts +20 -0
- package/src/hooks/useCommentCardTriggers.ts +20 -0
- package/src/hooks/useComments.test.ts +773 -0
- package/src/hooks/useComments.ts +332 -0
- package/src/hooks/useContextMenu.ts +48 -0
- package/src/hooks/useContextMenuItems.ts +392 -0
- package/src/hooks/useDiffSnapshot.test.ts +130 -0
- package/src/hooks/useDiffSnapshot.ts +67 -0
- package/src/hooks/useDragHandles.ts +417 -0
- package/src/hooks/useFileWatcher.ts +45 -0
- package/src/hooks/useHeadingTracking.ts +84 -0
- package/src/hooks/useMermaidRenderer.ts +75 -0
- package/src/hooks/useModalState.ts +22 -0
- package/src/hooks/usePageVisible.test.ts +69 -0
- package/src/hooks/usePageVisible.ts +19 -0
- package/src/hooks/usePaneLayout.test.ts +108 -0
- package/src/hooks/usePaneLayout.ts +102 -0
- package/src/hooks/useRecentFiles.test.ts +103 -0
- package/src/hooks/useRecentFiles.ts +99 -0
- package/src/hooks/useResizablePanel.test.ts +84 -0
- package/src/hooks/useResizablePanel.ts +118 -0
- package/src/hooks/useSearch.test.ts +72 -0
- package/src/hooks/useSearch.ts +53 -0
- package/src/hooks/useSelection.ts +48 -0
- package/src/hooks/useSessionPersistence.test.ts +59 -0
- package/src/hooks/useSessionPersistence.ts +43 -0
- package/src/hooks/useTabs.test.ts +127 -0
- package/src/hooks/useTabs.ts +561 -0
- package/src/hooks/useThemePersistence.ts +41 -0
- package/src/hooks/useToast.ts +27 -0
- package/src/index.css +1047 -0
- package/src/lib/agent-prompts.test.ts +34 -0
- package/src/lib/agent-prompts.ts +68 -0
- package/src/lib/comment-editor-state.ts +6 -0
- package/src/lib/comment-parser.test.ts +1959 -0
- package/src/lib/comment-parser.ts +1021 -0
- package/src/lib/diff.test.ts +164 -0
- package/src/lib/diff.ts +139 -0
- package/src/lib/heading-slugs.test.ts +85 -0
- package/src/lib/heading-slugs.ts +44 -0
- package/src/lib/http.test.ts +43 -0
- package/src/lib/http.ts +29 -0
- package/src/lib/mermaid-highlights.test.ts +517 -0
- package/src/lib/mermaid-highlights.ts +936 -0
- package/src/lib/mermaid-renderer.test.ts +114 -0
- package/src/lib/mermaid-renderer.ts +89 -0
- package/src/lib/path-utils.test.ts +17 -0
- package/src/lib/path-utils.ts +7 -0
- package/src/lib/platform.test.ts +58 -0
- package/src/lib/platform.ts +14 -0
- package/src/lib/preferences-client.test.ts +177 -0
- package/src/lib/preferences-client.ts +94 -0
- package/src/lib/selection-resolver.test.ts +118 -0
- package/src/lib/selection-resolver.ts +37 -0
- package/src/lib/settings.test.ts +152 -0
- package/src/lib/settings.ts +78 -0
- package/src/lib/shortcut-label.tsx +18 -0
- package/src/lib/themes.ts +21 -0
- package/src/lib/visible-text.test.ts +86 -0
- package/src/lib/visible-text.ts +77 -0
- package/src/main.tsx +22 -0
- package/src/markdown/pipeline.test.ts +82 -0
- package/src/markdown/pipeline.ts +33 -0
- package/src/types.test.ts +43 -0
- package/src/types.ts +46 -0
- package/tsconfig.app.json +28 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +50 -0
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
4
|
+
import { renderHook, act } from '@testing-library/react';
|
|
5
|
+
import { useComments, type UseCommentsParams } from './useComments';
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Helpers: build raw markdown strings with embedded comment markers
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
function makeComment(overrides: Record<string, unknown> = {}): string {
|
|
12
|
+
const data = {
|
|
13
|
+
id: 'c1',
|
|
14
|
+
anchor: 'hello',
|
|
15
|
+
text: 'note',
|
|
16
|
+
author: 'User',
|
|
17
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
18
|
+
...overrides,
|
|
19
|
+
};
|
|
20
|
+
return `<!-- @comment${JSON.stringify(data)} -->`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function rawWithComments(
|
|
24
|
+
...comments: { before?: string; marker: string; after?: string }[]
|
|
25
|
+
): string {
|
|
26
|
+
return comments.map((c) => `${c.before ?? ''}${c.marker}${c.after ?? ''}`).join('');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Default params factory
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
function defaultParams(overrides: Partial<UseCommentsParams> = {}): UseCommentsParams {
|
|
34
|
+
return {
|
|
35
|
+
rawMarkdown: '',
|
|
36
|
+
rawMarkdownRef: { current: '' },
|
|
37
|
+
setRawMarkdown: vi.fn(),
|
|
38
|
+
saveFile: vi.fn(),
|
|
39
|
+
author: 'Tester',
|
|
40
|
+
enableResolve: false,
|
|
41
|
+
tabs: [],
|
|
42
|
+
activeFilePath: null,
|
|
43
|
+
viewerRef: {
|
|
44
|
+
current: { scrollToComment: vi.fn() },
|
|
45
|
+
} as unknown as UseCommentsParams['viewerRef'],
|
|
46
|
+
rawViewRef: {
|
|
47
|
+
current: { scrollToComment: vi.fn() },
|
|
48
|
+
} as unknown as UseCommentsParams['rawViewRef'],
|
|
49
|
+
showToast: vi.fn(),
|
|
50
|
+
clearSelection: vi.fn(),
|
|
51
|
+
setAutoExpandForm: vi.fn(),
|
|
52
|
+
requestCommentFocus: vi.fn(),
|
|
53
|
+
...overrides,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Tests
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
describe('useComments', () => {
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
vi.restoreAllMocks();
|
|
64
|
+
// Stub crypto.randomUUID for deterministic IDs
|
|
65
|
+
vi.stubGlobal('crypto', {
|
|
66
|
+
...globalThis.crypto,
|
|
67
|
+
randomUUID: vi.fn(() => 'new-uuid-1'),
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// -----------------------------------------------------------------------
|
|
72
|
+
// 1. Comment parsing
|
|
73
|
+
// -----------------------------------------------------------------------
|
|
74
|
+
describe('comment parsing', () => {
|
|
75
|
+
it('populates comments array from raw markdown with comment markers', () => {
|
|
76
|
+
const raw = `Some text ${makeComment({ id: 'c1', anchor: 'hello', text: 'note one' })}hello world`;
|
|
77
|
+
const params = defaultParams({ rawMarkdown: raw });
|
|
78
|
+
const { result } = renderHook(() => useComments(params));
|
|
79
|
+
|
|
80
|
+
expect(result.current.comments).toHaveLength(1);
|
|
81
|
+
expect(result.current.comments[0].id).toBe('c1');
|
|
82
|
+
expect(result.current.comments[0].anchor).toBe('hello');
|
|
83
|
+
expect(result.current.comments[0].text).toBe('note one');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('strips comment markers from cleanMarkdown', () => {
|
|
87
|
+
const raw = `Some text ${makeComment({ id: 'c1', anchor: 'hello' })}hello world`;
|
|
88
|
+
const params = defaultParams({ rawMarkdown: raw });
|
|
89
|
+
const { result } = renderHook(() => useComments(params));
|
|
90
|
+
|
|
91
|
+
expect(result.current.cleanMarkdown).toBe('Some text hello world');
|
|
92
|
+
expect(result.current.cleanMarkdown).not.toContain('@comment');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('renders html from clean markdown', () => {
|
|
96
|
+
const raw = `# Title\n\n${makeComment({ id: 'c1', anchor: 'Title' })}`;
|
|
97
|
+
const params = defaultParams({ rawMarkdown: raw });
|
|
98
|
+
const { result } = renderHook(() => useComments(params));
|
|
99
|
+
|
|
100
|
+
expect(result.current.html).toContain('<h1');
|
|
101
|
+
expect(result.current.html).toContain('Title');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('handles undefined rawMarkdown', () => {
|
|
105
|
+
const params = defaultParams({ rawMarkdown: undefined });
|
|
106
|
+
const { result } = renderHook(() => useComments(params));
|
|
107
|
+
|
|
108
|
+
expect(result.current.comments).toEqual([]);
|
|
109
|
+
expect(result.current.cleanMarkdown).toBe('');
|
|
110
|
+
expect(result.current.html).toBe('');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// -----------------------------------------------------------------------
|
|
115
|
+
// 2. Comment counts per tab
|
|
116
|
+
// -----------------------------------------------------------------------
|
|
117
|
+
describe('commentCounts', () => {
|
|
118
|
+
it('counts open comments for the active tab', () => {
|
|
119
|
+
const raw = rawWithComments(
|
|
120
|
+
{ before: 'Text ', marker: makeComment({ id: 'c1', anchor: 'one' }), after: 'one ' },
|
|
121
|
+
{ marker: makeComment({ id: 'c2', anchor: 'two' }), after: 'two' },
|
|
122
|
+
);
|
|
123
|
+
const params = defaultParams({
|
|
124
|
+
rawMarkdown: raw,
|
|
125
|
+
tabs: [{ filePath: 'file.md', rawMarkdown: raw }],
|
|
126
|
+
activeFilePath: 'file.md',
|
|
127
|
+
});
|
|
128
|
+
const { result } = renderHook(() => useComments(params));
|
|
129
|
+
|
|
130
|
+
expect(result.current.commentCounts.get('file.md')).toBe(2);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('counts comments for non-active tabs by parsing their rawMarkdown', () => {
|
|
134
|
+
const activeRaw = `Active ${makeComment({ id: 'a1', anchor: 'Active' })}`;
|
|
135
|
+
const otherRaw = rawWithComments(
|
|
136
|
+
{ before: 'X ', marker: makeComment({ id: 'o1', anchor: 'X' }), after: '' },
|
|
137
|
+
{ before: ' Y ', marker: makeComment({ id: 'o2', anchor: 'Y' }), after: '' },
|
|
138
|
+
{ before: ' Z ', marker: makeComment({ id: 'o3', anchor: 'Z' }), after: '' },
|
|
139
|
+
);
|
|
140
|
+
const params = defaultParams({
|
|
141
|
+
rawMarkdown: activeRaw,
|
|
142
|
+
tabs: [
|
|
143
|
+
{ filePath: 'active.md', rawMarkdown: activeRaw },
|
|
144
|
+
{ filePath: 'other.md', rawMarkdown: otherRaw },
|
|
145
|
+
],
|
|
146
|
+
activeFilePath: 'active.md',
|
|
147
|
+
});
|
|
148
|
+
const { result } = renderHook(() => useComments(params));
|
|
149
|
+
|
|
150
|
+
expect(result.current.commentCounts.get('active.md')).toBe(1);
|
|
151
|
+
expect(result.current.commentCounts.get('other.md')).toBe(3);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// -----------------------------------------------------------------------
|
|
156
|
+
// 3. Resolved counts with enableResolve
|
|
157
|
+
// -----------------------------------------------------------------------
|
|
158
|
+
describe('resolvedCommentCounts', () => {
|
|
159
|
+
it('counts resolved comments when enableResolve is true', () => {
|
|
160
|
+
const raw = rawWithComments(
|
|
161
|
+
{ before: 'A ', marker: makeComment({ id: 'c1', anchor: 'A', status: 'open' }), after: '' },
|
|
162
|
+
{
|
|
163
|
+
before: ' B ',
|
|
164
|
+
marker: makeComment({ id: 'c2', anchor: 'B', status: 'resolved' }),
|
|
165
|
+
after: '',
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
before: ' C ',
|
|
169
|
+
marker: makeComment({ id: 'c3', anchor: 'C', status: 'open' }),
|
|
170
|
+
after: '',
|
|
171
|
+
},
|
|
172
|
+
);
|
|
173
|
+
const params = defaultParams({
|
|
174
|
+
rawMarkdown: raw,
|
|
175
|
+
enableResolve: true,
|
|
176
|
+
tabs: [{ filePath: 'file.md', rawMarkdown: raw }],
|
|
177
|
+
activeFilePath: 'file.md',
|
|
178
|
+
});
|
|
179
|
+
const { result } = renderHook(() => useComments(params));
|
|
180
|
+
|
|
181
|
+
// 2 open, 1 resolved
|
|
182
|
+
expect(result.current.commentCounts.get('file.md')).toBe(2);
|
|
183
|
+
expect(result.current.resolvedCommentCounts.get('file.md')).toBe(1);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('does not populate resolvedCommentCounts when enableResolve is false', () => {
|
|
187
|
+
const raw = `Text ${makeComment({ id: 'c1', anchor: 'Text', status: 'resolved' })}`;
|
|
188
|
+
const params = defaultParams({
|
|
189
|
+
rawMarkdown: raw,
|
|
190
|
+
enableResolve: false,
|
|
191
|
+
tabs: [{ filePath: 'file.md', rawMarkdown: raw }],
|
|
192
|
+
activeFilePath: 'file.md',
|
|
193
|
+
});
|
|
194
|
+
const { result } = renderHook(() => useComments(params));
|
|
195
|
+
|
|
196
|
+
expect(result.current.resolvedCommentCounts.has('file.md')).toBe(false);
|
|
197
|
+
// All comments counted (resolved ones included)
|
|
198
|
+
expect(result.current.commentCounts.get('file.md')).toBe(1);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// -----------------------------------------------------------------------
|
|
203
|
+
// 4. commentCount
|
|
204
|
+
// -----------------------------------------------------------------------
|
|
205
|
+
describe('commentCount', () => {
|
|
206
|
+
it('returns total open comments when enableResolve is true', () => {
|
|
207
|
+
const raw = rawWithComments(
|
|
208
|
+
{ before: 'A ', marker: makeComment({ id: 'c1', anchor: 'A', status: 'open' }), after: '' },
|
|
209
|
+
{
|
|
210
|
+
before: ' B ',
|
|
211
|
+
marker: makeComment({ id: 'c2', anchor: 'B', status: 'resolved' }),
|
|
212
|
+
after: '',
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
before: ' C ',
|
|
216
|
+
marker: makeComment({ id: 'c3', anchor: 'C', status: 'open' }),
|
|
217
|
+
after: '',
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
const params = defaultParams({ rawMarkdown: raw, enableResolve: true });
|
|
221
|
+
const { result } = renderHook(() => useComments(params));
|
|
222
|
+
|
|
223
|
+
expect(result.current.commentCount).toBe(2);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('returns total comments when enableResolve is false', () => {
|
|
227
|
+
const raw = rawWithComments(
|
|
228
|
+
{ before: 'A ', marker: makeComment({ id: 'c1', anchor: 'A', status: 'open' }), after: '' },
|
|
229
|
+
{
|
|
230
|
+
before: ' B ',
|
|
231
|
+
marker: makeComment({ id: 'c2', anchor: 'B', status: 'resolved' }),
|
|
232
|
+
after: '',
|
|
233
|
+
},
|
|
234
|
+
);
|
|
235
|
+
const params = defaultParams({ rawMarkdown: raw, enableResolve: false });
|
|
236
|
+
const { result } = renderHook(() => useComments(params));
|
|
237
|
+
|
|
238
|
+
expect(result.current.commentCount).toBe(2);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// -----------------------------------------------------------------------
|
|
243
|
+
// 5. handleJumpToNext — wraps from last to first
|
|
244
|
+
// -----------------------------------------------------------------------
|
|
245
|
+
describe('handleJumpToNext', () => {
|
|
246
|
+
it('wraps from last comment back to first', () => {
|
|
247
|
+
const raw = rawWithComments(
|
|
248
|
+
{ before: 'A ', marker: makeComment({ id: 'c1', anchor: 'A' }), after: '' },
|
|
249
|
+
{ before: ' B ', marker: makeComment({ id: 'c2', anchor: 'B' }), after: '' },
|
|
250
|
+
{ before: ' C ', marker: makeComment({ id: 'c3', anchor: 'C' }), after: '' },
|
|
251
|
+
);
|
|
252
|
+
const viewerRef = {
|
|
253
|
+
current: { scrollToComment: vi.fn() },
|
|
254
|
+
} as unknown as UseCommentsParams['viewerRef'];
|
|
255
|
+
const rawViewRef = {
|
|
256
|
+
current: { scrollToComment: vi.fn() },
|
|
257
|
+
} as unknown as UseCommentsParams['rawViewRef'];
|
|
258
|
+
const params = defaultParams({ rawMarkdown: raw, viewerRef, rawViewRef });
|
|
259
|
+
const { result } = renderHook(() => useComments(params));
|
|
260
|
+
|
|
261
|
+
// Navigate to c1 (first)
|
|
262
|
+
act(() => result.current.handleJumpToNext());
|
|
263
|
+
expect(result.current.activeCommentId).toBe('c1');
|
|
264
|
+
|
|
265
|
+
// Navigate to c2
|
|
266
|
+
act(() => result.current.handleJumpToNext());
|
|
267
|
+
expect(result.current.activeCommentId).toBe('c2');
|
|
268
|
+
|
|
269
|
+
// Navigate to c3
|
|
270
|
+
act(() => result.current.handleJumpToNext());
|
|
271
|
+
expect(result.current.activeCommentId).toBe('c3');
|
|
272
|
+
|
|
273
|
+
// Wrap to c1
|
|
274
|
+
act(() => result.current.handleJumpToNext());
|
|
275
|
+
expect(result.current.activeCommentId).toBe('c1');
|
|
276
|
+
|
|
277
|
+
expect(viewerRef.current!.scrollToComment).toHaveBeenCalledWith('c1');
|
|
278
|
+
expect(rawViewRef.current!.scrollToComment).toHaveBeenCalledWith('c1');
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// -----------------------------------------------------------------------
|
|
283
|
+
// 6. handleJumpToPrev — wraps from first to last
|
|
284
|
+
// -----------------------------------------------------------------------
|
|
285
|
+
describe('handleJumpToPrev', () => {
|
|
286
|
+
it('wraps from first comment back to last', () => {
|
|
287
|
+
const raw = rawWithComments(
|
|
288
|
+
{ before: 'A ', marker: makeComment({ id: 'c1', anchor: 'A' }), after: '' },
|
|
289
|
+
{ before: ' B ', marker: makeComment({ id: 'c2', anchor: 'B' }), after: '' },
|
|
290
|
+
{ before: ' C ', marker: makeComment({ id: 'c3', anchor: 'C' }), after: '' },
|
|
291
|
+
);
|
|
292
|
+
const viewerRef = {
|
|
293
|
+
current: { scrollToComment: vi.fn() },
|
|
294
|
+
} as unknown as UseCommentsParams['viewerRef'];
|
|
295
|
+
const rawViewRef = {
|
|
296
|
+
current: { scrollToComment: vi.fn() },
|
|
297
|
+
} as unknown as UseCommentsParams['rawViewRef'];
|
|
298
|
+
const params = defaultParams({ rawMarkdown: raw, viewerRef, rawViewRef });
|
|
299
|
+
const { result } = renderHook(() => useComments(params));
|
|
300
|
+
|
|
301
|
+
// No active comment — jumping prev should go to last
|
|
302
|
+
act(() => result.current.handleJumpToPrev());
|
|
303
|
+
expect(result.current.activeCommentId).toBe('c3');
|
|
304
|
+
|
|
305
|
+
// Prev from c3 → c2
|
|
306
|
+
act(() => result.current.handleJumpToPrev());
|
|
307
|
+
expect(result.current.activeCommentId).toBe('c2');
|
|
308
|
+
|
|
309
|
+
// Prev from c2 → c1
|
|
310
|
+
act(() => result.current.handleJumpToPrev());
|
|
311
|
+
expect(result.current.activeCommentId).toBe('c1');
|
|
312
|
+
|
|
313
|
+
// Wrap from c1 → c3
|
|
314
|
+
act(() => result.current.handleJumpToPrev());
|
|
315
|
+
expect(result.current.activeCommentId).toBe('c3');
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// -----------------------------------------------------------------------
|
|
320
|
+
// 7. handleJumpToNext with resolve mode — skips resolved comments
|
|
321
|
+
// -----------------------------------------------------------------------
|
|
322
|
+
describe('handleJumpToNext with enableResolve', () => {
|
|
323
|
+
it('skips resolved comments', () => {
|
|
324
|
+
const raw = rawWithComments(
|
|
325
|
+
{ before: 'A ', marker: makeComment({ id: 'c1', anchor: 'A', status: 'open' }), after: '' },
|
|
326
|
+
{
|
|
327
|
+
before: ' B ',
|
|
328
|
+
marker: makeComment({ id: 'c2', anchor: 'B', status: 'resolved' }),
|
|
329
|
+
after: '',
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
before: ' C ',
|
|
333
|
+
marker: makeComment({ id: 'c3', anchor: 'C', status: 'open' }),
|
|
334
|
+
after: '',
|
|
335
|
+
},
|
|
336
|
+
);
|
|
337
|
+
const params = defaultParams({ rawMarkdown: raw, enableResolve: true });
|
|
338
|
+
const { result } = renderHook(() => useComments(params));
|
|
339
|
+
|
|
340
|
+
// First jump → c1 (first open)
|
|
341
|
+
act(() => result.current.handleJumpToNext());
|
|
342
|
+
expect(result.current.activeCommentId).toBe('c1');
|
|
343
|
+
|
|
344
|
+
// Next → c3 (skip c2 which is resolved)
|
|
345
|
+
act(() => result.current.handleJumpToNext());
|
|
346
|
+
expect(result.current.activeCommentId).toBe('c3');
|
|
347
|
+
|
|
348
|
+
// Wrap back to c1
|
|
349
|
+
act(() => result.current.handleJumpToNext());
|
|
350
|
+
expect(result.current.activeCommentId).toBe('c1');
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// -----------------------------------------------------------------------
|
|
355
|
+
// 8. handleAddComment — calls insertComment and updateAndSave
|
|
356
|
+
// -----------------------------------------------------------------------
|
|
357
|
+
describe('handleAddComment', () => {
|
|
358
|
+
it('calls setRawMarkdown, saveFile, and sets activeCommentId', () => {
|
|
359
|
+
const setRawMarkdown = vi.fn();
|
|
360
|
+
const saveFile = vi.fn();
|
|
361
|
+
const clearSelection = vi.fn();
|
|
362
|
+
const requestCommentFocus = vi.fn();
|
|
363
|
+
const setAutoExpandForm = vi.fn();
|
|
364
|
+
const raw = 'Hello world';
|
|
365
|
+
const rawMarkdownRef = { current: raw };
|
|
366
|
+
const params = defaultParams({
|
|
367
|
+
rawMarkdown: raw,
|
|
368
|
+
rawMarkdownRef: rawMarkdownRef as unknown as UseCommentsParams['rawMarkdownRef'],
|
|
369
|
+
setRawMarkdown,
|
|
370
|
+
saveFile,
|
|
371
|
+
author: 'Tester',
|
|
372
|
+
clearSelection,
|
|
373
|
+
requestCommentFocus,
|
|
374
|
+
setAutoExpandForm,
|
|
375
|
+
});
|
|
376
|
+
const { result } = renderHook(() => useComments(params));
|
|
377
|
+
|
|
378
|
+
act(() => {
|
|
379
|
+
result.current.handleAddComment('Hello', 'Nice greeting');
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// Should have called setRawMarkdown and saveFile with new content containing comment marker
|
|
383
|
+
expect(setRawMarkdown).toHaveBeenCalledTimes(1);
|
|
384
|
+
expect(saveFile).toHaveBeenCalledTimes(1);
|
|
385
|
+
const savedContent = setRawMarkdown.mock.calls[0][0] as string;
|
|
386
|
+
expect(savedContent).toContain('@comment');
|
|
387
|
+
expect(savedContent).toContain('Nice greeting');
|
|
388
|
+
expect(savedContent).toContain('new-uuid-1');
|
|
389
|
+
|
|
390
|
+
// Should set active comment id to the new UUID
|
|
391
|
+
expect(result.current.activeCommentId).toBe('new-uuid-1');
|
|
392
|
+
|
|
393
|
+
// Should call requestCommentFocus with the new id
|
|
394
|
+
expect(requestCommentFocus).toHaveBeenCalledWith('new-uuid-1');
|
|
395
|
+
|
|
396
|
+
// Should clear selection
|
|
397
|
+
expect(clearSelection).toHaveBeenCalledTimes(1);
|
|
398
|
+
|
|
399
|
+
// Should collapse form
|
|
400
|
+
expect(setAutoExpandForm).toHaveBeenCalledWith(false);
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// -----------------------------------------------------------------------
|
|
405
|
+
// 9. handleDelete — removes comment and clears activeCommentId if deleted
|
|
406
|
+
// -----------------------------------------------------------------------
|
|
407
|
+
describe('handleDelete', () => {
|
|
408
|
+
it('calls setRawMarkdown and saveFile to remove the comment', () => {
|
|
409
|
+
const setRawMarkdown = vi.fn();
|
|
410
|
+
const saveFile = vi.fn();
|
|
411
|
+
const raw = `Hello ${makeComment({ id: 'c1', anchor: 'Hello' })}world`;
|
|
412
|
+
const rawMarkdownRef = { current: raw };
|
|
413
|
+
const params = defaultParams({
|
|
414
|
+
rawMarkdown: raw,
|
|
415
|
+
rawMarkdownRef: rawMarkdownRef as unknown as UseCommentsParams['rawMarkdownRef'],
|
|
416
|
+
setRawMarkdown,
|
|
417
|
+
saveFile,
|
|
418
|
+
});
|
|
419
|
+
const { result } = renderHook(() => useComments(params));
|
|
420
|
+
|
|
421
|
+
// First set active comment to c1
|
|
422
|
+
act(() => result.current.setActiveCommentId('c1'));
|
|
423
|
+
expect(result.current.activeCommentId).toBe('c1');
|
|
424
|
+
|
|
425
|
+
// Delete c1
|
|
426
|
+
act(() => result.current.handleDelete('c1'));
|
|
427
|
+
|
|
428
|
+
expect(setRawMarkdown).toHaveBeenCalled();
|
|
429
|
+
expect(saveFile).toHaveBeenCalled();
|
|
430
|
+
// Active comment should be cleared because it was the deleted one
|
|
431
|
+
expect(result.current.activeCommentId).toBeNull();
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('does not clear activeCommentId if a different comment was deleted', () => {
|
|
435
|
+
const raw = rawWithComments(
|
|
436
|
+
{ before: 'A ', marker: makeComment({ id: 'c1', anchor: 'A' }), after: '' },
|
|
437
|
+
{ before: ' B ', marker: makeComment({ id: 'c2', anchor: 'B' }), after: '' },
|
|
438
|
+
);
|
|
439
|
+
const rawMarkdownRef = { current: raw };
|
|
440
|
+
const params = defaultParams({
|
|
441
|
+
rawMarkdown: raw,
|
|
442
|
+
rawMarkdownRef: rawMarkdownRef as unknown as UseCommentsParams['rawMarkdownRef'],
|
|
443
|
+
});
|
|
444
|
+
const { result } = renderHook(() => useComments(params));
|
|
445
|
+
|
|
446
|
+
// Set active to c1
|
|
447
|
+
act(() => result.current.setActiveCommentId('c1'));
|
|
448
|
+
expect(result.current.activeCommentId).toBe('c1');
|
|
449
|
+
|
|
450
|
+
// Delete c2 (different comment)
|
|
451
|
+
act(() => result.current.handleDelete('c2'));
|
|
452
|
+
|
|
453
|
+
// c1 should still be active
|
|
454
|
+
expect(result.current.activeCommentId).toBe('c1');
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// -----------------------------------------------------------------------
|
|
459
|
+
// 10. handleResolve / handleUnresolve
|
|
460
|
+
// -----------------------------------------------------------------------
|
|
461
|
+
describe('handleResolve and handleUnresolve', () => {
|
|
462
|
+
it('handleResolve calls setRawMarkdown and saveFile with resolved content', () => {
|
|
463
|
+
const setRawMarkdown = vi.fn();
|
|
464
|
+
const saveFile = vi.fn();
|
|
465
|
+
const raw = `Text ${makeComment({ id: 'c1', anchor: 'Text', status: 'open' })}`;
|
|
466
|
+
const rawMarkdownRef = { current: raw };
|
|
467
|
+
const params = defaultParams({
|
|
468
|
+
rawMarkdown: raw,
|
|
469
|
+
rawMarkdownRef: rawMarkdownRef as unknown as UseCommentsParams['rawMarkdownRef'],
|
|
470
|
+
setRawMarkdown,
|
|
471
|
+
saveFile,
|
|
472
|
+
enableResolve: true,
|
|
473
|
+
});
|
|
474
|
+
const { result } = renderHook(() => useComments(params));
|
|
475
|
+
|
|
476
|
+
act(() => result.current.handleResolve('c1'));
|
|
477
|
+
|
|
478
|
+
expect(setRawMarkdown).toHaveBeenCalledTimes(1);
|
|
479
|
+
expect(saveFile).toHaveBeenCalledTimes(1);
|
|
480
|
+
const savedContent = setRawMarkdown.mock.calls[0][0] as string;
|
|
481
|
+
expect(savedContent).toContain('"status":"resolved"');
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('handleUnresolve calls setRawMarkdown and saveFile with unresolve content', () => {
|
|
485
|
+
const setRawMarkdown = vi.fn();
|
|
486
|
+
const saveFile = vi.fn();
|
|
487
|
+
const raw = `Text ${makeComment({ id: 'c1', anchor: 'Text', status: 'resolved' })}`;
|
|
488
|
+
const rawMarkdownRef = { current: raw };
|
|
489
|
+
const params = defaultParams({
|
|
490
|
+
rawMarkdown: raw,
|
|
491
|
+
rawMarkdownRef: rawMarkdownRef as unknown as UseCommentsParams['rawMarkdownRef'],
|
|
492
|
+
setRawMarkdown,
|
|
493
|
+
saveFile,
|
|
494
|
+
enableResolve: true,
|
|
495
|
+
});
|
|
496
|
+
const { result } = renderHook(() => useComments(params));
|
|
497
|
+
|
|
498
|
+
act(() => result.current.handleUnresolve('c1'));
|
|
499
|
+
|
|
500
|
+
expect(setRawMarkdown).toHaveBeenCalledTimes(1);
|
|
501
|
+
expect(saveFile).toHaveBeenCalledTimes(1);
|
|
502
|
+
const savedContent = setRawMarkdown.mock.calls[0][0] as string;
|
|
503
|
+
expect(savedContent).toContain('"status":"open"');
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// -----------------------------------------------------------------------
|
|
508
|
+
// 11. handleBulkDelete — calls removeAllComments
|
|
509
|
+
// -----------------------------------------------------------------------
|
|
510
|
+
describe('handleBulkDelete', () => {
|
|
511
|
+
it('removes all comments from the markdown', () => {
|
|
512
|
+
const setRawMarkdown = vi.fn();
|
|
513
|
+
const saveFile = vi.fn();
|
|
514
|
+
const raw = rawWithComments(
|
|
515
|
+
{ before: 'A ', marker: makeComment({ id: 'c1', anchor: 'A' }), after: '' },
|
|
516
|
+
{ before: ' B ', marker: makeComment({ id: 'c2', anchor: 'B' }), after: '' },
|
|
517
|
+
);
|
|
518
|
+
const rawMarkdownRef = { current: raw };
|
|
519
|
+
const params = defaultParams({
|
|
520
|
+
rawMarkdown: raw,
|
|
521
|
+
rawMarkdownRef: rawMarkdownRef as unknown as UseCommentsParams['rawMarkdownRef'],
|
|
522
|
+
setRawMarkdown,
|
|
523
|
+
saveFile,
|
|
524
|
+
});
|
|
525
|
+
const { result } = renderHook(() => useComments(params));
|
|
526
|
+
|
|
527
|
+
act(() => result.current.handleBulkDelete());
|
|
528
|
+
|
|
529
|
+
expect(setRawMarkdown).toHaveBeenCalledTimes(1);
|
|
530
|
+
expect(saveFile).toHaveBeenCalledTimes(1);
|
|
531
|
+
const savedContent = setRawMarkdown.mock.calls[0][0] as string;
|
|
532
|
+
expect(savedContent).not.toContain('@comment');
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// -----------------------------------------------------------------------
|
|
537
|
+
// 12. missingAnchors — detects comments whose anchor is not in clean markdown
|
|
538
|
+
// -----------------------------------------------------------------------
|
|
539
|
+
describe('missingAnchors', () => {
|
|
540
|
+
it('detects comments whose anchor text does not appear in clean markdown', () => {
|
|
541
|
+
// Anchor is "missing text" but clean markdown won't contain it
|
|
542
|
+
const raw = `Real content ${makeComment({ id: 'c1', anchor: 'missing text', status: 'open' })}here`;
|
|
543
|
+
const params = defaultParams({ rawMarkdown: raw });
|
|
544
|
+
const { result } = renderHook(() => useComments(params));
|
|
545
|
+
|
|
546
|
+
expect(result.current.missingAnchors.has('c1')).toBe(true);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it('does not flag comments whose anchor text is present', () => {
|
|
550
|
+
const raw = `Hello ${makeComment({ id: 'c1', anchor: 'Hello' })}world`;
|
|
551
|
+
const params = defaultParams({ rawMarkdown: raw });
|
|
552
|
+
const { result } = renderHook(() => useComments(params));
|
|
553
|
+
|
|
554
|
+
expect(result.current.missingAnchors.has('c1')).toBe(false);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it('does not flag resolved comments as missing', () => {
|
|
558
|
+
const raw = `Real content ${makeComment({ id: 'c1', anchor: 'missing text', status: 'resolved' })}here`;
|
|
559
|
+
const params = defaultParams({ rawMarkdown: raw, enableResolve: true });
|
|
560
|
+
const { result } = renderHook(() => useComments(params));
|
|
561
|
+
|
|
562
|
+
expect(result.current.missingAnchors.has('c1')).toBe(false);
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
// -----------------------------------------------------------------------
|
|
567
|
+
// Additional CRUD operations
|
|
568
|
+
// -----------------------------------------------------------------------
|
|
569
|
+
describe('handleEdit', () => {
|
|
570
|
+
it('calls setRawMarkdown and saveFile with edited comment text', () => {
|
|
571
|
+
const setRawMarkdown = vi.fn();
|
|
572
|
+
const saveFile = vi.fn();
|
|
573
|
+
const raw = `Hello ${makeComment({ id: 'c1', anchor: 'Hello', text: 'old note' })}world`;
|
|
574
|
+
const rawMarkdownRef = { current: raw };
|
|
575
|
+
const params = defaultParams({
|
|
576
|
+
rawMarkdown: raw,
|
|
577
|
+
rawMarkdownRef: rawMarkdownRef as unknown as UseCommentsParams['rawMarkdownRef'],
|
|
578
|
+
setRawMarkdown,
|
|
579
|
+
saveFile,
|
|
580
|
+
});
|
|
581
|
+
const { result } = renderHook(() => useComments(params));
|
|
582
|
+
|
|
583
|
+
act(() => result.current.handleEdit('c1', 'new note'));
|
|
584
|
+
|
|
585
|
+
expect(setRawMarkdown).toHaveBeenCalledTimes(1);
|
|
586
|
+
const savedContent = setRawMarkdown.mock.calls[0][0] as string;
|
|
587
|
+
expect(savedContent).toContain('new note');
|
|
588
|
+
expect(savedContent).not.toContain('old note');
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
describe('handleReply', () => {
|
|
593
|
+
it('calls setRawMarkdown and saveFile with reply added', () => {
|
|
594
|
+
const setRawMarkdown = vi.fn();
|
|
595
|
+
const saveFile = vi.fn();
|
|
596
|
+
const raw = `Hello ${makeComment({ id: 'c1', anchor: 'Hello', text: 'note' })}world`;
|
|
597
|
+
const rawMarkdownRef = { current: raw };
|
|
598
|
+
const params = defaultParams({
|
|
599
|
+
rawMarkdown: raw,
|
|
600
|
+
rawMarkdownRef: rawMarkdownRef as unknown as UseCommentsParams['rawMarkdownRef'],
|
|
601
|
+
setRawMarkdown,
|
|
602
|
+
saveFile,
|
|
603
|
+
author: 'Replier',
|
|
604
|
+
});
|
|
605
|
+
const { result } = renderHook(() => useComments(params));
|
|
606
|
+
|
|
607
|
+
act(() => result.current.handleReply('c1', 'reply text'));
|
|
608
|
+
|
|
609
|
+
expect(setRawMarkdown).toHaveBeenCalledTimes(1);
|
|
610
|
+
const savedContent = setRawMarkdown.mock.calls[0][0] as string;
|
|
611
|
+
expect(savedContent).toContain('reply text');
|
|
612
|
+
expect(savedContent).toContain('Replier');
|
|
613
|
+
expect(savedContent).toContain('"replies"');
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
describe('handleBulkResolve', () => {
|
|
618
|
+
it('resolves all comments', () => {
|
|
619
|
+
const setRawMarkdown = vi.fn();
|
|
620
|
+
const saveFile = vi.fn();
|
|
621
|
+
const raw = rawWithComments(
|
|
622
|
+
{ before: 'A ', marker: makeComment({ id: 'c1', anchor: 'A', status: 'open' }), after: '' },
|
|
623
|
+
{
|
|
624
|
+
before: ' B ',
|
|
625
|
+
marker: makeComment({ id: 'c2', anchor: 'B', status: 'open' }),
|
|
626
|
+
after: '',
|
|
627
|
+
},
|
|
628
|
+
);
|
|
629
|
+
const rawMarkdownRef = { current: raw };
|
|
630
|
+
const params = defaultParams({
|
|
631
|
+
rawMarkdown: raw,
|
|
632
|
+
rawMarkdownRef: rawMarkdownRef as unknown as UseCommentsParams['rawMarkdownRef'],
|
|
633
|
+
setRawMarkdown,
|
|
634
|
+
saveFile,
|
|
635
|
+
enableResolve: true,
|
|
636
|
+
});
|
|
637
|
+
const { result } = renderHook(() => useComments(params));
|
|
638
|
+
|
|
639
|
+
act(() => result.current.handleBulkResolve());
|
|
640
|
+
|
|
641
|
+
expect(setRawMarkdown).toHaveBeenCalledTimes(1);
|
|
642
|
+
const savedContent = setRawMarkdown.mock.calls[0][0] as string;
|
|
643
|
+
expect(savedContent).toContain('"status":"resolved"');
|
|
644
|
+
// Both should be resolved
|
|
645
|
+
const matches = savedContent.match(/"status":"resolved"/g);
|
|
646
|
+
expect(matches).toHaveLength(2);
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
describe('handleBulkDeleteResolved', () => {
|
|
651
|
+
it('removes only resolved comments', () => {
|
|
652
|
+
const setRawMarkdown = vi.fn();
|
|
653
|
+
const saveFile = vi.fn();
|
|
654
|
+
const raw = rawWithComments(
|
|
655
|
+
{ before: 'A ', marker: makeComment({ id: 'c1', anchor: 'A', status: 'open' }), after: '' },
|
|
656
|
+
{
|
|
657
|
+
before: ' B ',
|
|
658
|
+
marker: makeComment({ id: 'c2', anchor: 'B', status: 'resolved' }),
|
|
659
|
+
after: '',
|
|
660
|
+
},
|
|
661
|
+
);
|
|
662
|
+
const rawMarkdownRef = { current: raw };
|
|
663
|
+
const params = defaultParams({
|
|
664
|
+
rawMarkdown: raw,
|
|
665
|
+
rawMarkdownRef: rawMarkdownRef as unknown as UseCommentsParams['rawMarkdownRef'],
|
|
666
|
+
setRawMarkdown,
|
|
667
|
+
saveFile,
|
|
668
|
+
enableResolve: true,
|
|
669
|
+
});
|
|
670
|
+
const { result } = renderHook(() => useComments(params));
|
|
671
|
+
|
|
672
|
+
act(() => result.current.handleBulkDeleteResolved());
|
|
673
|
+
|
|
674
|
+
expect(setRawMarkdown).toHaveBeenCalledTimes(1);
|
|
675
|
+
const savedContent = setRawMarkdown.mock.calls[0][0] as string;
|
|
676
|
+
// c1 (open) should remain
|
|
677
|
+
expect(savedContent).toContain('"id":"c1"');
|
|
678
|
+
// c2 (resolved) should be removed
|
|
679
|
+
expect(savedContent).not.toContain('"id":"c2"');
|
|
680
|
+
});
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
describe('handleHighlightClick', () => {
|
|
684
|
+
it('sets activeCommentId', () => {
|
|
685
|
+
const raw = `Hello ${makeComment({ id: 'c1', anchor: 'Hello' })}world`;
|
|
686
|
+
const params = defaultParams({ rawMarkdown: raw });
|
|
687
|
+
const { result } = renderHook(() => useComments(params));
|
|
688
|
+
|
|
689
|
+
act(() => result.current.handleHighlightClick('c1'));
|
|
690
|
+
expect(result.current.activeCommentId).toBe('c1');
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
describe('handleSidebarActivate', () => {
|
|
695
|
+
it('sets activeCommentId and scrolls both viewers', () => {
|
|
696
|
+
const viewerRef = {
|
|
697
|
+
current: { scrollToComment: vi.fn() },
|
|
698
|
+
} as unknown as UseCommentsParams['viewerRef'];
|
|
699
|
+
const rawViewRef = {
|
|
700
|
+
current: { scrollToComment: vi.fn() },
|
|
701
|
+
} as unknown as UseCommentsParams['rawViewRef'];
|
|
702
|
+
const raw = `Hello ${makeComment({ id: 'c1', anchor: 'Hello' })}world`;
|
|
703
|
+
const params = defaultParams({ rawMarkdown: raw, viewerRef, rawViewRef });
|
|
704
|
+
const { result } = renderHook(() => useComments(params));
|
|
705
|
+
|
|
706
|
+
act(() => result.current.handleSidebarActivate('c1'));
|
|
707
|
+
|
|
708
|
+
expect(result.current.activeCommentId).toBe('c1');
|
|
709
|
+
expect(viewerRef.current!.scrollToComment).toHaveBeenCalledWith('c1');
|
|
710
|
+
expect(rawViewRef.current!.scrollToComment).toHaveBeenCalledWith('c1');
|
|
711
|
+
});
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
// -----------------------------------------------------------------------
|
|
715
|
+
// updateAndSave — synchronous ref update for back-to-back mutations
|
|
716
|
+
// -----------------------------------------------------------------------
|
|
717
|
+
describe('back-to-back mutations', () => {
|
|
718
|
+
it('second mutation sees the first mutation via rawMarkdownRef', () => {
|
|
719
|
+
const setRawMarkdown = vi.fn();
|
|
720
|
+
const saveFile = vi.fn();
|
|
721
|
+
const raw = `Hello ${makeComment({ id: 'c1', anchor: 'Hello', status: 'open' })}${makeComment({ id: 'c2', anchor: 'Hello', status: 'open' })}world`;
|
|
722
|
+
const rawMarkdownRef = { current: raw };
|
|
723
|
+
const params = defaultParams({
|
|
724
|
+
rawMarkdown: raw,
|
|
725
|
+
rawMarkdownRef: rawMarkdownRef as unknown as UseCommentsParams['rawMarkdownRef'],
|
|
726
|
+
setRawMarkdown,
|
|
727
|
+
saveFile,
|
|
728
|
+
enableResolve: true,
|
|
729
|
+
});
|
|
730
|
+
const { result } = renderHook(() => useComments(params));
|
|
731
|
+
|
|
732
|
+
// Resolve c1, then immediately delete c2 in the same synchronous block.
|
|
733
|
+
// Without the ref fix, handleDelete would read the pre-resolve rawMarkdownRef
|
|
734
|
+
// and the resolve would be lost.
|
|
735
|
+
act(() => {
|
|
736
|
+
result.current.handleResolve('c1');
|
|
737
|
+
result.current.handleDelete('c2');
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
expect(setRawMarkdown).toHaveBeenCalledTimes(2);
|
|
741
|
+
expect(saveFile).toHaveBeenCalledTimes(2);
|
|
742
|
+
|
|
743
|
+
// The second call should contain the resolve from the first call
|
|
744
|
+
const secondContent = setRawMarkdown.mock.calls[1][0] as string;
|
|
745
|
+
// c1 should be resolved (still present with status resolved)
|
|
746
|
+
expect(secondContent).toContain('"id":"c1"');
|
|
747
|
+
expect(secondContent).toContain('"status":"resolved"');
|
|
748
|
+
// c2 should be deleted
|
|
749
|
+
expect(secondContent).not.toContain('"id":"c2"');
|
|
750
|
+
|
|
751
|
+
// rawMarkdownRef should reflect the final state
|
|
752
|
+
expect(rawMarkdownRef.current).toBe(secondContent);
|
|
753
|
+
});
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
describe('navigation with no comments', () => {
|
|
757
|
+
it('handleJumpToNext does nothing with no comments', () => {
|
|
758
|
+
const params = defaultParams({ rawMarkdown: 'No comments here' });
|
|
759
|
+
const { result } = renderHook(() => useComments(params));
|
|
760
|
+
|
|
761
|
+
act(() => result.current.handleJumpToNext());
|
|
762
|
+
expect(result.current.activeCommentId).toBeNull();
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it('handleJumpToPrev does nothing with no comments', () => {
|
|
766
|
+
const params = defaultParams({ rawMarkdown: 'No comments here' });
|
|
767
|
+
const { result } = renderHook(() => useComments(params));
|
|
768
|
+
|
|
769
|
+
act(() => result.current.handleJumpToPrev());
|
|
770
|
+
expect(result.current.activeCommentId).toBeNull();
|
|
771
|
+
});
|
|
772
|
+
});
|
|
773
|
+
});
|