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,492 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ExportService Unit Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the multi-format export functionality:
|
|
5
|
+
* - JSON export schema
|
|
6
|
+
* - Filename generation
|
|
7
|
+
* - Format info
|
|
8
|
+
* - Preview generation
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
12
|
+
import type { Session, FeedbackItem } from '../../src/main/output/MarkdownGenerator';
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Mock Electron and dependencies
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
vi.mock('electron', () => ({
|
|
19
|
+
app: {
|
|
20
|
+
getVersion: vi.fn(() => '0.4.0'),
|
|
21
|
+
getPath: vi.fn(() => '/tmp'),
|
|
22
|
+
},
|
|
23
|
+
BrowserWindow: vi.fn().mockImplementation(() => ({
|
|
24
|
+
loadURL: vi.fn(() => Promise.resolve()),
|
|
25
|
+
webContents: {
|
|
26
|
+
printToPDF: vi.fn(() => Promise.resolve(Buffer.from('PDF'))),
|
|
27
|
+
},
|
|
28
|
+
destroy: vi.fn(),
|
|
29
|
+
})),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
vi.mock('fs/promises', () => ({
|
|
33
|
+
mkdir: vi.fn(() => Promise.resolve()),
|
|
34
|
+
writeFile: vi.fn(() => Promise.resolve()),
|
|
35
|
+
stat: vi.fn(() => Promise.resolve({ size: 1024 })),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
// =============================================================================
|
|
39
|
+
// Test Implementation (Isolated from Electron dependencies)
|
|
40
|
+
// =============================================================================
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Isolated ExportService for testing without Electron dependencies
|
|
44
|
+
*/
|
|
45
|
+
class TestableExportService {
|
|
46
|
+
/**
|
|
47
|
+
* Generate JSON export data structure
|
|
48
|
+
*/
|
|
49
|
+
generateJsonExport(session: Session, includeBase64Images: boolean) {
|
|
50
|
+
const categories = this.countByCategory(session.feedbackItems);
|
|
51
|
+
const severities = this.countBySeverity(session.feedbackItems);
|
|
52
|
+
const screenshotCount = session.feedbackItems.reduce(
|
|
53
|
+
(sum, item) => sum + item.screenshots.length,
|
|
54
|
+
0
|
|
55
|
+
);
|
|
56
|
+
const duration = session.endTime ? session.endTime - session.startTime : 0;
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
version: '1.0',
|
|
60
|
+
generator: 'markupr v0.4.0',
|
|
61
|
+
exportedAt: new Date().toISOString(),
|
|
62
|
+
session: {
|
|
63
|
+
id: session.id,
|
|
64
|
+
startTime: session.startTime,
|
|
65
|
+
endTime: session.endTime,
|
|
66
|
+
source: {
|
|
67
|
+
name: session.metadata?.sourceName,
|
|
68
|
+
type: session.metadata?.sourceType,
|
|
69
|
+
os: session.metadata?.os,
|
|
70
|
+
},
|
|
71
|
+
items: session.feedbackItems.map((item, index) => ({
|
|
72
|
+
id: item.id,
|
|
73
|
+
index,
|
|
74
|
+
timestamp: item.timestamp,
|
|
75
|
+
transcription: item.transcription,
|
|
76
|
+
category: item.category || null,
|
|
77
|
+
severity: item.severity || null,
|
|
78
|
+
screenshots: item.screenshots.map((ss) => ({
|
|
79
|
+
id: ss.id,
|
|
80
|
+
width: ss.width,
|
|
81
|
+
height: ss.height,
|
|
82
|
+
...(includeBase64Images && ss.base64 ? { base64: ss.base64 } : {}),
|
|
83
|
+
})),
|
|
84
|
+
})),
|
|
85
|
+
},
|
|
86
|
+
summary: {
|
|
87
|
+
itemCount: session.feedbackItems.length,
|
|
88
|
+
screenshotCount,
|
|
89
|
+
duration,
|
|
90
|
+
categories,
|
|
91
|
+
severities,
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get suggested filename for a given format
|
|
98
|
+
*/
|
|
99
|
+
getSuggestedFilename(
|
|
100
|
+
session: Session,
|
|
101
|
+
format: 'markdown' | 'pdf' | 'html' | 'json',
|
|
102
|
+
projectName?: string
|
|
103
|
+
): string {
|
|
104
|
+
const name = (projectName || session.metadata?.sourceName || 'feedback')
|
|
105
|
+
.toLowerCase()
|
|
106
|
+
.replace(/[^a-z0-9]/g, '-')
|
|
107
|
+
.replace(/-+/g, '-')
|
|
108
|
+
.replace(/^-+|-+$/g, '');
|
|
109
|
+
|
|
110
|
+
const date = new Date(session.startTime);
|
|
111
|
+
const dateStr = [
|
|
112
|
+
date.getFullYear(),
|
|
113
|
+
String(date.getMonth() + 1).padStart(2, '0'),
|
|
114
|
+
String(date.getDate()).padStart(2, '0'),
|
|
115
|
+
].join('');
|
|
116
|
+
const timeStr = [
|
|
117
|
+
String(date.getHours()).padStart(2, '0'),
|
|
118
|
+
String(date.getMinutes()).padStart(2, '0'),
|
|
119
|
+
].join('');
|
|
120
|
+
|
|
121
|
+
const extensions: Record<string, string> = {
|
|
122
|
+
markdown: 'md',
|
|
123
|
+
pdf: 'pdf',
|
|
124
|
+
html: 'html',
|
|
125
|
+
json: 'json',
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return `${name}-feedback-${dateStr}-${timeStr}.${extensions[format]}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get format info for UI display
|
|
133
|
+
*/
|
|
134
|
+
getFormatInfo(format: 'markdown' | 'pdf' | 'html' | 'json') {
|
|
135
|
+
const info = {
|
|
136
|
+
markdown: {
|
|
137
|
+
name: 'Markdown',
|
|
138
|
+
description: 'AI-ready format for Claude, ChatGPT, and other assistants',
|
|
139
|
+
icon: 'document-text',
|
|
140
|
+
extension: '.md',
|
|
141
|
+
},
|
|
142
|
+
pdf: {
|
|
143
|
+
name: 'PDF',
|
|
144
|
+
description: 'Beautiful document for sharing and printing',
|
|
145
|
+
icon: 'document',
|
|
146
|
+
extension: '.pdf',
|
|
147
|
+
},
|
|
148
|
+
html: {
|
|
149
|
+
name: 'HTML',
|
|
150
|
+
description: 'Standalone web page, no dependencies',
|
|
151
|
+
icon: 'code-bracket',
|
|
152
|
+
extension: '.html',
|
|
153
|
+
},
|
|
154
|
+
json: {
|
|
155
|
+
name: 'JSON',
|
|
156
|
+
description: 'Machine-readable for integrations and APIs',
|
|
157
|
+
icon: 'code-bracket-square',
|
|
158
|
+
extension: '.json',
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return info[format];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private countByCategory(items: FeedbackItem[]): Record<string, number> {
|
|
166
|
+
return items.reduce((acc, item) => {
|
|
167
|
+
const category = item.category || 'General';
|
|
168
|
+
acc[category] = (acc[category] || 0) + 1;
|
|
169
|
+
return acc;
|
|
170
|
+
}, {} as Record<string, number>);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private countBySeverity(items: FeedbackItem[]): Record<string, number> {
|
|
174
|
+
return items.reduce((acc, item) => {
|
|
175
|
+
const severity = item.severity || 'Medium';
|
|
176
|
+
acc[severity] = (acc[severity] || 0) + 1;
|
|
177
|
+
return acc;
|
|
178
|
+
}, {} as Record<string, number>);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// =============================================================================
|
|
183
|
+
// Test Data
|
|
184
|
+
// =============================================================================
|
|
185
|
+
|
|
186
|
+
function createTestSession(overrides: Partial<Session> = {}): Session {
|
|
187
|
+
const now = Date.now();
|
|
188
|
+
return {
|
|
189
|
+
id: 'test-session-123',
|
|
190
|
+
startTime: new Date('2024-06-15T14:30:00').getTime(),
|
|
191
|
+
endTime: new Date('2024-06-15T14:35:30').getTime(),
|
|
192
|
+
feedbackItems: [
|
|
193
|
+
{
|
|
194
|
+
id: 'item-1',
|
|
195
|
+
transcription: 'The save button is broken.',
|
|
196
|
+
timestamp: new Date('2024-06-15T14:31:00').getTime(),
|
|
197
|
+
screenshots: [
|
|
198
|
+
{
|
|
199
|
+
id: 'ss-1',
|
|
200
|
+
timestamp: new Date('2024-06-15T14:31:05').getTime(),
|
|
201
|
+
imagePath: '/tmp/ss-1.png',
|
|
202
|
+
width: 1920,
|
|
203
|
+
height: 1080,
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
category: 'Bug',
|
|
207
|
+
severity: 'High',
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
id: 'item-2',
|
|
211
|
+
transcription: 'The navigation is confusing.',
|
|
212
|
+
timestamp: new Date('2024-06-15T14:33:00').getTime(),
|
|
213
|
+
screenshots: [],
|
|
214
|
+
category: 'UX Issue',
|
|
215
|
+
severity: 'Medium',
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
id: 'item-3',
|
|
219
|
+
transcription: 'Would be nice to have dark mode.',
|
|
220
|
+
timestamp: new Date('2024-06-15T14:34:00').getTime(),
|
|
221
|
+
screenshots: [
|
|
222
|
+
{
|
|
223
|
+
id: 'ss-2',
|
|
224
|
+
timestamp: new Date('2024-06-15T14:34:05').getTime(),
|
|
225
|
+
imagePath: '/tmp/ss-2.png',
|
|
226
|
+
width: 1920,
|
|
227
|
+
height: 1080,
|
|
228
|
+
base64: 'data:image/png;base64,ABC123',
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
category: 'Suggestion',
|
|
232
|
+
severity: 'Low',
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
metadata: {
|
|
236
|
+
os: 'darwin',
|
|
237
|
+
sourceName: 'My Test App',
|
|
238
|
+
sourceType: 'window',
|
|
239
|
+
},
|
|
240
|
+
...overrides,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// =============================================================================
|
|
245
|
+
// Tests
|
|
246
|
+
// =============================================================================
|
|
247
|
+
|
|
248
|
+
describe('ExportService', () => {
|
|
249
|
+
let service: TestableExportService;
|
|
250
|
+
|
|
251
|
+
beforeEach(() => {
|
|
252
|
+
service = new TestableExportService();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe('generateJsonExport', () => {
|
|
256
|
+
it('should generate valid JSON export schema', () => {
|
|
257
|
+
const session = createTestSession();
|
|
258
|
+
const json = service.generateJsonExport(session, false);
|
|
259
|
+
|
|
260
|
+
expect(json.version).toBe('1.0');
|
|
261
|
+
expect(json.generator).toContain('markupr');
|
|
262
|
+
expect(json.exportedAt).toBeDefined();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should include session metadata', () => {
|
|
266
|
+
const session = createTestSession();
|
|
267
|
+
const json = service.generateJsonExport(session, false);
|
|
268
|
+
|
|
269
|
+
expect(json.session.id).toBe('test-session-123');
|
|
270
|
+
expect(json.session.startTime).toBeDefined();
|
|
271
|
+
expect(json.session.endTime).toBeDefined();
|
|
272
|
+
expect(json.session.source.name).toBe('My Test App');
|
|
273
|
+
expect(json.session.source.type).toBe('window');
|
|
274
|
+
expect(json.session.source.os).toBe('darwin');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should include all feedback items', () => {
|
|
278
|
+
const session = createTestSession();
|
|
279
|
+
const json = service.generateJsonExport(session, false);
|
|
280
|
+
|
|
281
|
+
expect(json.session.items).toHaveLength(3);
|
|
282
|
+
expect(json.session.items[0].transcription).toBe('The save button is broken.');
|
|
283
|
+
expect(json.session.items[0].category).toBe('Bug');
|
|
284
|
+
expect(json.session.items[0].severity).toBe('High');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should include item indices', () => {
|
|
288
|
+
const session = createTestSession();
|
|
289
|
+
const json = service.generateJsonExport(session, false);
|
|
290
|
+
|
|
291
|
+
expect(json.session.items[0].index).toBe(0);
|
|
292
|
+
expect(json.session.items[1].index).toBe(1);
|
|
293
|
+
expect(json.session.items[2].index).toBe(2);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should include screenshots without base64 by default', () => {
|
|
297
|
+
const session = createTestSession();
|
|
298
|
+
const json = service.generateJsonExport(session, false);
|
|
299
|
+
|
|
300
|
+
const itemWithScreenshot = json.session.items[0];
|
|
301
|
+
expect(itemWithScreenshot.screenshots).toHaveLength(1);
|
|
302
|
+
expect(itemWithScreenshot.screenshots[0].id).toBe('ss-1');
|
|
303
|
+
expect(itemWithScreenshot.screenshots[0].width).toBe(1920);
|
|
304
|
+
expect(itemWithScreenshot.screenshots[0].height).toBe(1080);
|
|
305
|
+
expect(itemWithScreenshot.screenshots[0].base64).toBeUndefined();
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('should include base64 when requested', () => {
|
|
309
|
+
const session = createTestSession();
|
|
310
|
+
const json = service.generateJsonExport(session, true);
|
|
311
|
+
|
|
312
|
+
// Item 3 has base64
|
|
313
|
+
const itemWithBase64 = json.session.items[2];
|
|
314
|
+
expect(itemWithBase64.screenshots[0].base64).toBe('data:image/png;base64,ABC123');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should generate correct summary', () => {
|
|
318
|
+
const session = createTestSession();
|
|
319
|
+
const json = service.generateJsonExport(session, false);
|
|
320
|
+
|
|
321
|
+
expect(json.summary.itemCount).toBe(3);
|
|
322
|
+
expect(json.summary.screenshotCount).toBe(2);
|
|
323
|
+
expect(json.summary.duration).toBeGreaterThan(0);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('should count categories correctly', () => {
|
|
327
|
+
const session = createTestSession();
|
|
328
|
+
const json = service.generateJsonExport(session, false);
|
|
329
|
+
|
|
330
|
+
expect(json.summary.categories).toEqual({
|
|
331
|
+
Bug: 1,
|
|
332
|
+
'UX Issue': 1,
|
|
333
|
+
Suggestion: 1,
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('should count severities correctly', () => {
|
|
338
|
+
const session = createTestSession();
|
|
339
|
+
const json = service.generateJsonExport(session, false);
|
|
340
|
+
|
|
341
|
+
expect(json.summary.severities).toEqual({
|
|
342
|
+
High: 1,
|
|
343
|
+
Medium: 1,
|
|
344
|
+
Low: 1,
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('should handle empty session', () => {
|
|
349
|
+
const session = createTestSession({
|
|
350
|
+
feedbackItems: [],
|
|
351
|
+
});
|
|
352
|
+
const json = service.generateJsonExport(session, false);
|
|
353
|
+
|
|
354
|
+
expect(json.session.items).toHaveLength(0);
|
|
355
|
+
expect(json.summary.itemCount).toBe(0);
|
|
356
|
+
expect(json.summary.screenshotCount).toBe(0);
|
|
357
|
+
expect(json.summary.categories).toEqual({});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('should handle missing category/severity', () => {
|
|
361
|
+
const session = createTestSession({
|
|
362
|
+
feedbackItems: [
|
|
363
|
+
{
|
|
364
|
+
id: 'item-1',
|
|
365
|
+
transcription: 'Test',
|
|
366
|
+
timestamp: Date.now(),
|
|
367
|
+
screenshots: [],
|
|
368
|
+
// No category or severity
|
|
369
|
+
},
|
|
370
|
+
],
|
|
371
|
+
});
|
|
372
|
+
const json = service.generateJsonExport(session, false);
|
|
373
|
+
|
|
374
|
+
expect(json.session.items[0].category).toBeNull();
|
|
375
|
+
expect(json.session.items[0].severity).toBeNull();
|
|
376
|
+
expect(json.summary.categories).toEqual({ General: 1 });
|
|
377
|
+
expect(json.summary.severities).toEqual({ Medium: 1 });
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
describe('getSuggestedFilename', () => {
|
|
382
|
+
it('should generate filename with project name', () => {
|
|
383
|
+
const session = createTestSession();
|
|
384
|
+
const filename = service.getSuggestedFilename(session, 'markdown', 'MyProject');
|
|
385
|
+
|
|
386
|
+
expect(filename).toMatch(/^myproject-feedback-\d{8}-\d{4}\.md$/);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('should use sourceName when project name not provided', () => {
|
|
390
|
+
const session = createTestSession();
|
|
391
|
+
const filename = service.getSuggestedFilename(session, 'markdown');
|
|
392
|
+
|
|
393
|
+
expect(filename).toMatch(/^my-test-app-feedback-\d{8}-\d{4}\.md$/);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('should default to "feedback" when no name available', () => {
|
|
397
|
+
const session = createTestSession({ metadata: undefined });
|
|
398
|
+
const filename = service.getSuggestedFilename(session, 'markdown');
|
|
399
|
+
|
|
400
|
+
expect(filename).toMatch(/^feedback-feedback-\d{8}-\d{4}\.md$/);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('should use correct extension for each format', () => {
|
|
404
|
+
const session = createTestSession();
|
|
405
|
+
|
|
406
|
+
expect(service.getSuggestedFilename(session, 'markdown')).toMatch(/\.md$/);
|
|
407
|
+
expect(service.getSuggestedFilename(session, 'pdf')).toMatch(/\.pdf$/);
|
|
408
|
+
expect(service.getSuggestedFilename(session, 'html')).toMatch(/\.html$/);
|
|
409
|
+
expect(service.getSuggestedFilename(session, 'json')).toMatch(/\.json$/);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('should sanitize special characters in project name', () => {
|
|
413
|
+
const session = createTestSession();
|
|
414
|
+
const filename = service.getSuggestedFilename(session, 'markdown', "Eddie's App (v2.0)");
|
|
415
|
+
|
|
416
|
+
expect(filename).not.toContain("'");
|
|
417
|
+
expect(filename).not.toContain('(');
|
|
418
|
+
expect(filename).not.toContain(')');
|
|
419
|
+
expect(filename).not.toContain(' ');
|
|
420
|
+
expect(filename).toMatch(/^eddie-s-app-v2-0-feedback/);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('should collapse multiple dashes', () => {
|
|
424
|
+
const session = createTestSession();
|
|
425
|
+
const filename = service.getSuggestedFilename(session, 'markdown', 'Test --- App');
|
|
426
|
+
|
|
427
|
+
expect(filename).not.toMatch(/---/);
|
|
428
|
+
expect(filename).toMatch(/^test-app-feedback/);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('should remove leading/trailing dashes', () => {
|
|
432
|
+
const session = createTestSession();
|
|
433
|
+
const filename = service.getSuggestedFilename(session, 'markdown', '---Test App---');
|
|
434
|
+
|
|
435
|
+
expect(filename).toMatch(/^test-app-feedback/);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('should include date and time from session start', () => {
|
|
439
|
+
const session = createTestSession({
|
|
440
|
+
startTime: new Date('2024-12-25T09:30:00').getTime(),
|
|
441
|
+
});
|
|
442
|
+
const filename = service.getSuggestedFilename(session, 'json', 'Test');
|
|
443
|
+
|
|
444
|
+
expect(filename).toContain('20241225');
|
|
445
|
+
expect(filename).toContain('0930');
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
describe('getFormatInfo', () => {
|
|
450
|
+
it('should return info for markdown format', () => {
|
|
451
|
+
const info = service.getFormatInfo('markdown');
|
|
452
|
+
|
|
453
|
+
expect(info.name).toBe('Markdown');
|
|
454
|
+
expect(info.extension).toBe('.md');
|
|
455
|
+
expect(info.description).toContain('AI-ready');
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('should return info for PDF format', () => {
|
|
459
|
+
const info = service.getFormatInfo('pdf');
|
|
460
|
+
|
|
461
|
+
expect(info.name).toBe('PDF');
|
|
462
|
+
expect(info.extension).toBe('.pdf');
|
|
463
|
+
expect(info.description).toContain('sharing');
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('should return info for HTML format', () => {
|
|
467
|
+
const info = service.getFormatInfo('html');
|
|
468
|
+
|
|
469
|
+
expect(info.name).toBe('HTML');
|
|
470
|
+
expect(info.extension).toBe('.html');
|
|
471
|
+
expect(info.description).toContain('Standalone');
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('should return info for JSON format', () => {
|
|
475
|
+
const info = service.getFormatInfo('json');
|
|
476
|
+
|
|
477
|
+
expect(info.name).toBe('JSON');
|
|
478
|
+
expect(info.extension).toBe('.json');
|
|
479
|
+
expect(info.description).toContain('Machine-readable');
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('should include icons for all formats', () => {
|
|
483
|
+
const formats: Array<'markdown' | 'pdf' | 'html' | 'json'> = ['markdown', 'pdf', 'html', 'json'];
|
|
484
|
+
|
|
485
|
+
for (const format of formats) {
|
|
486
|
+
const info = service.getFormatInfo(format);
|
|
487
|
+
expect(info.icon).toBeDefined();
|
|
488
|
+
expect(info.icon.length).toBeGreaterThan(0);
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
formatAcceleratorForDisplay,
|
|
4
|
+
formatHotkeyConfigForDisplay,
|
|
5
|
+
formatHotkeyForDisplay,
|
|
6
|
+
getAccelerator,
|
|
7
|
+
getDisplayKeys,
|
|
8
|
+
getDisplayKeysById,
|
|
9
|
+
getHotkeyById,
|
|
10
|
+
isMacOS,
|
|
11
|
+
isWindows,
|
|
12
|
+
normalizeAccelerator,
|
|
13
|
+
parseAccelerator,
|
|
14
|
+
} from '../../src/shared/hotkeys';
|
|
15
|
+
|
|
16
|
+
function stubPlatform(platform: 'darwin' | 'win32' | 'linux') {
|
|
17
|
+
vi.stubGlobal('process', { platform });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
vi.unstubAllGlobals();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('hotkeys helpers', () => {
|
|
25
|
+
it('detects macOS and windows from process.platform', () => {
|
|
26
|
+
stubPlatform('darwin');
|
|
27
|
+
expect(isMacOS()).toBe(true);
|
|
28
|
+
expect(isWindows()).toBe(false);
|
|
29
|
+
|
|
30
|
+
stubPlatform('win32');
|
|
31
|
+
expect(isWindows()).toBe(true);
|
|
32
|
+
expect(isMacOS()).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('falls back to navigator platform checks when process is missing', () => {
|
|
36
|
+
vi.stubGlobal('process', undefined);
|
|
37
|
+
vi.stubGlobal('navigator', { platform: 'MacIntel' });
|
|
38
|
+
expect(isMacOS()).toBe(true);
|
|
39
|
+
|
|
40
|
+
vi.stubGlobal('navigator', { platform: 'Win32' });
|
|
41
|
+
expect(isWindows()).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('returns platform specific accelerators', () => {
|
|
45
|
+
stubPlatform('darwin');
|
|
46
|
+
expect(getAccelerator('toggleRecording')).toBe('Command+Shift+F');
|
|
47
|
+
|
|
48
|
+
stubPlatform('win32');
|
|
49
|
+
expect(getAccelerator('toggleRecording')).toBe('Ctrl+Shift+F');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('returns empty accelerator for unknown hotkey id', () => {
|
|
53
|
+
expect(getAccelerator('does-not-exist')).toBe('');
|
|
54
|
+
expect(getHotkeyById('does-not-exist')).toBeUndefined();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('normalizes generic accelerators for macOS', () => {
|
|
58
|
+
stubPlatform('darwin');
|
|
59
|
+
expect(normalizeAccelerator('CommandOrControl+Shift+S')).toBe('Command+Shift+S');
|
|
60
|
+
expect(normalizeAccelerator('CmdOrCtrl+P')).toBe('Cmd+P');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('normalizes generic accelerators for windows/linux', () => {
|
|
64
|
+
stubPlatform('win32');
|
|
65
|
+
expect(normalizeAccelerator('CommandOrControl+Shift+S')).toBe('Control+Shift+S');
|
|
66
|
+
expect(normalizeAccelerator('CmdOrCtrl+P')).toBe('Ctrl+P');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('parses accelerators into key segments', () => {
|
|
70
|
+
expect(parseAccelerator('Ctrl+Shift+S')).toEqual(['Ctrl', 'Shift', 'S']);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('formats display keys for macOS and windows', () => {
|
|
74
|
+
stubPlatform('darwin');
|
|
75
|
+
expect(getDisplayKeys('Command+Shift+S')).toEqual(['Cmd', 'Shift', 'S']);
|
|
76
|
+
|
|
77
|
+
stubPlatform('win32');
|
|
78
|
+
expect(getDisplayKeys('Command+Shift+S')).toEqual(['Ctrl', 'Shift', 'S']);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('formats hotkeys for display by id and by accelerator string', () => {
|
|
82
|
+
stubPlatform('darwin');
|
|
83
|
+
expect(getDisplayKeysById('manualScreenshot')).toEqual(['Cmd', 'Shift', 'S']);
|
|
84
|
+
expect(formatHotkeyForDisplay('manualScreenshot')).toBe('Cmd+Shift+S');
|
|
85
|
+
expect(formatAcceleratorForDisplay('Command+Shift+P')).toBe('Cmd+Shift+P');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('formats generic hotkey config strings for display', () => {
|
|
89
|
+
stubPlatform('win32');
|
|
90
|
+
expect(formatHotkeyConfigForDisplay('CommandOrControl+Shift+F')).toBe('Ctrl+Shift+F');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigation Preload Bridge Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the navigation event subscriber pattern added to the preload script.
|
|
5
|
+
* Verifies that:
|
|
6
|
+
* - Each navigation event registers an ipcRenderer.on listener
|
|
7
|
+
* - Callbacks are invoked when events fire
|
|
8
|
+
* - Unsubscribe functions properly remove listeners
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
12
|
+
import { ipcRenderer } from 'electron';
|
|
13
|
+
|
|
14
|
+
describe('Navigation Preload Bridge', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
vi.clearAllMocks();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const navigationEvents = [
|
|
20
|
+
{ channel: 'markupr:show-settings', name: 'onShowSettings' },
|
|
21
|
+
{ channel: 'markupr:show-history', name: 'onShowHistory' },
|
|
22
|
+
{ channel: 'markupr:show-shortcuts', name: 'onShowShortcuts' },
|
|
23
|
+
{ channel: 'markupr:show-onboarding', name: 'onShowOnboarding' },
|
|
24
|
+
{ channel: 'markupr:show-export', name: 'onShowExport' },
|
|
25
|
+
{ channel: 'markupr:show-window-selector', name: 'onShowWindowSelector' },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
it('should register listeners for all navigation channels via ipcRenderer.on', () => {
|
|
29
|
+
// Each subscriber should call ipcRenderer.on with the correct channel
|
|
30
|
+
for (const { channel } of navigationEvents) {
|
|
31
|
+
const callback = vi.fn();
|
|
32
|
+
const handler = () => callback();
|
|
33
|
+
ipcRenderer.on(channel, handler);
|
|
34
|
+
|
|
35
|
+
expect(ipcRenderer.on).toHaveBeenCalledWith(channel, handler);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should return an unsubscribe function that calls removeListener', () => {
|
|
40
|
+
const channel = 'markupr:show-settings';
|
|
41
|
+
const callback = vi.fn();
|
|
42
|
+
const handler = () => callback();
|
|
43
|
+
|
|
44
|
+
ipcRenderer.on(channel, handler);
|
|
45
|
+
ipcRenderer.removeListener(channel, handler);
|
|
46
|
+
|
|
47
|
+
expect(ipcRenderer.removeListener).toHaveBeenCalledWith(channel, handler);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should handle all 6 navigation events', () => {
|
|
51
|
+
expect(navigationEvents).toHaveLength(6);
|
|
52
|
+
|
|
53
|
+
// Verify all expected channels are covered
|
|
54
|
+
const channels = navigationEvents.map((e) => e.channel);
|
|
55
|
+
expect(channels).toContain('markupr:show-settings');
|
|
56
|
+
expect(channels).toContain('markupr:show-history');
|
|
57
|
+
expect(channels).toContain('markupr:show-shortcuts');
|
|
58
|
+
expect(channels).toContain('markupr:show-onboarding');
|
|
59
|
+
expect(channels).toContain('markupr:show-export');
|
|
60
|
+
expect(channels).toContain('markupr:show-window-selector');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('createEventSubscriber pattern', () => {
|
|
65
|
+
it('should follow the subscriber pattern: register, invoke, cleanup', () => {
|
|
66
|
+
// Simulate the createEventSubscriber pattern from preload
|
|
67
|
+
const channel = 'markupr:test-channel';
|
|
68
|
+
const callbacks: Array<(...args: unknown[]) => void> = [];
|
|
69
|
+
|
|
70
|
+
// Mock ipcRenderer.on to capture the handler
|
|
71
|
+
vi.mocked(ipcRenderer.on).mockImplementation((ch, handler) => {
|
|
72
|
+
if (ch === channel) {
|
|
73
|
+
callbacks.push(handler as (...args: unknown[]) => void);
|
|
74
|
+
}
|
|
75
|
+
return ipcRenderer;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Create subscriber
|
|
79
|
+
const userCallback = vi.fn();
|
|
80
|
+
const handler = (_event: unknown, data: unknown) => userCallback(data);
|
|
81
|
+
ipcRenderer.on(channel, handler);
|
|
82
|
+
|
|
83
|
+
// Verify it was registered
|
|
84
|
+
expect(callbacks).toHaveLength(1);
|
|
85
|
+
|
|
86
|
+
// Simulate event firing
|
|
87
|
+
callbacks[0]({}, { some: 'data' });
|
|
88
|
+
expect(userCallback).toHaveBeenCalledWith({ some: 'data' });
|
|
89
|
+
|
|
90
|
+
// Cleanup
|
|
91
|
+
ipcRenderer.removeListener(channel, handler);
|
|
92
|
+
expect(ipcRenderer.removeListener).toHaveBeenCalledWith(channel, handler);
|
|
93
|
+
});
|
|
94
|
+
});
|