plugin infrastruktur
This commit is contained in:
1 parent
dd2a995f6c
commit
1545b90f39
43 files changed
No files matched your search
@@ -0,0 +1,230 @@
|
||||
import { describe, it, afterEach, expect, $, $$, click, rightClick, sleep, closeAll, csrf } from './runner.js';
|
||||
import { state } from '../modules/state.js';
|
||||
|
||||
describe('Card: Tag Operations', () => {
|
||||
afterEach(() => closeAll());
|
||||
|
||||
it('tag chips visible on cards (if setting on)', () => {
|
||||
if (localStorage.getItem('hyprcrd-show-tags') === '0') return;
|
||||
const card = $('#board [data-id]');
|
||||
if (!card) return;
|
||||
const tags = card.querySelectorAll('.tag-chip');
|
||||
// May or may not have tags, just verify no crash
|
||||
expect(tags).toExist();
|
||||
});
|
||||
|
||||
it('tag click filters board', async () => {
|
||||
const seg = document.querySelector('#board .tag-seg[data-tag]');
|
||||
if (!seg) return;
|
||||
const tag = seg.dataset.tag;
|
||||
seg.click();
|
||||
await sleep(200);
|
||||
expect(state.activeTags).toContain(tag);
|
||||
// Reset
|
||||
seg.click();
|
||||
await sleep(100);
|
||||
});
|
||||
|
||||
it('tag right-click removes tag from card', async () => {
|
||||
// Create a card with a tag
|
||||
const res = await fetch('api/cards', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf() },
|
||||
body: JSON.stringify({ title: '__test_tag_rm__', body: 'text #testtag123', color: '#38bdf8' }),
|
||||
}).then(r => r.json());
|
||||
await window._reloadAll?.();
|
||||
await sleep(200);
|
||||
|
||||
const cardEl = document.querySelector(`[data-id="${res.id}"]`);
|
||||
const tagSeg = cardEl?.querySelector('.tag-seg[data-tag="testtag123"]');
|
||||
if (tagSeg) {
|
||||
tagSeg.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, preventDefault: true }));
|
||||
await sleep(300);
|
||||
// Verify tag removed
|
||||
const updated = await fetch(`api/cards?ids=${res.id}`).then(r => r.json());
|
||||
const card = updated.find(c => c.id == res.id);
|
||||
expect(card.tags).toBeFalsy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Card: Due Date & Done', () => {
|
||||
afterEach(() => closeAll());
|
||||
|
||||
it('card with due_date shows due pill', async () => {
|
||||
const res = await fetch('api/cards', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf() },
|
||||
body: JSON.stringify({ title: '__test_due_pill__', body: '', color: '#38bdf8' }),
|
||||
}).then(r => r.json());
|
||||
await fetch(`api/cards/${res.id}`, {
|
||||
method: 'PATCH', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf() },
|
||||
body: JSON.stringify({ due_date: '2099-12-31' }),
|
||||
});
|
||||
await window._reloadAll?.();
|
||||
await sleep(200);
|
||||
|
||||
const cardEl = document.querySelector(`[data-id="${res.id}"]`);
|
||||
const pill = cardEl?.querySelector('.due-pill');
|
||||
expect(pill).toExist();
|
||||
expect(pill.textContent).toContain('2099-12-31');
|
||||
});
|
||||
|
||||
it('card with #done tag shows done badge', async () => {
|
||||
if (localStorage.getItem('hyprcrd-show-done-badge') === '0') return;
|
||||
const res = await fetch('api/cards', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf() },
|
||||
body: JSON.stringify({ title: '__test_done_badge__', body: '#done', color: '#38bdf8' }),
|
||||
}).then(r => r.json());
|
||||
await window._reloadAll?.();
|
||||
await sleep(200);
|
||||
|
||||
const cardEl = document.querySelector(`[data-id="${res.id}"]`);
|
||||
const badge = cardEl?.querySelector('.card-done-badge');
|
||||
expect(badge).toExist();
|
||||
});
|
||||
|
||||
it('overdue card has danger border', async () => {
|
||||
const res = await fetch('api/cards', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf() },
|
||||
body: JSON.stringify({ title: '__test_overdue__', body: '', color: '#38bdf8' }),
|
||||
}).then(r => r.json());
|
||||
await fetch(`api/cards/${res.id}`, {
|
||||
method: 'PATCH', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf() },
|
||||
body: JSON.stringify({ due_date: '2020-01-01' }),
|
||||
});
|
||||
await window._reloadAll?.();
|
||||
await sleep(200);
|
||||
|
||||
const cardEl = document.querySelector(`[data-id="${res.id}"]`);
|
||||
expect(cardEl).toExist();
|
||||
// Overdue cards get danger border via inline style
|
||||
expect(cardEl.style.borderBottom).toContain('var(--t-danger)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Card: Context Menu Actions', () => {
|
||||
afterEach(() => closeAll());
|
||||
|
||||
it('favorite toggles', async () => {
|
||||
const card = $('#board [data-id]');
|
||||
if (!card) return;
|
||||
rightClick(card);
|
||||
await sleep(150);
|
||||
const favBtn = $$('#ctx-items [data-action="favorite"]')[0];
|
||||
const wasFav = favBtn?.textContent.includes('unfavorite');
|
||||
click(favBtn);
|
||||
await sleep(200);
|
||||
|
||||
// Open menu again to check toggled state
|
||||
rightClick(card);
|
||||
await sleep(150);
|
||||
const favBtn2 = $$('#ctx-items [data-action="favorite"]')[0];
|
||||
const isFav = favBtn2?.textContent.includes('unfavorite');
|
||||
expect(isFav).toBe(!wasFav);
|
||||
// Toggle back
|
||||
click(favBtn2);
|
||||
await sleep(100);
|
||||
});
|
||||
|
||||
it('color submenu changes card color', async () => {
|
||||
const res = await fetch('api/cards', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf() },
|
||||
body: JSON.stringify({ title: '__test_color_ctx__', body: '', color: '#38bdf8' }),
|
||||
}).then(r => r.json());
|
||||
await window._reloadAll?.();
|
||||
await sleep(200);
|
||||
|
||||
const cardEl = document.querySelector(`[data-id="${res.id}"]`);
|
||||
if (!cardEl) return;
|
||||
rightClick(cardEl);
|
||||
await sleep(150);
|
||||
// Click color action to open submenu
|
||||
click($$('#ctx-items [data-action="color"]')[0]);
|
||||
await sleep(100);
|
||||
// Pick a different color
|
||||
const colorBtn = document.querySelector('#ctx-color-grid [data-ctx-color="#fb7185"]');
|
||||
if (colorBtn) {
|
||||
click(colorBtn);
|
||||
await sleep(300);
|
||||
const updated = await fetch(`api/cards?ids=${res.id}`).then(r => r.json());
|
||||
expect(updated.find(c => c.id == res.id).color).toBe('#fb7185');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Card: Bucket Assignment', () => {
|
||||
afterEach(() => closeAll());
|
||||
|
||||
it('card moves to bucket via context menu', async () => {
|
||||
// Create bucket and card
|
||||
const bucket = await fetch('api/buckets', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf() },
|
||||
body: JSON.stringify({ name: '__test_ctx_bucket__' }),
|
||||
}).then(r => r.json());
|
||||
|
||||
const card = await fetch('api/cards', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf() },
|
||||
body: JSON.stringify({ title: '__test_ctx_move__', body: '', color: '#38bdf8' }),
|
||||
}).then(r => r.json());
|
||||
await window._reloadAll?.();
|
||||
await sleep(200);
|
||||
|
||||
const cardEl = document.querySelector(`[data-id="${card.id}"]`);
|
||||
if (!cardEl) return;
|
||||
rightClick(cardEl);
|
||||
await sleep(150);
|
||||
// Click bucket action
|
||||
click($$('#ctx-items [data-action="bucket"]')[0]);
|
||||
await sleep(100);
|
||||
// Click the test bucket in submenu
|
||||
const bucketBtn = $$('#ctx-buckets [data-ctx-bucket]').find(b => b.dataset.ctxBucket == bucket.id);
|
||||
if (bucketBtn) {
|
||||
click(bucketBtn);
|
||||
await sleep(300);
|
||||
const updated = await fetch(`api/cards?ids=${card.id}`).then(r => r.json());
|
||||
expect(updated.find(c => c.id == card.id).bucket_id).toBe(bucket.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Card: Grouping', () => {
|
||||
afterEach(() => closeAll());
|
||||
|
||||
it('cards can be grouped via API', async () => {
|
||||
const parent = await fetch('api/cards', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf() },
|
||||
body: JSON.stringify({ title: '__test_parent__', body: '', color: '#38bdf8' }),
|
||||
}).then(r => r.json());
|
||||
const child = await fetch('api/cards', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf() },
|
||||
body: JSON.stringify({ title: '__test_child__', body: '', color: '#38bdf8' }),
|
||||
}).then(r => r.json());
|
||||
|
||||
await fetch(`api/cards/${child.id}`, {
|
||||
method: 'PATCH', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf() },
|
||||
body: JSON.stringify({ parent_id: parent.id }),
|
||||
});
|
||||
|
||||
const all = await fetch(`api/cards?ids=${child.id}`).then(r => r.json());
|
||||
expect(all.find(c => c.id == child.id).parent_id).toBe(parent.id);
|
||||
});
|
||||
|
||||
it('child card hidden on board (shown under parent)', async () => {
|
||||
const parent = await fetch('api/cards', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf() },
|
||||
body: JSON.stringify({ title: '__test_grp_parent__', body: '', color: '#38bdf8' }),
|
||||
}).then(r => r.json());
|
||||
const child = await fetch('api/cards', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf() },
|
||||
body: JSON.stringify({ title: '__test_grp_child__', body: '', color: '#38bdf8', parent_id: parent.id }),
|
||||
}).then(r => r.json());
|
||||
|
||||
await window._reloadAll?.();
|
||||
await sleep(200);
|
||||
|
||||
// Child should not be a standalone card on board
|
||||
const childEl = document.querySelector(`[data-id="${child.id}"]`);
|
||||
// It may or may not render depending on group view, but parent should exist
|
||||
const parentEl = document.querySelector(`[data-id="${parent.id}"]`);
|
||||
expect(parentEl).toExist();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user