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,146 @@
|
||||
// End-to-end check that a Kanban card is shown with the editor's own Markdown (lists, headings, code, math, images…).
|
||||
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-kcards-'));
|
||||
const vault = path.join(tmp, 'Vault');
|
||||
fs.mkdirSync(vault);
|
||||
const BOARD = `## To do
|
||||
|
||||
- [ ] Plain card with **bold**, *italic*, ==mark== and [[Spec]]
|
||||
- [ ] Release checklist
|
||||
- first sub item
|
||||
- second sub item
|
||||
1. numbered one
|
||||
> a quoted line
|
||||
- [ ] # Heading in a card
|
||||
with a paragraph below
|
||||
\`\`\`js
|
||||
const a = 1;
|
||||
\`\`\`
|
||||
- [x] Done one with \`code\` and math $x^2$
|
||||
|
||||
## Doing
|
||||
|
||||
- [ ] Card with image
|
||||

|
||||
`;
|
||||
fs.writeFileSync(path.join(vault, 'Spec.md'), 'The spec.\n');
|
||||
fs.writeFileSync(path.join(vault, 'Plan.md'), '---\nkanban-plugin: basic\n---\n\n' + BOARD);
|
||||
app.setPath('userData', tmp);
|
||||
fs.writeFileSync(path.join(tmp, 'settings.json'), JSON.stringify({ vault, lastPage: 'Plan', bounds: { width: 1300, height: 900 } }));
|
||||
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 count = (sel) => js(`return document.querySelectorAll(${JSON.stringify(sel)}).length`);
|
||||
const card = (i) => `document.querySelectorAll('.kcard')[${i}]`;
|
||||
|
||||
try {
|
||||
await sleep(2000);
|
||||
check('inline formatting is the editor\'s', await js(`const t = ${card(0)}.querySelector('.kc-text'); return !!t.querySelector('b') && !!t.querySelector('i') && !!t.querySelector('mark') && !!t.querySelector('.wl')`));
|
||||
check('lists inside a card are real lists', (await js(`return ${card(1)}.querySelectorAll('.b-bullet').length`)) === 2 && (await js(`return ${card(1)}.querySelectorAll('.b-number').length`)) === 1);
|
||||
check('a quote is a quote', (await js(`return ${card(1)}.querySelectorAll('.b-quote').length`)) === 1);
|
||||
check('a heading is a heading', (await js(`return ${card(2)}.querySelectorAll('.b-h1').length`)) === 1);
|
||||
check('a code block is highlighted', (await js(`return ${card(2)}.querySelectorAll('.code pre.c span').length`)) > 0);
|
||||
check('inline math is typeset', (await js(`return ${card(3)}.querySelectorAll('.katex').length`)) === 1);
|
||||
check('a picture that isn\'t there says so', (await count('.kcard .img.broken, .kcard .img .img-miss')) > 0);
|
||||
|
||||
await js(`${card(0)}.querySelector('.wl').click()`);
|
||||
await sleep(900);
|
||||
check('a [[link]] in a card still opens the page', (await js(`return document.querySelector('.title')?.textContent`)) === 'Spec');
|
||||
await js(`document.querySelector('.row[data-id="Plan"]').click()`);
|
||||
await sleep(1000);
|
||||
|
||||
await js(`${card(1)}.querySelector('.kc-text').click()`);
|
||||
await sleep(400);
|
||||
const raw = await js(`return document.querySelector('.kedit')?.value`);
|
||||
check('clicking a card edits its Markdown as written', /^Release checklist\n- first sub item\n- second sub item\n1\. numbered one\n> a quoted line$/.test(raw || ''), JSON.stringify(raw));
|
||||
await js(`document.querySelector('.kedit').dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))`);
|
||||
await sleep(600);
|
||||
check('...and leaving it unchanged leaves the file as it was', fs.readFileSync(path.join(vault, 'Plan.md'), 'utf8').endsWith(BOARD));
|
||||
|
||||
// Test card auto-closing and slash menu
|
||||
await js(`document.querySelector('.kadd').click()`);
|
||||
await sleep(400);
|
||||
check('new card textarea focused', await js(`return document.activeElement?.classList.contains('kedit')`));
|
||||
|
||||
const sendKey = (key, opts = {}) => js(`
|
||||
const el = document.querySelector('.kedit');
|
||||
el.dispatchEvent(new KeyboardEvent('keydown', { key: ${JSON.stringify(key)}, bubbles: true, cancelable: true, ...${JSON.stringify(opts)} }));
|
||||
`);
|
||||
const insertChar = (char) => js(`
|
||||
const el = document.querySelector('.kedit');
|
||||
const ev = new KeyboardEvent('keydown', { key: ${JSON.stringify(char)}, bubbles: true, cancelable: true });
|
||||
const notPrevented = el.dispatchEvent(ev);
|
||||
if (notPrevented) {
|
||||
const start = el.selectionStart, end = el.selectionEnd;
|
||||
el.setRangeText(${JSON.stringify(char)}, start, end, 'end');
|
||||
}
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
`);
|
||||
|
||||
// Type '*' then '*'
|
||||
await insertChar('*');
|
||||
await insertChar('*');
|
||||
const valStars = await js(`return [document.querySelector('.kedit').value, document.querySelector('.kedit').selectionStart, document.querySelector('.kedit').selectionEnd]`);
|
||||
check('typing ** auto-closes to **** with caret in middle', valStars[0] === '****' && valStars[1] === 2 && valStars[2] === 2, JSON.stringify(valStars));
|
||||
|
||||
// Backspace unpairing in ****
|
||||
await sendKey('Backspace');
|
||||
const valUnpaired = await js(`return document.querySelector('.kedit').value`);
|
||||
check('backspace inside **** unpairs both', valUnpaired === '', JSON.stringify(valUnpaired));
|
||||
|
||||
// Type '('
|
||||
await insertChar('(');
|
||||
const valParen = await js(`return [document.querySelector('.kedit').value, document.querySelector('.kedit').selectionStart]`);
|
||||
check('typing ( auto-closes to () with caret in middle', valParen[0] === '()' && valParen[1] === 1, JSON.stringify(valParen));
|
||||
|
||||
// Step over closer ')'
|
||||
await insertChar(')');
|
||||
const valStepOver = await js(`return [document.querySelector('.kedit').value, document.querySelector('.kedit').selectionStart]`);
|
||||
check('typing ) steps over existing closer', valStepOver[0] === '()' && valStepOver[1] === 2, JSON.stringify(valStepOver));
|
||||
|
||||
// Test / slash menu
|
||||
await js(`const el = document.querySelector('.kedit'); el.value = ''; el.dispatchEvent(new Event('input', { bubbles: true }));`);
|
||||
await insertChar('/');
|
||||
await sleep(300);
|
||||
const slashOpen = await js(`return !document.querySelector('.kslash')?.hidden && document.querySelector('.kslash')?.querySelectorAll('.si').length > 0`);
|
||||
check('typing / opens Kanban slash menu', !!slashOpen);
|
||||
|
||||
// Filter slash menu with 'c'
|
||||
await insertChar('c');
|
||||
await sleep(300);
|
||||
const slashMatch = await js(`return document.querySelector('.kslash .si.on b')?.textContent`);
|
||||
check('typing c filters slash commands to Callout', slashMatch === 'Callout', JSON.stringify(slashMatch));
|
||||
|
||||
// Press Enter to apply Callout
|
||||
await sendKey('Enter');
|
||||
await sleep(300);
|
||||
const afterSlash = await js(`return document.querySelector('.kedit')?.value`);
|
||||
check('Enter applies slash command to card', afterSlash === '> [!NOTE]\n> ', JSON.stringify(afterSlash));
|
||||
|
||||
// Cancel edit
|
||||
await sendKey('Escape');
|
||||
await sleep(400);
|
||||
|
||||
} catch (err) {
|
||||
check('harness', false, err.stack);
|
||||
}
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
console.log(failures.length ? `\n${failures.length} check(s) failed` : '\nkanban cards e2e passed!');
|
||||
app.exit(failures.length ? 1 : 0);
|
||||
});
|
||||
Reference in new issue
Block a user