Files
hyprcrd/public/plugins/tests/board.test.js
T
thandClaude Opus 4.6 d5ebbab6a5 tests: assert journal entries don't get the local pulse
Covers the board.js journal exemption on the 1× card-pulse-local
flash. Creates two probe cards via the raw API, flags both as
"just created locally" in state.recentlyCreatedIds, runs a render,
then asserts that the journal-format one (title 9999-12-31 — a
year we can't collide with real entries) skipped the pulse class
while the __prefixed__ control card still got it.

Both probes are deleted in a finally block so a failed assertion
doesn't leave them behind for later suites.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:53:49 +02:00

234 lines
9.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll, csrf } from './runner.js?v=4';
describe('Board Rendering', () => {
it('renders card elements', () => {
expect($$('#board [data-id]').length).toBeGreaterThan(-1);
});
it('cards are draggable', () => {
const card = $('#board [data-id]');
if (!card) return;
expect(card.getAttribute('draggable')).toBe('true');
});
it('cards have color bar', () => {
const card = $('#board [data-id]');
if (!card) return;
expect(card.querySelector('.card-bar')).toExist();
});
it('cards have valid data-id', () => {
const card = $('#board [data-id]');
if (!card) return;
expect(parseInt(card.dataset.id)).toBeGreaterThan(0);
});
});
describe('Board Split Preview', () => {
afterEach(() => closeAll());
it('opens split panel', async () => {
click('#board-split-toggle');
await sleep(150);
expect($('#board-preview').classList.contains('hidden')).toBe(false);
click('#board-split-toggle');
await sleep(100);
});
it('shows empty state initially', async () => {
click('#board-split-toggle');
await sleep(150);
expect($('#board-preview-empty').classList.contains('hidden')).toBe(false);
click('#board-split-toggle');
await sleep(100);
});
it('shows card on click', async () => {
click('#board-split-toggle');
await sleep(150);
const card = $('#board [data-id]');
if (!card) return;
click(card);
await sleep(250);
expect($('#board-preview-title').textContent.length).toBeGreaterThan(0);
click('#board-split-toggle');
await sleep(100);
});
it('highlights active card', async () => {
click('#board-split-toggle');
await sleep(150);
const card = $('#board [data-id]');
if (!card) return;
click(card);
await sleep(250);
expect(card.classList.contains('preview-active')).toBe(true);
click('#board-split-toggle');
await sleep(100);
});
});
describe('Board: Card Create & Delete', () => {
afterEach(() => closeAll());
it('new card appears on board', async () => {
const res = await fetch('api/cards', {
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf() },
body: JSON.stringify({ title: '__test_appear__', body: '', color: '#38bdf8' }),
}).then(r => r.json());
await window._reloadAll?.();
await sleep(200);
expect(document.querySelector(`[data-id="${res.id}"]`)).toExist();
});
it('deleted card gone from board', async () => {
const res = await fetch('api/cards', {
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf() },
body: JSON.stringify({ title: '__test_gone__', body: '', color: '#38bdf8' }),
}).then(r => r.json());
await window._reloadAll?.();
await sleep(100);
await fetch(`api/cards/${res.id}`, { method: 'DELETE', headers: { 'X-CSRF-Token': csrf() } });
await window._reloadAll?.();
await sleep(200);
expect(document.querySelector(`[data-id="${res.id}"]`)).toBe(null);
});
it('deleted card clears split preview', async () => {
const res = await fetch('api/cards', {
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf() },
body: JSON.stringify({ title: '__test_split_clear__', body: '', color: '#38bdf8' }),
}).then(r => r.json());
await window._reloadAll?.();
await sleep(200);
// Open split, select the card
if ($('#board-preview')?.classList.contains('hidden')) {
click('#board-split-toggle');
await sleep(150);
}
const el = document.querySelector(`[data-id="${res.id}"]`);
if (el) click(el);
await sleep(200);
// Delete and check preview resets
await fetch(`api/cards/${res.id}`, { method: 'DELETE', headers: { 'X-CSRF-Token': csrf() } });
await window._reloadAll?.();
await sleep(200);
expect($('#board-preview-empty').classList.contains('hidden')).toBe(false);
if (!$('#board-preview')?.classList.contains('hidden')) {
click('#board-split-toggle');
await sleep(100);
}
});
});
describe('Board: Focus restoration after overlay close', () => {
afterEach(() => closeAll());
it('closing the editor returns focus to body', async () => {
click('#add-btn');
await sleep(200);
// Editor is open — focus should be inside the modal (title input)
expect(document.activeElement !== document.body).toBe(true);
click('#modal-cancel');
await sleep(200);
expect(document.activeElement).toBe(document.body);
});
it('closing the detail view returns focus to body', async () => {
const card = $('#board [data-id]');
if (!card) return;
click(card);
await sleep(300);
if (!$('#detail')?.classList.contains('open')) return;
click('#detail-close');
await sleep(200);
expect(document.activeElement).toBe(document.body);
});
it('closing search returns focus to body', async () => {
click('#search-btn');
await sleep(200);
if (!$('#search')?.classList.contains('open')) return;
expect(document.activeElement?.id).toBe('search-input');
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
await sleep(200);
expect(document.activeElement).toBe(document.body);
});
it('arrow keys work after editor round-trip (enter → enter → escape)', async () => {
const card = $('#board [data-id]');
if (!card) return;
// Focus a card
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
await sleep(100);
if (!document.querySelector('#board .card-focused')) return;
// Enter → detail
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
await sleep(300);
if (!$('#detail')?.classList.contains('open')) return;
// Enter → close detail, open editor
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
await sleep(300);
if (!$('#modal')?.classList.contains('open')) return;
// Escape → close editor
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
await sleep(200);
expect(document.activeElement).toBe(document.body);
// Arrows should work again
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
await sleep(100);
expect(document.querySelector('#board .card-focused')).toExist();
});
});
describe('Board: new-card pulse', () => {
// Journal entries shouldn't get the 1× card-pulse-local flash —
// they're daily-log jottings, not "new thing" moments. We use
// title "9999-12-31" for the journal probe to ensure we can't
// collide with a real user entry; a control card with a
// __prefixed__ title verifies the pulse still fires for normal
// local creations.
it('locally-created journal-format cards skip the pulse', async () => {
const token = csrf();
const hJson = { 'Content-Type': 'application/json', 'X-CSRF-Token': token };
const hCsrf = { 'X-CSRF-Token': token };
let journal, control;
try {
journal = await fetch('api/cards', { method: 'POST', headers: hJson, body: JSON.stringify({ title: '9999-12-31', body: '' }) }).then(r => r.json());
control = await fetch('api/cards', { method: 'POST', headers: hJson, body: JSON.stringify({ title: '__test_pulse_ctrl__', body: '' }) }).then(r => r.json());
expect(journal?.id).toExist();
expect(control?.id).toExist();
// Flag both as "just created locally" so renderBoard routes
// them through the local-pulse branch. The journal
// exemption then has to filter out the journal one.
window.state.recentlyCreatedIds.add(String(journal.id));
window.state.recentlyCreatedIds.add(String(control.id));
await window._reloadAll();
// Wait past the rAF tick so pulse classes have been
// applied, but well within the 900ms lifetime so we
// can still observe them.
await sleep(150);
const journalEl = document.querySelector(`.board-card[data-id="${journal.id}"]`);
const controlEl = document.querySelector(`.board-card[data-id="${control.id}"]`);
expect(journalEl).toExist();
expect(controlEl).toExist();
expect(journalEl.classList.contains('card-pulse-local')).toBe(false);
expect(controlEl.classList.contains('card-pulse-local')).toBe(true);
} finally {
// Clean up even on failure. Backup/restore would catch it
// eventually, but between tests we don't want a stray
// 9999-12-31 card floating around upsetting other suites.
if (journal?.id) await fetch(`api/cards/${journal.id}`, { method: 'DELETE', headers: hCsrf });
if (control?.id) await fetch(`api/cards/${control.id}`, { method: 'DELETE', headers: hCsrf });
await window._reloadAll?.();
}
});
});