Files
ink-note-app/test/e2e-toggle.test.cjs
2026-10-15 11:57:09 +02:00

106 lines
6.9 KiB
JavaScript

// End-to-end check of a toggle's body: arrows in and out of it, Enter / Shift+Enter, and selecting across title and body.
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-toggle-'));
const vault = path.join(tmp, 'Vault');
fs.mkdirSync(vault);
fs.writeFileSync(path.join(vault, 'Note.md'), 'Before the toggle.\n\n<details>\n<summary>Title</summary>\n\nBody text\n\n</details>\n\nAfter the toggle.\n');
app.setPath('userData', tmp);
fs.writeFileSync(path.join(tmp, 'settings.json'), JSON.stringify({ vault, lastPage: 'Note', bounds: { width: 1200, height: 800 } }));
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 }); wc.sendInputEvent({ type: 'keyUp', keyCode, modifiers }); await sleep(250); };
const type = async (t) => { for (const ch of t) wc.sendInputEvent({ type: 'char', keyCode: ch }); await sleep(250); };
// which field has the caret: 'head', 'body', or the text of another block's field
const where = () => js(`const n = getSelection().focusNode; const f = (n?.nodeType === 1 ? n : n?.parentElement)?.closest('.c, .c2');
if (!f) return null; if (f.closest('.tg')) return f.classList.contains('c2') ? 'body' : 'head'; return f.textContent;`);
const blockTypes = () => js(`return [...document.querySelectorAll('.page .block')].map(b => b.querySelector('.tg') ? 'toggle' : (b.querySelector('.c')?.textContent ?? '?'))`);
const rect = (sel) => js(`const r = document.querySelector('${sel}').getBoundingClientRect(); return { x: Math.round(r.x), y: Math.round(r.y + r.height / 2), w: Math.round(r.width), b: Math.round(r.bottom) }`);
try {
for (let i = 0; i < 60 && !(await js(`return !!document.querySelector('.tg')`)); i++) await sleep(150);
// open the toggle
await js(`document.querySelector('.tg-chev').click()`);
await sleep(500);
// caret at the end of the first paragraph, then down: title, body, next block
await js(`const c = document.querySelector('.block .c'); c.focus(); const r = document.createRange(); r.selectNodeContents(c); r.collapse(false); getSelection().removeAllRanges(); getSelection().addRange(r);`);
await key('Down');
check('Down from the block above enters the toggle title', (await where()) === 'head', await where());
await key('End');
await key('Down');
check('Down at the end of the title enters the body', (await where()) === 'body', await where());
await key('End');
await key('Down');
check('Down at the end of the body leaves the toggle', (await where()) === 'After the toggle.', await where());
await key('Home');
await key('Up');
check('Up from the block below enters the body', (await where()) === 'body', await where());
await key('Home');
await key('Up');
check('Up at the start of the body goes to the title', (await where()) === 'head', await where());
// Shift+Enter: a new line in the body; Enter: a new block after the toggle
await js(`const c = document.querySelector('.c2'); c.focus(); const r = document.createRange(); r.selectNodeContents(c); r.collapse(false); getSelection().removeAllRanges(); getSelection().addRange(r);`);
await key('Enter', ['shift']);
await type('second');
const body = await js(`return document.querySelector('.c2').innerText`);
check('Shift+Enter makes a new line inside the body', /Body text\s*\nsecond/.test(body), JSON.stringify(body));
await key('Enter');
await type('new');
const types = await blockTypes();
check('Enter in the body starts a new block after the toggle', JSON.stringify(types) === JSON.stringify(['Before the toggle.', 'toggle', 'new', 'After the toggle.']), JSON.stringify(types));
check('...with the caret in it', (await where()) === 'new', await where());
const body2 = await js(`return document.querySelector('.c2').innerText.trim()`);
check('...and the body keeps its text', /^Body text\s*\nsecond$/.test(body2), JSON.stringify(body2));
// mouse: drag from the title into the body
const head = await rect('.tg .c'), b2 = await rect('.c2');
wc.sendInputEvent({ type: 'mouseMove', x: head.x + 3, y: head.y });
wc.sendInputEvent({ type: 'mouseDown', x: head.x + 3, y: head.y, button: 'left', clickCount: 1 });
for (let i = 1; i <= 10; i++) {
wc.sendInputEvent({ type: 'mouseMove', x: Math.round(head.x + 3 + (b2.x + 40 - head.x - 3) * i / 10), y: Math.round(head.y + (b2.b - 6 - head.y) * i / 10), button: 'left' });
await sleep(25);
}
wc.sendInputEvent({ type: 'mouseUp', x: b2.x + 40, y: b2.b - 6, button: 'left', clickCount: 1 });
await sleep(300);
const picked = await js(`const s = getSelection(); return s.rangeCount ? s.getRangeAt(0).toString() : ''`);
check('Dragging from the title into the body selects both', /itle/.test(picked) && /Body/.test(picked), JSON.stringify(picked));
// Shift+Down from the title extends into the body
await js(`const c = document.querySelector('.tg .c'); c.focus(); const r = document.createRange(); r.setStart(c.firstChild, 0); r.collapse(true); getSelection().removeAllRanges(); getSelection().addRange(r);`);
await key('Down', ['shift']);
await key('Down', ['shift']);
const picked2 = await js(`const s = getSelection(); return s.rangeCount ? s.getRangeAt(0).toString() : ''`);
check('Shift+Down from the title extends into the body', /Title/.test(picked2) && /Body/.test(picked2), JSON.stringify(picked2));
// Backspace over a title-to-body selection: the title keeps what was before, the body what was after
await js(`const h = document.querySelector('.tg .c'), b = document.querySelector('.c2'); const t = [...b.childNodes].find(n => n.nodeType === 3 && n.textContent.includes('Body'));
getSelection().setBaseAndExtent(h.firstChild, 2, t, 5);`);
await key('Backspace');
const after = await js(`return [document.querySelector('.tg .c').textContent, document.querySelector('.c2').innerText.trim()]`);
check('Backspace over title + body removes just the selection', after[0] === 'Ti' && /^text\s*\nsecond$/.test(after[1]), JSON.stringify(after));
} catch (err) {
check('harness', false, err.stack);
}
fs.rmSync(tmp, { recursive: true, force: true });
console.log(failures.length ? `\n${failures.length} check(s) failed` : '\ntoggle e2e passed!');
app.exit(failures.length ? 1 : 0);
});