save() PATCHed the card on the server but left state.cards stale. The next openOverlay read the old body from the local cache and the new column was gone. Now save() writes the updated body back into the matching state.cards entry so close + reopen sees the latest data without a full _reloadAll round-trip. Test: "new columns survive close + reopen" — adds a column via API + local state update, closes, reopens, asserts both column names are present in the DOM. Bumps to 1.9.28. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
204 lines
8.9 KiB
JavaScript
204 lines
8.9 KiB
JavaScript
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 persistence', () => {
|
|
it('new columns survive close + reopen', async () => {
|
|
let card;
|
|
try {
|
|
card = await fetch('api/cards', { method: 'POST', headers: hJson(), body: JSON.stringify({ title: '__test_bm_colsave__', body: emptyBody() }) }).then(r => r.json());
|
|
await window._reloadAll();
|
|
await sleep(200);
|
|
|
|
// Open overlay, add a column via the data model + save
|
|
// (simulates what addColumn does without the dialog).
|
|
const el = document.querySelector(`.board-card[data-id="${card.id}"]`);
|
|
click(el);
|
|
await sleep(150);
|
|
|
|
// Grab the plugin's internal data via a fresh body parse,
|
|
// add a column, and save through the overlay's save path.
|
|
const full = await fetch(`api/cards?ids=${card.id}`).then(r => r.json());
|
|
const body = (full[0]?.body || '').trim();
|
|
const jsonEnd = body.lastIndexOf('}');
|
|
const parsed = JSON.parse(body.substring(0, jsonEnd + 1));
|
|
parsed.columns.push({ name: 'Added', items: [] });
|
|
const newBody = JSON.stringify(parsed) + '\n\n#bookmarks';
|
|
await fetch(`api/cards/${card.id}`, {
|
|
method: 'PATCH', headers: hJson(),
|
|
body: JSON.stringify({ body: newBody }),
|
|
});
|
|
// Update local state like save() does.
|
|
const local = (window.state?.cards || []).find(c => c.id == card.id);
|
|
if (local) local.body = newBody;
|
|
|
|
click($('bookmarks-close'));
|
|
await sleep(100);
|
|
|
|
// Reopen — the new column must still be there.
|
|
const el2 = document.querySelector(`.board-card[data-id="${card.id}"]`);
|
|
click(el2);
|
|
await sleep(150);
|
|
const names = [...document.querySelectorAll('.bm-col-name')].map(n => n.textContent);
|
|
expect(names).toContain('General');
|
|
expect(names).toContain('Added');
|
|
click($('bookmarks-close'));
|
|
await sleep(80);
|
|
} finally {
|
|
if (card?.id) await fetch(`api/cards/${card.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();
|
|
}
|
|
});
|
|
});
|