test: unit and end-to-end suites

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Your NameandClaude Sonnet 5.5 committed 2026-10-15 11:57:09 +02:00
1 parent 27fafa0bcd
commit 9b00e21c44
49 files changed
+6694 -1

No files matched your search

+145
View File
@@ -0,0 +1,145 @@
// End-to-end check of dragging in a Kanban board: the card follows the pointer at once, never starts from the
// window's corner, and costs little per move. Run with `bun run test:e2e`.
const { app, BrowserWindow } = require('electron');
const path = require('path');
const os = require('os');
const fs = require('fs');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ink-e2e-board-drag-'));
const vault = path.join(tmp, 'Vault');
fs.mkdirSync(vault);
const cards = (n, tag) => Array.from({ length: n }, (_, i) => `- [ ] ${tag} card ${i + 1} with a few words of text`).join('\n');
fs.writeFileSync(path.join(vault, 'Board.md'), `---\nkanban-plugin: basic\n---\n\n## To do\n\n${cards(4, 'todo')}\n\n\n## Doing\n\n${cards(2, 'doing')}\n\n\n## Done\n\n`);
// a big board, to see what a drag costs when there is a lot on the page
fs.writeFileSync(path.join(vault, 'Big.md'), `---\nkanban-plugin: basic\n---\n\n` + ['A', 'B', 'C', 'D'].map((c) => `## Column ${c}\n\n${cards(60, c)}\n`).join('\n\n') + '\n');
app.setPath('userData', tmp);
fs.writeFileSync(path.join(tmp, 'settings.json'), JSON.stringify({ vault, lastPage: 'Board', theme: 'dark', bounds: { width: 1400, height: 900 } }));
require('../electron/main.cjs');
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const failures = [];
// the board file as { column: [card texts] }
const columnsOf = (md) => Object.fromEntries(md.split(/\n## /).slice(1).map((sec) => { const [title, ...rest] = sec.split('\n'); return [title.trim(), rest.filter((l) => l.startsWith('- [')).map((l) => l.replace(/^- \[.\] /, '').replace(/ with a few words of text$/, ''))]; }));
const check = (name, ok, extra = '') => {
console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${extra ? ' — ' + extra : ''}`);
if (!ok) failures.push(name);
};
app.whenReady().then(async () => {
let win;
for (let i = 0; i < 80 && !(win = BrowserWindow.getAllWindows()[0]); i++) await sleep(100);
const wc = win.webContents;
if (wc.isLoading()) await new Promise((r) => wc.once('did-finish-load', r));
const js = (code) => wc.executeJavaScript(`(async () => { ${code} })()`);
const mouse = (type, x, y) => wc.sendInputEvent({ type, x: Math.round(x), y: Math.round(y), button: 'left', clickCount: 1 });
const read = () => fs.readFileSync(path.join(vault, 'Board.md'), 'utf8');
const centerOf = (sel) => js(`const e = ${sel}; if (!e) return null; const r = e.getBoundingClientRect(); return [r.x + r.width / 2, r.y + r.height / 2, r.x, r.y, r.width, r.height]`);
const ghost = () => js(`const g = document.querySelector('.kghost'); if (!g) return null; const r = g.getBoundingClientRect(); return [r.x + r.width / 2, r.y + r.height / 2, getComputedStyle(g).transitionProperty, getComputedStyle(g).transitionDuration]`);
const card = (text) => `[...document.querySelectorAll('.kcard')].find(k => k.textContent.includes(${JSON.stringify(text)}))`;
try {
await sleep(2200);
// a frame log: where is the floating card at every animation frame?
await js(`window.__frames = []; const tick = () => { const g = document.querySelector('.kghost'); if (g) { const r = g.getBoundingClientRect(); window.__frames.push([Math.round(r.x + r.width / 2), Math.round(r.y + r.height / 2)]); } requestAnimationFrame(tick); }; tick();`);
// ---- grab a card and start moving ----
const c = await centerOf(card('todo card 2'));
mouse('mouseMove', c[0], c[1]);
mouse('mouseDown', c[0], c[1]);
await sleep(40);
// The pointer's offset inside the card is kept, so the floating card's centre = pointer + (card centre - grab point).
const dx = c[0] - c[0], dy = c[1] - c[1];
const moves = [[c[0] + 30, c[1] + 10], [c[0] + 120, c[1] + 40], [c[0] + 260, c[1] + 70]];
const seen = [];
for (const [x, y] of moves) {
mouse('mouseMove', x, y);
await sleep(40); // two frames: more than enough for something that follows the pointer
const g = await ghost();
seen.push({ want: [Math.round(x + dx), Math.round(y + dy)], got: g && [Math.round(g[0]), Math.round(g[1])], tr: g && g[2] + ' ' + g[3] });
}
const off = seen.map((s) => (s.got ? Math.hypot(s.got[0] - s.want[0], s.got[1] - s.want[1]) : Infinity));
check('the floating card is under the pointer 40 ms after every move', off.every((d) => d < 12), JSON.stringify(seen));
check('its position is not animated (no transition on it)', seen.every((s) => /^0s(, 0s)*$/.test((s.tr || '').split(' ').slice(1).join(' ')) || /none/.test(s.tr?.split(' ')[0] || '')), JSON.stringify(seen.map((s) => s.tr)));
const frames = await js(`return window.__frames`);
const nearCorner = frames.filter(([x, y]) => x < 150 && y < 150);
check('it never shows up in the window\'s top-left corner', nearCorner.length === 0, `${nearCorner.length} of ${frames.length} frames: ${JSON.stringify(nearCorner.slice(0, 4))}`);
// ---- quick, fine-grained movement ----
const last = moves[moves.length - 1];
for (let i = 1; i <= 40; i++) { mouse('mouseMove', last[0] + i * 4, last[1] + i * 2); await sleep(6); }
await sleep(40);
const g2 = await ghost();
const want2 = [last[0] + 160, last[1] + 80];
check('it keeps up with fast movement', g2 && Math.hypot(g2[0] - want2[0], g2[1] - want2[1]) < 12, JSON.stringify({ want: want2.map(Math.round), got: g2 && g2.slice(0, 2).map(Math.round) }));
// ---- finishing the drag still moves the card ----
const target = await centerOf(card('doing card 1'));
mouse('mouseMove', target[0], target[1] + 5);
await sleep(100);
mouse('mouseUp', target[0], target[1] + 5);
await sleep(900);
const cols = columnsOf(read());
check('dropping moves the card to the other column, at the spot it was dropped', JSON.stringify(cols['To do']) === '["todo card 1","todo card 3","todo card 4"]' && JSON.stringify(cols.Doing) === '["doing card 1","todo card 2","doing card 2"]', JSON.stringify(cols));
// ---- dragging a whole column ----
const head = await centerOf(`[...document.querySelectorAll('.kcol')][0].querySelector('.kh-n')`);
const doneHead = await centerOf(`[...document.querySelectorAll('.kcol')][2].querySelector('.khead')`);
mouse('mouseMove', head[0], head[1]);
mouse('mouseDown', head[0], head[1]);
await sleep(40);
mouse('mouseMove', head[0] + 60, head[1] + 4);
await sleep(60);
const gc = await ghost();
check('a dragged column follows the pointer too', gc && Math.hypot(gc[0] - (head[0] + 60 + (await centerOf(`[...document.querySelectorAll('.kcol')][0]`))[0] - head[0]), gc[1] - (head[1] + 4 + (await centerOf(`[...document.querySelectorAll('.kcol')][0]`))[1] - head[1])) < 14 && /none/.test(gc[2]), JSON.stringify(gc));
mouse('mouseMove', doneHead[0] + 120, doneHead[1]);
await sleep(100);
mouse('mouseUp', doneHead[0] + 120, doneHead[1]);
await sleep(900);
check('...and is dropped in its new place', Object.keys(columnsOf(read())).join() === 'Doing,Done,To do', Object.keys(columnsOf(read())).join());
// ---- a lot on the page: moving over it costs little ----
await js(`[...document.querySelectorAll('.tree .row')].find(r => r.dataset.id === 'Big').click()`);
await sleep(1800);
await js(`window.__long = []; try { new PerformanceObserver((l) => l.getEntries().forEach((e) => window.__long.push(Math.round(e.duration)))).observe({ entryTypes: ['longtask'] }); } catch {}`);
await js(`window.__muts = 0; window.__mo = new MutationObserver((l) => { window.__muts += l.length; }); window.__mo.observe(document.querySelector('.board'), { subtree: true, attributes: true, childList: true });`);
const b = await centerOf(card('A card 5'));
mouse('mouseMove', b[0], b[1]);
mouse('mouseDown', b[0], b[1]);
await sleep(60);
mouse('mouseMove', b[0] + 20, b[1] + 20);
await sleep(120);
await js(`window.__muts = 0`);
// 60 pointer events that all stay over the same drop position
for (let i = 0; i < 60; i++) { mouse('mouseMove', b[0] + 20 + (i % 6), b[1] + 20 + (i % 4)); await sleep(5); }
await sleep(100);
const muts = await js(`return window.__muts`);
check('moving within one drop position barely touches the page', muts <= 6, `${muts} DOM changes for 60 moves`);
// sweep down a column and across
for (let i = 0; i < 80; i++) { mouse('mouseMove', b[0] + i * 7, b[1] + 20 + i * 5); await sleep(8); }
await sleep(150);
mouse('mouseUp', b[0] + 80 * 7, b[1] + 20 + 80 * 5);
await sleep(500);
// the card is let go over the right edge of the board: sit there without moving
const edge = await js(`const r = document.querySelector('.board').getBoundingClientRect(); return [Math.round(r.right - 20), Math.round(r.top + 120)]`);
mouse('mouseMove', b[0], b[1]);
mouse('mouseDown', b[0], b[1]);
await sleep(60);
mouse('mouseMove', b[0] + 15, b[1] + 15);
await sleep(60);
mouse('mouseMove', edge[0], edge[1]);
await sleep(150);
const s1 = await js(`return document.querySelector('.board').scrollLeft`);
await sleep(400);
const s2 = await js(`return document.querySelector('.board').scrollLeft`);
mouse('mouseUp', edge[0], edge[1]);
await sleep(500);
check('holding a card at the board\'s edge keeps scrolling, without moving the mouse', s2 > s1 + 20, `${s1} -> ${s2}`);
const long = await js(`return window.__long`);
check('no long freezes while dragging over 240 cards', long.every((d) => d < 120), JSON.stringify(long));
} catch (err) {
check('harness', false, err.stack);
}
fs.rmSync(tmp, { recursive: true, force: true });
console.log(failures.length ? `\n${failures.length} check(s) failed` : '\nboard drag e2e passed!');
app.exit(failures.length ? 1 : 0);
});