import { describe, it, expect, $, click, sleep, waitFor, csrf } from './runner.js?v=4'; // Bookmarks plugin tests. The plugin injects a header button, decorates // #bookmarks-tagged board cards, and opens a column-based overlay. // Tests create throwaway cards and clean up in finally blocks. const token = () => csrf(); const hJson = () => ({ 'Content-Type': 'application/json', 'X-CSRF-Token': token() }); const hDel = () => ({ 'X-CSRF-Token': token() }); function emptyBody() { return JSON.stringify({ type: 'bookmarks', columns: [{ name: 'General', items: [] }], columnWidth: 220 }) + '\n\n#bookmarks'; } function bodyWith(columns, columnWidth = 220) { return JSON.stringify({ type: 'bookmarks', columns, columnWidth }) + '\n\n#bookmarks'; } describe('Bookmarks: plugin registration', () => { it('registers itself in window.hyprcrd.plugins', () => { const entry = (window.hyprcrd?.plugins || []).find(p => p?.name === 'bookmarks'); expect(entry).toExist(); }); }); describe('Bookmarks: header button', () => { it('injects a Bookmarks button into the header', () => { const btn = $('#bookmarks-btn'); expect(btn).toExist(); expect(btn.closest('#main-header')).toExist(); }); }); describe('Bookmarks: board card rendering', () => { it('decorates #bookmarks cards with bm-card class', async () => { let card; try { card = await fetch('api/cards', { method: 'POST', headers: hJson(), body: JSON.stringify({ title: '__test_bm_deco__', body: emptyBody(), color: '#fb923c' }) }).then(r => r.json()); expect(card?.id).toExist(); await window._reloadAll(); await sleep(200); const el = document.querySelector(`.board-card[data-id="${card.id}"]`); expect(el).toExist(); expect(el.classList.contains('bm-card')).toBe(true); expect(el.querySelector('.bm-card-icon')).toExist(); expect(el.querySelector('.bm-card-title').textContent).toContain('__test_bm_deco__'); } finally { if (card?.id) await fetch(`api/cards/${card.id}`, { method: 'DELETE', headers: hDel() }); await window._reloadAll(); } }); }); describe('Bookmarks: overlay', () => { it('opens on click and shows columns from the card body', async () => { const cols = [ { name: 'TestCol', items: [{ url: 'https://example.com', title: 'Example', favicon: '' }] } ]; let card; try { card = await fetch('api/cards', { method: 'POST', headers: hJson(), body: JSON.stringify({ title: '__test_bm_overlay__', body: bodyWith(cols) }) }).then(r => r.json()); await window._reloadAll(); await sleep(200); const el = document.querySelector(`.board-card[data-id="${card.id}"]`); expect(el).toExist(); click(el); await sleep(150); const overlay = $('#bookmarks-overlay'); expect(overlay.classList.contains('open')).toBe(true); expect($('bookmarks-title').textContent).toContain('__test_bm_overlay__'); // Column rendered const colName = document.querySelector('.bm-col-name'); expect(colName).toExist(); expect(colName.textContent).toBe('TestCol'); // Item rendered const item = document.querySelector('.bm-item'); expect(item).toExist(); expect(item.textContent).toContain('Example'); // Close click($('bookmarks-close')); await sleep(100); expect(overlay.classList.contains('open')).toBe(false); } finally { if (card?.id) await fetch(`api/cards/${card.id}`, { method: 'DELETE', headers: hDel() }); await window._reloadAll(); } }); }); describe('Bookmarks: fetch-meta API', () => { it('returns JSON with ok field for a valid URL', async () => { const r = await fetch('api/bookmarks/fetch-meta?url=' + encodeURIComponent('https://example.com')).then(x => x.json()); expect(r.ok).toBe(true); expect(typeof r.title).toBe('string'); // favicon is either a data: URI or empty string expect(typeof r.favicon).toBe('string'); }); it('returns 400 for missing URL', async () => { const r = await fetch('api/bookmarks/fetch-meta'); expect(r.status).toBe(400); }); }); describe('Bookmarks: card links', () => { it('renders card:ID items with a card icon', async () => { let target, bm; try { target = await fetch('api/cards', { method: 'POST', headers: hJson(), body: JSON.stringify({ title: '__test_bm_target__', body: 'hello' }) }).then(r => r.json()); const cols = [{ name: 'Links', items: [{ url: `card:${target.id}`, title: '__test_bm_target__', favicon: '' }] }]; bm = await fetch('api/cards', { method: 'POST', headers: hJson(), body: JSON.stringify({ title: '__test_bm_cardlink__', body: bodyWith(cols) }) }).then(r => r.json()); await window._reloadAll(); await sleep(200); const el = document.querySelector(`.board-card[data-id="${bm.id}"]`); click(el); await sleep(150); const item = document.querySelector('.bm-item'); expect(item).toExist(); // Card links get a ph-card icon, not ph-globe const cardIcon = item.querySelector('.ph-card'); expect(cardIcon).toExist(); click($('bookmarks-close')); await sleep(80); } finally { for (const c of [target, bm]) { if (c?.id) await fetch(`api/cards/${c.id}`, { method: 'DELETE', headers: hDel() }); } await window._reloadAll(); } }); }); describe('Bookmarks: column width persistence', () => { it('saves and restores columnWidth from card body', async () => { const cols = [{ name: 'W', items: [] }]; let card; try { card = await fetch('api/cards', { method: 'POST', headers: hJson(), body: JSON.stringify({ title: '__test_bm_width__', body: bodyWith(cols, 300) }) }).then(r => r.json()); await window._reloadAll(); await sleep(200); const el = document.querySelector(`.board-card[data-id="${card.id}"]`); click(el); await sleep(150); const slider = $('#bookmarks-col-width'); expect(slider).toExist(); expect(parseInt(slider.value)).toBe(300); click($('bookmarks-close')); await sleep(80); } finally { if (card?.id) await fetch(`api/cards/${card.id}`, { method: 'DELETE', headers: hDel() }); await window._reloadAll(); } }); });