test: unit and end-to-end suites
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
27fafa0bcd
commit
9b00e21c44
49 files changed
+6694
-1
No files matched your search
@@ -0,0 +1,151 @@
|
||||
// End-to-end check of mouse selection across blocks: a drag may start in one block and end in another.
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const fs = require('fs');
|
||||
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ink-e2e-selection-'));
|
||||
const vault = path.join(tmp, 'Vault');
|
||||
fs.mkdirSync(vault);
|
||||
fs.writeFileSync(path.join(vault, 'Note.md'), 'First paragraph of text here.\n\nSecond paragraph with more words.\n\n- a bullet item\n\nFourth paragraph at the end.\n');
|
||||
fs.writeFileSync(path.join(vault, 'Keys.md'), 'First paragraph of text here.\n\nSecond paragraph with more words.\n\n- a bullet item\n\nFourth paragraph at the end.\n');
|
||||
app.setPath('userData', tmp);
|
||||
fs.writeFileSync(path.join(tmp, 'settings.json'), JSON.stringify({ vault, lastPage: 'Note', bounds: { width: 1200, height: 800 } }));
|
||||
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 fields = () => js(`return [...document.querySelectorAll('.block .c')].map(c => { const r = c.getBoundingClientRect(); return { x: Math.round(r.x), y: Math.round(r.y + r.height / 2), w: Math.round(r.width), text: c.textContent }; })`);
|
||||
const drag = async (from, to, steps = 12) => {
|
||||
wc.sendInputEvent({ type: 'mouseMove', x: from[0], y: from[1] });
|
||||
wc.sendInputEvent({ type: 'mouseDown', x: from[0], y: from[1], button: 'left', clickCount: 1 });
|
||||
for (let i = 1; i <= steps; i++) {
|
||||
wc.sendInputEvent({ type: 'mouseMove', x: Math.round(from[0] + ((to[0] - from[0]) * i) / steps), y: Math.round(from[1] + ((to[1] - from[1]) * i) / steps), button: 'left' });
|
||||
await sleep(25);
|
||||
}
|
||||
wc.sendInputEvent({ type: 'mouseUp', x: to[0], y: to[1], button: 'left', clickCount: 1 });
|
||||
await sleep(300);
|
||||
};
|
||||
const state = () => js(`const s = getSelection(); const r = s.rangeCount ? s.getRangeAt(0) : null; return { blocks: r ? [...document.querySelectorAll('.block')].filter((b) => r.intersectsNode(b)).length : 0, text: r ? r.toString() : '', collapsed: s.isCollapsed, cls: document.body.className }`);
|
||||
const texts = async () => (await fields()).map((f) => f.text);
|
||||
|
||||
try {
|
||||
await sleep(1800);
|
||||
let f = await fields();
|
||||
|
||||
// inside one block: still the browser's own selection
|
||||
await drag([f[0].x + 30, f[0].y], [f[0].x + 120, f[0].y + 1]);
|
||||
let s = await state();
|
||||
check('a drag inside one block selects text there', s.blocks === 1 && s.text.length > 3, JSON.stringify(s.text));
|
||||
|
||||
// from the first block down into the last
|
||||
await drag([f[0].x + 40, f[0].y], [f[3].x + 90, f[3].y]);
|
||||
s = await state();
|
||||
check('a drag from one block into another selects across them', s.blocks === 4 && !s.collapsed, JSON.stringify(s));
|
||||
check('...from the first character it began at to where it ended', s.text.startsWith('paragraph') || s.text.startsWith(' paragraph') || s.text.startsWith('t paragraph') || /^[a-z ]+paragraph/.test(s.text.slice(0, 12)), JSON.stringify(s.text.slice(0, 30)));
|
||||
check('...including the text of the blocks between', s.text.includes('Second paragraph with more words.') && s.text.includes('a bullet item'));
|
||||
check('...and the browser gets its own selection back afterwards', !s.cls.includes('xsel'));
|
||||
|
||||
// typing replaces it, like any selection over several blocks
|
||||
await key('Delete');
|
||||
await sleep(400);
|
||||
const after = await texts();
|
||||
check('Delete removes it and joins what is left of the first and last blocks', after.length === 1 && /^Fir.*end\.$/.test(after[0]), JSON.stringify(after));
|
||||
await key('z', ['control']);
|
||||
await sleep(500);
|
||||
check('...and undo brings it all back', (await texts()).length === 4);
|
||||
|
||||
// upwards, from the last block to the second
|
||||
f = await fields();
|
||||
await drag([f[3].x + 60, f[3].y], [f[1].x + 50, f[1].y]);
|
||||
s = await state();
|
||||
check('a drag upwards works too', s.blocks === 3, JSON.stringify(s));
|
||||
|
||||
// a click afterwards puts the caret back and drops the selection
|
||||
await drag([f[2].x + 10, f[2].y], [f[2].x + 10, f[2].y], 1);
|
||||
s = await state();
|
||||
check('clicking again clears it', s.collapsed);
|
||||
|
||||
// a double click still selects a word
|
||||
wc.sendInputEvent({ type: 'mouseDown', x: f[1].x + 20, y: f[1].y, button: 'left', clickCount: 2 });
|
||||
wc.sendInputEvent({ type: 'mouseUp', x: f[1].x + 20, y: f[1].y, button: 'left', clickCount: 2 });
|
||||
await sleep(300);
|
||||
s = await state();
|
||||
check('double-click still selects a word', s.blocks === 1 && /^\w+$/.test(s.text.trim()), JSON.stringify(s.text));
|
||||
|
||||
// ---- the keyboard (on a fresh page) ----
|
||||
await js(`document.querySelector('.row[data-id="Keys"]').click()`);
|
||||
await sleep(1500);
|
||||
f = await fields();
|
||||
const click = async (x, y, shift = false) => {
|
||||
wc.sendInputEvent({ type: 'mouseMove', x, y });
|
||||
wc.sendInputEvent({ type: 'mouseDown', x, y, button: 'left', clickCount: 1, modifiers: shift ? ['shift'] : [] });
|
||||
wc.sendInputEvent({ type: 'mouseUp', x, y, button: 'left', clickCount: 1, modifiers: shift ? ['shift'] : [] });
|
||||
await sleep(300);
|
||||
};
|
||||
await click(f[0].x + 2, f[0].y);
|
||||
await key('Down', ['shift']);
|
||||
s = await state();
|
||||
check('Shift+Down selects to the end of the line', s.blocks === 1 && s.text === 'First paragraph of text here.', JSON.stringify(s));
|
||||
await key('Down', ['shift']);
|
||||
s = await state();
|
||||
check('...and once more carries it into the next block', s.blocks === 2 && s.text.startsWith('First paragraph of text here.'), JSON.stringify(s.text));
|
||||
await key('Down', ['shift']);
|
||||
s = await state();
|
||||
check('...where it goes on (to the end of that block)', s.blocks === 2 && s.text === 'First paragraph of text here.Second paragraph with more words.', JSON.stringify(s.text));
|
||||
await key('Up', ['shift']);
|
||||
await key('Up', ['shift']);
|
||||
s = await state();
|
||||
check('...and back', s.blocks === 1, JSON.stringify(s));
|
||||
await key('Down', ['shift']);
|
||||
await key('Down', ['shift']);
|
||||
await key('Up', ['shift']);
|
||||
s = await state();
|
||||
check('Shift+Up takes it back', s.blocks === 1, JSON.stringify(s));
|
||||
await key('End', ['shift', 'control']);
|
||||
s = await state();
|
||||
check('Ctrl+Shift+End selects to the end of the page', s.blocks === 4 && s.text.endsWith('at the end.'), JSON.stringify(s.text.slice(-20)));
|
||||
await key('Home', ['shift', 'control']);
|
||||
s = await state();
|
||||
check('...and Ctrl+Shift+Home from there back to the start', s.blocks === 1 && s.collapsed === true || s.text === '', JSON.stringify(s));
|
||||
|
||||
await click(f[1].x + 2, f[1].y);
|
||||
await key('Left', ['shift']);
|
||||
s = await state();
|
||||
check('Shift+Left at the start of a block reaches back into the one before', s.blocks === 2, JSON.stringify(s));
|
||||
await key('Left', ['shift', 'control']);
|
||||
s = await state();
|
||||
check('Ctrl+Shift+Left extends by words', s.blocks === 2 && s.text.length > 3, JSON.stringify(s.text));
|
||||
|
||||
await click(f[0].x + 20, f[0].y);
|
||||
await key('Right', ['shift', 'control']);
|
||||
s = await state();
|
||||
check('Ctrl+Shift+Right selects a word', s.blocks === 1 && /^\w+$/.test(s.text.trim()), JSON.stringify(s.text));
|
||||
|
||||
await click(f[0].x + 20, f[0].y);
|
||||
await click(f[3].x + 40, f[3].y, true);
|
||||
s = await state();
|
||||
check('Shift+click extends the selection across blocks', s.blocks === 4 && !s.collapsed, JSON.stringify(s));
|
||||
await key('Backspace');
|
||||
await sleep(300);
|
||||
const left = await texts();
|
||||
check('...and Backspace removes it', left.length === 1, JSON.stringify(left));
|
||||
} catch (err) {
|
||||
check('harness', false, err.stack);
|
||||
}
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
console.log(failures.length ? `\n${failures.length} check(s) failed` : '\nselection e2e passed!');
|
||||
app.exit(failures.length ? 1 : 0);
|
||||
});
|
||||
Reference in new issue
Block a user