Files
ink-note-app/test/e2e-cover.test.cjs
T
2026-10-15 11:57:09 +02:00

144 lines
8.9 KiB
JavaScript

// End-to-end check of picture covers: upload from the file dialog, drop onto the cover, remove, and moving with the page.
const { app, BrowserWindow, dialog } = require('electron');
const path = require('path');
const os = require('os');
const fs = require('fs');
const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ink-e2e-cover-'));
const vault = path.join(tmp, 'Vault');
fs.mkdirSync(path.join(vault, 'Folder'), { recursive: true });
fs.writeFileSync(path.join(vault, 'Note.md'), 'Some text.\n');
fs.writeFileSync(path.join(vault, 'Folder', 'Home.md'), 'Home.\n');
fs.mkdirSync(path.join(vault, 'Gal'));
fs.writeFileSync(path.join(vault, 'Gal.md'), '---\nink-database: basic\n---\n\n%% ink-database\n{"properties":[],"views":[{"id":"v1","name":"Gallery","type":"gallery"}]}\n%%\n');
fs.writeFileSync(path.join(vault, 'Gal', 'Item.md'), '---\ncover: first.png\n---\n');
fs.writeFileSync(path.join(vault, 'Gal', 'first.png'), PNG);
const outside = path.join(tmp, 'banner.png');
fs.writeFileSync(outside, PNG);
app.setPath('userData', tmp);
fs.writeFileSync(path.join(tmp, 'settings.json'), JSON.stringify({ vault, lastPage: 'Note', bounds: { width: 1300, height: 800 } }));
dialog.showOpenDialog = async () => ({ canceled: false, filePaths: [outside] });
// web pictures: no real network (and the address check is told these hosts are public)
const lp = require('../electron/linkpreview.cjs');
const asked = [];
lp.hooks.lookup = async () => [{ address: '93.184.216.34' }];
lp.hooks.fetch = async (url) => {
asked.push(url);
if (/\/page$/.test(url)) return new Response('<html></html>', { headers: { 'content-type': 'text/html' } });
return new Response(PNG, { headers: { 'content-type': 'image/png' } });
};
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 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(450);
};
const fm = (f) => fs.readFileSync(path.join(vault, f), 'utf8');
const loaded = () => js(`const i = document.querySelector('.cover .cover-img'); return !!i && i.complete && i.naturalWidth > 0`);
try {
await sleep(1800);
check('a page starts with a gradient cover', await js(`return !document.querySelector('.cover .cover-img') && !document.querySelector('.cover.photo')`));
await click(`document.querySelector('.cover-btn')`);
check('the cover picker offers an upload', !!(await js(`return [...document.querySelectorAll('.cover-picker .c-up')].find((b) => b.textContent.includes('Upload'))`)));
await click(`[...document.querySelectorAll('.cover-picker .c-up')].find((b) => b.textContent.includes('Upload'))`);
await sleep(900);
check('uploading copies the picture next to the page', fs.existsSync(path.join(vault, 'banner.png')));
check('...and the page\'s front matter points at it', /cover: banner\.png/.test(fm('Note.md')), JSON.stringify(fm('Note.md')));
check('...and it shows as the cover', await loaded() && await js(`return document.querySelector('.cover').classList.contains('photo')`));
// a dropped picture
await js(`
const dt = new DataTransfer();
const bytes = Uint8Array.from(atob(${JSON.stringify(PNG.toString('base64'))}), (c) => c.charCodeAt(0));
dt.items.add(new File([bytes], 'dropped.png', { type: 'image/png' }));
const el = document.querySelector('.cover');
el.dispatchEvent(new DragEvent('dragover', { bubbles: true, cancelable: true, dataTransfer: dt }));
el.dispatchEvent(new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: dt }));
`);
await sleep(1200);
check('dropping a picture on the cover replaces it', /cover: dropped\.png/.test(fm('Note.md')) && fs.existsSync(path.join(vault, 'dropped.png')), fm('Note.md'));
// it goes with the page
await js(`await window.vault.flush?.()`);
await sleep(800);
const moved = await js(`return await window.vault.move('Note', 'Folder')`);
await sleep(800);
check('moving the page moves its cover picture too', moved === 'Folder/Note' && fs.existsSync(path.join(vault, 'Folder', 'dropped.png')) && !fs.existsSync(path.join(vault, 'dropped.png')), String(moved));
check('...and the front matter still names it', /cover: dropped\.png/.test(fm('Folder/Note.md')));
// back to a gradient
await js(`document.querySelector('.row[data-id="Folder/Note"], .row[data-id="Folder"]')?.click()`);
await sleep(600);
await js(`await new Promise((r) => setTimeout(r, 200))`);
await js(`const row = document.querySelector('.row[data-id="Folder/Note"]'); if (row) row.click();`);
await sleep(1000);
await click(`document.querySelector('.cover-btn')`);
await click(`[...document.querySelectorAll('.cover-picker .c-up')].find((b) => b.textContent.includes('Remove'))`);
await sleep(1000);
check('"Remove the image" goes back to a gradient', !/cover:/.test(fm('Folder/Note.md')) && !(await js(`return !!document.querySelector('.cover .cover-img')`)), fm('Folder/Note.md'));
// a picture from a web address
await js(`document.querySelector('.row[data-id="Folder/Note"]').click()`);
await sleep(900);
await click(`document.querySelector('.cover-btn')`);
await click(`[...document.querySelectorAll('.cover-picker .c-up')].find((b) => b.textContent.includes('link'))`);
check('the picker offers "From a link"', !!(await js(`return document.querySelector('.cover-picker .c-url')`)));
const typeUrl = (v) => js(`const i = document.querySelector('.cover-picker .c-url'); i.value = ${JSON.stringify(v)}; i.dispatchEvent(new Event('input', { bubbles: true }))`);
await typeUrl('http://127.0.0.1/secret.png');
await click(`document.querySelector('.cover-picker .c-go')`);
await sleep(600);
check('an address on this machine is refused, with a message', /public web address/.test(await js(`return document.querySelector('.cover-picker .c-problem')?.textContent || ''`)));
check('...without anything being fetched', asked.length === 0, JSON.stringify(asked));
await typeUrl('https://example.com/blog/page');
await click(`document.querySelector('.cover-picker .c-go')`);
await sleep(600);
check('an address that is not a picture says so', /not a picture/.test(await js(`return document.querySelector('.cover-picker .c-problem')?.textContent || ''`)));
await typeUrl('https://example.com/img/lipstick.png?w=800');
await click(`document.querySelector('.cover-picker .c-go')`);
await sleep(1200);
check('a picture address is downloaded next to the page', fs.existsSync(path.join(vault, 'Folder', 'lipstick.png')), JSON.stringify(fs.readdirSync(path.join(vault, 'Folder'))));
check('...and becomes the cover', /cover: lipstick\.png/.test(fm('Folder/Note.md')) && await loaded());
check('...and the picker closes', !(await js(`return !!document.querySelector('.cover-picker')`)));
// a changed cover shows in the database gallery at once, not after a restart
await js(`document.querySelector('.row[data-id="Gal"]').click()`);
await sleep(1200);
const src = () => js(`return document.querySelector('.db-gcover img')?.getAttribute('src')`);
check('a gallery card shows the record\'s cover picture', /src=first\.png/.test(await src()), await src());
await js(`document.querySelector('.db-gcard').click()`);
await sleep(1000);
await click(`document.querySelector('.cover-btn')`);
await click(`[...document.querySelectorAll('.cover-picker .c-up')].find((b) => b.textContent.includes('Upload'))`);
await sleep(1200);
await js(`document.querySelector('.crumb').click()`);
await sleep(1200);
check('...and shows the new one right after it is changed', /src=banner\.png/.test(await src()), await src());
} catch (err) {
check('harness', false, err.stack);
}
fs.rmSync(tmp, { recursive: true, force: true });
console.log(failures.length ? `\n${failures.length} check(s) failed` : '\ncover e2e passed!');
app.exit(failures.length ? 1 : 0);
});