// 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); });