- DB + uploads stored outside app bundle (survives updates) - Router uses DOCUMENT_ROOT instead of __DIR__ (fixes AppImage paths) - Footer count shifts right below 1250px, min-width 1150px - Gallery opens gracefully on fetch errors - Stabilized gallery, export, and find/replace tests with poll loops Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
111 lines
4.0 KiB
JavaScript
111 lines
4.0 KiB
JavaScript
import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll, csrf } from './runner.js?v=4';
|
|
|
|
async function makeCard(title, body) {
|
|
return fetch('api/cards', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf() },
|
|
body: JSON.stringify({ title, body, color: '#38bdf8' }),
|
|
}).then(r => r.json());
|
|
}
|
|
|
|
async function getCard(id) {
|
|
const all = await fetch(`api/cards?ids=${id}`).then(r => r.json());
|
|
return all.find(c => c.id == id);
|
|
}
|
|
|
|
function openSearchReplace() {
|
|
document.dispatchEvent(new KeyboardEvent('keydown', {
|
|
key: 'H', ctrlKey: true, shiftKey: true, bubbles: true,
|
|
}));
|
|
}
|
|
|
|
describe('Search: Find/Replace', () => {
|
|
afterEach(async () => {
|
|
await closeAll();
|
|
});
|
|
|
|
it('Ctrl+Shift+H opens search with replace row visible', async () => {
|
|
openSearchReplace();
|
|
await sleep(200);
|
|
const overlay = document.getElementById('search');
|
|
expect(overlay?.classList.contains('open')).toBe(true);
|
|
expect(document.getElementById('search-replace-row')).toBeVisible();
|
|
expect(document.getElementById('search-replace-input')).toExist();
|
|
expect(document.getElementById('search-replace-all')).toExist();
|
|
});
|
|
|
|
it('replace toggle shows/hides replace row', async () => {
|
|
openSearchReplace();
|
|
await sleep(200);
|
|
expect(document.getElementById('search-replace-row')).toBeVisible();
|
|
click('#search-replace-toggle');
|
|
await sleep(50);
|
|
expect(document.getElementById('search-replace-row')?.classList.contains('hidden')).toBe(true);
|
|
click('#search-replace-toggle');
|
|
await sleep(50);
|
|
expect(document.getElementById('search-replace-row')).toBeVisible();
|
|
});
|
|
|
|
it('replace all updates card bodies after confirmation', async () => {
|
|
const c1 = await makeCard('__bfr_repl_a__', 'hello world hello');
|
|
const c2 = await makeCard('__bfr_repl_b__', 'hello there');
|
|
await window._reloadAll?.();
|
|
await sleep(200);
|
|
|
|
openSearchReplace();
|
|
await sleep(200);
|
|
|
|
const searchInput = document.getElementById('search-input');
|
|
const replInput = document.getElementById('search-replace-input');
|
|
if (!searchInput || !replInput) return;
|
|
|
|
searchInput.value = 'hello';
|
|
searchInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
await sleep(200);
|
|
|
|
replInput.value = 'goodbye';
|
|
replInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
await sleep(50);
|
|
|
|
const applyBtn = document.getElementById('search-replace-all');
|
|
expect(applyBtn.disabled).toBe(false);
|
|
click(applyBtn);
|
|
await sleep(200);
|
|
|
|
const dialog = document.getElementById('dialog');
|
|
if (dialog?.classList.contains('open')) {
|
|
const okBtn = document.getElementById('dialog-ok');
|
|
if (okBtn) click(okBtn);
|
|
}
|
|
|
|
// Poll until the replace has actually persisted
|
|
let updated1, updated2;
|
|
for (let i = 0; i < 40; i++) {
|
|
await sleep(100);
|
|
updated1 = await getCard(c1.id);
|
|
if (updated1?.body === 'goodbye world goodbye') break;
|
|
}
|
|
updated2 = await getCard(c2.id);
|
|
expect(updated1.body).toBe('goodbye world goodbye');
|
|
expect(updated2.body).toBe('goodbye there');
|
|
});
|
|
|
|
it('replace all button disabled without replace text', async () => {
|
|
await makeCard('__bfr_dis__', 'some text');
|
|
await window._reloadAll?.();
|
|
await sleep(200);
|
|
|
|
openSearchReplace();
|
|
await sleep(200);
|
|
|
|
const searchInput = document.getElementById('search-input');
|
|
if (!searchInput) return;
|
|
searchInput.value = 'some';
|
|
searchInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
await sleep(100);
|
|
|
|
const applyBtn = document.getElementById('search-replace-all');
|
|
expect(applyBtn.disabled).toBe(true);
|
|
});
|
|
});
|