// End-to-end: daily calendar and stepping, tabs, tag manager, typed properties, rich paste, web clip, imports. const { app, BrowserWindow, ipcMain } = require('electron'); const path = require('path'); const os = require('os'); const fs = require('fs'); const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ink-e2e-flow-')); const vault = path.join(tmp, 'Vault'); fs.mkdirSync(path.join(vault, 'Daily'), { recursive: true }); fs.writeFileSync(path.join(vault, 'Daily.md'), ''); fs.writeFileSync(path.join(vault, 'Daily', '2020-01-01.md'), '---\ncreated: 2020-01-01\n---\n\nfirst\n'); fs.writeFileSync(path.join(vault, 'Daily', '2020-01-05.md'), '---\ncreated: 2020-01-05\n---\n\nsecond\n'); fs.writeFileSync(path.join(vault, 'A.md'), '---\ntags: [alpha, beta]\ndone: false\nrating: 4\n---\n\nPage A\n'); fs.writeFileSync(path.join(vault, 'B.md'), '---\ntags: [beta]\n---\n\nPage B\n'); app.setPath('userData', tmp); fs.writeFileSync(path.join(tmp, 'settings.json'), JSON.stringify({ vault, lastPage: 'A', theme: 'dark', bounds: { width: 1200, height: 800 } })); const lp = require('../electron/linkpreview.cjs'); lp.hooks.lookup = async () => [{ address: '93.184.216.34' }]; lp.hooks.fetch = async () => new Response('Hello Web

Hello Web

Some bold text and a link.

', { headers: { 'content-type': 'text/html' } }); 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); }; const read = (f) => fs.readFileSync(path.join(vault, f), 'utf8'); 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 click = (sel) => js(`document.querySelector(${JSON.stringify(sel)}).click()`); const rowClick = (id, mods = '') => js(`document.querySelector('.tree .row[data-id=${JSON.stringify(id)}]').dispatchEvent(new MouseEvent('click', { bubbles: true, ${mods} }))`); try { await sleep(2200); // properties: typed by value check('a "true/false" property is a checkbox, a number a number', (await js(`return document.querySelectorAll('.props .db-pv').length`)) === 2); await js(`[...document.querySelectorAll('.props .chip-add')].find((b) => b.textContent.includes('Add property')).click()`); await sleep(300); const chips = await js(`return [...document.querySelectorAll('.type-chip')].map((b) => b.textContent.trim())`); check('Add property offers types', chips.includes('Date') && chips.includes('Checkbox'), chips.join(',')); await js(`[...document.querySelectorAll('.type-chip')].find((b) => b.textContent.includes('Date')).click()`); await sleep(200); await js(`const i = document.querySelector('.custom-pk[placeholder$="property name"]'); i.value = 'due'; i.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))`); await sleep(1500); check('a new Date property is written to the front matter', /due: \d{4}-\d{2}-\d{2}/.test(read('A.md')), read('A.md')); // tags: merge and remove await js(`document.querySelector('.sidebar button[aria-label="Manage tags"]').click()`); await sleep(500); check('the tag manager lists tags with counts', (await js(`return [...document.querySelectorAll('.tr .tr-t')].map((e) => e.textContent.replace(/\\s+/g, ' ').trim()).join('|')`)) === '#alpha1 page|#beta2 pages'); await js(`[...document.querySelectorAll('.tr')].find((r) => r.textContent.includes('#alpha')).querySelector('.btn').click()`); await sleep(200); await js(`const i = document.querySelector('.tg-edit'); i.value = 'beta'; i.dispatchEvent(new Event('input', { bubbles: true }))`); await sleep(200); await js(`[...document.querySelectorAll('.tr .btn')].find((b) => b.textContent.includes('Merge')).click()`); await sleep(1200); check('renaming into an existing tag merges without duplicates', /tags: \[beta\]/.test(read('A.md')), read('A.md').split('\n').slice(0, 5).join(' ')); await js(`[...document.querySelectorAll('.tr')].find((r) => r.textContent.includes('#beta')).querySelector('.btn.danger').click()`); await sleep(200); await js(`[...document.querySelectorAll('.tr')].find((r) => r.textContent.includes('#beta')).querySelector('.btn.danger').click()`); await sleep(1200); check('removing a tag takes it off every page', !/tags:/.test(read('A.md')) && !/tags:/.test(read('B.md'))); await js(`document.querySelector('.tasks-panel .btn-icon').click()`); await sleep(400); // daily: calendar + stepping await js(`document.querySelector('.tree .row[data-id="Daily"]').dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }))`); await sleep(500); await rowClick('Daily/2020-01-01'); await sleep(500); check('a daily note shows the day buttons', (await js(`return !!document.querySelector('.day-nav')`)) === true); await js(`document.querySelector('.day-nav button[aria-label="Next daily note"]').click()`); await sleep(700); check('"next" goes to the next existing daily note', (await js(`return document.querySelector('.tree .row.active')?.dataset.id`)) === 'Daily/2020-01-05'); await js(`document.querySelector('.day-nav button[aria-label="Pick a day"]').click()`); await sleep(400); check('the calendar marks days with notes', (await js(`return document.querySelectorAll('.cal-d.has').length`)) === 2); await js(`[...document.querySelectorAll('.cal-d')].find((b) => b.textContent.trim() === '3').click()`); await sleep(1500); check('picking an empty day creates its note', fs.existsSync(path.join(vault, 'Daily', '2020-01-03.md'))); // rich paste await rowClick('B'); await sleep(600); await js(`document.querySelector('.block .c').focus()`); await js(`const dt = new DataTransfer(); dt.setData('text/html', '

Title

a bold link

'); dt.setData('text/plain', 'Title one two a bold link'); document.querySelector('.block .c').dispatchEvent(new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true }))`); await sleep(1500); await sleep(2000); const b = read('B.md'); check('pasting web content keeps headings, lists, bold and links', /## Title/.test(b) && /- one/.test(b) && /\*\*bold\*\*/.test(b) && /\[link\]\(https:\/\/x\.test\/?\)/.test(b), b); // web clip await ipcMain._invokeHandlers.get('web:clip')({}, 'https://example.test/post').then(async (page) => { check('the clipper fetches a page', /Hello Web/.test(page.html)); }); } catch (err) { check('harness', false, err.stack); } fs.rmSync(tmp, { recursive: true, force: true }); console.log(failures.length ? `\n${failures.length} check(s) failed` : '\nworkflow e2e passed!'); app.exit(failures.length ? 1 : 0); });