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,594 @@
1
+ /**
2
+ * End-to-End Critical Path Tests
3
+ *
4
+ * Tests the most critical user journeys through markupr:
5
+ * - Recording session lifecycle
6
+ * - Output generation flow
7
+ * - Clipboard summary flow
8
+ *
9
+ * These tests use mocked services but simulate real user interactions.
10
+ */
11
+
12
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
13
+ import { EventEmitter } from 'events';
14
+
15
+ // =============================================================================
16
+ // Mocks
17
+ // =============================================================================
18
+
19
+ vi.mock('electron', () => ({
20
+ app: {
21
+ getPath: vi.fn(() => '/tmp'),
22
+ getName: vi.fn(() => 'markupr'),
23
+ getVersion: vi.fn(() => '0.4.0'),
24
+ },
25
+ clipboard: {
26
+ writeText: vi.fn(),
27
+ },
28
+ Notification: {
29
+ isSupported: vi.fn(() => false),
30
+ },
31
+ BrowserWindow: {
32
+ getAllWindows: vi.fn(() => []),
33
+ },
34
+ shell: {
35
+ openPath: vi.fn(() => Promise.resolve('')),
36
+ },
37
+ }));
38
+
39
+ vi.mock('electron-store', () => ({
40
+ default: vi.fn().mockImplementation(() => ({
41
+ get: vi.fn((key: string, defaultValue?: unknown) => defaultValue),
42
+ set: vi.fn(),
43
+ delete: vi.fn(),
44
+ clear: vi.fn(),
45
+ })),
46
+ }));
47
+
48
+ // =============================================================================
49
+ // E2E Test Application Simulation
50
+ // =============================================================================
51
+
52
+ interface SimulatedSession {
53
+ id: string;
54
+ startTime: number;
55
+ endTime?: number;
56
+ feedbackItems: Array<{
57
+ id: string;
58
+ timestamp: number;
59
+ text: string;
60
+ screenshot?: {
61
+ id: string;
62
+ width: number;
63
+ height: number;
64
+ imagePath: string;
65
+ };
66
+ category?: string;
67
+ }>;
68
+ metadata: {
69
+ sourceName: string;
70
+ sourceType: 'screen' | 'window';
71
+ os: string;
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Simulates the complete markupr application for E2E testing
77
+ */
78
+ class markuprSimulator {
79
+ private state: 'idle' | 'recording' | 'processing' | 'complete' = 'idle';
80
+ private currentSession: SimulatedSession | null = null;
81
+ private events = new EventEmitter();
82
+ private outputFiles: Map<string, string> = new Map();
83
+ private clipboardContent: string = '';
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // User Actions
87
+ // ---------------------------------------------------------------------------
88
+
89
+ /**
90
+ * Simulate user starting a recording session
91
+ */
92
+ async userStartsRecording(sourceName: string = 'Test Window'): Promise<void> {
93
+ if (this.state !== 'idle') {
94
+ throw new Error('Already recording');
95
+ }
96
+
97
+ this.currentSession = {
98
+ id: `session-${Date.now()}`,
99
+ startTime: Date.now(),
100
+ feedbackItems: [],
101
+ metadata: {
102
+ sourceName,
103
+ sourceType: 'window',
104
+ os: process.platform,
105
+ },
106
+ };
107
+
108
+ this.state = 'recording';
109
+ this.events.emit('recording:started', this.currentSession);
110
+ }
111
+
112
+ /**
113
+ * Simulate user speaking feedback
114
+ */
115
+ userSpeaksFeedback(text: string, category?: string): void {
116
+ if (this.state !== 'recording' || !this.currentSession) {
117
+ throw new Error('Not recording');
118
+ }
119
+
120
+ const item = {
121
+ id: `fb-${this.currentSession.feedbackItems.length + 1}`,
122
+ timestamp: Date.now(),
123
+ text,
124
+ screenshot: {
125
+ id: `ss-${this.currentSession.feedbackItems.length + 1}`,
126
+ width: 1920,
127
+ height: 1080,
128
+ imagePath: `/tmp/screenshot-${this.currentSession.feedbackItems.length + 1}.png`,
129
+ },
130
+ category: category || this.inferCategory(text),
131
+ };
132
+
133
+ this.currentSession.feedbackItems.push(item);
134
+ this.events.emit('feedback:captured', item);
135
+ }
136
+
137
+ /**
138
+ * Simulate user stopping the recording
139
+ */
140
+ async userStopsRecording(): Promise<SimulatedSession> {
141
+ if (this.state !== 'recording' || !this.currentSession) {
142
+ throw new Error('Not recording');
143
+ }
144
+
145
+ this.state = 'processing';
146
+ this.events.emit('recording:processing');
147
+
148
+ // Simulate processing delay
149
+ await new Promise((r) => setTimeout(r, 10));
150
+
151
+ this.currentSession.endTime = Date.now();
152
+ this.state = 'complete';
153
+ this.events.emit('recording:complete', this.currentSession);
154
+
155
+ return { ...this.currentSession };
156
+ }
157
+
158
+ /**
159
+ * Simulate user saving the session
160
+ */
161
+ async userSavesSession(format: 'markdown' | 'pdf' | 'html' | 'json' = 'markdown'): Promise<string> {
162
+ if (!this.currentSession) {
163
+ throw new Error('No session to save');
164
+ }
165
+
166
+ const filename = this.generateFilename(format);
167
+ const content = this.generateOutput(format);
168
+
169
+ this.outputFiles.set(filename, content);
170
+ this.events.emit('session:saved', { filename, format });
171
+
172
+ return filename;
173
+ }
174
+
175
+ /**
176
+ * Simulate user copying summary to clipboard
177
+ */
178
+ async userCopiesSummary(): Promise<string> {
179
+ if (!this.currentSession) {
180
+ throw new Error('No session');
181
+ }
182
+
183
+ const summary = this.generateClipboardSummary();
184
+ this.clipboardContent = summary;
185
+ this.events.emit('clipboard:copied', summary);
186
+
187
+ return summary;
188
+ }
189
+
190
+ /**
191
+ * Simulate user discarding the session
192
+ */
193
+ userDiscardsSession(): void {
194
+ this.currentSession = null;
195
+ this.state = 'idle';
196
+ this.events.emit('session:discarded');
197
+ }
198
+
199
+ // ---------------------------------------------------------------------------
200
+ // State Access
201
+ // ---------------------------------------------------------------------------
202
+
203
+ getState() {
204
+ return this.state;
205
+ }
206
+
207
+ getCurrentSession() {
208
+ return this.currentSession;
209
+ }
210
+
211
+ getOutputFiles() {
212
+ return new Map(this.outputFiles);
213
+ }
214
+
215
+ getClipboardContent() {
216
+ return this.clipboardContent;
217
+ }
218
+
219
+ // ---------------------------------------------------------------------------
220
+ // Event Subscriptions
221
+ // ---------------------------------------------------------------------------
222
+
223
+ on(event: string, callback: (...args: unknown[]) => void): () => void {
224
+ this.events.on(event, callback);
225
+ return () => this.events.off(event, callback);
226
+ }
227
+
228
+ // ---------------------------------------------------------------------------
229
+ // Internal Helpers
230
+ // ---------------------------------------------------------------------------
231
+
232
+ private inferCategory(text: string): string {
233
+ const lowerText = text.toLowerCase();
234
+
235
+ if (lowerText.includes('bug') || lowerText.includes('broken') || lowerText.includes('crash')) {
236
+ return 'Bug';
237
+ }
238
+ if (lowerText.includes('confusing') || lowerText.includes('hard to')) {
239
+ return 'UX Issue';
240
+ }
241
+ if (lowerText.includes('should') || lowerText.includes('would be nice')) {
242
+ return 'Suggestion';
243
+ }
244
+ if (lowerText.includes('?')) {
245
+ return 'Question';
246
+ }
247
+ return 'General';
248
+ }
249
+
250
+ private generateFilename(format: string): string {
251
+ const name = (this.currentSession?.metadata.sourceName || 'feedback')
252
+ .toLowerCase()
253
+ .replace(/[^a-z0-9]/g, '-');
254
+
255
+ const date = new Date(this.currentSession?.startTime || Date.now());
256
+ const dateStr = date.toISOString().split('T')[0].replace(/-/g, '');
257
+
258
+ const extensions: Record<string, string> = {
259
+ markdown: 'md',
260
+ pdf: 'pdf',
261
+ html: 'html',
262
+ json: 'json',
263
+ };
264
+
265
+ return `${name}-feedback-${dateStr}.${extensions[format]}`;
266
+ }
267
+
268
+ private generateOutput(format: string): string {
269
+ if (!this.currentSession) return '';
270
+
271
+ switch (format) {
272
+ case 'json':
273
+ return JSON.stringify({
274
+ session: this.currentSession,
275
+ exportedAt: new Date().toISOString(),
276
+ }, null, 2);
277
+
278
+ case 'html':
279
+ return `<!DOCTYPE html>
280
+ <html>
281
+ <head><title>${this.currentSession.metadata.sourceName} Feedback</title></head>
282
+ <body>
283
+ <h1>${this.currentSession.metadata.sourceName} Feedback Report</h1>
284
+ <p>Items: ${this.currentSession.feedbackItems.length}</p>
285
+ ${this.currentSession.feedbackItems.map((item, i) => `
286
+ <h2>FB-${String(i + 1).padStart(3, '0')}: ${item.text.slice(0, 50)}</h2>
287
+ <p>${item.text}</p>
288
+ `).join('')}
289
+ </body>
290
+ </html>`;
291
+
292
+ case 'markdown':
293
+ default:
294
+ let md = `# ${this.currentSession.metadata.sourceName} Feedback Report\n\n`;
295
+ md += `> Items: ${this.currentSession.feedbackItems.length}\n\n`;
296
+
297
+ this.currentSession.feedbackItems.forEach((item, i) => {
298
+ const id = `FB-${String(i + 1).padStart(3, '0')}`;
299
+ md += `### ${id}: ${item.text.slice(0, 50)}\n`;
300
+ md += `**Type:** ${item.category}\n\n`;
301
+ md += `> ${item.text}\n\n`;
302
+ if (item.screenshot) {
303
+ md += `![${id}](./screenshots/${id.toLowerCase()}.png)\n\n`;
304
+ }
305
+ md += `---\n\n`;
306
+ });
307
+
308
+ return md;
309
+ }
310
+ }
311
+
312
+ private generateClipboardSummary(): string {
313
+ if (!this.currentSession) return '';
314
+
315
+ const items = this.currentSession.feedbackItems;
316
+ let summary = `# Feedback: ${this.currentSession.metadata.sourceName} - ${items.length} items\n\n`;
317
+
318
+ summary += `## Priority Items\n`;
319
+ items.slice(0, 3).forEach((item, i) => {
320
+ const id = `FB-${String(i + 1).padStart(3, '0')}`;
321
+ summary += `- **${id}:** ${item.text.slice(0, 60)}...\n`;
322
+ });
323
+
324
+ if (items.length > 3) {
325
+ summary += `\n## Other\n`;
326
+ summary += `- ${items.length - 3} more items (see full report)\n`;
327
+ }
328
+
329
+ return summary;
330
+ }
331
+
332
+ reset(): void {
333
+ this.state = 'idle';
334
+ this.currentSession = null;
335
+ this.outputFiles.clear();
336
+ this.clipboardContent = '';
337
+ this.events.removeAllListeners();
338
+ }
339
+ }
340
+
341
+ // =============================================================================
342
+ // E2E Tests
343
+ // =============================================================================
344
+
345
+ describe('E2E: Critical User Paths', () => {
346
+ let app: markuprSimulator;
347
+
348
+ beforeEach(() => {
349
+ app = new markuprSimulator();
350
+ });
351
+
352
+ afterEach(() => {
353
+ app.reset();
354
+ });
355
+
356
+ describe('Recording Session Lifecycle', () => {
357
+ it('should complete a full recording session', async () => {
358
+ // User starts recording
359
+ await app.userStartsRecording('My Test App');
360
+ expect(app.getState()).toBe('recording');
361
+
362
+ // User provides feedback
363
+ app.userSpeaksFeedback('The save button is broken and crashes the app.');
364
+ app.userSpeaksFeedback('The navigation menu is confusing.');
365
+ app.userSpeaksFeedback('It would be nice to have dark mode.');
366
+
367
+ // User stops recording
368
+ const session = await app.userStopsRecording();
369
+
370
+ expect(app.getState()).toBe('complete');
371
+ expect(session.feedbackItems).toHaveLength(3);
372
+ expect(session.endTime).toBeDefined();
373
+ });
374
+
375
+ it('should track feedback items with correct categories', async () => {
376
+ await app.userStartsRecording('Test App');
377
+
378
+ app.userSpeaksFeedback('This is broken!'); // Bug
379
+ app.userSpeaksFeedback('This is confusing and hard to use.'); // UX Issue
380
+ app.userSpeaksFeedback('You should add a feature.'); // Suggestion
381
+ app.userSpeaksFeedback('How do I do this?'); // Question
382
+
383
+ const session = await app.userStopsRecording();
384
+
385
+ expect(session.feedbackItems[0].category).toBe('Bug');
386
+ expect(session.feedbackItems[1].category).toBe('UX Issue');
387
+ expect(session.feedbackItems[2].category).toBe('Suggestion');
388
+ expect(session.feedbackItems[3].category).toBe('Question');
389
+ });
390
+
391
+ it('should emit events throughout the session lifecycle', async () => {
392
+ const events: string[] = [];
393
+
394
+ app.on('recording:started', () => events.push('started'));
395
+ app.on('feedback:captured', () => events.push('feedback'));
396
+ app.on('recording:processing', () => events.push('processing'));
397
+ app.on('recording:complete', () => events.push('complete'));
398
+
399
+ await app.userStartsRecording();
400
+ app.userSpeaksFeedback('Test feedback');
401
+ await app.userStopsRecording();
402
+
403
+ expect(events).toEqual(['started', 'feedback', 'processing', 'complete']);
404
+ });
405
+
406
+ it('should allow discarding a session', async () => {
407
+ await app.userStartsRecording();
408
+ app.userSpeaksFeedback('Feedback to discard');
409
+
410
+ app.userDiscardsSession();
411
+
412
+ expect(app.getState()).toBe('idle');
413
+ expect(app.getCurrentSession()).toBeNull();
414
+ });
415
+ });
416
+
417
+ describe('Output Generation Flow', () => {
418
+ beforeEach(async () => {
419
+ await app.userStartsRecording('Test App');
420
+ app.userSpeaksFeedback('Bug: The save button crashes.');
421
+ app.userSpeaksFeedback('UX: Navigation is confusing.');
422
+ await app.userStopsRecording();
423
+ });
424
+
425
+ it('should save session as Markdown', async () => {
426
+ const filename = await app.userSavesSession('markdown');
427
+
428
+ expect(filename).toMatch(/\.md$/);
429
+
430
+ const files = app.getOutputFiles();
431
+ expect(files.has(filename)).toBe(true);
432
+
433
+ const content = files.get(filename)!;
434
+ expect(content).toContain('# Test App Feedback Report');
435
+ expect(content).toContain('FB-001');
436
+ expect(content).toContain('FB-002');
437
+ });
438
+
439
+ it('should save session as JSON', async () => {
440
+ const filename = await app.userSavesSession('json');
441
+
442
+ expect(filename).toMatch(/\.json$/);
443
+
444
+ const files = app.getOutputFiles();
445
+ const content = files.get(filename)!;
446
+ const parsed = JSON.parse(content);
447
+
448
+ expect(parsed.session.feedbackItems).toHaveLength(2);
449
+ expect(parsed.exportedAt).toBeDefined();
450
+ });
451
+
452
+ it('should save session as HTML', async () => {
453
+ const filename = await app.userSavesSession('html');
454
+
455
+ expect(filename).toMatch(/\.html$/);
456
+
457
+ const files = app.getOutputFiles();
458
+ const content = files.get(filename)!;
459
+
460
+ expect(content).toContain('<!DOCTYPE html>');
461
+ expect(content).toContain('Test App Feedback Report');
462
+ });
463
+
464
+ it('should emit save event', async () => {
465
+ const saveEvents: Array<{ filename: string; format: string }> = [];
466
+ app.on('session:saved', (data) => saveEvents.push(data as { filename: string; format: string }));
467
+
468
+ await app.userSavesSession('markdown');
469
+
470
+ expect(saveEvents).toHaveLength(1);
471
+ expect(saveEvents[0].format).toBe('markdown');
472
+ });
473
+ });
474
+
475
+ describe('Clipboard Summary Flow', () => {
476
+ beforeEach(async () => {
477
+ await app.userStartsRecording('My App');
478
+ app.userSpeaksFeedback('First priority feedback item about a critical bug.');
479
+ app.userSpeaksFeedback('Second priority feedback about UX issues.');
480
+ app.userSpeaksFeedback('Third priority suggestion for improvement.');
481
+ app.userSpeaksFeedback('Fourth item that should be in "other" section.');
482
+ await app.userStopsRecording();
483
+ });
484
+
485
+ it('should copy summary to clipboard', async () => {
486
+ const summary = await app.userCopiesSummary();
487
+
488
+ expect(summary).toContain('# Feedback: My App - 4 items');
489
+ expect(summary).toContain('## Priority Items');
490
+ expect(summary).toContain('FB-001');
491
+ expect(summary).toContain('FB-002');
492
+ expect(summary).toContain('FB-003');
493
+ });
494
+
495
+ it('should show remaining items in Other section', async () => {
496
+ const summary = await app.userCopiesSummary();
497
+
498
+ expect(summary).toContain('## Other');
499
+ expect(summary).toContain('1 more items');
500
+ });
501
+
502
+ it('should store summary in clipboard content', async () => {
503
+ await app.userCopiesSummary();
504
+
505
+ const clipboardContent = app.getClipboardContent();
506
+ expect(clipboardContent).toContain('# Feedback: My App');
507
+ });
508
+
509
+ it('should emit clipboard event', async () => {
510
+ let copiedContent = '';
511
+ app.on('clipboard:copied', (content) => { copiedContent = content as string; });
512
+
513
+ await app.userCopiesSummary();
514
+
515
+ expect(copiedContent).toContain('# Feedback:');
516
+ });
517
+ });
518
+
519
+ describe('Error Handling', () => {
520
+ it('should prevent starting recording when already recording', async () => {
521
+ await app.userStartsRecording();
522
+
523
+ await expect(app.userStartsRecording()).rejects.toThrow('Already recording');
524
+ });
525
+
526
+ it('should prevent speaking feedback when not recording', async () => {
527
+ expect(() => app.userSpeaksFeedback('Test')).toThrow('Not recording');
528
+ });
529
+
530
+ it('should prevent stopping when not recording', async () => {
531
+ await expect(app.userStopsRecording()).rejects.toThrow('Not recording');
532
+ });
533
+
534
+ it('should prevent saving when no session exists', async () => {
535
+ await expect(app.userSavesSession()).rejects.toThrow('No session to save');
536
+ });
537
+
538
+ it('should prevent copying summary when no session exists', async () => {
539
+ await expect(app.userCopiesSummary()).rejects.toThrow('No session');
540
+ });
541
+ });
542
+
543
+ describe('Real-World Scenario: Bug Report Session', () => {
544
+ it('should handle a realistic bug reporting session', async () => {
545
+ // User discovers a bug and starts markupr
546
+ await app.userStartsRecording('Acme Dashboard');
547
+
548
+ // User describes the bug
549
+ app.userSpeaksFeedback(
550
+ 'I found a critical bug. When I click the export button with more than 100 items selected, ' +
551
+ 'the entire application freezes and I have to force quit.'
552
+ );
553
+
554
+ // User provides steps to reproduce
555
+ app.userSpeaksFeedback(
556
+ 'To reproduce: First, go to the data table view. Then select all items using the checkbox. ' +
557
+ 'Finally, click the export button in the toolbar.'
558
+ );
559
+
560
+ // User mentions the expected behavior
561
+ app.userSpeaksFeedback(
562
+ 'I would expect the export to start processing in the background without freezing the UI.'
563
+ );
564
+
565
+ // User notes the severity
566
+ app.userSpeaksFeedback(
567
+ 'This is blocking my work because I need to export reports daily. Very urgent fix needed.'
568
+ );
569
+
570
+ // User stops recording
571
+ const session = await app.userStopsRecording();
572
+
573
+ // Verify session integrity
574
+ expect(session.feedbackItems).toHaveLength(4);
575
+ expect(session.feedbackItems[0].category).toBe('Bug');
576
+ expect(session.metadata.sourceName).toBe('Acme Dashboard');
577
+
578
+ // User saves the report
579
+ const filename = await app.userSavesSession('markdown');
580
+ const files = app.getOutputFiles();
581
+ const report = files.get(filename)!;
582
+
583
+ // Verify report contains all feedback
584
+ expect(report).toContain('critical bug');
585
+ expect(report).toContain('To reproduce');
586
+ expect(report).toContain('export'); // The word "expected" gets truncated in title, check full content
587
+ expect(report).toContain('blocking');
588
+
589
+ // User copies summary for quick sharing
590
+ const summary = await app.userCopiesSummary();
591
+ expect(summary).toContain('4 items');
592
+ });
593
+ });
594
+ });