// End-to-end check that a Kanban card is shown with the editor's own Markdown (lists, headings, code, math, images…). 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-kcards-')); const vault = path.join(tmp, 'Vault'); fs.mkdirSync(vault); const BOARD = `## To do - [ ] Plain card with **bold**, *italic*, ==mark== and [[Spec]] - [ ] Release checklist - first sub item - second sub item 1. numbered one > a quoted line - [ ] # Heading in a card with a paragraph below \`\`\`js const a = 1; \`\`\` - [x] Done one with \`code\` and math $x^2$ ## Doing - [ ] Card with image ![](missing.png) `; fs.writeFileSync(path.join(vault, 'Spec.md'), 'The spec.\n'); fs.writeFileSync(path.join(vault, 'Plan.md'), '---\nkanban-plugin: basic\n---\n\n' + BOARD); app.setPath('userData', tmp); fs.writeFileSync(path.join(tmp, 'settings.json'), JSON.stringify({ vault, lastPage: 'Plan', bounds: { width: 1300, 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 count = (sel) => js(`return document.querySelectorAll(${JSON.stringify(sel)}).length`); const card = (i) => `document.querySelectorAll('.kcard')[${i}]`; try { await sleep(2000); check('inline formatting is the editor\'s', await js(`const t = ${card(0)}.querySelector('.kc-text'); return !!t.querySelector('b') && !!t.querySelector('i') && !!t.querySelector('mark') && !!t.querySelector('.wl')`)); check('lists inside a card are real lists', (await js(`return ${card(1)}.querySelectorAll('.b-bullet').length`)) === 2 && (await js(`return ${card(1)}.querySelectorAll('.b-number').length`)) === 1); check('a quote is a quote', (await js(`return ${card(1)}.querySelectorAll('.b-quote').length`)) === 1); check('a heading is a heading', (await js(`return ${card(2)}.querySelectorAll('.b-h1').length`)) === 1); check('a code block is highlighted', (await js(`return ${card(2)}.querySelectorAll('.code pre.c span').length`)) > 0); check('inline math is typeset', (await js(`return ${card(3)}.querySelectorAll('.katex').length`)) === 1); check('a picture that isn\'t there says so', (await count('.kcard .img.broken, .kcard .img .img-miss')) > 0); await js(`${card(0)}.querySelector('.wl').click()`); await sleep(900); check('a [[link]] in a card still opens the page', (await js(`return document.querySelector('.title')?.textContent`)) === 'Spec'); await js(`document.querySelector('.row[data-id="Plan"]').click()`); await sleep(1000); await js(`${card(1)}.querySelector('.kc-text').click()`); await sleep(400); const raw = await js(`return document.querySelector('.kedit')?.value`); check('clicking a card edits its Markdown as written', /^Release checklist\n- first sub item\n- second sub item\n1\. numbered one\n> a quoted line$/.test(raw || ''), JSON.stringify(raw)); await js(`document.querySelector('.kedit').dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))`); await sleep(600); check('...and leaving it unchanged leaves the file as it was', fs.readFileSync(path.join(vault, 'Plan.md'), 'utf8').endsWith(BOARD)); // Test card auto-closing and slash menu await js(`document.querySelector('.kadd').click()`); await sleep(400); check('new card textarea focused', await js(`return document.activeElement?.classList.contains('kedit')`)); const sendKey = (key, opts = {}) => js(` const el = document.querySelector('.kedit'); el.dispatchEvent(new KeyboardEvent('keydown', { key: ${JSON.stringify(key)}, bubbles: true, cancelable: true, ...${JSON.stringify(opts)} })); `); const insertChar = (char) => js(` const el = document.querySelector('.kedit'); const ev = new KeyboardEvent('keydown', { key: ${JSON.stringify(char)}, bubbles: true, cancelable: true }); const notPrevented = el.dispatchEvent(ev); if (notPrevented) { const start = el.selectionStart, end = el.selectionEnd; el.setRangeText(${JSON.stringify(char)}, start, end, 'end'); } el.dispatchEvent(new Event('input', { bubbles: true })); `); // Type '*' then '*' await insertChar('*'); await insertChar('*'); const valStars = await js(`return [document.querySelector('.kedit').value, document.querySelector('.kedit').selectionStart, document.querySelector('.kedit').selectionEnd]`); check('typing ** auto-closes to **** with caret in middle', valStars[0] === '****' && valStars[1] === 2 && valStars[2] === 2, JSON.stringify(valStars)); // Backspace unpairing in **** await sendKey('Backspace'); const valUnpaired = await js(`return document.querySelector('.kedit').value`); check('backspace inside **** unpairs both', valUnpaired === '', JSON.stringify(valUnpaired)); // Type '(' await insertChar('('); const valParen = await js(`return [document.querySelector('.kedit').value, document.querySelector('.kedit').selectionStart]`); check('typing ( auto-closes to () with caret in middle', valParen[0] === '()' && valParen[1] === 1, JSON.stringify(valParen)); // Step over closer ')' await insertChar(')'); const valStepOver = await js(`return [document.querySelector('.kedit').value, document.querySelector('.kedit').selectionStart]`); check('typing ) steps over existing closer', valStepOver[0] === '()' && valStepOver[1] === 2, JSON.stringify(valStepOver)); // Test / slash menu await js(`const el = document.querySelector('.kedit'); el.value = ''; el.dispatchEvent(new Event('input', { bubbles: true }));`); await insertChar('/'); await sleep(300); const slashOpen = await js(`return !document.querySelector('.kslash')?.hidden && document.querySelector('.kslash')?.querySelectorAll('.si').length > 0`); check('typing / opens Kanban slash menu', !!slashOpen); // Filter slash menu with 'c' await insertChar('c'); await sleep(300); const slashMatch = await js(`return document.querySelector('.kslash .si.on b')?.textContent`); check('typing c filters slash commands to Callout', slashMatch === 'Callout', JSON.stringify(slashMatch)); // Press Enter to apply Callout await sendKey('Enter'); await sleep(300); const afterSlash = await js(`return document.querySelector('.kedit')?.value`); check('Enter applies slash command to card', afterSlash === '> [!NOTE]\n> ', JSON.stringify(afterSlash)); // Cancel edit await sendKey('Escape'); await sleep(400); } catch (err) { check('harness', false, err.stack); } fs.rmSync(tmp, { recursive: true, force: true }); console.log(failures.length ? `\n${failures.length} check(s) failed` : '\nkanban cards e2e passed!'); app.exit(failures.length ? 1 : 0); });