markupr 2.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (299) hide show
  1. package/.claude/commands/review-feedback.md +47 -0
  2. package/.eslintrc.json +35 -0
  3. package/.github/CODEOWNERS +16 -0
  4. package/.github/FUNDING.yml +1 -0
  5. package/.github/ISSUE_TEMPLATE/bug_report.md +56 -0
  6. package/.github/ISSUE_TEMPLATE/feature_request.md +54 -0
  7. package/.github/PULL_REQUEST_TEMPLATE.md +89 -0
  8. package/.github/dependabot.yml +70 -0
  9. package/.github/workflows/ci.yml +184 -0
  10. package/.github/workflows/deploy-landing.yml +134 -0
  11. package/.github/workflows/nightly.yml +288 -0
  12. package/.github/workflows/release.yml +318 -0
  13. package/CHANGELOG.md +127 -0
  14. package/CLAUDE.md +137 -0
  15. package/CODE_OF_CONDUCT.md +9 -0
  16. package/CONTRIBUTING.md +390 -0
  17. package/LICENSE +21 -0
  18. package/PRODUCT_VISION.md +277 -0
  19. package/README.md +517 -0
  20. package/SECURITY.md +51 -0
  21. package/SIGNING_INSTRUCTIONS.md +284 -0
  22. package/assets/DMG_BACKGROUND_INSTRUCTIONS.md +130 -0
  23. package/assets/svg-source/dmg-background.svg +70 -0
  24. package/assets/svg-source/icon.svg +20 -0
  25. package/assets/svg-source/tray-icon-processing.svg +7 -0
  26. package/assets/svg-source/tray-icon-recording.svg +7 -0
  27. package/assets/svg-source/tray-icon.svg +6 -0
  28. package/assets/tray-complete.png +0 -0
  29. package/assets/tray-complete@2x.png +0 -0
  30. package/assets/tray-completeTemplate.png +0 -0
  31. package/assets/tray-completeTemplate@2x.png +0 -0
  32. package/assets/tray-error.png +0 -0
  33. package/assets/tray-error@2x.png +0 -0
  34. package/assets/tray-errorTemplate.png +0 -0
  35. package/assets/tray-errorTemplate@2x.png +0 -0
  36. package/assets/tray-icon-processing.png +0 -0
  37. package/assets/tray-icon-processing@2x.png +0 -0
  38. package/assets/tray-icon-processingTemplate.png +0 -0
  39. package/assets/tray-icon-processingTemplate@2x.png +0 -0
  40. package/assets/tray-icon-recording.png +0 -0
  41. package/assets/tray-icon-recording@2x.png +0 -0
  42. package/assets/tray-icon-recordingTemplate.png +0 -0
  43. package/assets/tray-icon-recordingTemplate@2x.png +0 -0
  44. package/assets/tray-icon.png +0 -0
  45. package/assets/tray-icon@2x.png +0 -0
  46. package/assets/tray-iconTemplate.png +0 -0
  47. package/assets/tray-iconTemplate@2x.png +0 -0
  48. package/assets/tray-idle.png +0 -0
  49. package/assets/tray-idle@2x.png +0 -0
  50. package/assets/tray-idleTemplate.png +0 -0
  51. package/assets/tray-idleTemplate@2x.png +0 -0
  52. package/assets/tray-processing-0.png +0 -0
  53. package/assets/tray-processing-0@2x.png +0 -0
  54. package/assets/tray-processing-0Template.png +0 -0
  55. package/assets/tray-processing-0Template@2x.png +0 -0
  56. package/assets/tray-processing-1.png +0 -0
  57. package/assets/tray-processing-1@2x.png +0 -0
  58. package/assets/tray-processing-1Template.png +0 -0
  59. package/assets/tray-processing-1Template@2x.png +0 -0
  60. package/assets/tray-processing-2.png +0 -0
  61. package/assets/tray-processing-2@2x.png +0 -0
  62. package/assets/tray-processing-2Template.png +0 -0
  63. package/assets/tray-processing-2Template@2x.png +0 -0
  64. package/assets/tray-processing-3.png +0 -0
  65. package/assets/tray-processing-3@2x.png +0 -0
  66. package/assets/tray-processing-3Template.png +0 -0
  67. package/assets/tray-processing-3Template@2x.png +0 -0
  68. package/assets/tray-processing.png +0 -0
  69. package/assets/tray-processing@2x.png +0 -0
  70. package/assets/tray-processingTemplate.png +0 -0
  71. package/assets/tray-processingTemplate@2x.png +0 -0
  72. package/assets/tray-recording.png +0 -0
  73. package/assets/tray-recording@2x.png +0 -0
  74. package/assets/tray-recordingTemplate.png +0 -0
  75. package/assets/tray-recordingTemplate@2x.png +0 -0
  76. package/build/DMG_BACKGROUND_SPEC.md +50 -0
  77. package/build/dmg-background.png +0 -0
  78. package/build/dmg-background@2x.png +0 -0
  79. package/build/entitlements.mac.inherit.plist +27 -0
  80. package/build/entitlements.mac.plist +41 -0
  81. package/build/favicon-16.png +0 -0
  82. package/build/favicon-180.png +0 -0
  83. package/build/favicon-192.png +0 -0
  84. package/build/favicon-32.png +0 -0
  85. package/build/favicon-48.png +0 -0
  86. package/build/favicon-512.png +0 -0
  87. package/build/favicon-64.png +0 -0
  88. package/build/icon-128.png +0 -0
  89. package/build/icon-16.png +0 -0
  90. package/build/icon-24.png +0 -0
  91. package/build/icon-256.png +0 -0
  92. package/build/icon-32.png +0 -0
  93. package/build/icon-48.png +0 -0
  94. package/build/icon-64.png +0 -0
  95. package/build/icon.icns +0 -0
  96. package/build/icon.ico +0 -0
  97. package/build/icon.iconset/icon_128x128.png +0 -0
  98. package/build/icon.iconset/icon_128x128@2x.png +0 -0
  99. package/build/icon.iconset/icon_16x16.png +0 -0
  100. package/build/icon.iconset/icon_16x16@2x.png +0 -0
  101. package/build/icon.iconset/icon_256x256.png +0 -0
  102. package/build/icon.iconset/icon_256x256@2x.png +0 -0
  103. package/build/icon.iconset/icon_32x32.png +0 -0
  104. package/build/icon.iconset/icon_32x32@2x.png +0 -0
  105. package/build/icon.iconset/icon_512x512.png +0 -0
  106. package/build/icon.iconset/icon_512x512@2x.png +0 -0
  107. package/build/icon.png +0 -0
  108. package/build/installer-header.bmp +0 -0
  109. package/build/installer-header.png +0 -0
  110. package/build/installer-sidebar.bmp +0 -0
  111. package/build/installer-sidebar.png +0 -0
  112. package/build/installer.nsh +45 -0
  113. package/build/overlay-processing.png +0 -0
  114. package/build/overlay-recording.png +0 -0
  115. package/build/toolbar-record.png +0 -0
  116. package/build/toolbar-screenshot.png +0 -0
  117. package/build/toolbar-settings.png +0 -0
  118. package/build/toolbar-stop.png +0 -0
  119. package/dist/main/index.mjs +12612 -0
  120. package/dist/preload/index.mjs +907 -0
  121. package/dist/renderer/assets/index-CCmUjl9K.js +19495 -0
  122. package/dist/renderer/assets/index-CUqz_Gs6.css +2270 -0
  123. package/dist/renderer/index.html +27 -0
  124. package/docs/AI_AGENT_QUICKSTART.md +42 -0
  125. package/docs/AI_PIPELINE_DESIGN.md +595 -0
  126. package/docs/API.md +514 -0
  127. package/docs/ARCHITECTURE.md +460 -0
  128. package/docs/CONFIGURATION.md +336 -0
  129. package/docs/DEVELOPMENT.md +508 -0
  130. package/docs/EXPORT_FORMATS.md +451 -0
  131. package/docs/GETTING_STARTED.md +236 -0
  132. package/docs/KEYBOARD_SHORTCUTS.md +334 -0
  133. package/docs/TROUBLESHOOTING.md +418 -0
  134. package/docs/landing/index.html +672 -0
  135. package/docs/landing/script.js +342 -0
  136. package/docs/landing/styles.css +1543 -0
  137. package/electron-builder.yml +140 -0
  138. package/electron.vite.config.ts +63 -0
  139. package/package.json +108 -0
  140. package/railway.json +12 -0
  141. package/scripts/build.mjs +51 -0
  142. package/scripts/generate-icons.mjs +314 -0
  143. package/scripts/generate-installer-images.cjs +253 -0
  144. package/scripts/generate-tray-icons.mjs +258 -0
  145. package/scripts/notarize.cjs +180 -0
  146. package/scripts/one-click-clean-test.sh +147 -0
  147. package/scripts/postinstall.mjs +36 -0
  148. package/scripts/setup-markupr.sh +55 -0
  149. package/setup +17 -0
  150. package/site/index.html +1835 -0
  151. package/site/package.json +11 -0
  152. package/site/railway.json +12 -0
  153. package/site/server.js +31 -0
  154. package/src/main/AutoUpdater.ts +392 -0
  155. package/src/main/CrashRecovery.ts +655 -0
  156. package/src/main/ErrorHandler.ts +703 -0
  157. package/src/main/HotkeyManager.ts +399 -0
  158. package/src/main/MenuManager.ts +529 -0
  159. package/src/main/PermissionManager.ts +420 -0
  160. package/src/main/SessionController.ts +1465 -0
  161. package/src/main/TrayManager.ts +540 -0
  162. package/src/main/ai/AIPipelineManager.ts +199 -0
  163. package/src/main/ai/ClaudeAnalyzer.ts +339 -0
  164. package/src/main/ai/ImageOptimizer.ts +176 -0
  165. package/src/main/ai/StructuredMarkdownBuilder.ts +379 -0
  166. package/src/main/ai/index.ts +16 -0
  167. package/src/main/ai/types.ts +258 -0
  168. package/src/main/analysis/ClarificationGenerator.ts +385 -0
  169. package/src/main/analysis/FeedbackAnalyzer.ts +531 -0
  170. package/src/main/analysis/index.ts +19 -0
  171. package/src/main/audio/AudioCapture.ts +978 -0
  172. package/src/main/audio/audioUtils.ts +100 -0
  173. package/src/main/audio/index.ts +20 -0
  174. package/src/main/capture/index.ts +1 -0
  175. package/src/main/index.ts +1693 -0
  176. package/src/main/ipc/captureHandlers.ts +272 -0
  177. package/src/main/ipc/index.ts +45 -0
  178. package/src/main/ipc/outputHandlers.ts +302 -0
  179. package/src/main/ipc/sessionHandlers.ts +56 -0
  180. package/src/main/ipc/settingsHandlers.ts +471 -0
  181. package/src/main/ipc/types.ts +56 -0
  182. package/src/main/ipc/windowHandlers.ts +277 -0
  183. package/src/main/output/ClipboardService.ts +369 -0
  184. package/src/main/output/ExportService.ts +539 -0
  185. package/src/main/output/FileManager.ts +416 -0
  186. package/src/main/output/MarkdownGenerator.ts +791 -0
  187. package/src/main/output/MarkdownPatcher.ts +299 -0
  188. package/src/main/output/index.ts +186 -0
  189. package/src/main/output/sessionAdapter.ts +207 -0
  190. package/src/main/output/templates/html-template.ts +553 -0
  191. package/src/main/pipeline/FrameExtractor.ts +330 -0
  192. package/src/main/pipeline/PostProcessor.ts +399 -0
  193. package/src/main/pipeline/TranscriptAnalyzer.ts +226 -0
  194. package/src/main/pipeline/index.ts +36 -0
  195. package/src/main/platform/WindowsTaskbar.ts +600 -0
  196. package/src/main/platform/index.ts +16 -0
  197. package/src/main/settings/SettingsManager.ts +730 -0
  198. package/src/main/settings/index.ts +19 -0
  199. package/src/main/transcription/ModelDownloadManager.ts +494 -0
  200. package/src/main/transcription/TierManager.ts +219 -0
  201. package/src/main/transcription/TranscriptionRecoveryService.ts +340 -0
  202. package/src/main/transcription/WhisperService.ts +748 -0
  203. package/src/main/transcription/index.ts +56 -0
  204. package/src/main/transcription/types.ts +135 -0
  205. package/src/main/windows/PopoverManager.ts +284 -0
  206. package/src/main/windows/TaskbarIntegration.ts +452 -0
  207. package/src/main/windows/index.ts +23 -0
  208. package/src/preload/index.ts +1047 -0
  209. package/src/renderer/App.tsx +515 -0
  210. package/src/renderer/AppWrapper.tsx +28 -0
  211. package/src/renderer/assets/logo-dark.svg +7 -0
  212. package/src/renderer/assets/logo.svg +7 -0
  213. package/src/renderer/audio/AudioCaptureRenderer.ts +454 -0
  214. package/src/renderer/capture/ScreenRecordingRenderer.ts +492 -0
  215. package/src/renderer/components/AnnotationOverlay.tsx +836 -0
  216. package/src/renderer/components/AudioWaveform.tsx +811 -0
  217. package/src/renderer/components/ClarificationQuestions.tsx +656 -0
  218. package/src/renderer/components/CountdownTimer.tsx +495 -0
  219. package/src/renderer/components/CrashRecoveryDialog.tsx +632 -0
  220. package/src/renderer/components/DonateButton.tsx +127 -0
  221. package/src/renderer/components/ErrorBoundary.tsx +308 -0
  222. package/src/renderer/components/ExportDialog.tsx +872 -0
  223. package/src/renderer/components/HotkeyHint.tsx +261 -0
  224. package/src/renderer/components/KeyboardShortcuts.tsx +787 -0
  225. package/src/renderer/components/ModelDownloadDialog.tsx +844 -0
  226. package/src/renderer/components/Onboarding.tsx +1830 -0
  227. package/src/renderer/components/ProcessingOverlay.tsx +157 -0
  228. package/src/renderer/components/RecordingOverlay.tsx +423 -0
  229. package/src/renderer/components/SessionHistory.tsx +1746 -0
  230. package/src/renderer/components/SessionReview.tsx +1321 -0
  231. package/src/renderer/components/SettingsPanel.tsx +217 -0
  232. package/src/renderer/components/Skeleton.tsx +347 -0
  233. package/src/renderer/components/StatusIndicator.tsx +86 -0
  234. package/src/renderer/components/ThemeProvider.tsx +429 -0
  235. package/src/renderer/components/Tooltip.tsx +370 -0
  236. package/src/renderer/components/TranscriptionPreview.tsx +183 -0
  237. package/src/renderer/components/TranscriptionTierSelector.tsx +640 -0
  238. package/src/renderer/components/UpdateNotification.tsx +377 -0
  239. package/src/renderer/components/WindowSelector.tsx +947 -0
  240. package/src/renderer/components/index.ts +99 -0
  241. package/src/renderer/components/primitives/ApiKeyInput.tsx +98 -0
  242. package/src/renderer/components/primitives/ColorPicker.tsx +65 -0
  243. package/src/renderer/components/primitives/DangerButton.tsx +45 -0
  244. package/src/renderer/components/primitives/DirectoryPicker.tsx +41 -0
  245. package/src/renderer/components/primitives/Dropdown.tsx +34 -0
  246. package/src/renderer/components/primitives/KeyRecorder.tsx +117 -0
  247. package/src/renderer/components/primitives/SettingsSection.tsx +32 -0
  248. package/src/renderer/components/primitives/Slider.tsx +43 -0
  249. package/src/renderer/components/primitives/Toggle.tsx +36 -0
  250. package/src/renderer/components/primitives/index.ts +10 -0
  251. package/src/renderer/components/settings/AdvancedTab.tsx +174 -0
  252. package/src/renderer/components/settings/AppearanceTab.tsx +77 -0
  253. package/src/renderer/components/settings/GeneralTab.tsx +40 -0
  254. package/src/renderer/components/settings/HotkeysTab.tsx +79 -0
  255. package/src/renderer/components/settings/RecordingTab.tsx +84 -0
  256. package/src/renderer/components/settings/index.ts +9 -0
  257. package/src/renderer/components/settings/settingsStyles.ts +673 -0
  258. package/src/renderer/components/settings/tabConfig.tsx +85 -0
  259. package/src/renderer/components/settings/useSettingsPanel.ts +447 -0
  260. package/src/renderer/contexts/ProcessingContext.tsx +227 -0
  261. package/src/renderer/contexts/RecordingContext.tsx +683 -0
  262. package/src/renderer/contexts/UIContext.tsx +326 -0
  263. package/src/renderer/contexts/index.ts +24 -0
  264. package/src/renderer/donateMessages.ts +69 -0
  265. package/src/renderer/hooks/index.ts +75 -0
  266. package/src/renderer/hooks/useAnimation.tsx +544 -0
  267. package/src/renderer/hooks/useTheme.ts +313 -0
  268. package/src/renderer/index.html +26 -0
  269. package/src/renderer/main.tsx +52 -0
  270. package/src/renderer/styles/animations.css +1093 -0
  271. package/src/renderer/styles/app-shell.css +662 -0
  272. package/src/renderer/styles/globals.css +515 -0
  273. package/src/renderer/styles/theme.ts +578 -0
  274. package/src/renderer/types/electron.d.ts +385 -0
  275. package/src/shared/hotkeys.ts +283 -0
  276. package/src/shared/types.ts +809 -0
  277. package/tests/clipboard.test.ts +228 -0
  278. package/tests/e2e/criticalPaths.test.ts +594 -0
  279. package/tests/feedbackAnalyzer.test.ts +303 -0
  280. package/tests/integration/sessionFlow.test.ts +583 -0
  281. package/tests/markdownGenerator.test.ts +418 -0
  282. package/tests/output.test.ts +96 -0
  283. package/tests/setup.ts +486 -0
  284. package/tests/unit/appIntegration.test.ts +676 -0
  285. package/tests/unit/appViewState.test.ts +281 -0
  286. package/tests/unit/audioIpcChannels.test.ts +17 -0
  287. package/tests/unit/exportService.test.ts +492 -0
  288. package/tests/unit/hotkeys.test.ts +92 -0
  289. package/tests/unit/navigationPreload.test.ts +94 -0
  290. package/tests/unit/onboardingFlow.test.ts +345 -0
  291. package/tests/unit/permissionManager.test.ts +175 -0
  292. package/tests/unit/permissionManagerExpanded.test.ts +296 -0
  293. package/tests/unit/screenRecordingRenderer.test.ts +368 -0
  294. package/tests/unit/sessionController.test.ts +515 -0
  295. package/tests/unit/tierManager.test.ts +61 -0
  296. package/tests/unit/tierManagerExpanded.test.ts +142 -0
  297. package/tests/unit/transcriptAnalyzer.test.ts +64 -0
  298. package/tsconfig.json +25 -0
  299. package/vitest.config.ts +46 -0
