Files
hyprcrd/public/plugins/tests/card-join.test.js
T
thandClaude Opus 4.6 22a43fdef7 help: add relay, context menu, find/replace, assets, super-buckets docs; fix flaky tests
Updates help screen with all undocumented features. Fixes F1 blocked
by input suppression filter (allow function keys). Stabilizes card-join
tests with poll loop instead of fixed sleep.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 13:11:52 +02:00

254 lines
9.3 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, rightClick, sleep, closeAll, csrf } from './runner.js?v=4';
import { state } from '../../js/modules/state.js';
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 selectIds(ids, { bulk = false } = {}) {
state.selectedIds.clear();
state.selectionWasBulk = bulk;
for (const id of ids) state.selectedIds.add(String(id));
}
async function clickJoin(rightClickTargetEl, winnerId) {
rightClick(rightClickTargetEl);
await sleep(150);
const joinBtn = $$('#ctx-items [data-action="join"]')[0];
if (!joinBtn) return false;
click(joinBtn);
// joinSelectedCards does: fetch full bodies → PATCH winner → N × DELETE
// losers → _reloadAll. Poll for the winner to have merged body.
for (let i = 0; i < 30; i++) {
await sleep(100);
if (winnerId) {
const c = await getCard(winnerId);
if (c?.body?.includes('## ')) return true;
} else {
await sleep(600);
return true;
}
}
return true;
}
describe('Card: Join — context menu visibility', () => {
afterEach(async () => {
state.selectedIds.clear();
state.selectionWasBulk = false;
await closeAll();
});
it('shows Join action when ≥2 cards are selected and right-clicked card is one of them', async () => {
const a = await makeCard('__join_vis_a__');
const b = await makeCard('__join_vis_b__');
await window._reloadAll?.();
await sleep(200);
const aEl = document.querySelector(`[data-id="${a.id}"]`);
if (!aEl) return;
selectIds([a.id, b.id]);
rightClick(aEl);
await sleep(150);
const joinBtn = $$('#ctx-items [data-action="join"]')[0];
expect(joinBtn).toExist();
expect(joinBtn.textContent.toLowerCase()).toContain('join');
expect(joinBtn.textContent).toContain('2');
});
it('does NOT show Join with only one card selected', async () => {
const a = await makeCard('__join_vis_solo__');
await window._reloadAll?.();
await sleep(200);
const aEl = document.querySelector(`[data-id="${a.id}"]`);
if (!aEl) return;
selectIds([a.id]);
rightClick(aEl);
await sleep(150);
const joinBtn = $$('#ctx-items [data-action="join"]')[0];
expect(joinBtn).toBe(undefined);
});
it('does NOT show Join when right-clicked card is not part of the selection', async () => {
const a = await makeCard('__join_vis_outside_a__');
const b = await makeCard('__join_vis_outside_b__');
const c = await makeCard('__join_vis_outside_c__');
await window._reloadAll?.();
await sleep(200);
const cEl = document.querySelector(`[data-id="${c.id}"]`);
if (!cEl) return;
selectIds([a.id, b.id]);
rightClick(cEl);
await sleep(150);
const joinBtn = $$('#ctx-items [data-action="join"]')[0];
expect(joinBtn).toBe(undefined);
});
});
describe('Card: Join — sequential selection (first wins)', () => {
afterEach(async () => {
state.selectedIds.clear();
state.selectionWasBulk = false;
await closeAll();
});
it('first ctrl-clicked card becomes the winner; others are deleted', async () => {
const first = await makeCard('__join_seq_first__', 'body of first');
const second = await makeCard('__join_seq_second__', 'body of second');
const third = await makeCard('__join_seq_third__', 'body of third');
await window._reloadAll?.();
await sleep(200);
const firstEl = document.querySelector(`[data-id="${first.id}"]`);
if (!firstEl) return;
selectIds([first.id, second.id, third.id], { bulk: false });
const ok = await clickJoin(firstEl, first.id);
if (!ok) return;
// Winner survives
const survivor = await getCard(first.id);
expect(survivor).toExist();
expect(survivor.title).toBe('__join_seq_first__');
// Losers deleted
expect(await getCard(second.id)).toBe(undefined);
expect(await getCard(third.id)).toBe(undefined);
});
it('merged body lists every original title at the top', async () => {
const a = await makeCard('__join_titles_a__', 'aaa');
const b = await makeCard('__join_titles_b__', 'bbb');
const c = await makeCard('__join_titles_c__', 'ccc');
await window._reloadAll?.();
await sleep(200);
const aEl = document.querySelector(`[data-id="${a.id}"]`);
if (!aEl) return;
selectIds([a.id, b.id, c.id], { bulk: false });
const ok = await clickJoin(aEl, a.id);
if (!ok) return;
const survivor = await getCard(a.id);
expect(survivor.body).toContain('__join_titles_a__');
expect(survivor.body).toContain('__join_titles_b__');
expect(survivor.body).toContain('__join_titles_c__');
// All three bodies preserved too
expect(survivor.body).toContain('aaa');
expect(survivor.body).toContain('bbb');
expect(survivor.body).toContain('ccc');
// Title list comes before the body content
const titlePos = survivor.body.indexOf('__join_titles_b__');
const bodyPos = survivor.body.indexOf('bbb');
expect(titlePos).toBeLessThan(bodyPos);
});
it('selection is cleared after a successful join', async () => {
const a = await makeCard('__join_clearsel_a__');
const b = await makeCard('__join_clearsel_b__');
await window._reloadAll?.();
await sleep(200);
const aEl = document.querySelector(`[data-id="${a.id}"]`);
if (!aEl) return;
selectIds([a.id, b.id], { bulk: false });
const ok = await clickJoin(aEl, a.id);
if (!ok) return;
expect(state.selectedIds.size).toBe(0);
expect(state.selectionWasBulk).toBe(false);
});
});
describe('Card: Join — bulk selection (longest title wins)', () => {
afterEach(async () => {
state.selectedIds.clear();
state.selectionWasBulk = false;
await closeAll();
});
it('longest-title card becomes the winner regardless of insertion order', async () => {
const short = await makeCard('__jbulk_x__', 'x body');
const longer = await makeCard('__jbulk_loooooong_winner_title__', 'l body');
const medium = await makeCard('__jbulk_med_xx__', 'm body');
await window._reloadAll?.();
await sleep(200);
const shortEl = document.querySelector(`[data-id="${short.id}"]`);
if (!shortEl) return;
// Insert in order short, longer, medium — but mark as bulk so the
// join code uses longest-title strategy.
selectIds([short.id, longer.id, medium.id], { bulk: true });
const ok = await clickJoin(shortEl, longer.id);
if (!ok) return;
// The longest-titled card should be the survivor
const survivor = await getCard(longer.id);
expect(survivor).toExist();
expect(survivor.title).toBe('__jbulk_loooooong_winner_title__');
// The other two are deleted
expect(await getCard(short.id)).toBe(undefined);
expect(await getCard(medium.id)).toBe(undefined);
// Survivor body still has all titles
expect(survivor.body).toContain('__jbulk_x__');
expect(survivor.body).toContain('__jbulk_med_xx__');
});
});
describe('Card: Join — undo', () => {
afterEach(async () => {
state.selectedIds.clear();
state.selectionWasBulk = false;
await closeAll();
});
it('Ctrl+Z reverts a join: winner body restored, losers recreated', async () => {
const winner = await makeCard('__join_undo_winner__', 'original winner body');
const loser = await makeCard('__join_undo_loser__', 'original loser body');
await window._reloadAll?.();
await sleep(200);
const winnerEl = document.querySelector(`[data-id="${winner.id}"]`);
if (!winnerEl) return;
selectIds([winner.id, loser.id], { bulk: false });
const ok = await clickJoin(winnerEl, winner.id);
if (!ok) return;
// Sanity: loser is gone, winner has merged body
expect(await getCard(loser.id)).toBe(undefined);
let survivor = await getCard(winner.id);
expect(survivor.body).toContain('__join_undo_loser__');
// Undo via global Ctrl+Z
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'z', ctrlKey: true, bubbles: true }));
await sleep(400);
// Winner body restored to original
survivor = await getCard(winner.id);
expect(survivor.body).toBe('original winner body');
// A loser card was recreated with the same title + body
// (its id will differ — find by title since the original is gone)
const all = await fetch('api/cards').then(r => r.json());
const restored = all.find(c => c.title === '__join_undo_loser__');
expect(restored).toExist();
});
});