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.
- package/.claude/commands/review-feedback.md +47 -0
- package/.eslintrc.json +35 -0
- package/.github/CODEOWNERS +16 -0
- package/.github/FUNDING.yml +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +56 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +54 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +89 -0
- package/.github/dependabot.yml +70 -0
- package/.github/workflows/ci.yml +184 -0
- package/.github/workflows/deploy-landing.yml +134 -0
- package/.github/workflows/nightly.yml +288 -0
- package/.github/workflows/release.yml +318 -0
- package/CHANGELOG.md +127 -0
- package/CLAUDE.md +137 -0
- package/CODE_OF_CONDUCT.md +9 -0
- package/CONTRIBUTING.md +390 -0
- package/LICENSE +21 -0
- package/PRODUCT_VISION.md +277 -0
- package/README.md +517 -0
- package/SECURITY.md +51 -0
- package/SIGNING_INSTRUCTIONS.md +284 -0
- package/assets/DMG_BACKGROUND_INSTRUCTIONS.md +130 -0
- package/assets/svg-source/dmg-background.svg +70 -0
- package/assets/svg-source/icon.svg +20 -0
- package/assets/svg-source/tray-icon-processing.svg +7 -0
- package/assets/svg-source/tray-icon-recording.svg +7 -0
- package/assets/svg-source/tray-icon.svg +6 -0
- package/assets/tray-complete.png +0 -0
- package/assets/tray-complete@2x.png +0 -0
- package/assets/tray-completeTemplate.png +0 -0
- package/assets/tray-completeTemplate@2x.png +0 -0
- package/assets/tray-error.png +0 -0
- package/assets/tray-error@2x.png +0 -0
- package/assets/tray-errorTemplate.png +0 -0
- package/assets/tray-errorTemplate@2x.png +0 -0
- package/assets/tray-icon-processing.png +0 -0
- package/assets/tray-icon-processing@2x.png +0 -0
- package/assets/tray-icon-processingTemplate.png +0 -0
- package/assets/tray-icon-processingTemplate@2x.png +0 -0
- package/assets/tray-icon-recording.png +0 -0
- package/assets/tray-icon-recording@2x.png +0 -0
- package/assets/tray-icon-recordingTemplate.png +0 -0
- package/assets/tray-icon-recordingTemplate@2x.png +0 -0
- package/assets/tray-icon.png +0 -0
- package/assets/tray-icon@2x.png +0 -0
- package/assets/tray-iconTemplate.png +0 -0
- package/assets/tray-iconTemplate@2x.png +0 -0
- package/assets/tray-idle.png +0 -0
- package/assets/tray-idle@2x.png +0 -0
- package/assets/tray-idleTemplate.png +0 -0
- package/assets/tray-idleTemplate@2x.png +0 -0
- package/assets/tray-processing-0.png +0 -0
- package/assets/tray-processing-0@2x.png +0 -0
- package/assets/tray-processing-0Template.png +0 -0
- package/assets/tray-processing-0Template@2x.png +0 -0
- package/assets/tray-processing-1.png +0 -0
- package/assets/tray-processing-1@2x.png +0 -0
- package/assets/tray-processing-1Template.png +0 -0
- package/assets/tray-processing-1Template@2x.png +0 -0
- package/assets/tray-processing-2.png +0 -0
- package/assets/tray-processing-2@2x.png +0 -0
- package/assets/tray-processing-2Template.png +0 -0
- package/assets/tray-processing-2Template@2x.png +0 -0
- package/assets/tray-processing-3.png +0 -0
- package/assets/tray-processing-3@2x.png +0 -0
- package/assets/tray-processing-3Template.png +0 -0
- package/assets/tray-processing-3Template@2x.png +0 -0
- package/assets/tray-processing.png +0 -0
- package/assets/tray-processing@2x.png +0 -0
- package/assets/tray-processingTemplate.png +0 -0
- package/assets/tray-processingTemplate@2x.png +0 -0
- package/assets/tray-recording.png +0 -0
- package/assets/tray-recording@2x.png +0 -0
- package/assets/tray-recordingTemplate.png +0 -0
- package/assets/tray-recordingTemplate@2x.png +0 -0
- package/build/DMG_BACKGROUND_SPEC.md +50 -0
- package/build/dmg-background.png +0 -0
- package/build/dmg-background@2x.png +0 -0
- package/build/entitlements.mac.inherit.plist +27 -0
- package/build/entitlements.mac.plist +41 -0
- package/build/favicon-16.png +0 -0
- package/build/favicon-180.png +0 -0
- package/build/favicon-192.png +0 -0
- package/build/favicon-32.png +0 -0
- package/build/favicon-48.png +0 -0
- package/build/favicon-512.png +0 -0
- package/build/favicon-64.png +0 -0
- package/build/icon-128.png +0 -0
- package/build/icon-16.png +0 -0
- package/build/icon-24.png +0 -0
- package/build/icon-256.png +0 -0
- package/build/icon-32.png +0 -0
- package/build/icon-48.png +0 -0
- package/build/icon-64.png +0 -0
- package/build/icon.icns +0 -0
- package/build/icon.ico +0 -0
- package/build/icon.iconset/icon_128x128.png +0 -0
- package/build/icon.iconset/icon_128x128@2x.png +0 -0
- package/build/icon.iconset/icon_16x16.png +0 -0
- package/build/icon.iconset/icon_16x16@2x.png +0 -0
- package/build/icon.iconset/icon_256x256.png +0 -0
- package/build/icon.iconset/icon_256x256@2x.png +0 -0
- package/build/icon.iconset/icon_32x32.png +0 -0
- package/build/icon.iconset/icon_32x32@2x.png +0 -0
- package/build/icon.iconset/icon_512x512.png +0 -0
- package/build/icon.iconset/icon_512x512@2x.png +0 -0
- package/build/icon.png +0 -0
- package/build/installer-header.bmp +0 -0
- package/build/installer-header.png +0 -0
- package/build/installer-sidebar.bmp +0 -0
- package/build/installer-sidebar.png +0 -0
- package/build/installer.nsh +45 -0
- package/build/overlay-processing.png +0 -0
- package/build/overlay-recording.png +0 -0
- package/build/toolbar-record.png +0 -0
- package/build/toolbar-screenshot.png +0 -0
- package/build/toolbar-settings.png +0 -0
- package/build/toolbar-stop.png +0 -0
- package/dist/main/index.mjs +12612 -0
- package/dist/preload/index.mjs +907 -0
- package/dist/renderer/assets/index-CCmUjl9K.js +19495 -0
- package/dist/renderer/assets/index-CUqz_Gs6.css +2270 -0
- package/dist/renderer/index.html +27 -0
- package/docs/AI_AGENT_QUICKSTART.md +42 -0
- package/docs/AI_PIPELINE_DESIGN.md +595 -0
- package/docs/API.md +514 -0
- package/docs/ARCHITECTURE.md +460 -0
- package/docs/CONFIGURATION.md +336 -0
- package/docs/DEVELOPMENT.md +508 -0
- package/docs/EXPORT_FORMATS.md +451 -0
- package/docs/GETTING_STARTED.md +236 -0
- package/docs/KEYBOARD_SHORTCUTS.md +334 -0
- package/docs/TROUBLESHOOTING.md +418 -0
- package/docs/landing/index.html +672 -0
- package/docs/landing/script.js +342 -0
- package/docs/landing/styles.css +1543 -0
- package/electron-builder.yml +140 -0
- package/electron.vite.config.ts +63 -0
- package/package.json +108 -0
- package/railway.json +12 -0
- package/scripts/build.mjs +51 -0
- package/scripts/generate-icons.mjs +314 -0
- package/scripts/generate-installer-images.cjs +253 -0
- package/scripts/generate-tray-icons.mjs +258 -0
- package/scripts/notarize.cjs +180 -0
- package/scripts/one-click-clean-test.sh +147 -0
- package/scripts/postinstall.mjs +36 -0
- package/scripts/setup-markupr.sh +55 -0
- package/setup +17 -0
- package/site/index.html +1835 -0
- package/site/package.json +11 -0
- package/site/railway.json +12 -0
- package/site/server.js +31 -0
- package/src/main/AutoUpdater.ts +392 -0
- package/src/main/CrashRecovery.ts +655 -0
- package/src/main/ErrorHandler.ts +703 -0
- package/src/main/HotkeyManager.ts +399 -0
- package/src/main/MenuManager.ts +529 -0
- package/src/main/PermissionManager.ts +420 -0
- package/src/main/SessionController.ts +1465 -0
- package/src/main/TrayManager.ts +540 -0
- package/src/main/ai/AIPipelineManager.ts +199 -0
- package/src/main/ai/ClaudeAnalyzer.ts +339 -0
- package/src/main/ai/ImageOptimizer.ts +176 -0
- package/src/main/ai/StructuredMarkdownBuilder.ts +379 -0
- package/src/main/ai/index.ts +16 -0
- package/src/main/ai/types.ts +258 -0
- package/src/main/analysis/ClarificationGenerator.ts +385 -0
- package/src/main/analysis/FeedbackAnalyzer.ts +531 -0
- package/src/main/analysis/index.ts +19 -0
- package/src/main/audio/AudioCapture.ts +978 -0
- package/src/main/audio/audioUtils.ts +100 -0
- package/src/main/audio/index.ts +20 -0
- package/src/main/capture/index.ts +1 -0
- package/src/main/index.ts +1693 -0
- package/src/main/ipc/captureHandlers.ts +272 -0
- package/src/main/ipc/index.ts +45 -0
- package/src/main/ipc/outputHandlers.ts +302 -0
- package/src/main/ipc/sessionHandlers.ts +56 -0
- package/src/main/ipc/settingsHandlers.ts +471 -0
- package/src/main/ipc/types.ts +56 -0
- package/src/main/ipc/windowHandlers.ts +277 -0
- package/src/main/output/ClipboardService.ts +369 -0
- package/src/main/output/ExportService.ts +539 -0
- package/src/main/output/FileManager.ts +416 -0
- package/src/main/output/MarkdownGenerator.ts +791 -0
- package/src/main/output/MarkdownPatcher.ts +299 -0
- package/src/main/output/index.ts +186 -0
- package/src/main/output/sessionAdapter.ts +207 -0
- package/src/main/output/templates/html-template.ts +553 -0
- package/src/main/pipeline/FrameExtractor.ts +330 -0
- package/src/main/pipeline/PostProcessor.ts +399 -0
- package/src/main/pipeline/TranscriptAnalyzer.ts +226 -0
- package/src/main/pipeline/index.ts +36 -0
- package/src/main/platform/WindowsTaskbar.ts +600 -0
- package/src/main/platform/index.ts +16 -0
- package/src/main/settings/SettingsManager.ts +730 -0
- package/src/main/settings/index.ts +19 -0
- package/src/main/transcription/ModelDownloadManager.ts +494 -0
- package/src/main/transcription/TierManager.ts +219 -0
- package/src/main/transcription/TranscriptionRecoveryService.ts +340 -0
- package/src/main/transcription/WhisperService.ts +748 -0
- package/src/main/transcription/index.ts +56 -0
- package/src/main/transcription/types.ts +135 -0
- package/src/main/windows/PopoverManager.ts +284 -0
- package/src/main/windows/TaskbarIntegration.ts +452 -0
- package/src/main/windows/index.ts +23 -0
- package/src/preload/index.ts +1047 -0
- package/src/renderer/App.tsx +515 -0
- package/src/renderer/AppWrapper.tsx +28 -0
- package/src/renderer/assets/logo-dark.svg +7 -0
- package/src/renderer/assets/logo.svg +7 -0
- package/src/renderer/audio/AudioCaptureRenderer.ts +454 -0
- package/src/renderer/capture/ScreenRecordingRenderer.ts +492 -0
- package/src/renderer/components/AnnotationOverlay.tsx +836 -0
- package/src/renderer/components/AudioWaveform.tsx +811 -0
- package/src/renderer/components/ClarificationQuestions.tsx +656 -0
- package/src/renderer/components/CountdownTimer.tsx +495 -0
- package/src/renderer/components/CrashRecoveryDialog.tsx +632 -0
- package/src/renderer/components/DonateButton.tsx +127 -0
- package/src/renderer/components/ErrorBoundary.tsx +308 -0
- package/src/renderer/components/ExportDialog.tsx +872 -0
- package/src/renderer/components/HotkeyHint.tsx +261 -0
- package/src/renderer/components/KeyboardShortcuts.tsx +787 -0
- package/src/renderer/components/ModelDownloadDialog.tsx +844 -0
- package/src/renderer/components/Onboarding.tsx +1830 -0
- package/src/renderer/components/ProcessingOverlay.tsx +157 -0
- package/src/renderer/components/RecordingOverlay.tsx +423 -0
- package/src/renderer/components/SessionHistory.tsx +1746 -0
- package/src/renderer/components/SessionReview.tsx +1321 -0
- package/src/renderer/components/SettingsPanel.tsx +217 -0
- package/src/renderer/components/Skeleton.tsx +347 -0
- package/src/renderer/components/StatusIndicator.tsx +86 -0
- package/src/renderer/components/ThemeProvider.tsx +429 -0
- package/src/renderer/components/Tooltip.tsx +370 -0
- package/src/renderer/components/TranscriptionPreview.tsx +183 -0
- package/src/renderer/components/TranscriptionTierSelector.tsx +640 -0
- package/src/renderer/components/UpdateNotification.tsx +377 -0
- package/src/renderer/components/WindowSelector.tsx +947 -0
- package/src/renderer/components/index.ts +99 -0
- package/src/renderer/components/primitives/ApiKeyInput.tsx +98 -0
- package/src/renderer/components/primitives/ColorPicker.tsx +65 -0
- package/src/renderer/components/primitives/DangerButton.tsx +45 -0
- package/src/renderer/components/primitives/DirectoryPicker.tsx +41 -0
- package/src/renderer/components/primitives/Dropdown.tsx +34 -0
- package/src/renderer/components/primitives/KeyRecorder.tsx +117 -0
- package/src/renderer/components/primitives/SettingsSection.tsx +32 -0
- package/src/renderer/components/primitives/Slider.tsx +43 -0
- package/src/renderer/components/primitives/Toggle.tsx +36 -0
- package/src/renderer/components/primitives/index.ts +10 -0
- package/src/renderer/components/settings/AdvancedTab.tsx +174 -0
- package/src/renderer/components/settings/AppearanceTab.tsx +77 -0
- package/src/renderer/components/settings/GeneralTab.tsx +40 -0
- package/src/renderer/components/settings/HotkeysTab.tsx +79 -0
- package/src/renderer/components/settings/RecordingTab.tsx +84 -0
- package/src/renderer/components/settings/index.ts +9 -0
- package/src/renderer/components/settings/settingsStyles.ts +673 -0
- package/src/renderer/components/settings/tabConfig.tsx +85 -0
- package/src/renderer/components/settings/useSettingsPanel.ts +447 -0
- package/src/renderer/contexts/ProcessingContext.tsx +227 -0
- package/src/renderer/contexts/RecordingContext.tsx +683 -0
- package/src/renderer/contexts/UIContext.tsx +326 -0
- package/src/renderer/contexts/index.ts +24 -0
- package/src/renderer/donateMessages.ts +69 -0
- package/src/renderer/hooks/index.ts +75 -0
- package/src/renderer/hooks/useAnimation.tsx +544 -0
- package/src/renderer/hooks/useTheme.ts +313 -0
- package/src/renderer/index.html +26 -0
- package/src/renderer/main.tsx +52 -0
- package/src/renderer/styles/animations.css +1093 -0
- package/src/renderer/styles/app-shell.css +662 -0
- package/src/renderer/styles/globals.css +515 -0
- package/src/renderer/styles/theme.ts +578 -0
- package/src/renderer/types/electron.d.ts +385 -0
- package/src/shared/hotkeys.ts +283 -0
- package/src/shared/types.ts +809 -0
- package/tests/clipboard.test.ts +228 -0
- package/tests/e2e/criticalPaths.test.ts +594 -0
- package/tests/feedbackAnalyzer.test.ts +303 -0
- package/tests/integration/sessionFlow.test.ts +583 -0
- package/tests/markdownGenerator.test.ts +418 -0
- package/tests/output.test.ts +96 -0
- package/tests/setup.ts +486 -0
- package/tests/unit/appIntegration.test.ts +676 -0
- package/tests/unit/appViewState.test.ts +281 -0
- package/tests/unit/audioIpcChannels.test.ts +17 -0
- package/tests/unit/exportService.test.ts +492 -0
- package/tests/unit/hotkeys.test.ts +92 -0
- package/tests/unit/navigationPreload.test.ts +94 -0
- package/tests/unit/onboardingFlow.test.ts +345 -0
- package/tests/unit/permissionManager.test.ts +175 -0
- package/tests/unit/permissionManagerExpanded.test.ts +296 -0
- package/tests/unit/screenRecordingRenderer.test.ts +368 -0
- package/tests/unit/sessionController.test.ts +515 -0
- package/tests/unit/tierManager.test.ts +61 -0
- package/tests/unit/tierManagerExpanded.test.ts +142 -0
- package/tests/unit/transcriptAnalyzer.test.ts +64 -0
- package/tsconfig.json +25 -0
- 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 += `}.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
|
+
});
|