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

+144
View File
@@ -0,0 +1,144 @@
// End-to-end check of columns: dragging into, out of and beside them, the 3-column cap, resizing, Backspace in an
// empty column, arrow keys, and select-all + delete. Run with `bun run e2e columns`.
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-columns-'));
const vault = path.join(tmp, 'Vault');
fs.mkdirSync(vault);
const PAGE = 'Top line one\n\nTop line two\n\n:::: columns\n::: column\nLeft A\n\nLeft B\n:::\n::: column\nRight A\n:::\n::::\n\nBottom line\n\nLast line\n';
fs.writeFileSync(path.join(vault, 'Cols.md'), PAGE);
app.setPath('userData', tmp);
fs.writeFileSync(path.join(tmp, 'settings.json'), JSON.stringify({ vault, lastPage: 'Cols', theme: 'dark', bounds: { width: 1280, height: 900 } }));
require('../electron/main.cjs');
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const failures = [];
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 key = async (keyCode, modifiers = []) => { wc.sendInputEvent({ type: 'keyDown', keyCode, modifiers }); if (keyCode.length === 1 && !modifiers.length) wc.sendInputEvent({ type: 'char', keyCode }); wc.sendInputEvent({ type: 'keyUp', keyCode, modifiers }); await sleep(200); };
const file = () => fs.readFileSync(path.join(vault, 'Cols.md'), 'utf8');
// the page as [top-level text | [column texts...]] in order
const shape = () => js(`return [...document.querySelector('.blocks').children].filter((n) => n.matches('.block, .cols')).map((n) => n.matches('.cols') ? [...n.querySelectorAll(':scope > .col')].map((c) => [...c.querySelectorAll(':scope > .block .c')].map((x) => x.textContent)) : n.querySelector('.c')?.textContent)`);
const rectOf = (expr) => js(`const e = ${expr}; if (!e) return null; const r = e.getBoundingClientRect(); return { x: r.x, y: r.y, w: r.width, h: r.height, cx: r.x + r.width / 2, cy: r.y + r.height / 2, right: r.right, bottom: r.bottom }`);
const blockEl = (text) => `[...document.querySelectorAll('.blocks .block')].find((b) => b.querySelector('.c')?.textContent === ${JSON.stringify(text)})`;
const m = (type, x, y, extra = {}) => wc.sendInputEvent({ type, x: Math.round(x), y: Math.round(y), ...extra });
// drag a block by its grip to (x, y)
const dragBlock = async (text, x, y) => {
const b = await rectOf(blockEl(text));
m('mouseMove', b.x + 20, b.cy);
await sleep(150);
const g = await rectOf(`${blockEl(text)}.querySelector('.g-drag')`);
m('mouseMove', g.cx, g.cy);
m('mouseDown', g.cx, g.cy, { button: 'left', clickCount: 1 });
for (let i = 1; i <= 14; i++) { m('mouseMove', g.cx + ((x - g.cx) * i) / 14, g.cy + ((y - g.cy) * i) / 14, { button: 'left' }); await sleep(20); }
await sleep(120);
m('mouseUp', x, y, { button: 'left', clickCount: 1 });
await sleep(500);
};
const S = (v) => JSON.stringify(v);
try {
await sleep(2200);
check('the page opens with its columns', S(await shape()) === S(['Top line one', 'Top line two', [['Left A', 'Left B'], ['Right A']], 'Bottom line', 'Last line']), S(await shape()));
// ---- dragging ----
let ra = await rectOf(blockEl('Right A'));
await dragBlock('Top line two', ra.cx, ra.bottom + 6);
check('a block dragged into a column lands in it', S(await shape()) === S(['Top line one', [['Left A', 'Left B'], ['Right A', 'Top line two']], 'Bottom line', 'Last line']), S(await shape()));
let bl = await rectOf(blockEl('Bottom line'));
await dragBlock('Left B', bl.x + 60, bl.bottom + 4);
check('a block dragged out of a column becomes an ordinary block again', S(await shape()) === S(['Top line one', [['Left A'], ['Right A', 'Top line two']], 'Bottom line', 'Left B', 'Last line']), S(await shape()));
bl = await rectOf(blockEl('Last line'));
await dragBlock('Left A', bl.x + 60, bl.bottom + 4);
check('emptying a column removes it; a single column left is no group any more', S(await shape()) === S(['Top line one', 'Right A', 'Top line two', 'Bottom line', 'Left B', 'Last line', 'Left A']), S(await shape()));
await sleep(1500);
check('...and the file has no columns then', !file().includes(':::'), S(file()));
// beside a block: the two become columns
let t1 = await rectOf(blockEl('Top line one'));
await dragBlock('Bottom line', t1.right - 30, t1.cy);
check('dropping beside a block makes columns of the two', S(await shape()) === S([[['Top line one'], ['Bottom line']], 'Right A', 'Top line two', 'Left B', 'Last line', 'Left A']), S(await shape()));
// the gap / outer edge of a group: a new column
let cols = await rectOf(`document.querySelector('.cols')`);
await dragBlock('Left B', cols.right - 4, cols.cy);
check('dropping at the edge of a group adds a column', S(await shape()) === S([[['Top line one'], ['Bottom line'], ['Left B']], 'Right A', 'Top line two', 'Last line', 'Left A']), S(await shape()));
// ---- at most three ----
check('with three columns the + button is gone', await js(`const w = document.querySelector('.cols'); return w.classList.contains('full') && getComputedStyle(w.querySelector('.cols-add')).display === 'none'`));
await js(`document.querySelector('.cols .cols-add').click()`);
await sleep(300);
cols = await rectOf(`document.querySelector('.cols')`);
await dragBlock('Top line two', cols.right - 4, cols.cy);
const c0 = await rectOf(`document.querySelectorAll('.cols > .col')[0]`), c1 = await rectOf(`document.querySelectorAll('.cols > .col')[1]`);
await dragBlock('Right A', (c0.right + c1.x) / 2, c0.cy);
check('...and neither the button nor a drop makes a fourth', (await shape())[0].length === 3, S(await shape()));
await sleep(1500);
check('three columns are saved', (file().match(/^::: column$/gm) || []).length === 3, S(file()));
// ---- resizing ----
const h = await rectOf(`document.querySelectorAll('.cols > .col')[1].querySelector('.col-rs')`);
m('mouseMove', h.cx, h.cy);
m('mouseDown', h.cx, h.cy, { button: 'left', clickCount: 1 });
for (let i = 1; i <= 8; i++) { m('mouseMove', h.cx + i * 10, h.cy, { button: 'left' }); await sleep(20); }
m('mouseUp', h.cx + 80, h.cy, { button: 'left', clickCount: 1 });
await sleep(400);
const w0 = (await rectOf(`document.querySelectorAll('.cols > .col')[0]`)).w, w1 = (await rectOf(`document.querySelectorAll('.cols > .col')[1]`)).w;
check('dragging the handle between two columns changes their widths', w0 - w1 > 100, `${w0} vs ${w1}`);
await sleep(1500);
check('...and the widths are saved', /::: \{\.column width="[\d.]+%"\}/.test(file()), S(file().slice(0, 200)));
const h2 = await rectOf(`document.querySelectorAll('.cols > .col')[1].querySelector('.col-rs')`);
m('mouseMove', h2.cx, h2.cy);
m('mouseDown', h2.cx, h2.cy, { button: 'left', clickCount: 1 }); m('mouseUp', h2.cx, h2.cy, { button: 'left', clickCount: 1 });
m('mouseDown', h2.cx, h2.cy, { button: 'left', clickCount: 2 }); m('mouseUp', h2.cx, h2.cy, { button: 'left', clickCount: 2 });
await sleep(1800);
const ws = await js(`return [...document.querySelectorAll('.cols > .col')].map((c) => Math.round(c.getBoundingClientRect().width))`);
check('double-clicking the handle makes them equal again', Math.max(...ws) - Math.min(...ws) <= 2 && !file().includes('width='), S(ws));
// ---- Backspace in an empty column, arrow keys ----
await js(`const c = document.querySelectorAll('.cols > .col')[2].querySelector('.c'); c.focus();`);
await sleep(100);
await key('End');
await key('a', ['control']); // (Left B's column: empty it first)
await key('Backspace');
await key('Backspace');
await sleep(300);
check('Backspace in an empty column removes that column', (await shape())[0].length === 2, S(await shape()));
await js(`const c = document.querySelectorAll('.cols > .col')[0].querySelector('.c'); c.focus(); const r = document.createRange(); r.selectNodeContents(c); r.collapse(false); getSelection().removeAllRanges(); getSelection().addRange(r);`);
await key('Down');
check('ArrowDown at the bottom of a column leaves the group (not into the next column)', await js(`return !document.activeElement.closest('.cols') && document.activeElement.textContent === 'Right A'`), await js(`return document.activeElement.textContent`));
// ---- select all, then delete ----
await js(`const c = ${blockEl('Last line')}.querySelector('.c'); c.focus(); const r = document.createRange(); r.setStart(c.firstChild, 2); r.collapse(true); getSelection().removeAllRanges(); getSelection().addRange(r);`);
// a selection over two lines: the start of one and the end of the other join
await js(`const a = ${blockEl('Top line two')}.querySelector('.c').firstChild, z = ${blockEl('Last line')}.querySelector('.c').firstChild; const r = document.createRange(); r.setStart(a, 3); r.setEnd(z, 4); getSelection().removeAllRanges(); getSelection().addRange(r);`);
await key('Backspace');
check('Backspace over a selection across lines deletes it and joins the two ends', S((await shape()).slice(1)) === S(['Right A', 'Top line', 'Left A']), S(await shape()));
await key('a', ['control']);
await key('a', ['control']);
check('(Ctrl+A twice selects the whole page)', await js(`return getSelection().toString().includes('Top line one') && getSelection().toString().includes('Left A')`));
await key('Backspace');
await sleep(300);
check('...and Backspace then empties the page, columns and all', S(await shape()) === S(['']) && !(await js(`return !!document.querySelector('.cols')`)), S(await shape()));
await key('x');
await sleep(1500);
check('typing goes into the now empty page', S(await shape()) === S(['x']) && file().trim().endsWith('x') && !file().includes('Top line'), S(file()));
await key('z', ['control']);
await sleep(400);
check('Ctrl+Z brings it all back', (await shape()).length > 2, S(await shape()));
} catch (err) {
check('harness', false, err.stack);
}
fs.rmSync(tmp, { recursive: true, force: true });
console.log(failures.length ? `\n${failures.length} check(s) failed` : '\ncolumns e2e passed!');
app.exit(failures.length ? 1 : 0);
});