120 lines
7.5 KiB
JavaScript
120 lines
7.5 KiB
JavaScript
// End-to-end check of the "New page" dialog (blank page or template). Run with `bun run test:e2e`.
|
|
const { app, BrowserWindow, shell } = require('electron');
|
|
const path = require('path');
|
|
const os = require('os');
|
|
const fs = require('fs');
|
|
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ink-e2e-templates-'));
|
|
const vault = path.join(tmp, 'Vault');
|
|
fs.mkdirSync(vault);
|
|
fs.writeFileSync(path.join(vault, 'First.md'), 'First page.\n');
|
|
app.setPath('userData', tmp);
|
|
fs.writeFileSync(path.join(tmp, 'settings.json'), JSON.stringify({ vault, lastPage: 'First', bounds: { width: 1280, height: 860 } }));
|
|
const opened = [];
|
|
shell.openPath = async (p) => { opened.push(p); return ''; }; // don't open a real file manager
|
|
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 key = async (keyCode, modifiers = []) => { wc.sendInputEvent({ type: 'keyDown', keyCode, modifiers }); wc.sendInputEvent({ type: 'keyUp', keyCode, modifiers }); await sleep(350); };
|
|
const centerOf = (sel) => js(`const e = ${sel}; if (!e) return null; const r = e.getBoundingClientRect(); return [Math.round(r.x + r.width / 2), Math.round(r.y + r.height / 2)]`);
|
|
const click = async (sel) => {
|
|
const c = await centerOf(sel);
|
|
if (!c) throw new Error('not found: ' + sel);
|
|
wc.sendInputEvent({ type: 'mouseMove', x: c[0], y: c[1] });
|
|
wc.sendInputEvent({ type: 'mouseDown', x: c[0], y: c[1], button: 'left', clickCount: 1 });
|
|
wc.sendInputEvent({ type: 'mouseUp', x: c[0], y: c[1], button: 'left', clickCount: 1 });
|
|
await sleep(500);
|
|
};
|
|
const open = async () => { await key('n', ['control']); await sleep(500); };
|
|
const cards = () => js(`return [...document.querySelectorAll('.tpl-card')].map(c => c.querySelector('.tpl-name').textContent)`);
|
|
const focused = () => js(`return document.activeElement?.querySelector?.('.tpl-name')?.textContent || null`);
|
|
const title = () => js(`return document.querySelector('.title')?.textContent`);
|
|
const dialog = () => js(`return !!document.querySelector('.tpl-panel')`);
|
|
|
|
try {
|
|
await sleep(1800);
|
|
|
|
// ---- no templates yet ----
|
|
await open();
|
|
check('Ctrl+N opens the dialog with just "Blank page", "Board", "Database" and "Canvas"', (await dialog()) && JSON.stringify(await cards()) === '["Blank page","Board","Database","Canvas"]', JSON.stringify(await cards()));
|
|
check('"Blank page" is focused, so Enter is enough', (await focused()) === 'Blank page');
|
|
check('an empty state explains where templates live', (await js(`return document.querySelector('.tpl-foot').textContent`)).includes('.notes/templates'));
|
|
await key('Return');
|
|
await sleep(700);
|
|
check('Enter creates a blank page', !(await dialog()) && (await js(`return document.querySelector('.blocks')?.textContent.trim()`)) === '', await title());
|
|
const blankFile = fs.readdirSync(vault).filter((f) => f.endsWith('.md') && f !== 'First.md');
|
|
check('...as a new file', blankFile.length === 1, JSON.stringify(blankFile));
|
|
|
|
// ---- the folder button ----
|
|
await open();
|
|
await click(`[...document.querySelectorAll('.tpl-foot .btn')].find(b => b.textContent.includes('Open folder'))`);
|
|
const tdir = path.join(vault, '.notes', 'templates');
|
|
check('"Open folder" makes the templates folder and opens it', fs.existsSync(tdir) && opened[0] === tdir, JSON.stringify(opened));
|
|
await key('Escape');
|
|
check('Escape closes the dialog', !(await dialog()));
|
|
|
|
// ---- with templates ----
|
|
fs.writeFileSync(path.join(tdir, 'Meeting notes.md'), '---\nicon: users\ntags: [meeting]\n---\n\n# Meeting notes\n\n## Agenda\n\n- [ ] first item\n\n## Decisions\n');
|
|
fs.writeFileSync(path.join(tdir, 'Journal.md'), '---\nicon: "🌤️"\n---\n\nHow was **today**?\n');
|
|
fs.writeFileSync(path.join(tdir, 'Empty.md'), '');
|
|
await open();
|
|
check('templates show as cards after the blank page', JSON.stringify(await cards()) === '["Blank page","Board","Database","Canvas","Empty","Journal","Meeting notes"]', JSON.stringify(await cards()));
|
|
const prev = await js(`return Object.fromEntries([...document.querySelectorAll('.tpl-card')].map(c => [c.querySelector('.tpl-name').textContent, c.querySelector('.tpl-prev').textContent]))`);
|
|
check('each card previews its text in plain words', prev['Meeting notes'] === 'Meeting notes · Agenda · first item · Decisions' && prev.Journal === 'How was today?' && prev.Empty === 'An empty template.', JSON.stringify(prev));
|
|
check('and its icon (stroke icon or emoji)', await js(`return !!document.querySelector('.tpl-card[data-name="Meeting notes"] svg') && document.querySelector('.tpl-card[data-name="Journal"] .emo')?.textContent === '🌤️'`));
|
|
|
|
await key('Right');
|
|
await key('Right');
|
|
await key('Right');
|
|
await key('Right');
|
|
check('arrow keys move between cards', (await focused()) === 'Empty', await focused());
|
|
await key('Down');
|
|
await key('Left');
|
|
check('...in both directions', (await focused()) !== null);
|
|
await js(`document.querySelector('.tpl-card[data-name="Meeting notes"]').focus()`);
|
|
await key('Return');
|
|
await sleep(900);
|
|
check('picking a template creates a page from it', (await title()) === 'Meeting notes' && (await js(`return document.querySelector('.blocks').textContent`)).includes('first item'), await title());
|
|
const made = fs.readFileSync(path.join(vault, 'Meeting notes.md'), 'utf8');
|
|
check('...with its tags and text', made.includes('tags: [meeting]') && made.includes('- [ ] first item'), JSON.stringify(made.slice(0, 120)));
|
|
|
|
// ---- new templates appear when you come back to the window ----
|
|
await open();
|
|
fs.writeFileSync(path.join(tdir, 'Added later.md'), 'New one\n');
|
|
await js(`window.dispatchEvent(new Event('focus'))`);
|
|
await sleep(600);
|
|
check('a template added meanwhile appears on refocus', (await cards()).includes('Added later'), JSON.stringify(await cards()));
|
|
await click(`document.querySelector('.tpl-card[data-name="Journal"]')`);
|
|
await sleep(900);
|
|
check('clicking a card creates that page', (await title()) === 'Journal', await title());
|
|
// ---- a board from the same dialog ----
|
|
await open();
|
|
const before = fs.readdirSync(vault).filter((f) => f.endsWith('.md'));
|
|
await click(`document.querySelector('.tpl-card[data-board]')`);
|
|
await sleep(900);
|
|
const madeBoard = fs.readdirSync(vault).filter((f) => f.endsWith('.md') && !before.includes(f));
|
|
check('"Board" makes a board page and opens it', !(await dialog()) && madeBoard.length === 1 && /kanban-plugin/.test(fs.readFileSync(path.join(vault, madeBoard[0]), 'utf8')) && !!(await js(`return document.querySelector('.kanban, .board, .kcol')`)), JSON.stringify(madeBoard));
|
|
await open();
|
|
await click(`document.querySelector('.overlay')`); // the dim area outside the dialog
|
|
await sleep(400);
|
|
check('clicking outside closes it', !(await dialog()));
|
|
} catch (err) {
|
|
check('harness', false, err.stack);
|
|
}
|
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
console.log(failures.length ? `\n${failures.length} check(s) failed` : '\ntemplates e2e passed!');
|
|
app.exit(failures.length ? 1 : 0);
|
|
});
|