floatnote 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.beads/config.json +6 -0
- package/.beads/issues/floatnote-1.md +21 -0
- package/.beads/issues/floatnote-10.md +28 -0
- package/.beads/issues/floatnote-11.md +36 -0
- package/.beads/issues/floatnote-12.md +25 -0
- package/.beads/issues/floatnote-13.md +37 -0
- package/.beads/issues/floatnote-14.md +22 -0
- package/.beads/issues/floatnote-15.md +22 -0
- package/.beads/issues/floatnote-16.md +20 -0
- package/.beads/issues/floatnote-17.md +20 -0
- package/.beads/issues/floatnote-18.md +21 -0
- package/.beads/issues/floatnote-19.md +19 -0
- package/.beads/issues/floatnote-2.md +32 -0
- package/.beads/issues/floatnote-20.md +22 -0
- package/.beads/issues/floatnote-3.md +50 -0
- package/.beads/issues/floatnote-4.md +31 -0
- package/.beads/issues/floatnote-5.md +28 -0
- package/.beads/issues/floatnote-6.md +30 -0
- package/.beads/issues/floatnote-7.md +38 -0
- package/.beads/issues/floatnote-8.md +29 -0
- package/.beads/issues/floatnote-9.md +32 -0
- package/CLAUDE.md +61 -0
- package/README.md +95 -0
- package/bin/floatnote.js +218 -0
- package/coverage/base.css +224 -0
- package/coverage/bin/floatnote.js.html +739 -0
- package/coverage/bin/index.html +116 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +131 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/bin/floatnote.js.html +739 -0
- package/coverage/lcov-report/bin/index.html +116 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +131 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/src/index.html +146 -0
- package/coverage/lcov-report/src/main.js.html +1483 -0
- package/coverage/lcov-report/src/preload.js.html +361 -0
- package/coverage/lcov-report/src/renderer.js.html +8767 -0
- package/coverage/lcov.info +3273 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/src/index.html +146 -0
- package/coverage/src/main.js.html +1483 -0
- package/coverage/src/preload.js.html +361 -0
- package/coverage/src/renderer.js.html +8767 -0
- package/jest.config.js +48 -0
- package/package.json +59 -0
- package/src/icon-template.png +0 -0
- package/src/index.html +296 -0
- package/src/main.js +494 -0
- package/src/preload.js +96 -0
- package/src/renderer.js +3203 -0
- package/src/styles.css +1448 -0
- package/tests/cli/floatnote.test.js +167 -0
- package/tests/main/main.test.js +287 -0
- package/tests/mocks/electron.js +126 -0
- package/tests/mocks/fs.js +17 -0
- package/tests/preload/preload.test.js +218 -0
- package/tests/renderer/history.test.js +234 -0
- package/tests/renderer/notes.test.js +262 -0
- package/tests/renderer/settings.test.js +178 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Tests for bin/floatnote.js CLI
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
// Mock fs module
|
|
7
|
+
jest.mock('fs', () => ({
|
|
8
|
+
existsSync: jest.fn(),
|
|
9
|
+
readFileSync: jest.fn(),
|
|
10
|
+
writeFileSync: jest.fn(),
|
|
11
|
+
mkdirSync: jest.fn(),
|
|
12
|
+
rmSync: jest.fn(),
|
|
13
|
+
unlinkSync: jest.fn(),
|
|
14
|
+
createWriteStream: jest.fn(() => ({
|
|
15
|
+
on: jest.fn((event, cb) => {
|
|
16
|
+
if (event === 'finish') setTimeout(cb, 0);
|
|
17
|
+
return { on: jest.fn() };
|
|
18
|
+
}),
|
|
19
|
+
close: jest.fn()
|
|
20
|
+
}))
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
// Mock https module
|
|
24
|
+
jest.mock('https', () => ({
|
|
25
|
+
get: jest.fn()
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
// Mock child_process
|
|
29
|
+
jest.mock('child_process', () => ({
|
|
30
|
+
spawn: jest.fn(() => ({
|
|
31
|
+
unref: jest.fn()
|
|
32
|
+
})),
|
|
33
|
+
execSync: jest.fn()
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
describe('CLI utility functions', () => {
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
jest.clearAllMocks();
|
|
39
|
+
// Reset process.argv
|
|
40
|
+
process.argv = ['node', 'floatnote'];
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('formatBytes', () => {
|
|
44
|
+
// We can't directly test formatBytes since it's not exported,
|
|
45
|
+
// but we can test it indirectly through the download progress
|
|
46
|
+
test('should handle byte formatting logic', () => {
|
|
47
|
+
// Test the logic that would be in formatBytes
|
|
48
|
+
const formatBytes = (bytes) => {
|
|
49
|
+
if (bytes < 1024) return bytes + ' B';
|
|
50
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
51
|
+
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
expect(formatBytes(500)).toBe('500 B');
|
|
55
|
+
expect(formatBytes(1024)).toBe('1.0 KB');
|
|
56
|
+
expect(formatBytes(1536)).toBe('1.5 KB');
|
|
57
|
+
expect(formatBytes(1048576)).toBe('1.0 MB');
|
|
58
|
+
expect(formatBytes(2621440)).toBe('2.5 MB');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('log function', () => {
|
|
63
|
+
test('should format log messages correctly', () => {
|
|
64
|
+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
65
|
+
|
|
66
|
+
// Test the log function logic
|
|
67
|
+
const log = (message) => {
|
|
68
|
+
console.log(`[floatnote] ${message}`);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
log('test message');
|
|
72
|
+
expect(consoleSpy).toHaveBeenCalledWith('[floatnote] test message');
|
|
73
|
+
|
|
74
|
+
consoleSpy.mockRestore();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('APP_DIR and paths', () => {
|
|
79
|
+
test('should use correct paths', () => {
|
|
80
|
+
const APP_NAME = 'Floatnote';
|
|
81
|
+
const APP_DIR = path.join(process.env.HOME, '.floatnote');
|
|
82
|
+
const APP_PATH = path.join(APP_DIR, `${APP_NAME}.app`);
|
|
83
|
+
const VERSION_FILE = path.join(APP_DIR, 'version');
|
|
84
|
+
|
|
85
|
+
expect(APP_DIR).toContain('.floatnote');
|
|
86
|
+
expect(APP_PATH).toContain('Floatnote.app');
|
|
87
|
+
expect(VERSION_FILE).toContain('version');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('CLI argument handling', () => {
|
|
93
|
+
test('--version flag should be recognized', () => {
|
|
94
|
+
const args = ['--version'];
|
|
95
|
+
expect(args.includes('--version') || args.includes('-v')).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('-v flag should be recognized', () => {
|
|
99
|
+
const args = ['-v'];
|
|
100
|
+
expect(args.includes('--version') || args.includes('-v')).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('--help flag should be recognized', () => {
|
|
104
|
+
const args = ['--help'];
|
|
105
|
+
expect(args.includes('--help') || args.includes('-h')).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('-h flag should be recognized', () => {
|
|
109
|
+
const args = ['-h'];
|
|
110
|
+
expect(args.includes('--help') || args.includes('-h')).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('--update flag should be recognized', () => {
|
|
114
|
+
const args = ['--update'];
|
|
115
|
+
expect(args.includes('--update')).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('--uninstall flag should be recognized', () => {
|
|
119
|
+
const args = ['--uninstall'];
|
|
120
|
+
expect(args.includes('--uninstall')).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('GitHub API integration', () => {
|
|
125
|
+
test('should construct correct API URL', () => {
|
|
126
|
+
const GITHUB_REPO = 'josmanvis/floatnote';
|
|
127
|
+
const apiPath = `/repos/${GITHUB_REPO}/releases/latest`;
|
|
128
|
+
expect(apiPath).toBe('/repos/josmanvis/floatnote/releases/latest');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('should use correct User-Agent header', () => {
|
|
132
|
+
const headers = { 'User-Agent': 'floatnote-cli' };
|
|
133
|
+
expect(headers['User-Agent']).toBe('floatnote-cli');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('File operations', () => {
|
|
138
|
+
test('should check if app directory exists', () => {
|
|
139
|
+
fs.existsSync.mockReturnValue(false);
|
|
140
|
+
const result = fs.existsSync('/mock/.floatnote');
|
|
141
|
+
expect(result).toBe(false);
|
|
142
|
+
expect(fs.existsSync).toHaveBeenCalled();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('should create app directory if not exists', () => {
|
|
146
|
+
fs.existsSync.mockReturnValue(false);
|
|
147
|
+
fs.mkdirSync('/mock/.floatnote', { recursive: true });
|
|
148
|
+
expect(fs.mkdirSync).toHaveBeenCalledWith('/mock/.floatnote', { recursive: true });
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('should read version file', () => {
|
|
152
|
+
fs.existsSync.mockReturnValue(true);
|
|
153
|
+
fs.readFileSync.mockReturnValue('v1.0.0');
|
|
154
|
+
const version = fs.readFileSync('/mock/.floatnote/version', 'utf8');
|
|
155
|
+
expect(version).toBe('v1.0.0');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('should write version file', () => {
|
|
159
|
+
fs.writeFileSync('/mock/.floatnote/version', 'v1.0.1');
|
|
160
|
+
expect(fs.writeFileSync).toHaveBeenCalledWith('/mock/.floatnote/version', 'v1.0.1');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('should remove directory on uninstall', () => {
|
|
164
|
+
fs.rmSync('/mock/.floatnote', { recursive: true });
|
|
165
|
+
expect(fs.rmSync).toHaveBeenCalledWith('/mock/.floatnote', { recursive: true });
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// Tests for src/main.js - Main Process
|
|
2
|
+
|
|
3
|
+
describe('Main Process', () => {
|
|
4
|
+
let mockBrowserWindow;
|
|
5
|
+
let mockApp;
|
|
6
|
+
let mockDialog;
|
|
7
|
+
let mockScreen;
|
|
8
|
+
let mockShell;
|
|
9
|
+
let mockFs;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
jest.clearAllMocks();
|
|
13
|
+
|
|
14
|
+
// Create fresh mocks for each test
|
|
15
|
+
mockBrowserWindow = {
|
|
16
|
+
loadFile: jest.fn(),
|
|
17
|
+
show: jest.fn(),
|
|
18
|
+
hide: jest.fn(),
|
|
19
|
+
focus: jest.fn(),
|
|
20
|
+
close: jest.fn(),
|
|
21
|
+
destroy: jest.fn(),
|
|
22
|
+
isDestroyed: jest.fn(() => false),
|
|
23
|
+
isMinimized: jest.fn(() => false),
|
|
24
|
+
isVisible: jest.fn(() => true),
|
|
25
|
+
restore: jest.fn(),
|
|
26
|
+
setAlwaysOnTop: jest.fn(),
|
|
27
|
+
setVisibleOnAllWorkspaces: jest.fn(),
|
|
28
|
+
setVibrancy: jest.fn(),
|
|
29
|
+
setBackgroundColor: jest.fn(),
|
|
30
|
+
setBounds: jest.fn(),
|
|
31
|
+
getBounds: jest.fn(() => ({ x: 0, y: 0, width: 800, height: 600 })),
|
|
32
|
+
on: jest.fn(),
|
|
33
|
+
webContents: {
|
|
34
|
+
send: jest.fn()
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
mockApp = {
|
|
39
|
+
getPath: jest.fn((name) => {
|
|
40
|
+
if (name === 'userData') return '/mock/userData';
|
|
41
|
+
if (name === 'home') return '/mock/home';
|
|
42
|
+
return '/mock/path';
|
|
43
|
+
}),
|
|
44
|
+
requestSingleInstanceLock: jest.fn(() => true)
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
mockDialog = {
|
|
48
|
+
showMessageBox: jest.fn(() => Promise.resolve({ response: 0 })),
|
|
49
|
+
showSaveDialog: jest.fn(() => Promise.resolve({ canceled: false, filePath: '/test/path.png' }))
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
mockScreen = {
|
|
53
|
+
getCursorScreenPoint: jest.fn(() => ({ x: 500, y: 500 })),
|
|
54
|
+
getDisplayNearestPoint: jest.fn(() => ({
|
|
55
|
+
workAreaSize: { width: 1920, height: 1080 },
|
|
56
|
+
workArea: { x: 0, y: 0 }
|
|
57
|
+
}))
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
mockShell = {
|
|
61
|
+
openPath: jest.fn(() => Promise.resolve(''))
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
mockFs = {
|
|
65
|
+
existsSync: jest.fn(() => false),
|
|
66
|
+
readFileSync: jest.fn(() => '{}'),
|
|
67
|
+
writeFileSync: jest.fn(),
|
|
68
|
+
mkdirSync: jest.fn()
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('Data storage paths', () => {
|
|
73
|
+
test('should use correct user data path', () => {
|
|
74
|
+
const userDataPath = mockApp.getPath('userData');
|
|
75
|
+
expect(userDataPath).toBe('/mock/userData');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('should use correct home path', () => {
|
|
79
|
+
const homePath = mockApp.getPath('home');
|
|
80
|
+
expect(homePath).toBe('/mock/home');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('Single instance lock', () => {
|
|
85
|
+
test('should request single instance lock', () => {
|
|
86
|
+
const gotLock = mockApp.requestSingleInstanceLock();
|
|
87
|
+
expect(gotLock).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('Window creation', () => {
|
|
92
|
+
test('window should load correct file', () => {
|
|
93
|
+
mockBrowserWindow.loadFile('src/index.html');
|
|
94
|
+
expect(mockBrowserWindow.loadFile).toHaveBeenCalledWith('src/index.html');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('window should set always on top', () => {
|
|
98
|
+
mockBrowserWindow.setAlwaysOnTop(true, 'floating', 1);
|
|
99
|
+
expect(mockBrowserWindow.setAlwaysOnTop).toHaveBeenCalledWith(true, 'floating', 1);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('window should be visible on all workspaces', () => {
|
|
103
|
+
mockBrowserWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
|
104
|
+
expect(mockBrowserWindow.setVisibleOnAllWorkspaces).toHaveBeenCalledWith(true, { visibleOnFullScreen: true });
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('IPC handlers', () => {
|
|
109
|
+
test('save-data handler should write file', () => {
|
|
110
|
+
const data = { notes: [], settings: {} };
|
|
111
|
+
mockFs.writeFileSync('/mock/userData/floatnote-data.json', JSON.stringify(data, null, 2));
|
|
112
|
+
expect(mockFs.writeFileSync).toHaveBeenCalled();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('load-data handler should read file when exists', () => {
|
|
116
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
117
|
+
mockFs.readFileSync.mockReturnValue('{"notes":[],"settings":{}}');
|
|
118
|
+
|
|
119
|
+
if (mockFs.existsSync('/mock/userData/floatnote-data.json')) {
|
|
120
|
+
const data = mockFs.readFileSync('/mock/userData/floatnote-data.json', 'utf-8');
|
|
121
|
+
expect(data).toBeDefined();
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('load-data handler should return null when file does not exist', () => {
|
|
126
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
127
|
+
const exists = mockFs.existsSync('/mock/userData/floatnote-data.json');
|
|
128
|
+
expect(exists).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('Export to ~/.floatnote', () => {
|
|
133
|
+
test('should create folder if not exists', () => {
|
|
134
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
135
|
+
|
|
136
|
+
const floatnoteFolder = '/mock/home/.floatnote';
|
|
137
|
+
if (!mockFs.existsSync(floatnoteFolder)) {
|
|
138
|
+
mockFs.mkdirSync(floatnoteFolder, { recursive: true });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
expect(mockFs.mkdirSync).toHaveBeenCalledWith(floatnoteFolder, { recursive: true });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('should write note file', () => {
|
|
145
|
+
const noteData = { id: '123', lines: [], textItems: [] };
|
|
146
|
+
const filePath = '/mock/home/.floatnote/note-123.json';
|
|
147
|
+
mockFs.writeFileSync(filePath, JSON.stringify(noteData, null, 2));
|
|
148
|
+
expect(mockFs.writeFileSync).toHaveBeenCalled();
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('Open folder in Finder', () => {
|
|
153
|
+
test('should call shell.openPath', async () => {
|
|
154
|
+
const floatnoteFolder = '/mock/home/.floatnote';
|
|
155
|
+
await mockShell.openPath(floatnoteFolder);
|
|
156
|
+
expect(mockShell.openPath).toHaveBeenCalledWith(floatnoteFolder);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('Export PNG', () => {
|
|
161
|
+
test('should show save dialog', async () => {
|
|
162
|
+
await mockDialog.showSaveDialog(mockBrowserWindow, {
|
|
163
|
+
title: 'Export Note as PNG',
|
|
164
|
+
defaultPath: 'floatnote.png',
|
|
165
|
+
filters: [{ name: 'PNG Images', extensions: ['png'] }]
|
|
166
|
+
});
|
|
167
|
+
expect(mockDialog.showSaveDialog).toHaveBeenCalled();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('should write PNG file', () => {
|
|
171
|
+
const base64Data = 'iVBORw0KGgo=';
|
|
172
|
+
mockFs.writeFileSync('/test/path.png', Buffer.from(base64Data, 'base64'));
|
|
173
|
+
expect(mockFs.writeFileSync).toHaveBeenCalled();
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('Screen utilities', () => {
|
|
178
|
+
test('should get cursor screen point', () => {
|
|
179
|
+
const point = mockScreen.getCursorScreenPoint();
|
|
180
|
+
expect(point).toEqual({ x: 500, y: 500 });
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('should get display nearest to cursor', () => {
|
|
184
|
+
const point = mockScreen.getCursorScreenPoint();
|
|
185
|
+
const display = mockScreen.getDisplayNearestPoint(point);
|
|
186
|
+
expect(display.workAreaSize).toBeDefined();
|
|
187
|
+
expect(display.workArea).toBeDefined();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('Window size presets', () => {
|
|
192
|
+
test('should calculate small size (33% x 50%)', () => {
|
|
193
|
+
const screenWidth = 1920;
|
|
194
|
+
const screenHeight = 1080;
|
|
195
|
+
const newWidth = Math.round(screenWidth * 0.33);
|
|
196
|
+
const newHeight = Math.round(screenHeight * 0.5);
|
|
197
|
+
expect(newWidth).toBe(634);
|
|
198
|
+
expect(newHeight).toBe(540);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('should calculate medium size (33% x 100%)', () => {
|
|
202
|
+
const screenWidth = 1920;
|
|
203
|
+
const screenHeight = 1080;
|
|
204
|
+
const newWidth = Math.round(screenWidth * 0.33);
|
|
205
|
+
const newHeight = screenHeight;
|
|
206
|
+
expect(newWidth).toBe(634);
|
|
207
|
+
expect(newHeight).toBe(1080);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('should calculate large size (100% x 100%)', () => {
|
|
211
|
+
const screenWidth = 1920;
|
|
212
|
+
const screenHeight = 1080;
|
|
213
|
+
const newWidth = screenWidth;
|
|
214
|
+
const newHeight = screenHeight;
|
|
215
|
+
expect(newWidth).toBe(1920);
|
|
216
|
+
expect(newHeight).toBe(1080);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('Background modes', () => {
|
|
221
|
+
test('blur mode should set vibrancy', () => {
|
|
222
|
+
mockBrowserWindow.setVibrancy('fullscreen-ui');
|
|
223
|
+
mockBrowserWindow.setBackgroundColor('#00000000');
|
|
224
|
+
expect(mockBrowserWindow.setVibrancy).toHaveBeenCalledWith('fullscreen-ui');
|
|
225
|
+
expect(mockBrowserWindow.setBackgroundColor).toHaveBeenCalledWith('#00000000');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('dark mode should clear vibrancy', () => {
|
|
229
|
+
mockBrowserWindow.setVibrancy(null);
|
|
230
|
+
mockBrowserWindow.setBackgroundColor('#00000000');
|
|
231
|
+
expect(mockBrowserWindow.setVibrancy).toHaveBeenCalledWith(null);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('transparent mode should clear vibrancy', () => {
|
|
235
|
+
mockBrowserWindow.setVibrancy(null);
|
|
236
|
+
mockBrowserWindow.setBackgroundColor('#00000000');
|
|
237
|
+
expect(mockBrowserWindow.setVibrancy).toHaveBeenCalledWith(null);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('Left edge resize', () => {
|
|
242
|
+
test('should calculate new bounds correctly', () => {
|
|
243
|
+
const bounds = { x: 1000, y: 0, width: 400, height: 600 };
|
|
244
|
+
const deltaX = 50;
|
|
245
|
+
const minWidth = 200;
|
|
246
|
+
const newWidth = bounds.width - deltaX;
|
|
247
|
+
|
|
248
|
+
if (newWidth >= minWidth) {
|
|
249
|
+
const newBounds = {
|
|
250
|
+
x: bounds.x + deltaX,
|
|
251
|
+
y: bounds.y,
|
|
252
|
+
width: newWidth,
|
|
253
|
+
height: bounds.height
|
|
254
|
+
};
|
|
255
|
+
expect(newBounds.x).toBe(1050);
|
|
256
|
+
expect(newBounds.width).toBe(350);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('should not resize below minimum width', () => {
|
|
261
|
+
const bounds = { x: 1000, y: 0, width: 250, height: 600 };
|
|
262
|
+
const deltaX = 100;
|
|
263
|
+
const minWidth = 200;
|
|
264
|
+
const newWidth = bounds.width - deltaX;
|
|
265
|
+
|
|
266
|
+
expect(newWidth).toBe(150);
|
|
267
|
+
expect(newWidth < minWidth).toBe(true);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe('Dialog interactions', () => {
|
|
273
|
+
test('close confirmation dialog should have correct options', async () => {
|
|
274
|
+
const mockDialog = {
|
|
275
|
+
showMessageBox: jest.fn(() => Promise.resolve({ response: 0 }))
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const result = await mockDialog.showMessageBox({}, {
|
|
279
|
+
type: 'warning',
|
|
280
|
+
buttons: ['Close', 'Cancel'],
|
|
281
|
+
defaultId: 1,
|
|
282
|
+
title: 'Close Floatnote?',
|
|
283
|
+
message: 'Are you sure you want to close Floatnote?'
|
|
284
|
+
});
|
|
285
|
+
expect(result.response).toBe(0);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Mock Electron for testing
|
|
2
|
+
|
|
3
|
+
const mockBrowserWindow = {
|
|
4
|
+
loadFile: jest.fn(),
|
|
5
|
+
show: jest.fn(),
|
|
6
|
+
hide: jest.fn(),
|
|
7
|
+
focus: jest.fn(),
|
|
8
|
+
close: jest.fn(),
|
|
9
|
+
destroy: jest.fn(),
|
|
10
|
+
isDestroyed: jest.fn(() => false),
|
|
11
|
+
isMinimized: jest.fn(() => false),
|
|
12
|
+
isVisible: jest.fn(() => true),
|
|
13
|
+
restore: jest.fn(),
|
|
14
|
+
setAlwaysOnTop: jest.fn(),
|
|
15
|
+
setVisibleOnAllWorkspaces: jest.fn(),
|
|
16
|
+
setVibrancy: jest.fn(),
|
|
17
|
+
setBackgroundColor: jest.fn(),
|
|
18
|
+
setBounds: jest.fn(),
|
|
19
|
+
getBounds: jest.fn(() => ({ x: 0, y: 0, width: 800, height: 600 })),
|
|
20
|
+
on: jest.fn(),
|
|
21
|
+
webContents: {
|
|
22
|
+
send: jest.fn()
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const BrowserWindow = jest.fn(() => mockBrowserWindow);
|
|
27
|
+
BrowserWindow.fromWebContents = jest.fn(() => mockBrowserWindow);
|
|
28
|
+
|
|
29
|
+
const mockTray = {
|
|
30
|
+
setToolTip: jest.fn(),
|
|
31
|
+
on: jest.fn(),
|
|
32
|
+
popUpContextMenu: jest.fn()
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const Tray = jest.fn(() => mockTray);
|
|
36
|
+
|
|
37
|
+
const Menu = {
|
|
38
|
+
buildFromTemplate: jest.fn(() => ({}))
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const nativeImage = {
|
|
42
|
+
createFromDataURL: jest.fn(() => ({
|
|
43
|
+
setTemplateImage: jest.fn()
|
|
44
|
+
}))
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const dialog = {
|
|
48
|
+
showMessageBox: jest.fn(() => Promise.resolve({ response: 0 })),
|
|
49
|
+
showSaveDialog: jest.fn(() => Promise.resolve({ canceled: false, filePath: '/test/path.png' }))
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const ipcMain = {
|
|
53
|
+
on: jest.fn(),
|
|
54
|
+
handle: jest.fn()
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const ipcRenderer = {
|
|
58
|
+
on: jest.fn(),
|
|
59
|
+
send: jest.fn(),
|
|
60
|
+
invoke: jest.fn(() => Promise.resolve({ success: true }))
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const globalShortcut = {
|
|
64
|
+
register: jest.fn(),
|
|
65
|
+
unregisterAll: jest.fn()
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const screen = {
|
|
69
|
+
getCursorScreenPoint: jest.fn(() => ({ x: 500, y: 500 })),
|
|
70
|
+
getDisplayNearestPoint: jest.fn(() => ({
|
|
71
|
+
workAreaSize: { width: 1920, height: 1080 },
|
|
72
|
+
workArea: { x: 0, y: 0 }
|
|
73
|
+
}))
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const shell = {
|
|
77
|
+
openPath: jest.fn(() => Promise.resolve(''))
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const clipboard = {
|
|
81
|
+
availableFormats: jest.fn(() => []),
|
|
82
|
+
readImage: jest.fn(() => ({
|
|
83
|
+
isEmpty: jest.fn(() => true),
|
|
84
|
+
toDataURL: jest.fn(() => ''),
|
|
85
|
+
getSize: jest.fn(() => ({ width: 0, height: 0 }))
|
|
86
|
+
})),
|
|
87
|
+
readText: jest.fn(() => '')
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const contextBridge = {
|
|
91
|
+
exposeInMainWorld: jest.fn()
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const app = {
|
|
95
|
+
getPath: jest.fn((name) => {
|
|
96
|
+
if (name === 'userData') return '/mock/userData';
|
|
97
|
+
if (name === 'home') return '/mock/home';
|
|
98
|
+
return '/mock/path';
|
|
99
|
+
}),
|
|
100
|
+
whenReady: jest.fn(() => Promise.resolve()),
|
|
101
|
+
on: jest.fn(),
|
|
102
|
+
quit: jest.fn(),
|
|
103
|
+
requestSingleInstanceLock: jest.fn(() => true),
|
|
104
|
+
dock: {
|
|
105
|
+
setIcon: jest.fn()
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
app,
|
|
111
|
+
BrowserWindow,
|
|
112
|
+
Tray,
|
|
113
|
+
Menu,
|
|
114
|
+
nativeImage,
|
|
115
|
+
dialog,
|
|
116
|
+
ipcMain,
|
|
117
|
+
ipcRenderer,
|
|
118
|
+
globalShortcut,
|
|
119
|
+
screen,
|
|
120
|
+
shell,
|
|
121
|
+
clipboard,
|
|
122
|
+
contextBridge,
|
|
123
|
+
// Export mocks for direct access in tests
|
|
124
|
+
mockBrowserWindow,
|
|
125
|
+
mockTray
|
|
126
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Mock fs module for testing
|
|
2
|
+
|
|
3
|
+
const mockFs = {
|
|
4
|
+
existsSync: jest.fn(() => false),
|
|
5
|
+
readFileSync: jest.fn(() => '{}'),
|
|
6
|
+
writeFileSync: jest.fn(),
|
|
7
|
+
mkdirSync: jest.fn(),
|
|
8
|
+
rmSync: jest.fn(),
|
|
9
|
+
unlinkSync: jest.fn(),
|
|
10
|
+
readdirSync: jest.fn(() => []),
|
|
11
|
+
statSync: jest.fn(() => ({
|
|
12
|
+
isDirectory: jest.fn(() => false),
|
|
13
|
+
isFile: jest.fn(() => true)
|
|
14
|
+
}))
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
module.exports = mockFs;
|