@@ -0,0 +1,418 @@
1
+ /**
2
+ * MarkdownGenerator Tests
3
+ *
4
+ * Tests for the llms.txt-inspired markdown output generator.
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach } from 'vitest';
8
+ import {
9
+ MarkdownGenerator,
10
+ markdownGenerator,
11
+ adaptFeedbackSession,
12
+ type Session,
13
+ type FeedbackItem,
14
+ type GenerateOptions,
15
+ } from '../src/main/output/MarkdownGenerator';
16
+ import type { FeedbackSession, Screenshot, TranscriptionSegment } from '../src/shared/types';
17
+
18
+ describe('MarkdownGenerator', () => {
19
+ let generator: MarkdownGenerator;
20
+
21
+ beforeEach(() => {
22
+ generator = new MarkdownGenerator();
23
+ });
24
+
25
+ describe('generateFeedbackItemId', () => {
26
+ it('should generate FB-001 format IDs', () => {
27
+ expect(generator.generateFeedbackItemId(0)).toBe('FB-001');
28
+ expect(generator.generateFeedbackItemId(1)).toBe('FB-002');
29
+ expect(generator.generateFeedbackItemId(9)).toBe('FB-010');
30
+ expect(generator.generateFeedbackItemId(99)).toBe('FB-100');
31
+ });
32
+ });
33
+
34
+ describe('generateFullDocument', () => {
35
+ const mockSession: Session = {
36
+ id: 'test-session-123',
37
+ startTime: new Date('2024-01-15T10:00:00').getTime(),
38
+ endTime: new Date('2024-01-15T10:05:30').getTime(),
39
+ feedbackItems: [
40
+ {
41
+ id: 'item-1',
42
+ transcription: 'The button is broken and does not submit the form.',
43
+ timestamp: new Date('2024-01-15T10:00:30').getTime(),
44
+ screenshots: [
45
+ {
46
+ id: 'ss-1',
47
+ timestamp: new Date('2024-01-15T10:00:35').getTime(),
48
+ imagePath: '/tmp/ss-1.png',
49
+ width: 1920,
50
+ height: 1080,
51
+ },
52
+ ],
53
+ category: 'Bug',
54
+ },
55
+ {
56
+ id: 'item-2',
57
+ transcription: 'This form is confusing. The labels are unclear.',
58
+ timestamp: new Date('2024-01-15T10:02:00').getTime(),
59
+ screenshots: [],
60
+ category: 'UX Issue',
61
+ },
62
+ {
63
+ id: 'item-3',
64
+ transcription: 'It would be nice if there was a dark mode option.',
65
+ timestamp: new Date('2024-01-15T10:04:00').getTime(),
66
+ screenshots: [
67
+ {
68
+ id: 'ss-2',
69
+ timestamp: new Date('2024-01-15T10:04:05').getTime(),
70
+ imagePath: '/tmp/ss-2.png',
71
+ width: 1920,
72
+ height: 1080,
73
+ },
74
+ {
75
+ id: 'ss-3',
76
+ timestamp: new Date('2024-01-15T10:04:10').getTime(),
77
+ imagePath: '/tmp/ss-3.png',
78
+ width: 1920,
79
+ height: 1080,
80
+ },
81
+ ],
82
+ category: 'Suggestion',
83
+ },
84
+ ],
85
+ metadata: {
86
+ os: 'darwin',
87
+ sourceName: 'TestApp',
88
+ sourceType: 'window',
89
+ },
90
+ };
91
+
92
+ const defaultOptions: GenerateOptions = {
93
+ projectName: 'TestApp',
94
+ screenshotDir: './screenshots',
95
+ };
96
+
97
+ it('should generate a complete markdown document', () => {
98
+ const result = generator.generateFullDocument(mockSession, defaultOptions);
99
+
100
+ expect(result.content).toContain('# TestApp Feedback Report');
101
+ expect(result.content).toContain('Duration: 5:30');
102
+ expect(result.content).toContain('Items: 3');
103
+ expect(result.content).toContain('Screenshots: 3');
104
+ });
105
+
106
+ it('should include session context', () => {
107
+ const result = generator.generateFullDocument(mockSession, defaultOptions);
108
+
109
+ expect(result.content).toContain('**Session ID:** `test-session-123`');
110
+ expect(result.content).toContain('darwin');
111
+ expect(result.content).toContain('TestApp');
112
+ });
113
+
114
+ it('should format feedback items with FB-XXX IDs', () => {
115
+ const result = generator.generateFullDocument(mockSession, defaultOptions);
116
+
117
+ expect(result.content).toContain('### FB-001:');
118
+ expect(result.content).toContain('### FB-002:');
119
+ expect(result.content).toContain('### FB-003:');
120
+ });
121
+
122
+ it('should include transcription in blockquote', () => {
123
+ const result = generator.generateFullDocument(mockSession, defaultOptions);
124
+
125
+ expect(result.content).toContain('> The button is broken');
126
+ });
127
+
128
+ it('should include category/type for each item', () => {
129
+ const result = generator.generateFullDocument(mockSession, defaultOptions);
130
+
131
+ expect(result.content).toContain('**Type:** Bug');
132
+ expect(result.content).toContain('**Type:** UX Issue');
133
+ expect(result.content).toContain('**Type:** Suggestion');
134
+ });
135
+
136
+ it('should include screenshot references', () => {
137
+ const result = generator.generateFullDocument(mockSession, defaultOptions);
138
+
139
+ expect(result.content).toContain('![FB-001](./screenshots/fb-001.png)');
140
+ expect(result.content).toContain('![FB-003-1](./screenshots/fb-003-1.png)');
141
+ expect(result.content).toContain('![FB-003-2](./screenshots/fb-003-2.png)');
142
+ });
143
+
144
+ it('should include summary table', () => {
145
+ const result = generator.generateFullDocument(mockSession, defaultOptions);
146
+
147
+ expect(result.content).toContain('## Summary');
148
+ expect(result.content).toContain('| Bug | 1 |');
149
+ expect(result.content).toContain('| UX Issue | 1 |');
150
+ expect(result.content).toContain('| Suggestion | 1 |');
151
+ expect(result.content).toContain('| **Total** | **3** |');
152
+ });
153
+
154
+ it('should generate correct filename', () => {
155
+ const result = generator.generateFullDocument(mockSession, defaultOptions);
156
+
157
+ expect(result.filename).toMatch(/^testapp-feedback-\d{8}-\d{6}\.md$/);
158
+ });
159
+
160
+ it('should return correct metadata', () => {
161
+ const result = generator.generateFullDocument(mockSession, defaultOptions);
162
+
163
+ expect(result.metadata.itemCount).toBe(3);
164
+ expect(result.metadata.screenshotCount).toBe(3);
165
+ expect(result.metadata.duration).toBe(5.5 * 60 * 1000); // 5:30 in ms
166
+ expect(result.metadata.types).toEqual({
167
+ Bug: 1,
168
+ 'UX Issue': 1,
169
+ Suggestion: 1,
170
+ });
171
+ });
172
+
173
+ it('should handle empty session', () => {
174
+ const emptySession: Session = {
175
+ id: 'empty-session',
176
+ startTime: Date.now(),
177
+ endTime: Date.now() + 1000,
178
+ feedbackItems: [],
179
+ };
180
+
181
+ const result = generator.generateFullDocument(emptySession, defaultOptions);
182
+
183
+ expect(result.content).toContain('# TestApp Feedback Report');
184
+ expect(result.content).toContain('_No feedback items were captured during this session._');
185
+ expect(result.content).not.toContain('## Actionable Feedback');
186
+ expect(result.metadata.itemCount).toBe(0);
187
+ expect(result.metadata.screenshotCount).toBe(0);
188
+ });
189
+
190
+ it('should handle special characters in project name', () => {
191
+ const options = {
192
+ projectName: "Eddie's App (Beta)",
193
+ screenshotDir: './screenshots',
194
+ };
195
+
196
+ const result = generator.generateFullDocument(mockSession, options);
197
+
198
+ expect(result.content).toContain("# Eddie's App (Beta) Feedback Report");
199
+ // Special chars get converted to dashes, consecutive dashes get collapsed
200
+ expect(result.filename).toMatch(/^eddie-s-app-beta--feedback-/);
201
+ });
202
+ });
203
+
204
+ describe('generateClipboardSummary', () => {
205
+ const mockSession: Session = {
206
+ id: 'test-session',
207
+ startTime: Date.now() - 60000,
208
+ endTime: Date.now(),
209
+ feedbackItems: [
210
+ {
211
+ id: '1',
212
+ transcription: 'First feedback item about a bug.',
213
+ timestamp: Date.now() - 50000,
214
+ screenshots: [],
215
+ category: 'Bug',
216
+ },
217
+ {
218
+ id: '2',
219
+ transcription: 'Second feedback item about UX.',
220
+ timestamp: Date.now() - 40000,
221
+ screenshots: [],
222
+ category: 'UX Issue',
223
+ },
224
+ {
225
+ id: '3',
226
+ transcription: 'Third feedback item suggestion.',
227
+ timestamp: Date.now() - 30000,
228
+ screenshots: [],
229
+ category: 'Suggestion',
230
+ },
231
+ {
232
+ id: '4',
233
+ transcription: 'Fourth feedback item question.',
234
+ timestamp: Date.now() - 20000,
235
+ screenshots: [],
236
+ category: 'Question',
237
+ },
238
+ ],
239
+ metadata: {
240
+ sourceName: 'MyApp',
241
+ },
242
+ };
243
+
244
+ it('should generate summary under 1500 chars', () => {
245
+ const summary = generator.generateClipboardSummary(mockSession, 'MyApp');
246
+
247
+ expect(summary.length).toBeLessThanOrEqual(1500);
248
+ });
249
+
250
+ it('should include project name and item count', () => {
251
+ const summary = generator.generateClipboardSummary(mockSession, 'MyApp');
252
+
253
+ expect(summary).toContain('# Feedback: MyApp - 4 items');
254
+ });
255
+
256
+ it('should show first 3 items as priority', () => {
257
+ const summary = generator.generateClipboardSummary(mockSession, 'MyApp');
258
+
259
+ expect(summary).toContain('## Priority Items');
260
+ expect(summary).toContain('FB-001');
261
+ expect(summary).toContain('FB-002');
262
+ expect(summary).toContain('FB-003');
263
+ });
264
+
265
+ it('should reference remaining items', () => {
266
+ const summary = generator.generateClipboardSummary(mockSession, 'MyApp');
267
+
268
+ expect(summary).toContain('## Other');
269
+ expect(summary).toContain('FB-004');
270
+ expect(summary).toContain('see full report');
271
+ });
272
+
273
+ it('should include link to full report', () => {
274
+ const summary = generator.generateClipboardSummary(mockSession, 'MyApp');
275
+
276
+ expect(summary).toContain('**Full report:** ./feedback-report.md');
277
+ });
278
+
279
+ it('should use sourceName when projectName not provided', () => {
280
+ const summary = generator.generateClipboardSummary(mockSession);
281
+
282
+ expect(summary).toContain('# Feedback: MyApp');
283
+ });
284
+ });
285
+
286
+ describe('singleton export', () => {
287
+ it('should export a singleton instance', () => {
288
+ expect(markdownGenerator).toBeDefined();
289
+ expect(markdownGenerator.generateFeedbackItemId(0)).toBe('FB-001');
290
+ });
291
+ });
292
+ });
293
+
294
+ describe('adaptFeedbackSession', () => {
295
+ it('should convert FeedbackSession to Session', () => {
296
+ const feedbackSession: FeedbackSession = {
297
+ id: 'test-123',
298
+ startedAt: Date.now() - 60000,
299
+ endedAt: Date.now(),
300
+ status: 'complete',
301
+ screenshots: [
302
+ {
303
+ id: 'ss-1',
304
+ timestamp: Date.now() - 30000,
305
+ imagePath: '/tmp/ss1.png',
306
+ width: 1920,
307
+ height: 1080,
308
+ },
309
+ ],
310
+ transcription: [
311
+ {
312
+ id: 'seg-1',
313
+ text: 'This is broken.',
314
+ startTime: Date.now() - 50000,
315
+ endTime: Date.now() - 45000,
316
+ confidence: 0.95,
317
+ isFinal: true,
318
+ },
319
+ {
320
+ id: 'seg-2',
321
+ text: 'It should work better.',
322
+ startTime: Date.now() - 44000,
323
+ endTime: Date.now() - 40000,
324
+ confidence: 0.92,
325
+ isFinal: true,
326
+ },
327
+ ],
328
+ };
329
+
330
+ const session = adaptFeedbackSession(feedbackSession);
331
+
332
+ expect(session.id).toBe('test-123');
333
+ expect(session.startTime).toBe(feedbackSession.startedAt);
334
+ expect(session.endTime).toBe(feedbackSession.endedAt);
335
+ expect(session.feedbackItems.length).toBeGreaterThan(0);
336
+ });
337
+
338
+ it('should group transcription segments by pause threshold', () => {
339
+ const now = Date.now();
340
+ const feedbackSession: FeedbackSession = {
341
+ id: 'test-456',
342
+ startedAt: now - 60000,
343
+ status: 'complete',
344
+ screenshots: [],
345
+ transcription: [
346
+ {
347
+ id: 'seg-1',
348
+ text: 'First thought.',
349
+ startTime: now - 50000,
350
+ endTime: now - 48000,
351
+ confidence: 0.95,
352
+ isFinal: true,
353
+ },
354
+ {
355
+ id: 'seg-2',
356
+ text: 'Still first thought.',
357
+ startTime: now - 47000, // Close to previous - same item
358
+ endTime: now - 45000,
359
+ confidence: 0.95,
360
+ isFinal: true,
361
+ },
362
+ {
363
+ id: 'seg-3',
364
+ text: 'New thought after pause.',
365
+ startTime: now - 30000, // Big gap - new item
366
+ endTime: now - 28000,
367
+ confidence: 0.95,
368
+ isFinal: true,
369
+ },
370
+ ],
371
+ };
372
+
373
+ const session = adaptFeedbackSession(feedbackSession, { pauseThresholdMs: 5000 });
374
+
375
+ expect(session.feedbackItems.length).toBe(2);
376
+ expect(session.feedbackItems[0].transcription).toContain('First thought');
377
+ expect(session.feedbackItems[0].transcription).toContain('Still first thought');
378
+ expect(session.feedbackItems[1].transcription).toContain('New thought');
379
+ });
380
+
381
+ it('should infer category from transcription text', () => {
382
+ const now = Date.now();
383
+ const feedbackSession: FeedbackSession = {
384
+ id: 'test-789',
385
+ startedAt: now - 60000,
386
+ status: 'complete',
387
+ screenshots: [],
388
+ transcription: [
389
+ {
390
+ id: 'seg-1',
391
+ text: 'This feature is broken and crashes.',
392
+ startTime: now - 50000,
393
+ endTime: now - 48000,
394
+ confidence: 0.95,
395
+ isFinal: true,
396
+ },
397
+ ],
398
+ };
399
+
400
+ const session = adaptFeedbackSession(feedbackSession);
401
+
402
+ expect(session.feedbackItems[0].category).toBe('Bug');
403
+ });
404
+
405
+ it('should handle empty transcription', () => {
406
+ const feedbackSession: FeedbackSession = {
407
+ id: 'empty-test',
408
+ startedAt: Date.now(),
409
+ status: 'complete',
410
+ screenshots: [],
411
+ transcription: [],
412
+ };
413
+
414
+ const session = adaptFeedbackSession(feedbackSession);
415
+
416
+ expect(session.feedbackItems).toHaveLength(0);
417
+ });
418
+ });
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Output Module Tests
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import type { FeedbackSession, Screenshot, TranscriptionSegment } from '../src/shared/types';
7
+
8
+ // Mock OutputManager for testing without Electron dependencies
9
+ class MockOutputManager {
10
+ generateMarkdown(session: FeedbackSession) {
11
+ const { id, screenshots, transcription, startedAt, endedAt } = session;
12
+
13
+ let markdown = '# Feedback Session\n\n';
14
+ markdown += `**Session ID:** ${id}\n`;
15
+ markdown += `**Started:** ${new Date(startedAt).toISOString()}\n`;
16
+ if (endedAt) {
17
+ markdown += `**Ended:** ${new Date(endedAt).toISOString()}\n`;
18
+ }
19
+ markdown += '\n---\n\n';
20
+
21
+ // Add transcription
22
+ for (const segment of transcription) {
23
+ markdown += `${segment.text}\n\n`;
24
+ }
25
+
26
+ // Add screenshots
27
+ for (const screenshot of screenshots) {
28
+ markdown += `![Screenshot](${screenshot.base64 || screenshot.imagePath})\n\n`;
29
+ }
30
+
31
+ return {
32
+ sessionId: id,
33
+ generatedAt: Date.now(),
34
+ markdown,
35
+ screenshots,
36
+ };
37
+ }
38
+ }
39
+
40
+ describe('OutputManager', () => {
41
+ it('should generate markdown from a session', () => {
42
+ const manager = new MockOutputManager();
43
+
44
+ const session: FeedbackSession = {
45
+ id: 'test-session-1',
46
+ startedAt: Date.now() - 60000,
47
+ endedAt: Date.now(),
48
+ status: 'complete',
49
+ screenshots: [
50
+ {
51
+ id: 'screenshot-1',
52
+ timestamp: Date.now() - 30000,
53
+ imagePath: '/tmp/screenshot-1.png',
54
+ width: 1920,
55
+ height: 1080,
56
+ },
57
+ ],
58
+ transcription: [
59
+ {
60
+ id: 'segment-1',
61
+ text: 'This is a test transcription.',
62
+ startTime: Date.now() - 45000,
63
+ endTime: Date.now() - 35000,
64
+ confidence: 0.95,
65
+ isFinal: true,
66
+ },
67
+ ],
68
+ };
69
+
70
+ const output = manager.generateMarkdown(session);
71
+
72
+ expect(output.sessionId).toBe('test-session-1');
73
+ expect(output.markdown).toContain('# Feedback Session');
74
+ expect(output.markdown).toContain('test-session-1');
75
+ expect(output.markdown).toContain('This is a test transcription.');
76
+ expect(output.markdown).toContain('![Screenshot]');
77
+ });
78
+
79
+ it('should handle empty sessions', () => {
80
+ const manager = new MockOutputManager();
81
+
82
+ const session: FeedbackSession = {
83
+ id: 'empty-session',
84
+ startedAt: Date.now(),
85
+ status: 'complete',
86
+ screenshots: [],
87
+ transcription: [],
88
+ };
89
+
90
+ const output = manager.generateMarkdown(session);
91
+
92
+ expect(output.sessionId).toBe('empty-session');
93
+ expect(output.markdown).toContain('# Feedback Session');
94
+ expect(output.screenshots).toHaveLength(0);
95
+ });
96
+ });