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