Editor find/replace (public/js/modules/editor.js, public/index.php):
* New #editor-find-bar inside #editor-pane (above the textarea, pushes
it down via flex-col). Two rows: find input + match count + prev/next
+ close, and replace input + replace-one + replace-all.
* Ctrl+H opens find+replace, Ctrl+F opens find-only (replace row
hidden). Same shortcut toggles off if already open in the same mode.
* runFind(): case-insensitive indexOf scan, all match positions stored.
jumpTo() sets textarea.setSelectionRange for the Nth match.
findNext/findPrev cycle with wrap-around.
* replaceOne(): body.setRangeText at the current match position, re-scan.
replaceAll(): regex-based global replace on body.value.
* Enter in find input = next (Shift+Enter = prev). Enter in replace
input = replace one. Escape closes the bar + refocuses textarea.
* closeModal() calls closeFindBar() to clean up on editor close.
Board-wide find/replace (public/js/app.js):
* Ctrl+Shift+H opens a centered overlay that fetches all cards with
full bodies (api('api/cards'), bypasses lazy-load).
* Live search: typing in the find input scans all card bodies case-
insensitive. Matches shown as a scrollable list with color dot,
card title, occurrence count, and a context snippet with <mark>
highlight around the first match.
* Status line: "12 occurrences in 5 cards" or "no matches".
* Replace All button (danger-styled, disabled until both inputs have
text and matches exist). Click opens a showDialog confirmation:
"Replace 12 occurrences of 'foo' with 'bar' across 5 cards?"
On confirm: regex replace per card body → PATCH → _reloadAll.
* Escape / click-outside / Close button dismiss the overlay.
Shortcuts registered in the central registry for the help screen:
* ⌃ H (editor), ⌃ F (editor), ⌃ ⇧ H (global board-wide).
Tests (public/plugins/tests/board-find-replace.test.js):
* Ctrl+Shift+H opens the overlay (polls for #board-find-replace).
* Typing in find shows matching cards (creates 3 cards, 2 match,
asserts status count + list contains matching titles + excludes
non-matching title).
* Replace all updates card bodies after confirmation (creates 2
cards with "hello", replaces with "goodbye", confirms dialog,
verifies via API that bodies are "goodbye world goodbye" and
"goodbye there").
* Replace all button disabled without replace text.
detail.test.js: "closes on ESC" stabilized — guard that detail is
actually open before dispatching Escape, poll (20×50ms) instead of
fixed sleep(150) for the .open class to be removed.
chore: bump version to 0.9.21
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
74 lines
2.3 KiB
JavaScript
74 lines
2.3 KiB
JavaScript
import { describe, it, afterEach, expect, $, click, pressKey, sleep, closeAll } from './runner.js?v=4';
|
|
|
|
describe('Detail View', () => {
|
|
afterEach(() => closeAll());
|
|
|
|
it('has structural elements', () => {
|
|
for (const id of ['detail', 'detail-title', 'detail-body', 'detail-meta', 'detail-tags']) {
|
|
expect($(`#${id}`)).toExist();
|
|
}
|
|
});
|
|
|
|
it('has control buttons', () => {
|
|
for (const id of ['detail-edit', 'detail-delete', 'detail-done', 'detail-zen', 'detail-close']) {
|
|
expect($(`#${id}`)).toExist();
|
|
}
|
|
});
|
|
|
|
it('opens on card click (non-split)', async () => {
|
|
if (!$('#board-preview')?.classList.contains('hidden')) {
|
|
click('#board-split-toggle');
|
|
await sleep(100);
|
|
}
|
|
const card = $('#board [data-id]');
|
|
if (!card) return;
|
|
click(card);
|
|
await sleep(300);
|
|
expect($('#detail')).toBeVisible();
|
|
});
|
|
|
|
it('shows card title', async () => {
|
|
if (!$('#board-preview')?.classList.contains('hidden')) {
|
|
click('#board-split-toggle');
|
|
await sleep(100);
|
|
}
|
|
const card = $('#board [data-id]');
|
|
if (!card) return;
|
|
click(card);
|
|
await sleep(300);
|
|
expect($('#detail-title').textContent.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('closes on ESC', async () => {
|
|
if (!$('#board-preview')?.classList.contains('hidden')) {
|
|
click('#board-split-toggle');
|
|
await sleep(100);
|
|
}
|
|
const card = $('#board [data-id]');
|
|
if (!card) return;
|
|
click(card);
|
|
await sleep(300);
|
|
if (!$('#detail')?.classList.contains('open')) return;
|
|
pressKey('Escape');
|
|
for (let i = 0; i < 20; i++) {
|
|
if (!$('#detail')?.classList.contains('open')) break;
|
|
await sleep(50);
|
|
}
|
|
expect($('#detail')).toBeHidden();
|
|
});
|
|
|
|
it('closes on close button', async () => {
|
|
if (!$('#board-preview')?.classList.contains('hidden')) {
|
|
click('#board-split-toggle');
|
|
await sleep(100);
|
|
}
|
|
const card = $('#board [data-id]');
|
|
if (!card) return;
|
|
click(card);
|
|
await sleep(300);
|
|
click('#detail-close');
|
|
await sleep(150);
|
|
expect($('#detail')).toBeHidden();
|
|
});
|
|
});
|