Files
hyprcrd/public/plugins/tests/board-find-replace.test.js
T
thandClaude Opus 4.7 87ebcd156b v2.0.9 — sync toast split, relay group chat UI, find/replace flaky fix
- sync: the post-sync toast lumped relay pulls into the CalDAV stats
  and always labelled the banner "CalDAV". Split into two separate
  toasts (CalDAV cloud-arrow + Relay broadcast icon), each only shown
  when its numbers are non-zero. Banners now stack vertically so both
  can appear together on a sync that pulled on both sides.
- relay UI: the session chat view was 1:1-only. Group rooms (which
  the client API already supports) now surface as first-class threads
  in the sidebar — sorted chronologically with 1:1 peers, tagged
  "ROOM", with a users-three icon. Sending posts to `room_uid`
  instead of `recipient`; history shows the sender name above each
  incoming message so it's clear who's talking in the room.
  Relay backend: `/api/chat/threads` returns rooms alongside 1:1
  threads; `/api/chat/history` accepts `?room_uid=`; `/api/chat/send`
  accepts `room_uid` in the body.
- tests: bump "replace all updates card bodies" poll budget from 4s
  to 10s and poll both updated cards. Flaky on cold runs because a
  fresh PHP worker + SQLite WAL warm-up plus two sequential PATCH
  calls can just barely exceed 4 seconds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 18:28:00 +02:00

114 lines
4.4 KiB
JavaScript

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);
});
});