import { describe, it, afterEach, expect, $, $$, click, sleep, waitFor, 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); // The replace-all button opens a confirm dialog — wait for it // to appear rather than relying on a fixed sleep, since the // dialog creation is async and can lag on slow runs. await waitFor(() => document.getElementById('dialog')?.classList.contains('open'), 2000); const okBtn = document.getElementById('dialog-ok'); if (okBtn) click(okBtn); // Poll until both replaces have persisted. Budget is generous // (~10s) because a cold PHP run plus two sequential PATCH calls // can easily blow past the original 4-second window — the retry // always passes because opcache + SQLite WAL are warm by then. let updated1, updated2; for (let i = 0; i < 100; i++) { await sleep(100); updated1 = await getCard(c1.id); updated2 = await getCard(c2.id); if (updated1?.body === 'goodbye world goodbye' && updated2?.body === 'goodbye there') break; } 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); }); });