1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
|
// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2 as
// published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// Run: node desktop/modules/notes/test_notes.js
// Notes.js is a QML library, so its .pragma line is stripped before node
// evaluates it, as test_clip.js does for Clip.js.
const fs = require("fs");
const assert = require("assert");
const src = fs.readFileSync(__dirname + "/Notes.js", "utf8")
.replace(/^\.pragma library$/m, "");
const N = new Function(src + "\nreturn { parse, valid, stringify, sorted, formatDate, nextId,"
+ " filter, addNote, updateNote, removeNote, setLastpull };")();
const db = {
params: { version: "0.4git", dbversion: "0.4git_1" },
git: { lastpull: "1700000000" },
notes: [
{ id: "1", title: "a", file: "999" },
{ id: "2", title: "b", file: "1000" },
{ id: "3", title: "c", file: "300" },
],
};
// numeric, not lexicographic: 1000 > 999 > 300
assert.deepStrictEqual(N.sorted(db.notes).map(n => n.id), ["2", "1", "3"]);
assert.strictEqual(db.notes[0].id, "1"); // input not reordered
// a missing or non-numeric file sorts last, input order, and formats as ""
const bad = [{ id: "1", title: "x" }, { id: "2", title: "y", file: "abc" },
{ id: "3", title: "z", file: "10" }];
assert.deepStrictEqual(N.sorted(bad).map(n => n.id), ["3", "1", "2"]);
assert.strictEqual(N.formatDate(undefined), "");
assert.strictEqual(N.formatDate("abc"), "");
assert.match(N.formatDate("1000"), /^\d{2}\/\d{2}\/\d{4} \d{2}:\d{2}$/);
assert.strictEqual(N.nextId(db.notes), "4");
assert.strictEqual(N.nextId([]), "1");
// the whole object round-trips; params and git are kept
assert.deepStrictEqual(N.parse(N.stringify(db)), db);
assert.deepStrictEqual(N.parse("not json"), { notes: [] });
assert.deepStrictEqual(N.parse('{"a":1}'), { notes: [] });
// valid(): the object, or null when the file cannot be read. A write must
// refuse on null rather than replace an unreadable index, which is what a
// conflicted db.json is: merge markers from an autostash pop over a remote
// change. An empty or missing file is an empty store, not an error.
assert.deepStrictEqual(N.valid(N.stringify(db)), db);
assert.deepStrictEqual(N.valid(""), { notes: [] });
assert.deepStrictEqual(N.valid(" \n"), { notes: [] });
assert.strictEqual(N.valid("not json"), null);
assert.strictEqual(N.valid('{"a":1}'), null);
assert.strictEqual(N.valid("<<<<<<< HEAD\n{}\n=======\n{}\n>>>>>>> other\n"), null);
assert.deepStrictEqual(N.parse("not json"), { notes: [] }); // display stays tolerant
// pure transforms preserve everything else and do not mutate
const added = N.addNote(db, "T", "1750");
assert.strictEqual(added.notes.length, 4);
assert.deepStrictEqual(added.notes[3], { id: "4", title: "T", file: "1750" });
assert.strictEqual(db.notes.length, 3);
assert.deepStrictEqual(added.params, db.params);
assert.deepStrictEqual(added.git, db.git);
assert.deepStrictEqual(N.removeNote(added, "4"), db);
assert.strictEqual(N.updateNote(db, "2", "Renamed").notes[1].title, "Renamed");
assert.deepStrictEqual(N.updateNote(db, "99", "x"), db);
assert.strictEqual(N.setLastpull(db, 1234).git.lastpull, "1234");
assert.strictEqual(db.git.lastpull, "1700000000");
assert.deepStrictEqual(N.filter(db.notes, " A ").map(n => n.id), ["1"]);
assert.strictEqual(N.filter(db.notes, "").length, 3);
console.log("test_notes: ok");
|