99 lines
7.6 KiB
JavaScript
99 lines
7.6 KiB
JavaScript
// End-to-end check of math ($$) and mermaid blocks: they fill the row, are styled, and their preview follows what
|
|
// you type. Run with `bun run test:e2e`.
|
|
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-math-'));
|
|
const vault = path.join(tmp, 'Vault');
|
|
fs.mkdirSync(vault);
|
|
fs.writeFileSync(path.join(vault, 'Page.md'), 'Intro text.\n\n$$\n3+3=12\n$$\n\n```js\nlet a = 1\n```\n\n```mermaid\ngraph TD\nA[Start] --> B[End]\n```\n\nOutro.\n');
|
|
app.setPath('userData', tmp);
|
|
const settingsFile = path.join(tmp, 'settings.json');
|
|
fs.writeFileSync(settingsFile, JSON.stringify({ vault, lastPage: 'Page', 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 type = async (t) => { for (const ch of t) { wc.sendInputEvent({ type: 'char', keyCode: ch }); await sleep(40); } await sleep(200); };
|
|
const read = () => fs.readFileSync(path.join(vault, 'Page.md'), 'utf8');
|
|
const caretEnd = (sel) => js(`const c = ${sel}; c.focus(); const r = document.createRange(); r.selectNodeContents(c); r.collapse(false); getSelection().removeAllRanges(); getSelection().addRange(r);`);
|
|
const wait = async (fn, ms = 6000) => { const t0 = Date.now(); while (Date.now() - t0 < ms) { if (await fn()) return true; await sleep(150); } return false; };
|
|
const widths = () => js(`const w = (s) => Math.round(document.querySelector(s).getBoundingClientRect().width); return { math: w('.math'), mermaid: w('.mermaid-wrap'), code: w('.block .code'), page: w('.blocks') }`);
|
|
const mathPre = `document.querySelector('.math pre')`;
|
|
const mermaidPre = `document.querySelector('.mermaid-wrap pre')`;
|
|
|
|
try {
|
|
await sleep(2200);
|
|
|
|
// ---- layout and style ----
|
|
const w = await widths();
|
|
check('math and mermaid fill the row like a code block', w.math === w.code && w.mermaid === w.code && w.code > 400, JSON.stringify(w));
|
|
const st = await js(`const cs = (s) => getComputedStyle(document.querySelector(s)); return { bg: cs('.math').backgroundColor, ring: cs('.math').boxShadow, font: cs('.math pre').fontFamily, mermaidBg: cs('.mermaid-wrap').backgroundColor }`);
|
|
check('they have a background and a border', st.bg !== 'rgba(0, 0, 0, 0)' && st.mermaidBg !== 'rgba(0, 0, 0, 0)' && st.ring !== 'none', JSON.stringify(st));
|
|
check('the source is in the monospace font', /mono|monospace/i.test(st.font), st.font);
|
|
const label = await js(`const m = document.querySelector('.math').getBoundingClientRect(); const l = document.querySelector('.math .lang').getBoundingClientRect(); return { fromLeft: Math.round(l.left - m.left), inside: l.right <= m.right && l.left >= m.left }`);
|
|
check('the label sits at the left, inside the block', label.fromLeft < 30 && label.inside, JSON.stringify(label));
|
|
const mlabel = await js(`const m = document.querySelector('.mermaid-wrap').getBoundingClientRect(); const l = document.querySelector('.mermaid-wrap .lang').getBoundingClientRect(); return l.left >= m.left && l.right <= m.right`);
|
|
check('...and the mermaid label is not cut off', mlabel);
|
|
check('math and mermaid source has no code colouring (code blocks still do)', (await js(`return !document.querySelector('.math pre span') && !document.querySelector('.mermaid-wrap pre span') && !!document.querySelector('.block .code pre span')`)));
|
|
|
|
// ---- previews ----
|
|
check('the formula is rendered', await js(`return !!document.querySelector('.math-preview .katex') && document.querySelector('.math-preview').textContent.includes('12')`));
|
|
check('the diagram is rendered', await wait(() => js(`return !!document.querySelector('.mermaid-preview svg')`)));
|
|
const darkSvg = await js(`return document.querySelector('.mermaid-preview svg style')?.textContent || ''`);
|
|
check('in the dark theme it uses mermaid\'s dark colours', /fill:#ccc/i.test(darkSvg), darkSvg.match(/fill:#[0-9a-f]{3,6}/gi)?.slice(0, 3).join(' '));
|
|
|
|
// ---- the preview follows the source ----
|
|
await caretEnd(mathPre);
|
|
await type('+4');
|
|
check('typing in the formula updates its preview', await wait(() => js(`return document.querySelector('.math-preview').textContent.includes('4')`)), await js(`return document.querySelector('.math-preview').textContent`));
|
|
await js(`${mathPre}.focus(); document.execCommand('selectAll')`);
|
|
await type('x^2');
|
|
await sleep(400);
|
|
const after = await js(`return document.querySelector('.math-preview').textContent`);
|
|
check('...and replacing it shows only the new formula', after.includes('x') && !after.includes('12') && !after.includes('+4'), JSON.stringify(after));
|
|
await sleep(900);
|
|
check('the source is saved as $$ … $$', read().includes('$$\nx^2\n$$'), JSON.stringify(read().split('\n').slice(0, 6)));
|
|
|
|
// ---- mermaid: errors, recovery, empty ----
|
|
await js(`${mermaidPre}.focus(); document.execCommand('selectAll')`);
|
|
await type('this is not a diagram');
|
|
check('an invalid diagram shows its error, not an empty box', await wait(() => js(`return !!document.querySelector('.mermaid-preview .preview-error')`)), await js(`return document.querySelector('.mermaid-preview').textContent.slice(0, 80)`));
|
|
check('...in the error colour', await js(`const e = document.querySelector('.mermaid-preview .preview-error'); return !!e && getComputedStyle(e).color !== getComputedStyle(document.body).color`));
|
|
check('no stray mermaid elements are left in the page', await js(`return document.querySelectorAll('body > [id^="dmermaid"]').length === 0`));
|
|
await js(`${mermaidPre}.focus(); document.execCommand('selectAll')`);
|
|
await type('graph LR; X-->Y');
|
|
check('fixing it brings the diagram back', await wait(() => js(`return !!document.querySelector('.mermaid-preview svg') && !document.querySelector('.mermaid-preview .preview-error')`)));
|
|
await js(`${mermaidPre}.focus(); document.execCommand('selectAll'); document.execCommand('delete')`);
|
|
await sleep(900);
|
|
check('an empty block shows a hint', await js(`return document.querySelector('.mermaid-preview').textContent.includes('appears here')`));
|
|
|
|
// ---- light theme ----
|
|
await js(`document.documentElement.dataset.theme = 'light'`);
|
|
await caretEnd(mermaidPre);
|
|
await type('graph TD; P-->Q');
|
|
await wait(() => js(`return !!document.querySelector('.mermaid-preview svg')`));
|
|
const light = await js(`return { bg: getComputedStyle(document.querySelector('.math')).backgroundColor, svg: document.querySelector('.mermaid-preview svg style')?.textContent || '' }`);
|
|
check('light theme: block background follows the theme', light.bg !== st.bg, `${st.bg} -> ${light.bg}`);
|
|
check('light theme: a new diagram is drawn with mermaid\'s light colours', /fill:#333/i.test(light.svg) && !/fill:#ccc/i.test(light.svg), light.svg.match(/fill:#[0-9a-f]{3,6}/gi)?.slice(0, 3).join(' '));
|
|
} catch (err) {
|
|
check('harness', false, err.stack);
|
|
}
|
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
console.log(failures.length ? `\n${failures.length} check(s) failed` : '\nmath e2e passed!');
|
|
app.exit(failures.length ? 1 : 0);
|
|
});
|