apple-notes-mcp 1.1.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/LICENSE +21 -0
- package/README.md +522 -0
- package/build/index.js +289 -0
- package/build/index.test.js +446 -0
- package/build/services/appleNotesManager.js +720 -0
- package/build/services/appleNotesManager.test.js +684 -0
- package/build/types.js +13 -0
- package/build/utils/applescript.js +141 -0
- package/build/utils/applescript.test.js +129 -0
- package/package.json +70 -0
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit Tests for Apple Notes Manager
|
|
3
|
+
*
|
|
4
|
+
* These tests verify the AppleNotesManager class and its helper functions.
|
|
5
|
+
* The AppleScript execution is mocked to allow testing without macOS.
|
|
6
|
+
*
|
|
7
|
+
* Test Strategy:
|
|
8
|
+
* - Helper functions (escapeForAppleScript, parseAppleScriptDate) are tested
|
|
9
|
+
* with various inputs to ensure correct escaping and parsing
|
|
10
|
+
* - Manager methods are tested for success/failure paths
|
|
11
|
+
* - Script generation is verified by checking for expected AppleScript patterns
|
|
12
|
+
*/
|
|
13
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
14
|
+
import { AppleNotesManager, escapeForAppleScript, parseAppleScriptDate, } from "./appleNotesManager.js";
|
|
15
|
+
// Mock the AppleScript execution module
|
|
16
|
+
// This prevents actual osascript calls during testing
|
|
17
|
+
vi.mock("@/utils/applescript.js", () => ({
|
|
18
|
+
executeAppleScript: vi.fn(),
|
|
19
|
+
}));
|
|
20
|
+
import { executeAppleScript } from "../utils/applescript.js";
|
|
21
|
+
const mockExecuteAppleScript = vi.mocked(executeAppleScript);
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// Text Escaping Tests
|
|
24
|
+
// =============================================================================
|
|
25
|
+
describe("escapeForAppleScript", () => {
|
|
26
|
+
describe("empty and null handling", () => {
|
|
27
|
+
it("returns empty string for empty input", () => {
|
|
28
|
+
expect(escapeForAppleScript("")).toBe("");
|
|
29
|
+
});
|
|
30
|
+
it("returns empty string for null-like input", () => {
|
|
31
|
+
// TypeScript prevents actual null, but runtime might have undefined
|
|
32
|
+
expect(escapeForAppleScript(undefined)).toBe("");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
describe("single quote escaping (shell safety)", () => {
|
|
36
|
+
it("escapes single quotes for shell embedding", () => {
|
|
37
|
+
// Single quotes in: osascript -e 'tell app...'
|
|
38
|
+
// Need to become: '\'' (end quote, escaped quote, start quote)
|
|
39
|
+
const result = escapeForAppleScript("it's working");
|
|
40
|
+
expect(result).toBe("it'\\''s working");
|
|
41
|
+
});
|
|
42
|
+
it("handles multiple single quotes", () => {
|
|
43
|
+
const result = escapeForAppleScript("Rob's mom's note");
|
|
44
|
+
expect(result).toBe("Rob'\\''s mom'\\''s note");
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
describe("double quote escaping (AppleScript strings)", () => {
|
|
48
|
+
it("escapes double quotes for AppleScript", () => {
|
|
49
|
+
// AppleScript strings: "hello \"quoted\" world"
|
|
50
|
+
const result = escapeForAppleScript('say "hello"');
|
|
51
|
+
expect(result).toBe('say \\"hello\\"');
|
|
52
|
+
});
|
|
53
|
+
it("handles mixed quotes", () => {
|
|
54
|
+
const result = escapeForAppleScript('He said "it\'s fine"');
|
|
55
|
+
expect(result).toBe("He said \\\"it'\\''s fine\\\"");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe("control character conversion (HTML for Notes.app)", () => {
|
|
59
|
+
it("converts newlines to <br> tags", () => {
|
|
60
|
+
const result = escapeForAppleScript("line 1\nline 2\nline 3");
|
|
61
|
+
expect(result).toBe("line 1<br>line 2<br>line 3");
|
|
62
|
+
});
|
|
63
|
+
it("converts tabs to <br> tags", () => {
|
|
64
|
+
const result = escapeForAppleScript("col1\tcol2\tcol3");
|
|
65
|
+
expect(result).toBe("col1<br>col2<br>col3");
|
|
66
|
+
});
|
|
67
|
+
it("handles mixed control characters", () => {
|
|
68
|
+
const result = escapeForAppleScript("row1\tcol2\nrow2\tcol2");
|
|
69
|
+
expect(result).toBe("row1<br>col2<br>row2<br>col2");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
describe("complex content", () => {
|
|
73
|
+
it("handles real-world note content", () => {
|
|
74
|
+
const content = 'John\'s "Meeting Notes"\n- Item 1\n- Item 2';
|
|
75
|
+
const result = escapeForAppleScript(content);
|
|
76
|
+
expect(result).toBe("John'\\''s \\\"Meeting Notes\\\"<br>- Item 1<br>- Item 2");
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
describe("unicode and special characters", () => {
|
|
80
|
+
it("preserves unicode characters", () => {
|
|
81
|
+
const result = escapeForAppleScript("日本語テスト 🎉");
|
|
82
|
+
expect(result).toBe("日本語テスト 🎉");
|
|
83
|
+
});
|
|
84
|
+
it("preserves emoji in content", () => {
|
|
85
|
+
const result = escapeForAppleScript("Shopping 🛒\n- Eggs 🥚\n- Milk 🥛");
|
|
86
|
+
expect(result).toBe("Shopping 🛒<br>- Eggs 🥚<br>- Milk 🥛");
|
|
87
|
+
});
|
|
88
|
+
it("handles accented characters", () => {
|
|
89
|
+
const result = escapeForAppleScript("Café résumé naïve");
|
|
90
|
+
expect(result).toBe("Café résumé naïve");
|
|
91
|
+
});
|
|
92
|
+
it("handles backslashes", () => {
|
|
93
|
+
const result = escapeForAppleScript("path\\to\\file");
|
|
94
|
+
expect(result).toBe("path\\to\\file");
|
|
95
|
+
});
|
|
96
|
+
it("handles angle brackets (HTML-like content)", () => {
|
|
97
|
+
// Single quotes become '\'' (shell escape pattern)
|
|
98
|
+
const result = escapeForAppleScript("<script>alert('xss')</script>");
|
|
99
|
+
expect(result).toBe("<script>alert('\\''xss'\\'')</script>");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe("boundary conditions", () => {
|
|
103
|
+
it("handles very short strings", () => {
|
|
104
|
+
expect(escapeForAppleScript("a")).toBe("a");
|
|
105
|
+
expect(escapeForAppleScript("'")).toBe("'\\''");
|
|
106
|
+
expect(escapeForAppleScript('"')).toBe('\\"');
|
|
107
|
+
});
|
|
108
|
+
it("handles string with only whitespace", () => {
|
|
109
|
+
expect(escapeForAppleScript(" ")).toBe(" ");
|
|
110
|
+
});
|
|
111
|
+
it("handles multiple consecutive special characters", () => {
|
|
112
|
+
// Three single quotes become three '\'' sequences
|
|
113
|
+
// Three double quotes become three \" sequences
|
|
114
|
+
const result = escapeForAppleScript("'''\"\"\"");
|
|
115
|
+
expect(result).toBe("'\\'''\\'''\\''\\\"\\\"\\\"");
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
// =============================================================================
|
|
120
|
+
// Date Parsing Tests
|
|
121
|
+
// =============================================================================
|
|
122
|
+
describe("parseAppleScriptDate", () => {
|
|
123
|
+
describe("standard format parsing", () => {
|
|
124
|
+
it("parses AppleScript date with 'date' prefix", () => {
|
|
125
|
+
const dateStr = "date Saturday, December 27, 2025 at 3:44:02 PM";
|
|
126
|
+
const result = parseAppleScriptDate(dateStr);
|
|
127
|
+
expect(result.getFullYear()).toBe(2025);
|
|
128
|
+
expect(result.getMonth()).toBe(11); // December is month 11 (0-indexed)
|
|
129
|
+
expect(result.getDate()).toBe(27);
|
|
130
|
+
});
|
|
131
|
+
it("parses date without 'date' prefix", () => {
|
|
132
|
+
const dateStr = "Saturday, December 27, 2025 at 3:44:02 PM";
|
|
133
|
+
const result = parseAppleScriptDate(dateStr);
|
|
134
|
+
expect(result.getFullYear()).toBe(2025);
|
|
135
|
+
expect(result.getMonth()).toBe(11);
|
|
136
|
+
});
|
|
137
|
+
it("correctly handles AM/PM times", () => {
|
|
138
|
+
const morningDate = "date Monday, January 1, 2025 at 9:30:00 AM";
|
|
139
|
+
const eveningDate = "date Monday, January 1, 2025 at 9:30:00 PM";
|
|
140
|
+
const morning = parseAppleScriptDate(morningDate);
|
|
141
|
+
const evening = parseAppleScriptDate(eveningDate);
|
|
142
|
+
expect(morning.getHours()).toBe(9);
|
|
143
|
+
expect(evening.getHours()).toBe(21);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
describe("fallback behavior", () => {
|
|
147
|
+
it("returns current date for invalid input", () => {
|
|
148
|
+
const before = new Date();
|
|
149
|
+
const result = parseAppleScriptDate("not a valid date");
|
|
150
|
+
const after = new Date();
|
|
151
|
+
// Result should be between before and after (i.e., "now")
|
|
152
|
+
expect(result.getTime()).toBeGreaterThanOrEqual(before.getTime());
|
|
153
|
+
expect(result.getTime()).toBeLessThanOrEqual(after.getTime());
|
|
154
|
+
});
|
|
155
|
+
it("returns current date for empty string", () => {
|
|
156
|
+
const before = new Date();
|
|
157
|
+
const result = parseAppleScriptDate("");
|
|
158
|
+
const after = new Date();
|
|
159
|
+
expect(result.getTime()).toBeGreaterThanOrEqual(before.getTime());
|
|
160
|
+
expect(result.getTime()).toBeLessThanOrEqual(after.getTime());
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
// =============================================================================
|
|
165
|
+
// AppleNotesManager Tests
|
|
166
|
+
// =============================================================================
|
|
167
|
+
describe("AppleNotesManager", () => {
|
|
168
|
+
let manager;
|
|
169
|
+
beforeEach(() => {
|
|
170
|
+
manager = new AppleNotesManager();
|
|
171
|
+
vi.clearAllMocks();
|
|
172
|
+
});
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Note Creation
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
describe("createNote", () => {
|
|
177
|
+
it("returns Note object on successful creation", () => {
|
|
178
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
179
|
+
success: true,
|
|
180
|
+
output: "note id x-coredata://12345/ICNote/p100",
|
|
181
|
+
});
|
|
182
|
+
const result = manager.createNote("Shopping List", "Eggs, Milk, Bread");
|
|
183
|
+
expect(result).not.toBeNull();
|
|
184
|
+
expect(result?.title).toBe("Shopping List");
|
|
185
|
+
expect(result?.content).toBe("Eggs, Milk, Bread");
|
|
186
|
+
expect(result?.account).toBe("iCloud"); // Default account
|
|
187
|
+
});
|
|
188
|
+
it("returns null when AppleScript fails", () => {
|
|
189
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
190
|
+
success: false,
|
|
191
|
+
output: "",
|
|
192
|
+
error: "Notes.app not responding",
|
|
193
|
+
});
|
|
194
|
+
const result = manager.createNote("Test", "Content");
|
|
195
|
+
expect(result).toBeNull();
|
|
196
|
+
});
|
|
197
|
+
it("uses specified account instead of default", () => {
|
|
198
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
199
|
+
success: true,
|
|
200
|
+
output: "note id x-coredata://...",
|
|
201
|
+
});
|
|
202
|
+
const result = manager.createNote("Draft", "Email content", [], undefined, "Gmail");
|
|
203
|
+
expect(result?.account).toBe("Gmail");
|
|
204
|
+
expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining('tell account "Gmail"'));
|
|
205
|
+
});
|
|
206
|
+
it("creates note in specified folder", () => {
|
|
207
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
208
|
+
success: true,
|
|
209
|
+
output: "note id x-coredata://...",
|
|
210
|
+
});
|
|
211
|
+
manager.createNote("Work Note", "Content", [], "Work Projects");
|
|
212
|
+
expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining('at folder "Work Projects"'));
|
|
213
|
+
});
|
|
214
|
+
it("stores tags in returned Note object", () => {
|
|
215
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
216
|
+
success: true,
|
|
217
|
+
output: "note id x-coredata://...",
|
|
218
|
+
});
|
|
219
|
+
const result = manager.createNote("Tagged Note", "Content", ["work", "urgent"]);
|
|
220
|
+
expect(result?.tags).toEqual(["work", "urgent"]);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// Note Search
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
describe("searchNotes", () => {
|
|
227
|
+
it("returns array of matching notes with folder info", () => {
|
|
228
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
229
|
+
success: true,
|
|
230
|
+
output: "Meeting Notes|||Work|||ITEM|||Project Plan|||Notes|||ITEM|||Weekly Review|||Archive",
|
|
231
|
+
});
|
|
232
|
+
const results = manager.searchNotes("notes");
|
|
233
|
+
expect(results).toHaveLength(3);
|
|
234
|
+
expect(results[0].title).toBe("Meeting Notes");
|
|
235
|
+
expect(results[0].folder).toBe("Work");
|
|
236
|
+
expect(results[1].title).toBe("Project Plan");
|
|
237
|
+
expect(results[1].folder).toBe("Notes");
|
|
238
|
+
expect(results[2].title).toBe("Weekly Review");
|
|
239
|
+
expect(results[2].folder).toBe("Archive");
|
|
240
|
+
});
|
|
241
|
+
it("returns empty array when no matches found", () => {
|
|
242
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
243
|
+
success: true,
|
|
244
|
+
output: "",
|
|
245
|
+
});
|
|
246
|
+
const results = manager.searchNotes("nonexistent");
|
|
247
|
+
expect(results).toHaveLength(0);
|
|
248
|
+
});
|
|
249
|
+
it("returns empty array on AppleScript error", () => {
|
|
250
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
251
|
+
success: false,
|
|
252
|
+
output: "",
|
|
253
|
+
error: "Search failed",
|
|
254
|
+
});
|
|
255
|
+
const results = manager.searchNotes("test");
|
|
256
|
+
expect(results).toHaveLength(0);
|
|
257
|
+
});
|
|
258
|
+
it("searches content when searchContent is true", () => {
|
|
259
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
260
|
+
success: true,
|
|
261
|
+
output: "Note with keyword|||Notes",
|
|
262
|
+
});
|
|
263
|
+
manager.searchNotes("project alpha", true);
|
|
264
|
+
expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining('body contains "project alpha"'));
|
|
265
|
+
});
|
|
266
|
+
it("searches titles when searchContent is false", () => {
|
|
267
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
268
|
+
success: true,
|
|
269
|
+
output: "Project Alpha Notes|||Notes",
|
|
270
|
+
});
|
|
271
|
+
manager.searchNotes("Project Alpha", false);
|
|
272
|
+
expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining('name contains "Project Alpha"'));
|
|
273
|
+
});
|
|
274
|
+
it("identifies notes in Recently Deleted folder", () => {
|
|
275
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
276
|
+
success: true,
|
|
277
|
+
output: "Old Note|||Recently Deleted|||ITEM|||Active Note|||Notes",
|
|
278
|
+
});
|
|
279
|
+
const results = manager.searchNotes("note");
|
|
280
|
+
expect(results).toHaveLength(2);
|
|
281
|
+
expect(results[0].title).toBe("Old Note");
|
|
282
|
+
expect(results[0].folder).toBe("Recently Deleted");
|
|
283
|
+
expect(results[1].title).toBe("Active Note");
|
|
284
|
+
expect(results[1].folder).toBe("Notes");
|
|
285
|
+
});
|
|
286
|
+
it("scopes search to specified account", () => {
|
|
287
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
288
|
+
success: true,
|
|
289
|
+
output: "",
|
|
290
|
+
});
|
|
291
|
+
manager.searchNotes("work", false, "Exchange");
|
|
292
|
+
expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining('tell account "Exchange"'));
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
// Note Content Retrieval
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
describe("getNoteContent", () => {
|
|
299
|
+
it("returns HTML content of note", () => {
|
|
300
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
301
|
+
success: true,
|
|
302
|
+
output: "<div>Shopping List</div><div>- Eggs<br>- Milk</div>",
|
|
303
|
+
});
|
|
304
|
+
const content = manager.getNoteContent("Shopping List");
|
|
305
|
+
expect(content).toBe("<div>Shopping List</div><div>- Eggs<br>- Milk</div>");
|
|
306
|
+
});
|
|
307
|
+
it("returns empty string when note not found", () => {
|
|
308
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
309
|
+
success: false,
|
|
310
|
+
output: "",
|
|
311
|
+
error: 'Can\'t get note "Missing"',
|
|
312
|
+
});
|
|
313
|
+
const content = manager.getNoteContent("Missing Note");
|
|
314
|
+
expect(content).toBe("");
|
|
315
|
+
});
|
|
316
|
+
it("uses specified account", () => {
|
|
317
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
318
|
+
success: true,
|
|
319
|
+
output: "<div>Content</div>",
|
|
320
|
+
});
|
|
321
|
+
manager.getNoteContent("My Note", "Gmail");
|
|
322
|
+
expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining('tell account "Gmail"'));
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
// Get Note By ID
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
describe("getNoteById", () => {
|
|
329
|
+
it("returns Note object with metadata for valid ID", () => {
|
|
330
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
331
|
+
success: true,
|
|
332
|
+
output: "My Note, x-coredata://ABC123/ICNote/p100, date Saturday, December 27, 2025 at 3:00:00 PM, date Saturday, December 27, 2025 at 4:00:00 PM, false, false",
|
|
333
|
+
});
|
|
334
|
+
const result = manager.getNoteById("x-coredata://ABC123/ICNote/p100");
|
|
335
|
+
expect(result).not.toBeNull();
|
|
336
|
+
expect(result?.title).toBe("My Note");
|
|
337
|
+
expect(result?.id).toBe("x-coredata://ABC123/ICNote/p100");
|
|
338
|
+
expect(result?.shared).toBe(false);
|
|
339
|
+
expect(result?.passwordProtected).toBe(false);
|
|
340
|
+
});
|
|
341
|
+
it("returns null when note ID not found", () => {
|
|
342
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
343
|
+
success: false,
|
|
344
|
+
output: "",
|
|
345
|
+
error: "Can't get note id",
|
|
346
|
+
});
|
|
347
|
+
const result = manager.getNoteById("x-coredata://invalid");
|
|
348
|
+
expect(result).toBeNull();
|
|
349
|
+
});
|
|
350
|
+
it("returns null when response format is unexpected (no commas)", () => {
|
|
351
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
352
|
+
success: true,
|
|
353
|
+
output: "incomplete data with no commas",
|
|
354
|
+
});
|
|
355
|
+
const result = manager.getNoteById("x-coredata://ABC123/ICNote/p100");
|
|
356
|
+
expect(result).toBeNull();
|
|
357
|
+
});
|
|
358
|
+
it("returns null when response format is missing second comma", () => {
|
|
359
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
360
|
+
success: true,
|
|
361
|
+
output: "title only, no more data",
|
|
362
|
+
});
|
|
363
|
+
const result = manager.getNoteById("x-coredata://ABC123/ICNote/p100");
|
|
364
|
+
// The new parsing requires at least title and ID separated by commas
|
|
365
|
+
expect(result).toBeNull();
|
|
366
|
+
});
|
|
367
|
+
it("correctly parses shared and passwordProtected as true", () => {
|
|
368
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
369
|
+
success: true,
|
|
370
|
+
output: "Shared Note, x-coredata://ABC/ICNote/p1, date Monday, January 1, 2025 at 12:00:00 PM, date Monday, January 1, 2025 at 12:00:00 PM, true, true",
|
|
371
|
+
});
|
|
372
|
+
const result = manager.getNoteById("x-coredata://ABC/ICNote/p1");
|
|
373
|
+
expect(result?.shared).toBe(true);
|
|
374
|
+
expect(result?.passwordProtected).toBe(true);
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
// ---------------------------------------------------------------------------
|
|
378
|
+
// Get Note Details
|
|
379
|
+
// ---------------------------------------------------------------------------
|
|
380
|
+
describe("getNoteDetails", () => {
|
|
381
|
+
it("returns Note object with full metadata", () => {
|
|
382
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
383
|
+
success: true,
|
|
384
|
+
output: "Project Notes, x-coredata://ABC123/ICNote/p200, date Friday, December 20, 2025 at 10:00:00 AM, date Saturday, December 27, 2025 at 2:30:00 PM, false, false",
|
|
385
|
+
});
|
|
386
|
+
const result = manager.getNoteDetails("Project Notes");
|
|
387
|
+
expect(result).not.toBeNull();
|
|
388
|
+
expect(result?.title).toBe("Project Notes");
|
|
389
|
+
expect(result?.id).toBe("x-coredata://ABC123/ICNote/p200");
|
|
390
|
+
expect(result?.account).toBe("iCloud");
|
|
391
|
+
});
|
|
392
|
+
it("returns null when note not found", () => {
|
|
393
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
394
|
+
success: false,
|
|
395
|
+
output: "",
|
|
396
|
+
error: "Can't get note",
|
|
397
|
+
});
|
|
398
|
+
const result = manager.getNoteDetails("Nonexistent");
|
|
399
|
+
expect(result).toBeNull();
|
|
400
|
+
});
|
|
401
|
+
it("uses specified account", () => {
|
|
402
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
403
|
+
success: true,
|
|
404
|
+
output: "Note, id123, date Monday, January 1, 2025 at 12:00:00 PM, date Monday, January 1, 2025 at 12:00:00 PM, false, false",
|
|
405
|
+
});
|
|
406
|
+
const result = manager.getNoteDetails("My Note", "Exchange");
|
|
407
|
+
expect(result?.account).toBe("Exchange");
|
|
408
|
+
expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining('tell account "Exchange"'));
|
|
409
|
+
});
|
|
410
|
+
it("handles shared notes correctly", () => {
|
|
411
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
412
|
+
success: true,
|
|
413
|
+
output: "Shared Doc, id456, date Monday, January 1, 2025 at 12:00:00 PM, date Monday, January 1, 2025 at 12:00:00 PM, true, false",
|
|
414
|
+
});
|
|
415
|
+
const result = manager.getNoteDetails("Shared Doc");
|
|
416
|
+
expect(result?.shared).toBe(true);
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
// ---------------------------------------------------------------------------
|
|
420
|
+
// Note Deletion
|
|
421
|
+
// ---------------------------------------------------------------------------
|
|
422
|
+
describe("deleteNote", () => {
|
|
423
|
+
it("returns true on successful deletion", () => {
|
|
424
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
425
|
+
success: true,
|
|
426
|
+
output: "",
|
|
427
|
+
});
|
|
428
|
+
const result = manager.deleteNote("Old Note");
|
|
429
|
+
expect(result).toBe(true);
|
|
430
|
+
});
|
|
431
|
+
it("returns false when deletion fails", () => {
|
|
432
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
433
|
+
success: false,
|
|
434
|
+
output: "",
|
|
435
|
+
error: "Cannot delete protected note",
|
|
436
|
+
});
|
|
437
|
+
const result = manager.deleteNote("Protected Note");
|
|
438
|
+
expect(result).toBe(false);
|
|
439
|
+
});
|
|
440
|
+
it("uses specified account", () => {
|
|
441
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
442
|
+
success: true,
|
|
443
|
+
output: "",
|
|
444
|
+
});
|
|
445
|
+
manager.deleteNote("Draft", "Gmail");
|
|
446
|
+
expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining('tell account "Gmail"'));
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
// ---------------------------------------------------------------------------
|
|
450
|
+
// Note Updates
|
|
451
|
+
// ---------------------------------------------------------------------------
|
|
452
|
+
describe("updateNote", () => {
|
|
453
|
+
it("returns true on successful update", () => {
|
|
454
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
455
|
+
success: true,
|
|
456
|
+
output: "",
|
|
457
|
+
});
|
|
458
|
+
const result = manager.updateNote("Old Title", "New Title", "Updated content");
|
|
459
|
+
expect(result).toBe(true);
|
|
460
|
+
});
|
|
461
|
+
it("returns false when update fails", () => {
|
|
462
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
463
|
+
success: false,
|
|
464
|
+
output: "",
|
|
465
|
+
error: "Note not found",
|
|
466
|
+
});
|
|
467
|
+
const result = manager.updateNote("Missing", "New Title", "Content");
|
|
468
|
+
expect(result).toBe(false);
|
|
469
|
+
});
|
|
470
|
+
it("preserves original title when newTitle is undefined", () => {
|
|
471
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
472
|
+
success: true,
|
|
473
|
+
output: "",
|
|
474
|
+
});
|
|
475
|
+
manager.updateNote("Keep This Title", undefined, "New content only");
|
|
476
|
+
// The generated body should use the original title
|
|
477
|
+
expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining("<div>Keep This Title</div>"));
|
|
478
|
+
});
|
|
479
|
+
it("uses new title when provided", () => {
|
|
480
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
481
|
+
success: true,
|
|
482
|
+
output: "",
|
|
483
|
+
});
|
|
484
|
+
manager.updateNote("Old Title", "Brand New Title", "Content");
|
|
485
|
+
expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining("<div>Brand New Title</div>"));
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
// ---------------------------------------------------------------------------
|
|
489
|
+
// Note Listing
|
|
490
|
+
// ---------------------------------------------------------------------------
|
|
491
|
+
describe("listNotes", () => {
|
|
492
|
+
it("returns array of note titles", () => {
|
|
493
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
494
|
+
success: true,
|
|
495
|
+
output: "Note A, Note B, Note C",
|
|
496
|
+
});
|
|
497
|
+
const titles = manager.listNotes();
|
|
498
|
+
expect(titles).toEqual(["Note A", "Note B", "Note C"]);
|
|
499
|
+
});
|
|
500
|
+
it("filters out empty entries", () => {
|
|
501
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
502
|
+
success: true,
|
|
503
|
+
output: "Note A, , Note B, , ",
|
|
504
|
+
});
|
|
505
|
+
const titles = manager.listNotes();
|
|
506
|
+
expect(titles).toEqual(["Note A", "Note B"]);
|
|
507
|
+
});
|
|
508
|
+
it("returns empty array on failure", () => {
|
|
509
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
510
|
+
success: false,
|
|
511
|
+
output: "",
|
|
512
|
+
error: "Account not found",
|
|
513
|
+
});
|
|
514
|
+
const titles = manager.listNotes();
|
|
515
|
+
expect(titles).toEqual([]);
|
|
516
|
+
});
|
|
517
|
+
it("filters by folder when specified", () => {
|
|
518
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
519
|
+
success: true,
|
|
520
|
+
output: "Work Note 1, Work Note 2",
|
|
521
|
+
});
|
|
522
|
+
manager.listNotes("iCloud", "Work");
|
|
523
|
+
expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining('notes of folder "Work"'));
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
// ---------------------------------------------------------------------------
|
|
527
|
+
// Folder Operations
|
|
528
|
+
// ---------------------------------------------------------------------------
|
|
529
|
+
describe("listFolders", () => {
|
|
530
|
+
it("returns array of Folder objects", () => {
|
|
531
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
532
|
+
success: true,
|
|
533
|
+
output: "Notes, Archive, Work",
|
|
534
|
+
});
|
|
535
|
+
const folders = manager.listFolders();
|
|
536
|
+
expect(folders).toHaveLength(3);
|
|
537
|
+
expect(folders[0].name).toBe("Notes");
|
|
538
|
+
expect(folders[1].name).toBe("Archive");
|
|
539
|
+
expect(folders[2].name).toBe("Work");
|
|
540
|
+
});
|
|
541
|
+
it("includes account in Folder objects", () => {
|
|
542
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
543
|
+
success: true,
|
|
544
|
+
output: "Notes",
|
|
545
|
+
});
|
|
546
|
+
const folders = manager.listFolders("Gmail");
|
|
547
|
+
expect(folders[0].account).toBe("Gmail");
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
describe("createFolder", () => {
|
|
551
|
+
it("returns Folder object on success", () => {
|
|
552
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
553
|
+
success: true,
|
|
554
|
+
output: "folder id x-coredata://ABC123/ICFolder/p456",
|
|
555
|
+
});
|
|
556
|
+
const result = manager.createFolder("New Project");
|
|
557
|
+
expect(result).not.toBeNull();
|
|
558
|
+
expect(result?.name).toBe("New Project");
|
|
559
|
+
expect(result?.id).toBe("x-coredata://ABC123/ICFolder/p456");
|
|
560
|
+
});
|
|
561
|
+
it("returns null on failure", () => {
|
|
562
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
563
|
+
success: false,
|
|
564
|
+
output: "",
|
|
565
|
+
error: "Folder already exists",
|
|
566
|
+
});
|
|
567
|
+
const result = manager.createFolder("Existing Folder");
|
|
568
|
+
expect(result).toBeNull();
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
describe("deleteFolder", () => {
|
|
572
|
+
it("returns true on successful deletion", () => {
|
|
573
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
574
|
+
success: true,
|
|
575
|
+
output: "",
|
|
576
|
+
});
|
|
577
|
+
const result = manager.deleteFolder("Empty Folder");
|
|
578
|
+
expect(result).toBe(true);
|
|
579
|
+
});
|
|
580
|
+
it("returns false when deletion fails", () => {
|
|
581
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
582
|
+
success: false,
|
|
583
|
+
output: "",
|
|
584
|
+
error: "Folder contains notes",
|
|
585
|
+
});
|
|
586
|
+
const result = manager.deleteFolder("Non-Empty Folder");
|
|
587
|
+
expect(result).toBe(false);
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
// ---------------------------------------------------------------------------
|
|
591
|
+
// Note Moving
|
|
592
|
+
// ---------------------------------------------------------------------------
|
|
593
|
+
describe("moveNote", () => {
|
|
594
|
+
it("returns true when move completes successfully", () => {
|
|
595
|
+
// Mock sequence: getNoteContent -> createNote -> deleteNote
|
|
596
|
+
mockExecuteAppleScript
|
|
597
|
+
.mockReturnValueOnce({
|
|
598
|
+
success: true,
|
|
599
|
+
output: "<div>Note Title</div><div>Content</div>",
|
|
600
|
+
})
|
|
601
|
+
.mockReturnValueOnce({
|
|
602
|
+
success: true,
|
|
603
|
+
output: "note id x-coredata://...",
|
|
604
|
+
})
|
|
605
|
+
.mockReturnValueOnce({
|
|
606
|
+
success: true,
|
|
607
|
+
output: "",
|
|
608
|
+
});
|
|
609
|
+
const result = manager.moveNote("My Note", "Archive");
|
|
610
|
+
expect(result).toBe(true);
|
|
611
|
+
expect(mockExecuteAppleScript).toHaveBeenCalledTimes(3);
|
|
612
|
+
});
|
|
613
|
+
it("returns false when source note cannot be read", () => {
|
|
614
|
+
mockExecuteAppleScript.mockReturnValueOnce({
|
|
615
|
+
success: false,
|
|
616
|
+
output: "",
|
|
617
|
+
error: "Note not found",
|
|
618
|
+
});
|
|
619
|
+
const result = manager.moveNote("Missing Note", "Archive");
|
|
620
|
+
expect(result).toBe(false);
|
|
621
|
+
expect(mockExecuteAppleScript).toHaveBeenCalledTimes(1); // Only tried to read
|
|
622
|
+
});
|
|
623
|
+
it("returns false when copy to destination fails", () => {
|
|
624
|
+
mockExecuteAppleScript
|
|
625
|
+
.mockReturnValueOnce({
|
|
626
|
+
success: true,
|
|
627
|
+
output: "<div>Content</div>",
|
|
628
|
+
})
|
|
629
|
+
.mockReturnValueOnce({
|
|
630
|
+
success: false,
|
|
631
|
+
output: "",
|
|
632
|
+
error: "Folder not found",
|
|
633
|
+
});
|
|
634
|
+
const result = manager.moveNote("My Note", "Nonexistent Folder");
|
|
635
|
+
expect(result).toBe(false);
|
|
636
|
+
expect(mockExecuteAppleScript).toHaveBeenCalledTimes(2); // Read + failed create
|
|
637
|
+
});
|
|
638
|
+
it("returns true even if delete fails (note exists in new location)", () => {
|
|
639
|
+
// This is partial success - note was copied but original couldn't be deleted
|
|
640
|
+
mockExecuteAppleScript
|
|
641
|
+
.mockReturnValueOnce({
|
|
642
|
+
success: true,
|
|
643
|
+
output: "<div>Content</div>",
|
|
644
|
+
})
|
|
645
|
+
.mockReturnValueOnce({
|
|
646
|
+
success: true,
|
|
647
|
+
output: "note id x-coredata://...",
|
|
648
|
+
})
|
|
649
|
+
.mockReturnValueOnce({
|
|
650
|
+
success: false,
|
|
651
|
+
output: "",
|
|
652
|
+
error: "Cannot delete original",
|
|
653
|
+
});
|
|
654
|
+
const result = manager.moveNote("My Note", "Archive");
|
|
655
|
+
// Should still return true because the note exists in the destination
|
|
656
|
+
expect(result).toBe(true);
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
// ---------------------------------------------------------------------------
|
|
660
|
+
// Account Operations
|
|
661
|
+
// ---------------------------------------------------------------------------
|
|
662
|
+
describe("listAccounts", () => {
|
|
663
|
+
it("returns array of Account objects", () => {
|
|
664
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
665
|
+
success: true,
|
|
666
|
+
output: "iCloud, Gmail, Exchange",
|
|
667
|
+
});
|
|
668
|
+
const accounts = manager.listAccounts();
|
|
669
|
+
expect(accounts).toHaveLength(3);
|
|
670
|
+
expect(accounts[0].name).toBe("iCloud");
|
|
671
|
+
expect(accounts[1].name).toBe("Gmail");
|
|
672
|
+
expect(accounts[2].name).toBe("Exchange");
|
|
673
|
+
});
|
|
674
|
+
it("returns empty array on failure", () => {
|
|
675
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
676
|
+
success: false,
|
|
677
|
+
output: "",
|
|
678
|
+
error: "Notes.app not available",
|
|
679
|
+
});
|
|
680
|
+
const accounts = manager.listAccounts();
|
|
681
|
+
expect(accounts).toEqual([]);
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
});
|