123 lines
8.0 KiB
JavaScript
123 lines
8.0 KiB
JavaScript
// End-to-end check of the outline panel. Run with `bun run test:e2e` (needs a display or --ozone-platform=headless).
|
|
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-outline-'));
|
|
const vault = path.join(tmp, 'Vault');
|
|
fs.mkdirSync(vault);
|
|
|
|
const filler = (n) => Array.from({ length: n }, (_, i) => `Paragraph ${i + 1} with a few words so the page gets tall enough to scroll.`).join('\n\n');
|
|
fs.writeFileSync(path.join(vault, 'Long.md'), [
|
|
'# Intro', filler(14), '## Setup', filler(14), '### Install', filler(14), '## Usage', filler(14), '# Appendix', filler(14), 'Last line',
|
|
].join('\n\n') + '\n');
|
|
fs.writeFileSync(path.join(vault, 'Plain.md'), 'Just text, no headings.\n');
|
|
fs.writeFileSync(path.join(vault, 'Tasks.md'), '---\nkanban-plugin: basic\n---\n\n## To do\n\n- [ ] One\n\n\n## Done\n');
|
|
|
|
app.setPath('userData', tmp);
|
|
const settingsFile = path.join(tmp, 'settings.json');
|
|
fs.writeFileSync(settingsFile, JSON.stringify({ vault, lastPage: 'Long', theme: 'dark', bounds: { width: 1280, height: 860 } }));
|
|
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(200);
|
|
};
|
|
const type = async (t) => { for (const ch of t) wc.sendInputEvent({ type: 'char', keyCode: ch }); await sleep(200); };
|
|
const click = async (sel) => {
|
|
const c = await js(`const e = ${sel}; if (!e) return null; const r = e.getBoundingClientRect(); return [Math.round(r.x + r.width / 2), Math.round(r.y + r.height / 2)]`);
|
|
if (!c) throw new Error('not found: ' + sel);
|
|
wc.sendInputEvent({ type: 'mouseMove', x: c[0], y: c[1] });
|
|
wc.sendInputEvent({ type: 'mouseDown', x: c[0], y: c[1], button: 'left', clickCount: 1 });
|
|
wc.sendInputEvent({ type: 'mouseUp', x: c[0], y: c[1], button: 'left', clickCount: 1 });
|
|
await sleep(350);
|
|
};
|
|
const items = () => js(`return [...document.querySelectorAll('.ol-item')].map(b => ({ t: b.textContent.trim(), lv: b.style.getPropertyValue('--lv'), on: b.classList.contains('on') }))`);
|
|
const outlineBtn = `document.querySelector('.topbar [aria-label="Outline"]')`;
|
|
const scrollTop = () => js(`return Math.round(document.querySelector('.main .scroller').scrollTop)`);
|
|
const headingTop = (text) => js(`const sc = document.querySelector('.main .scroller'); const h = [...document.querySelectorAll('.blocks .block')].find(b => /^b-h/.test(b.className.match(/b-\\w+/)[0]) && b.textContent.trim() === ${JSON.stringify(text)}); return h ? Math.round(h.getBoundingClientRect().top - sc.getBoundingClientRect().top) : null`);
|
|
// Settings reach the disk ~200 ms after a change.
|
|
const saved = async () => { await sleep(500); return JSON.parse(fs.readFileSync(settingsFile, 'utf8')); };
|
|
|
|
try {
|
|
await sleep(1800);
|
|
check('closed by default', !(await js(`return !!document.querySelector('.outline')`)));
|
|
// (a layout wrapper once reused the class of the property values and silently greyed all page text)
|
|
check('page text has the normal text colour', await js(`return getComputedStyle(document.querySelector('.blocks .c')).color === getComputedStyle(document.body).color`));
|
|
|
|
await click(outlineBtn);
|
|
const list = await items();
|
|
check('lists the headings in order', JSON.stringify(list.map((i) => i.t)) === '["Intro","Setup","Install","Usage","Appendix"]', JSON.stringify(list.map((i) => i.t)));
|
|
check('indents by level', JSON.stringify(list.map((i) => i.lv)) === '["0","1","2","1","0"]', JSON.stringify(list.map((i) => i.lv)));
|
|
check('count shown', (await js(`return document.querySelector('.ol-head em')?.textContent`)) === '5');
|
|
check('remembered in settings', (await saved()).outline === true);
|
|
|
|
await click(`[...document.querySelectorAll('.ol-item')].find(b => b.textContent.trim() === 'Usage')`);
|
|
check('landing flash on the heading', await js(`return !!document.querySelector('.block.flash')`));
|
|
await sleep(900);
|
|
const top = await headingTop('Usage');
|
|
check('click scrolls to the heading', top !== null && top > 0 && top < 120, `heading ${top}px below the top`);
|
|
check('clicked entry is highlighted', (await items()).filter((i) => i.on).map((i) => i.t).join() === 'Usage');
|
|
|
|
await js(`document.querySelector('.main .scroller').scrollTop = 0`);
|
|
await sleep(500);
|
|
check('nothing highlighted above the first heading', (await items()).every((i) => !i.on), JSON.stringify((await items()).filter((i) => i.on)));
|
|
const setupAt = await js(`const sc = document.querySelector('.main .scroller'); const h = [...document.querySelectorAll('.blocks .block')].find(b => b.textContent.trim() === 'Setup'); return Math.round(h.getBoundingClientRect().top - sc.getBoundingClientRect().top + sc.scrollTop)`);
|
|
await js(`document.querySelector('.main .scroller').scrollTop = ${setupAt + 200}`);
|
|
await sleep(500);
|
|
check('highlight follows scrolling', (await items()).filter((i) => i.on).map((i) => i.t).join() === 'Setup', JSON.stringify(await items()));
|
|
|
|
// typing: a new heading appears, its text updates live, deleting it removes it
|
|
await js(`const c = [...document.querySelectorAll('.blocks .c')].at(-1); c.focus(); const r = document.createRange(); r.selectNodeContents(c); r.collapse(false); getSelection().removeAllRanges(); getSelection().addRange(r);`);
|
|
await key('Return');
|
|
await type('## Extra');
|
|
check('typing "## " adds an entry', (await items()).map((i) => i.t).join() === 'Intro,Setup,Install,Usage,Appendix,Extra', (await items()).map((i) => i.t).join());
|
|
await type(' stuff');
|
|
check('entry text follows typing', (await items()).at(-1).t === 'Extra stuff', (await items()).at(-1).t);
|
|
await key('z', ['control']);
|
|
await key('z', ['control']);
|
|
await key('z', ['control']);
|
|
await sleep(300);
|
|
check('undo removes it', (await items()).length === 5, (await items()).map((i) => i.t).join());
|
|
|
|
// pages without headings, and boards
|
|
await click(`[...document.querySelectorAll('.tree .row')].find(r => r.textContent.includes('Plain'))`);
|
|
await sleep(500);
|
|
check('page without headings: hint', (await js(`return document.querySelector('.ol-empty')?.textContent || ''`)).includes('No headings yet'));
|
|
await click(`[...document.querySelectorAll('.tree .row')].find(r => r.textContent.includes('Tasks'))`);
|
|
await sleep(600);
|
|
check('boards: no panel, button disabled', !(await js(`return !!document.querySelector('.outline')`)) && (await js(`return ${outlineBtn}.disabled`)));
|
|
await click(`[...document.querySelectorAll('.tree .row')].find(r => r.textContent.includes('Long'))`);
|
|
await sleep(600);
|
|
check('back on a page: panel is back', (await items()).length === 5);
|
|
|
|
// shortcut and close button
|
|
await key('o', ['control', 'shift']);
|
|
check('Ctrl+Shift+O closes', !(await js(`return !!document.querySelector('.outline')`)) && (await saved()).outline === false);
|
|
await key('o', ['control', 'shift']);
|
|
check('Ctrl+Shift+O opens', await js(`return !!document.querySelector('.outline')`));
|
|
await click(`document.querySelector('.outline [aria-label="Close outline"]')`);
|
|
check('close button', !(await js(`return !!document.querySelector('.outline')`)));
|
|
} catch (err) {
|
|
check('harness', false, err.stack);
|
|
}
|
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
console.log(failures.length ? `\n${failures.length} check(s) failed` : '\noutline e2e passed!');
|
|
app.exit(failures.length ? 1 : 0);
|
|
});
|