// 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('', { 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); });