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,218 @@
|
|
|
1
|
+
// Tests for src/preload.js IPC bridge
|
|
2
|
+
|
|
3
|
+
describe('Preload IPC Bridge', () => {
|
|
4
|
+
let mockIpcRenderer;
|
|
5
|
+
let mockClipboard;
|
|
6
|
+
let exposedAPI;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
jest.clearAllMocks();
|
|
10
|
+
|
|
11
|
+
// Create mock implementations
|
|
12
|
+
mockIpcRenderer = {
|
|
13
|
+
on: jest.fn(),
|
|
14
|
+
send: jest.fn(),
|
|
15
|
+
invoke: jest.fn(() => Promise.resolve({ success: true }))
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
mockClipboard = {
|
|
19
|
+
availableFormats: jest.fn(() => []),
|
|
20
|
+
readImage: jest.fn(() => ({
|
|
21
|
+
isEmpty: jest.fn(() => true),
|
|
22
|
+
toDataURL: jest.fn(() => ''),
|
|
23
|
+
getSize: jest.fn(() => ({ width: 0, height: 0 }))
|
|
24
|
+
})),
|
|
25
|
+
readText: jest.fn(() => '')
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Create mock API similar to what preload exposes
|
|
29
|
+
exposedAPI = {
|
|
30
|
+
onFocusChange: (callback) => {
|
|
31
|
+
mockIpcRenderer.on('window-focus', (event, focused) => callback(focused));
|
|
32
|
+
},
|
|
33
|
+
closeWindow: () => {
|
|
34
|
+
mockIpcRenderer.send('close-window');
|
|
35
|
+
},
|
|
36
|
+
hideWindow: () => {
|
|
37
|
+
mockIpcRenderer.send('hide-window');
|
|
38
|
+
},
|
|
39
|
+
setPinned: (pinned) => {
|
|
40
|
+
mockIpcRenderer.send('set-pinned', pinned);
|
|
41
|
+
},
|
|
42
|
+
setWindowSize: (size) => {
|
|
43
|
+
mockIpcRenderer.send('set-window-size', size);
|
|
44
|
+
},
|
|
45
|
+
setBackgroundMode: (mode) => {
|
|
46
|
+
mockIpcRenderer.send('set-background-mode', mode);
|
|
47
|
+
},
|
|
48
|
+
getClipboardContent: () => {
|
|
49
|
+
const formats = mockClipboard.availableFormats();
|
|
50
|
+
const hasImage = formats.some(f => f.includes('image'));
|
|
51
|
+
const hasText = formats.some(f => f.includes('text'));
|
|
52
|
+
if (hasImage) {
|
|
53
|
+
const image = mockClipboard.readImage();
|
|
54
|
+
if (!image.isEmpty()) {
|
|
55
|
+
return { type: 'image', dataUrl: image.toDataURL() };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (hasText) {
|
|
59
|
+
const text = mockClipboard.readText();
|
|
60
|
+
if (text && text.trim()) {
|
|
61
|
+
return { type: 'text', content: text };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
},
|
|
66
|
+
readClipboardImage: () => {
|
|
67
|
+
const image = mockClipboard.readImage();
|
|
68
|
+
if (!image.isEmpty()) {
|
|
69
|
+
return image.toDataURL();
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
},
|
|
73
|
+
readClipboardText: () => {
|
|
74
|
+
return mockClipboard.readText();
|
|
75
|
+
},
|
|
76
|
+
saveData: (data) => {
|
|
77
|
+
return mockIpcRenderer.invoke('save-data', data);
|
|
78
|
+
},
|
|
79
|
+
loadData: () => {
|
|
80
|
+
return mockIpcRenderer.invoke('load-data');
|
|
81
|
+
},
|
|
82
|
+
resizeWindowLeft: (deltaX) => {
|
|
83
|
+
mockIpcRenderer.send('resize-window-left', deltaX);
|
|
84
|
+
},
|
|
85
|
+
exportToFloatnote: (noteData) => {
|
|
86
|
+
return mockIpcRenderer.invoke('export-to-floatnote', noteData);
|
|
87
|
+
},
|
|
88
|
+
openFloatnoteFolder: () => {
|
|
89
|
+
return mockIpcRenderer.invoke('open-floatnote-folder');
|
|
90
|
+
},
|
|
91
|
+
exportPNG: (imageDataUrl) => {
|
|
92
|
+
return mockIpcRenderer.invoke('export-png', imageDataUrl);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('Window control functions', () => {
|
|
98
|
+
test('closeWindow sends close-window IPC message', () => {
|
|
99
|
+
exposedAPI.closeWindow();
|
|
100
|
+
expect(mockIpcRenderer.send).toHaveBeenCalledWith('close-window');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('hideWindow sends hide-window IPC message', () => {
|
|
104
|
+
exposedAPI.hideWindow();
|
|
105
|
+
expect(mockIpcRenderer.send).toHaveBeenCalledWith('hide-window');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('setPinned sends set-pinned with value', () => {
|
|
109
|
+
exposedAPI.setPinned(true);
|
|
110
|
+
expect(mockIpcRenderer.send).toHaveBeenCalledWith('set-pinned', true);
|
|
111
|
+
|
|
112
|
+
exposedAPI.setPinned(false);
|
|
113
|
+
expect(mockIpcRenderer.send).toHaveBeenCalledWith('set-pinned', false);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('setWindowSize sends correct size', () => {
|
|
117
|
+
exposedAPI.setWindowSize('sm');
|
|
118
|
+
expect(mockIpcRenderer.send).toHaveBeenCalledWith('set-window-size', 'sm');
|
|
119
|
+
|
|
120
|
+
exposedAPI.setWindowSize('md');
|
|
121
|
+
expect(mockIpcRenderer.send).toHaveBeenCalledWith('set-window-size', 'md');
|
|
122
|
+
|
|
123
|
+
exposedAPI.setWindowSize('lg');
|
|
124
|
+
expect(mockIpcRenderer.send).toHaveBeenCalledWith('set-window-size', 'lg');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('setBackgroundMode sends correct mode', () => {
|
|
128
|
+
exposedAPI.setBackgroundMode('blur');
|
|
129
|
+
expect(mockIpcRenderer.send).toHaveBeenCalledWith('set-background-mode', 'blur');
|
|
130
|
+
|
|
131
|
+
exposedAPI.setBackgroundMode('dark');
|
|
132
|
+
expect(mockIpcRenderer.send).toHaveBeenCalledWith('set-background-mode', 'dark');
|
|
133
|
+
|
|
134
|
+
exposedAPI.setBackgroundMode('transparent');
|
|
135
|
+
expect(mockIpcRenderer.send).toHaveBeenCalledWith('set-background-mode', 'transparent');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('Focus change handling', () => {
|
|
140
|
+
test('onFocusChange registers callback for window-focus', () => {
|
|
141
|
+
const callback = jest.fn();
|
|
142
|
+
exposedAPI.onFocusChange(callback);
|
|
143
|
+
expect(mockIpcRenderer.on).toHaveBeenCalledWith('window-focus', expect.any(Function));
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('Clipboard functions', () => {
|
|
148
|
+
test('getClipboardContent returns null when empty', () => {
|
|
149
|
+
mockClipboard.availableFormats.mockReturnValue([]);
|
|
150
|
+
const result = exposedAPI.getClipboardContent();
|
|
151
|
+
expect(result).toBeNull();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('getClipboardContent returns text when available', () => {
|
|
155
|
+
mockClipboard.availableFormats.mockReturnValue(['text/plain']);
|
|
156
|
+
mockClipboard.readText.mockReturnValue('Hello World');
|
|
157
|
+
const result = exposedAPI.getClipboardContent();
|
|
158
|
+
expect(result).toEqual({ type: 'text', content: 'Hello World' });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('readClipboardText returns clipboard text', () => {
|
|
162
|
+
mockClipboard.readText.mockReturnValue('Test text');
|
|
163
|
+
const result = exposedAPI.readClipboardText();
|
|
164
|
+
expect(result).toBe('Test text');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('readClipboardImage returns null when empty', () => {
|
|
168
|
+
mockClipboard.readImage.mockReturnValue({
|
|
169
|
+
isEmpty: () => true,
|
|
170
|
+
toDataURL: () => ''
|
|
171
|
+
});
|
|
172
|
+
const result = exposedAPI.readClipboardImage();
|
|
173
|
+
expect(result).toBeNull();
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('Data persistence functions', () => {
|
|
178
|
+
test('saveData invokes save-data IPC', async () => {
|
|
179
|
+
const data = { notes: [], settings: {} };
|
|
180
|
+
await exposedAPI.saveData(data);
|
|
181
|
+
expect(mockIpcRenderer.invoke).toHaveBeenCalledWith('save-data', data);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('loadData invokes load-data IPC', async () => {
|
|
185
|
+
await exposedAPI.loadData();
|
|
186
|
+
expect(mockIpcRenderer.invoke).toHaveBeenCalledWith('load-data');
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe('Window resize', () => {
|
|
191
|
+
test('resizeWindowLeft sends resize-window-left with delta', () => {
|
|
192
|
+
exposedAPI.resizeWindowLeft(50);
|
|
193
|
+
expect(mockIpcRenderer.send).toHaveBeenCalledWith('resize-window-left', 50);
|
|
194
|
+
|
|
195
|
+
exposedAPI.resizeWindowLeft(-30);
|
|
196
|
+
expect(mockIpcRenderer.send).toHaveBeenCalledWith('resize-window-left', -30);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('Export functions', () => {
|
|
201
|
+
test('exportToFloatnote invokes export-to-floatnote IPC', async () => {
|
|
202
|
+
const noteData = { id: '123', lines: [], textItems: [] };
|
|
203
|
+
await exposedAPI.exportToFloatnote(noteData);
|
|
204
|
+
expect(mockIpcRenderer.invoke).toHaveBeenCalledWith('export-to-floatnote', noteData);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('openFloatnoteFolder invokes open-floatnote-folder IPC', async () => {
|
|
208
|
+
await exposedAPI.openFloatnoteFolder();
|
|
209
|
+
expect(mockIpcRenderer.invoke).toHaveBeenCalledWith('open-floatnote-folder');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('exportPNG invokes export-png IPC with image data', async () => {
|
|
213
|
+
const imageDataUrl = 'data:image/png;base64,test123';
|
|
214
|
+
await exposedAPI.exportPNG(imageDataUrl);
|
|
215
|
+
expect(mockIpcRenderer.invoke).toHaveBeenCalledWith('export-png', imageDataUrl);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
});
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// Tests for renderer undo/redo history
|
|
2
|
+
|
|
3
|
+
describe('History Management', () => {
|
|
4
|
+
let history;
|
|
5
|
+
let historyIndex;
|
|
6
|
+
const MAX_HISTORY = 50;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
history = [];
|
|
10
|
+
historyIndex = -1;
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('saveState', () => {
|
|
14
|
+
test('should add state to history', () => {
|
|
15
|
+
const state = { lines: [], textItems: [], images: [] };
|
|
16
|
+
history.push(JSON.stringify(state));
|
|
17
|
+
historyIndex = 0;
|
|
18
|
+
|
|
19
|
+
expect(history.length).toBe(1);
|
|
20
|
+
expect(historyIndex).toBe(0);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('should increment history index', () => {
|
|
24
|
+
for (let i = 0; i < 5; i++) {
|
|
25
|
+
history.push(JSON.stringify({ lines: [i] }));
|
|
26
|
+
historyIndex++;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
expect(historyIndex).toBe(4);
|
|
30
|
+
expect(history.length).toBe(5);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('should trim history when exceeding max', () => {
|
|
34
|
+
for (let i = 0; i < MAX_HISTORY + 10; i++) {
|
|
35
|
+
history.push(JSON.stringify({ lines: [i] }));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Trim to max
|
|
39
|
+
if (history.length > MAX_HISTORY) {
|
|
40
|
+
history = history.slice(-MAX_HISTORY);
|
|
41
|
+
historyIndex = Math.min(historyIndex, history.length - 1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
expect(history.length).toBe(MAX_HISTORY);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('should truncate future history when adding new state', () => {
|
|
48
|
+
// Add 5 states
|
|
49
|
+
for (let i = 0; i < 5; i++) {
|
|
50
|
+
history.push(JSON.stringify({ lines: [i] }));
|
|
51
|
+
historyIndex++;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Undo twice
|
|
55
|
+
historyIndex -= 2;
|
|
56
|
+
|
|
57
|
+
// Add new state (should truncate future)
|
|
58
|
+
history = history.slice(0, historyIndex + 1);
|
|
59
|
+
history.push(JSON.stringify({ lines: [100] }));
|
|
60
|
+
historyIndex++;
|
|
61
|
+
|
|
62
|
+
expect(history.length).toBe(4);
|
|
63
|
+
expect(historyIndex).toBe(3);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('undo', () => {
|
|
68
|
+
test('should decrement history index', () => {
|
|
69
|
+
for (let i = 0; i < 5; i++) {
|
|
70
|
+
history.push(JSON.stringify({ lines: [i] }));
|
|
71
|
+
historyIndex++;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Perform undo
|
|
75
|
+
if (historyIndex > 0) {
|
|
76
|
+
historyIndex--;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
expect(historyIndex).toBe(3);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('should not go below 0', () => {
|
|
83
|
+
history.push(JSON.stringify({ lines: [] }));
|
|
84
|
+
historyIndex = 0;
|
|
85
|
+
|
|
86
|
+
// Try to undo when at beginning
|
|
87
|
+
if (historyIndex > 0) {
|
|
88
|
+
historyIndex--;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
expect(historyIndex).toBe(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('should restore previous state', () => {
|
|
95
|
+
history.push(JSON.stringify({ lines: [1, 2, 3] }));
|
|
96
|
+
historyIndex = 0;
|
|
97
|
+
history.push(JSON.stringify({ lines: [1, 2, 3, 4] }));
|
|
98
|
+
historyIndex = 1;
|
|
99
|
+
|
|
100
|
+
// Undo
|
|
101
|
+
historyIndex--;
|
|
102
|
+
const state = JSON.parse(history[historyIndex]);
|
|
103
|
+
|
|
104
|
+
expect(state.lines).toEqual([1, 2, 3]);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('redo', () => {
|
|
109
|
+
test('should increment history index', () => {
|
|
110
|
+
for (let i = 0; i < 5; i++) {
|
|
111
|
+
history.push(JSON.stringify({ lines: [i] }));
|
|
112
|
+
}
|
|
113
|
+
historyIndex = 2;
|
|
114
|
+
|
|
115
|
+
// Perform redo
|
|
116
|
+
if (historyIndex < history.length - 1) {
|
|
117
|
+
historyIndex++;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
expect(historyIndex).toBe(3);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('should not go beyond history length', () => {
|
|
124
|
+
for (let i = 0; i < 3; i++) {
|
|
125
|
+
history.push(JSON.stringify({ lines: [i] }));
|
|
126
|
+
}
|
|
127
|
+
historyIndex = 2;
|
|
128
|
+
|
|
129
|
+
// Try to redo when at end
|
|
130
|
+
if (historyIndex < history.length - 1) {
|
|
131
|
+
historyIndex++;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
expect(historyIndex).toBe(2);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('should restore next state', () => {
|
|
138
|
+
history.push(JSON.stringify({ lines: [1] }));
|
|
139
|
+
history.push(JSON.stringify({ lines: [1, 2] }));
|
|
140
|
+
history.push(JSON.stringify({ lines: [1, 2, 3] }));
|
|
141
|
+
historyIndex = 0;
|
|
142
|
+
|
|
143
|
+
// Redo
|
|
144
|
+
historyIndex++;
|
|
145
|
+
const state = JSON.parse(history[historyIndex]);
|
|
146
|
+
|
|
147
|
+
expect(state.lines).toEqual([1, 2]);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('canUndo and canRedo', () => {
|
|
152
|
+
test('canUndo should be false at beginning', () => {
|
|
153
|
+
history.push(JSON.stringify({ lines: [] }));
|
|
154
|
+
historyIndex = 0;
|
|
155
|
+
|
|
156
|
+
const canUndo = historyIndex > 0;
|
|
157
|
+
expect(canUndo).toBe(false);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('canUndo should be true with history', () => {
|
|
161
|
+
for (let i = 0; i < 3; i++) {
|
|
162
|
+
history.push(JSON.stringify({ lines: [i] }));
|
|
163
|
+
historyIndex++;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const canUndo = historyIndex > 0;
|
|
167
|
+
expect(canUndo).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('canRedo should be false at end', () => {
|
|
171
|
+
for (let i = 0; i < 3; i++) {
|
|
172
|
+
history.push(JSON.stringify({ lines: [i] }));
|
|
173
|
+
historyIndex++;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const canRedo = historyIndex < history.length - 1;
|
|
177
|
+
expect(canRedo).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('canRedo should be true after undo', () => {
|
|
181
|
+
for (let i = 0; i < 3; i++) {
|
|
182
|
+
history.push(JSON.stringify({ lines: [i] }));
|
|
183
|
+
historyIndex++;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Undo
|
|
187
|
+
historyIndex--;
|
|
188
|
+
|
|
189
|
+
const canRedo = historyIndex < history.length - 1;
|
|
190
|
+
expect(canRedo).toBe(true);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('State restoration', () => {
|
|
195
|
+
test('should restore lines correctly', () => {
|
|
196
|
+
const state = {
|
|
197
|
+
lines: [
|
|
198
|
+
{ points: [{ x: 0, y: 0 }, { x: 10, y: 10 }], color: '#fff', width: 2 }
|
|
199
|
+
]
|
|
200
|
+
};
|
|
201
|
+
history.push(JSON.stringify(state));
|
|
202
|
+
|
|
203
|
+
const restored = JSON.parse(history[0]);
|
|
204
|
+
expect(restored.lines.length).toBe(1);
|
|
205
|
+
expect(restored.lines[0].color).toBe('#fff');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('should restore textItems correctly', () => {
|
|
209
|
+
const state = {
|
|
210
|
+
textItems: [
|
|
211
|
+
{ id: '1', text: 'Hello', x: 100, y: 100 }
|
|
212
|
+
]
|
|
213
|
+
};
|
|
214
|
+
history.push(JSON.stringify(state));
|
|
215
|
+
|
|
216
|
+
const restored = JSON.parse(history[0]);
|
|
217
|
+
expect(restored.textItems.length).toBe(1);
|
|
218
|
+
expect(restored.textItems[0].text).toBe('Hello');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('should restore images correctly', () => {
|
|
222
|
+
const state = {
|
|
223
|
+
images: [
|
|
224
|
+
{ id: '1', dataUrl: 'data:image/png;base64,test', x: 50, y: 50 }
|
|
225
|
+
]
|
|
226
|
+
};
|
|
227
|
+
history.push(JSON.stringify(state));
|
|
228
|
+
|
|
229
|
+
const restored = JSON.parse(history[0]);
|
|
230
|
+
expect(restored.images.length).toBe(1);
|
|
231
|
+
expect(restored.images[0].x).toBe(50);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
});
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// Tests for renderer multi-note system
|
|
2
|
+
|
|
3
|
+
describe('Multi-Note System', () => {
|
|
4
|
+
let notes;
|
|
5
|
+
let currentNoteIndex;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
notes = [];
|
|
9
|
+
currentNoteIndex = 0;
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('Note creation', () => {
|
|
13
|
+
test('should create a new note with correct structure', () => {
|
|
14
|
+
const newNote = {
|
|
15
|
+
id: Date.now().toString(),
|
|
16
|
+
lines: [],
|
|
17
|
+
textItems: [],
|
|
18
|
+
images: [],
|
|
19
|
+
createdAt: Date.now(),
|
|
20
|
+
lastModified: Date.now()
|
|
21
|
+
};
|
|
22
|
+
notes.push(newNote);
|
|
23
|
+
|
|
24
|
+
expect(notes.length).toBe(1);
|
|
25
|
+
expect(notes[0].lines).toEqual([]);
|
|
26
|
+
expect(notes[0].textItems).toEqual([]);
|
|
27
|
+
expect(notes[0].images).toEqual([]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('should generate unique IDs', () => {
|
|
31
|
+
const id1 = Date.now().toString();
|
|
32
|
+
// Small delay to ensure different timestamp
|
|
33
|
+
const id2 = (Date.now() + 1).toString();
|
|
34
|
+
|
|
35
|
+
expect(id1).not.toBe(id2);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('Note navigation', () => {
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
// Create 3 notes
|
|
42
|
+
for (let i = 0; i < 3; i++) {
|
|
43
|
+
notes.push({
|
|
44
|
+
id: `note-${i}`,
|
|
45
|
+
lines: [{ id: `line-${i}` }],
|
|
46
|
+
textItems: [],
|
|
47
|
+
images: [],
|
|
48
|
+
createdAt: Date.now() + i,
|
|
49
|
+
lastModified: Date.now() + i
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
currentNoteIndex = 1;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('previousNote should decrement index', () => {
|
|
56
|
+
if (currentNoteIndex > 0) {
|
|
57
|
+
currentNoteIndex--;
|
|
58
|
+
}
|
|
59
|
+
expect(currentNoteIndex).toBe(0);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('previousNote should not go below 0', () => {
|
|
63
|
+
currentNoteIndex = 0;
|
|
64
|
+
if (currentNoteIndex > 0) {
|
|
65
|
+
currentNoteIndex--;
|
|
66
|
+
}
|
|
67
|
+
expect(currentNoteIndex).toBe(0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('nextNote should increment index', () => {
|
|
71
|
+
if (currentNoteIndex < notes.length - 1) {
|
|
72
|
+
currentNoteIndex++;
|
|
73
|
+
}
|
|
74
|
+
expect(currentNoteIndex).toBe(2);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('nextNote should not exceed notes length', () => {
|
|
78
|
+
currentNoteIndex = 2;
|
|
79
|
+
if (currentNoteIndex < notes.length - 1) {
|
|
80
|
+
currentNoteIndex++;
|
|
81
|
+
}
|
|
82
|
+
expect(currentNoteIndex).toBe(2);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('nextNote should create new note at end', () => {
|
|
86
|
+
currentNoteIndex = 2;
|
|
87
|
+
if (currentNoteIndex === notes.length - 1) {
|
|
88
|
+
const newNote = {
|
|
89
|
+
id: `note-${notes.length}`,
|
|
90
|
+
lines: [],
|
|
91
|
+
textItems: [],
|
|
92
|
+
images: [],
|
|
93
|
+
createdAt: Date.now(),
|
|
94
|
+
lastModified: Date.now()
|
|
95
|
+
};
|
|
96
|
+
notes.push(newNote);
|
|
97
|
+
currentNoteIndex++;
|
|
98
|
+
}
|
|
99
|
+
expect(notes.length).toBe(4);
|
|
100
|
+
expect(currentNoteIndex).toBe(3);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('Note counter', () => {
|
|
105
|
+
test('should display correct counter format', () => {
|
|
106
|
+
notes.push({ id: '1' }, { id: '2' }, { id: '3' });
|
|
107
|
+
currentNoteIndex = 1;
|
|
108
|
+
|
|
109
|
+
const counter = `${currentNoteIndex + 1}/${notes.length}`;
|
|
110
|
+
expect(counter).toBe('2/3');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('should update when navigating', () => {
|
|
114
|
+
notes.push({ id: '1' }, { id: '2' });
|
|
115
|
+
currentNoteIndex = 0;
|
|
116
|
+
|
|
117
|
+
let counter = `${currentNoteIndex + 1}/${notes.length}`;
|
|
118
|
+
expect(counter).toBe('1/2');
|
|
119
|
+
|
|
120
|
+
currentNoteIndex++;
|
|
121
|
+
counter = `${currentNoteIndex + 1}/${notes.length}`;
|
|
122
|
+
expect(counter).toBe('2/2');
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('Note state management', () => {
|
|
127
|
+
test('should save current note state before switching', () => {
|
|
128
|
+
const currentNote = {
|
|
129
|
+
id: 'note-1',
|
|
130
|
+
lines: [{ id: 'line-1' }],
|
|
131
|
+
textItems: [{ id: 'text-1' }],
|
|
132
|
+
images: []
|
|
133
|
+
};
|
|
134
|
+
notes.push(currentNote);
|
|
135
|
+
notes.push({ id: 'note-2', lines: [], textItems: [], images: [] });
|
|
136
|
+
|
|
137
|
+
// Simulate saveCurrentNoteState
|
|
138
|
+
notes[currentNoteIndex] = { ...currentNote };
|
|
139
|
+
|
|
140
|
+
expect(notes[0].lines.length).toBe(1);
|
|
141
|
+
expect(notes[0].textItems.length).toBe(1);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('should load note state when switching', () => {
|
|
145
|
+
notes.push({
|
|
146
|
+
id: 'note-1',
|
|
147
|
+
lines: [{ id: 'line-1', points: [] }],
|
|
148
|
+
textItems: [{ id: 'text-1', text: 'Hello' }],
|
|
149
|
+
images: []
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const loadedNote = notes[currentNoteIndex];
|
|
153
|
+
expect(loadedNote.lines.length).toBe(1);
|
|
154
|
+
expect(loadedNote.textItems.length).toBe(1);
|
|
155
|
+
expect(loadedNote.textItems[0].text).toBe('Hello');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('should update lastModified timestamp', () => {
|
|
159
|
+
const note = {
|
|
160
|
+
id: 'note-1',
|
|
161
|
+
lines: [],
|
|
162
|
+
textItems: [],
|
|
163
|
+
images: [],
|
|
164
|
+
createdAt: 1000,
|
|
165
|
+
lastModified: 1000
|
|
166
|
+
};
|
|
167
|
+
notes.push(note);
|
|
168
|
+
|
|
169
|
+
// Simulate modification
|
|
170
|
+
note.lastModified = 2000;
|
|
171
|
+
|
|
172
|
+
expect(note.lastModified).toBeGreaterThan(note.createdAt);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('Note data structure', () => {
|
|
177
|
+
test('lines should have required properties', () => {
|
|
178
|
+
const line = {
|
|
179
|
+
points: [{ x: 0, y: 0 }, { x: 10, y: 10 }],
|
|
180
|
+
color: '#ffffff',
|
|
181
|
+
width: 4,
|
|
182
|
+
objectId: 'obj-1'
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
expect(line.points).toBeDefined();
|
|
186
|
+
expect(line.color).toBeDefined();
|
|
187
|
+
expect(line.width).toBeDefined();
|
|
188
|
+
expect(line.objectId).toBeDefined();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('textItems should have required properties', () => {
|
|
192
|
+
const textItem = {
|
|
193
|
+
id: 'text-1',
|
|
194
|
+
text: 'Sample text',
|
|
195
|
+
x: 100,
|
|
196
|
+
y: 200,
|
|
197
|
+
color: '#ffffff',
|
|
198
|
+
width: 150
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
expect(textItem.id).toBeDefined();
|
|
202
|
+
expect(textItem.x).toBeDefined();
|
|
203
|
+
expect(textItem.y).toBeDefined();
|
|
204
|
+
expect(textItem.color).toBeDefined();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('images should have required properties', () => {
|
|
208
|
+
const image = {
|
|
209
|
+
id: 'img-1',
|
|
210
|
+
dataUrl: 'data:image/png;base64,test',
|
|
211
|
+
x: 50,
|
|
212
|
+
y: 50,
|
|
213
|
+
width: 200,
|
|
214
|
+
height: 150
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
expect(image.id).toBeDefined();
|
|
218
|
+
expect(image.dataUrl).toBeDefined();
|
|
219
|
+
expect(image.x).toBeDefined();
|
|
220
|
+
expect(image.y).toBeDefined();
|
|
221
|
+
expect(image.width).toBeDefined();
|
|
222
|
+
expect(image.height).toBeDefined();
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('Clean slate mode', () => {
|
|
227
|
+
test('should start with empty note when enabled', () => {
|
|
228
|
+
const settings = { openWithCleanSlate: true };
|
|
229
|
+
|
|
230
|
+
if (settings.openWithCleanSlate) {
|
|
231
|
+
notes = [];
|
|
232
|
+
notes.push({
|
|
233
|
+
id: Date.now().toString(),
|
|
234
|
+
lines: [],
|
|
235
|
+
textItems: [],
|
|
236
|
+
images: [],
|
|
237
|
+
createdAt: Date.now(),
|
|
238
|
+
lastModified: Date.now()
|
|
239
|
+
});
|
|
240
|
+
currentNoteIndex = 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
expect(notes.length).toBe(1);
|
|
244
|
+
expect(notes[0].lines).toEqual([]);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test('should load saved notes when disabled', () => {
|
|
248
|
+
const settings = { openWithCleanSlate: false };
|
|
249
|
+
const savedNotes = [
|
|
250
|
+
{ id: '1', lines: [{ id: 'l1' }], textItems: [], images: [] },
|
|
251
|
+
{ id: '2', lines: [], textItems: [{ id: 't1' }], images: [] }
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
if (!settings.openWithCleanSlate && savedNotes.length > 0) {
|
|
255
|
+
notes = savedNotes;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
expect(notes.length).toBe(2);
|
|
259
|
+
expect(notes[0].lines.length).toBe(1);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
});
|