// 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
\nTitle\n\nBody text\n\n
\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); });