145 lines
10 KiB
JavaScript
145 lines
10 KiB
JavaScript
// 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);
|
|
});
|