test: unit and end-to-end suites
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
27fafa0bcd
commit
9b00e21c44
49 files changed
+6694
-1
No files matched your search
@@ -256,3 +256,209 @@ test('version history: throttled copies, restore keeps the current text, follows
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('version history works for canvases: the file text comes back exactly, and it follows a rename', async () => {
|
||||
const root = await tmp();
|
||||
try {
|
||||
const v = new Vault(root);
|
||||
const id = await v.createCanvas(null, 'Map');
|
||||
const first = '{\n\t"nodes":[\n\t\t{"id":"aaaaaaaaaaaaaaaa","type":"text","text":"---\\nnot front matter","x":0,"y":0,"width":250,"height":60}\n\t],\n\t"edges":[]\n}';
|
||||
// making the canvas kept its empty state as the first version; age that so the next save makes a new one
|
||||
const dir = path.join(root, '.notes', 'history', id);
|
||||
const [t0] = await v.listHistory(id);
|
||||
await writeFile(path.join(dir, `${t0 - 10 * 60 * 1000}.md`), await readFile(path.join(dir, `${t0}.md`), 'utf8'));
|
||||
await rm(path.join(dir, `${t0}.md`));
|
||||
await v.write(id, {}, first);
|
||||
const [ts] = await v.listHistory(id);
|
||||
assert.equal((await v.readHistory(id, ts)).body, first, 'the file text comes back as saved, not read as front matter');
|
||||
const second = '{\n\t"nodes":[],\n\t"edges":[]\n}';
|
||||
await v.write(id, {}, second.replace('[]', '[ ]')); // within the gap: no version of its own
|
||||
await v.restoreHistory(id, ts);
|
||||
assert.equal(await readFile(v.abs(id), 'utf8'), first, 'written back byte for byte, without front matter added');
|
||||
const kept = await v.listHistory(id);
|
||||
assert.ok((await Promise.all(kept.map((t) => v.readHistory(id, t)))).some((x) => x.body === second.replace('[]', '[ ]')), 'the canvas before the restore was kept');
|
||||
const next = await v.rename(id, 'Roadmap');
|
||||
assert.equal(next, 'Roadmap.canvas');
|
||||
assert.deepEqual(await v.listHistory(next), kept);
|
||||
assert.deepEqual(await v.listHistory(id), []);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('duplicate: a page (with its sub-pages), a board and a canvas, next to the original', async () => {
|
||||
const root = await tmp();
|
||||
try {
|
||||
const v = new Vault(root);
|
||||
const id = await v.create(null, 'Plan', { icon: 'star' }, 'Hello\n');
|
||||
const child = await v.create(id, 'Child', {}, 'inside\n');
|
||||
const pic = await v.saveImage(id, 'pic.png', Buffer.from([1, 2, 3]));
|
||||
const copy = await v.duplicate(id);
|
||||
assert.equal(copy, 'Plan copy');
|
||||
assert.deepEqual(await readFile(v.abs(copy), 'utf8'), await readFile(v.abs(id), 'utf8'), 'the file is copied as it is');
|
||||
assert.equal((await v.read(`${copy}/Child`)).body, 'inside\n', 'sub-pages come along');
|
||||
assert.ok(v.imagePath(copy, pic) && (await readFile(v.imagePath(copy, pic))).length === 3, 'pictures (kept beside the page) are still found by the copy');
|
||||
// the copy is its own page
|
||||
await v.write(copy, {}, 'changed\n');
|
||||
assert.equal((await v.read(id)).body, 'Hello\n');
|
||||
assert.equal((await v.read(child)).body, 'inside\n');
|
||||
// again: a free name
|
||||
assert.equal(await v.duplicate(id), 'Plan copy 2');
|
||||
// the copy of a copy
|
||||
assert.equal(await v.duplicate(copy), 'Plan copy copy');
|
||||
// inside a parent: stays there
|
||||
assert.equal(await v.duplicate(child), `${id}/Child copy`);
|
||||
// a canvas keeps its text exactly
|
||||
const cv = await v.createCanvas(null, 'Map');
|
||||
const text = '{\n\t"nodes":[\n\t\t{"id":"aaaaaaaaaaaaaaaa","type":"text","text":"hi","x":0,"y":0,"width":250,"height":60}\n\t],\n\t"edges":[]\n}';
|
||||
await v.write(cv, {}, text);
|
||||
const cvCopy = await v.duplicate(cv);
|
||||
assert.equal(cvCopy, 'Map copy.canvas');
|
||||
assert.equal(await readFile(v.abs(cvCopy), 'utf8'), text);
|
||||
assert.equal((await v.listHistory(cvCopy)).length, 1, 'the copy starts its own history');
|
||||
// nothing to copy
|
||||
await assert.rejects(v.duplicate('Nope'), /no file to copy/);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
test('a database record keeps its property values in front matter, and the list reports them', async () => {
|
||||
const root = await tmp();
|
||||
const v = new Vault(root);
|
||||
const db = await v.create(null, 'Tasks', { 'ink-database': 'basic' }, '%% ink-database\n{}\n%%\n');
|
||||
const rec = await v.create(db, 'Write', { status: 'Doing', due: '2026-03-01', owner: ['Ana', 'Ben'], done: 'true', 'due-date': '2026-04-02' });
|
||||
const pages = await v.list();
|
||||
const d = pages.find((p) => p.id === db), r = pages.find((p) => p.id === rec);
|
||||
assert.equal(d.db, true);
|
||||
assert.equal(r.db, undefined);
|
||||
assert.deepEqual(r.props, { status: 'Doing', due: '2026-03-01', owner: ['Ana', 'Ben'], done: 'true', 'due-date': '2026-04-02' });
|
||||
assert.equal(r.parent, 'Tasks');
|
||||
assert.equal(r.tags.length, 0); // page keys are not properties
|
||||
// a value change keeps every other key and the body
|
||||
const before = await v.read(rec);
|
||||
await v.write(rec, { ...before.meta, status: 'Done' }, 'Notes\n');
|
||||
const after = (await v.list()).find((p) => p.id === rec);
|
||||
assert.equal(after.props.status, 'Done');
|
||||
assert.deepEqual(after.props.owner, ['Ana', 'Ben']);
|
||||
assert.equal((await v.read(rec)).body, 'Notes\n');
|
||||
await rm(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('property values are found as words and by key:value, with the same operators as tag: and in:', async () => {
|
||||
const dir = await tmp();
|
||||
const db = openDb(path.join(dir, 'i.sqlite'));
|
||||
db.upsert({ id: 'T/Write docs', title: 'Write docs', tags: ['work'], body: 'Some text', props: { status: 'Doing', points: '5', owner: ['Ana', 'Ben'], due: '2026-03-01' } });
|
||||
db.upsert({ id: 'T/Ship it', title: 'Ship it', tags: [], body: 'Release', props: { status: 'In progress', points: '12', owner: ['Ben'] } });
|
||||
db.upsert({ id: 'T/Plan', title: 'Plan', tags: [], body: 'Roadmap', props: {} });
|
||||
const ids = (q) => db.search(q).map((r) => r.id).sort();
|
||||
assert.deepEqual(ids('Ana'), ['T/Write docs']); // a value, as a plain word
|
||||
assert.deepEqual(ids('status:doing'), ['T/Write docs']);
|
||||
assert.deepEqual(ids('status:do'), ['T/Write docs']); // like tag:, a prefix matches
|
||||
assert.deepEqual(ids('status:"In progress"'), ['T/Ship it']);
|
||||
assert.deepEqual(ids('owner:ben'), ['T/Ship it', 'T/Write docs']);
|
||||
assert.deepEqual(ids('owner:!ana'), ['T/Ship it']);
|
||||
assert.deepEqual(ids('points:>5'), ['T/Ship it']);
|
||||
assert.deepEqual(ids('points:<=5'), ['T/Write docs']);
|
||||
assert.deepEqual(ids('has:due'), ['T/Write docs']);
|
||||
assert.deepEqual(ids('owner:ben release'), ['T/Ship it']); // words narrow it too
|
||||
assert.deepEqual(ids('tag:work status:doing'), ['T/Write docs']);
|
||||
assert.deepEqual(ids('status:done'), []);
|
||||
assert.deepEqual(ids('nokey:value'), []); // not a property: searched as words, which match nothing
|
||||
assert.match(db.search('status:doing')[0].snip, /status: doing/);
|
||||
// a changed value replaces the old one; a removed page leaves no values behind
|
||||
db.upsert({ id: 'T/Write docs', title: 'Write docs', tags: [], body: '', props: { status: 'Done' } });
|
||||
assert.deepEqual(ids('status:doing'), []);
|
||||
assert.deepEqual(ids('status:done'), ['T/Write docs']);
|
||||
db.remove('T');
|
||||
assert.deepEqual(ids('status:done'), []);
|
||||
db.close();
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('an index made before properties were indexed is started again', async () => {
|
||||
const dir = await tmp();
|
||||
const file = path.join(dir, 'old.sqlite');
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const old = new DatabaseSync(file);
|
||||
old.exec("CREATE VIRTUAL TABLE notes USING fts5(id UNINDEXED, title, tags, body, tokenize = 'unicode61 remove_diacritics 2')");
|
||||
old.exec("INSERT INTO notes (id, title, tags, body) VALUES ('A', 'Old', '', 'old words')");
|
||||
old.close();
|
||||
const db = openDb(file);
|
||||
assert.deepEqual(db.search('old'), []); // emptied; the app rebuilds it on launch
|
||||
db.rebuild([{ id: 'A', title: 'Old', tags: [], body: 'old words', exists: true, props: { status: 'Done' } }]);
|
||||
assert.deepEqual(db.search('status:done').map((r) => r.id), ['A']);
|
||||
db.close();
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('links in property values and relation properties are links', () => {
|
||||
const links = require('../electron/links.cjs');
|
||||
const schema = '%% ink-database\n{"properties":[{"key":"after","name":"After","type":"relation"},{"key":"ref","name":"Ref","type":"text"}],"views":[]}\n%%\n';
|
||||
const pages = [
|
||||
{ id: 'Tasks', title: 'Tasks', exists: true, parent: null, body: schema, props: {} },
|
||||
{ id: 'Tasks/A', title: 'A', exists: true, parent: 'Tasks', body: '', props: { after: ['B'], ref: 'see [[Spec]]' } },
|
||||
{ id: 'Tasks/B', title: 'B', exists: true, parent: 'Tasks', body: '', props: {} },
|
||||
{ id: 'Spec', title: 'Spec', exists: true, parent: null, body: '', props: {} }
|
||||
];
|
||||
const g = links.graph(pages);
|
||||
assert.ok(g.edges.some(([a, b]) => a === 'Tasks/A' && b === 'Spec'), 'link in a property value');
|
||||
assert.ok(g.edges.some(([a, b]) => a === 'Tasks/A' && b === 'Tasks/B'), 'relation');
|
||||
assert.deepEqual(links.backlinks('Spec', pages).map((b) => b.id), ['Tasks/A']);
|
||||
assert.deepEqual(links.backlinks('Tasks/B', pages).map((b) => [b.id, b.snip]), [['Tasks/A', 'After']]);
|
||||
const after = pages.map((p) => (p.id === 'Spec' ? { ...p, id: 'Specs', title: 'Specs' } : p));
|
||||
const patch = links.rewriteProps(pages[1].props, pages, after, (id) => (id === 'Spec' ? 'Specs' : null));
|
||||
assert.deepEqual(patch, { ref: 'see [[Specs]]' });
|
||||
assert.equal(links.rewriteProps({ after: ['B'] }, pages, after, () => null), null);
|
||||
});
|
||||
|
||||
test('the watcher reports changes made outside, also after the app has saved and moved things', async () => {
|
||||
const root = await tmp();
|
||||
const v = new Vault(root);
|
||||
await v.create(null, 'A', {}, 'a\n');
|
||||
await mkdir(path.join(root, 'Sub'));
|
||||
await writeFile(path.join(root, 'Sub', 'Inner.md'), 'inner\n');
|
||||
const seen = [];
|
||||
const stop = v.watch((ids) => seen.push(...ids));
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const expectId = async (id, label) => {
|
||||
for (let i = 0; i < 30 && !seen.includes(id); i++) await sleep(100);
|
||||
assert.ok(seen.includes(id), `${label}: expected ${id}, saw ${JSON.stringify(seen)}`);
|
||||
seen.length = 0;
|
||||
};
|
||||
await sleep(300);
|
||||
await writeFile(path.join(root, 'Outside.md'), 'x');
|
||||
await expectId('Outside', 'a new file');
|
||||
await writeFile(path.join(root, 'Sub', 'Inner.md'), 'changed\n');
|
||||
await expectId('Sub/Inner', 'a file in a folder');
|
||||
// the app saves (which makes .notes/history): outside changes must still come through
|
||||
await sleep(1700); // past the "just written by us" window
|
||||
await v.write('A', {}, 'mine\n');
|
||||
await sleep(1700);
|
||||
seen.length = 0;
|
||||
await writeFile(path.join(root, 'Outside.md'), 'again');
|
||||
await expectId('Outside', 'after the app saved');
|
||||
// a folder made later is watched too
|
||||
await mkdir(path.join(root, 'Later'));
|
||||
await sleep(500);
|
||||
await writeFile(path.join(root, 'Later', 'New.md'), 'n');
|
||||
await expectId('Later/New', 'a folder created after the watch began');
|
||||
// a renamed folder reports under its new name
|
||||
await rm(path.join(root, 'Sub', 'Inner.md'));
|
||||
await sleep(300);
|
||||
seen.length = 0;
|
||||
await (await import('node:fs/promises')).rename(path.join(root, 'Later'), path.join(root, 'Moved'));
|
||||
await sleep(900);
|
||||
seen.length = 0;
|
||||
await writeFile(path.join(root, 'Moved', 'New.md'), 'edited');
|
||||
await expectId('Moved/New', 'a renamed folder');
|
||||
// own writes and dot folders stay quiet
|
||||
seen.length = 0;
|
||||
await sleep(1700);
|
||||
await v.write('A', {}, 'again\n');
|
||||
await sleep(900);
|
||||
assert.deepEqual(seen, [], 'our own write is not reported');
|
||||
stop();
|
||||
await rm(root, { recursive: true, force: true });
|
||||
});
|
||||
Reference in new issue
Block a user