334 lines
13 KiB
JavaScript
334 lines
13 KiB
JavaScript
import { describe, it, afterEach, expect, $, $$, click, type, rightClick, pressKey, sleep, closeAll, csrf } from './runner.js';
|
|
|
|
function ctrlZ() {
|
|
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'z', ctrlKey: true, bubbles: true }));
|
|
}
|
|
|
|
describe('Undo: Color Change', () => {
|
|
afterEach(() => closeAll());
|
|
|
|
it('Ctrl+Z reverts color via context menu', async () => {
|
|
const res = await fetch('api/cards', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf() },
|
|
body: JSON.stringify({ title: '__test_undo_color__', 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($$('#ctx-items [data-action="color"]')[0]);
|
|
await sleep(100);
|
|
click(document.querySelector('#ctx-color-grid [data-ctx-color="#fb7185"]'));
|
|
await sleep(300);
|
|
|
|
let card = (await fetch(`api/cards?ids=${res.id}`).then(r => r.json())).find(c => c.id == res.id);
|
|
expect(card.color).toBe('#fb7185');
|
|
|
|
ctrlZ();
|
|
await sleep(400);
|
|
|
|
card = (await fetch(`api/cards?ids=${res.id}`).then(r => r.json())).find(c => c.id == res.id);
|
|
expect(card.color).toBe('#38bdf8');
|
|
});
|
|
});
|
|
|
|
describe('Undo: Editor Save', () => {
|
|
afterEach(() => closeAll());
|
|
|
|
it('Ctrl+Z reverts title change', async () => {
|
|
const res = await fetch('api/cards', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf() },
|
|
body: JSON.stringify({ title: '__test_undo_title_orig__', body: 'original body', color: '#38bdf8' }),
|
|
}).then(r => r.json());
|
|
await window._reloadAll?.();
|
|
await sleep(200);
|
|
|
|
// Open editor via context menu
|
|
const cardEl = document.querySelector(`[data-id="${res.id}"]`);
|
|
if (!cardEl) return;
|
|
cardEl.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, clientX: 100, clientY: 100 }));
|
|
await sleep(150);
|
|
click(document.querySelector('#ctx-items [data-action="edit"]'));
|
|
await sleep(300);
|
|
|
|
// Change title
|
|
type('#inp-title', '__test_undo_title_changed__');
|
|
click('#modal-save-close');
|
|
await sleep(400);
|
|
|
|
let card = (await fetch(`api/cards?ids=${res.id}`).then(r => r.json())).find(c => c.id == res.id);
|
|
expect(card.title).toBe('__test_undo_title_changed__');
|
|
|
|
// Undo
|
|
ctrlZ();
|
|
await sleep(400);
|
|
|
|
card = (await fetch(`api/cards?ids=${res.id}`).then(r => r.json())).find(c => c.id == res.id);
|
|
expect(card.title).toBe('__test_undo_title_orig__');
|
|
});
|
|
|
|
it('Ctrl+Z reverts body change', async () => {
|
|
const res = await fetch('api/cards', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf() },
|
|
body: JSON.stringify({ title: '__test_undo_body__', body: 'before edit', color: '#38bdf8' }),
|
|
}).then(r => r.json());
|
|
await window._reloadAll?.();
|
|
await sleep(200);
|
|
|
|
const cardEl = document.querySelector(`[data-id="${res.id}"]`);
|
|
if (!cardEl) return;
|
|
cardEl.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, clientX: 100, clientY: 100 }));
|
|
await sleep(150);
|
|
click(document.querySelector('#ctx-items [data-action="edit"]'));
|
|
await sleep(300);
|
|
|
|
const bodyEl = $('#editor-body');
|
|
bodyEl.value = 'after edit';
|
|
bodyEl.dispatchEvent(new Event('input', { bubbles: true }));
|
|
click('#modal-save-close');
|
|
await sleep(400);
|
|
|
|
let card = (await fetch(`api/cards?ids=${res.id}`).then(r => r.json())).find(c => c.id == res.id);
|
|
expect(card.body).toContain('after edit');
|
|
|
|
ctrlZ();
|
|
await sleep(400);
|
|
|
|
card = (await fetch(`api/cards?ids=${res.id}`).then(r => r.json())).find(c => c.id == res.id);
|
|
expect(card.body).toContain('before edit');
|
|
});
|
|
|
|
it('Ctrl+Z reverts due_date change', async () => {
|
|
const res = await fetch('api/cards', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf() },
|
|
body: JSON.stringify({ title: '__test_undo_due__', body: '', color: '#38bdf8' }),
|
|
}).then(r => r.json());
|
|
// Set a due date
|
|
await fetch(`api/cards/${res.id}`, {
|
|
method: 'PATCH', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf() },
|
|
body: JSON.stringify({ due_date: '2099-06-15' }),
|
|
});
|
|
await window._reloadAll?.();
|
|
await sleep(200);
|
|
|
|
// Edit and change due date via editor
|
|
const cardEl = document.querySelector(`[data-id="${res.id}"]`);
|
|
if (!cardEl) return;
|
|
cardEl.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, clientX: 100, clientY: 100 }));
|
|
await sleep(150);
|
|
click(document.querySelector('#ctx-items [data-action="edit"]'));
|
|
await sleep(300);
|
|
|
|
// Change due date by directly clearing it
|
|
const dueInput = $('#inp-due-value');
|
|
if (dueInput) dueInput.value = '';
|
|
click('#modal-save-close');
|
|
await sleep(400);
|
|
|
|
let card = (await fetch(`api/cards?ids=${res.id}`).then(r => r.json())).find(c => c.id == res.id);
|
|
expect(card.due_date).toBe(null);
|
|
|
|
// Undo should restore the due date
|
|
ctrlZ();
|
|
await sleep(400);
|
|
|
|
card = (await fetch(`api/cards?ids=${res.id}`).then(r => r.json())).find(c => c.id == res.id);
|
|
expect(card.due_date).toBe('2099-06-15');
|
|
});
|
|
});
|
|
|
|
describe('Undo: Card Delete', () => {
|
|
afterEach(() => closeAll());
|
|
|
|
it('Ctrl+Z after delete recreates card with body and color', async () => {
|
|
const res = await fetch('api/cards', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf() },
|
|
body: JSON.stringify({ title: '__test_undo_del__', body: 'restore me #testtag', color: '#fb7185' }),
|
|
}).then(r => r.json());
|
|
await window._reloadAll?.();
|
|
await sleep(200);
|
|
|
|
// Delete via context menu
|
|
// First disable confirm-delete
|
|
const prevConfirm = localStorage.getItem('hyprcrd-confirm-delete');
|
|
localStorage.setItem('hyprcrd-confirm-delete', '0');
|
|
|
|
const cardEl = document.querySelector(`[data-id="${res.id}"]`);
|
|
if (!cardEl) return;
|
|
cardEl.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, clientX: 100, clientY: 100 }));
|
|
await sleep(150);
|
|
click(document.querySelector('#ctx-items [data-action="delete"]'));
|
|
await sleep(400);
|
|
|
|
// Verify deleted
|
|
let cards = await fetch('api/cards').then(r => r.json());
|
|
expect(cards.some(c => c.id == res.id)).toBe(false);
|
|
|
|
// Undo
|
|
ctrlZ();
|
|
await sleep(500);
|
|
|
|
// Card should be recreated (with new ID)
|
|
cards = await fetch('api/cards').then(r => r.json());
|
|
const restored = cards.find(c => c.title === '__test_undo_del__');
|
|
expect(restored).toExist();
|
|
expect(restored.body).toContain('restore me');
|
|
expect(restored.color).toBe('#fb7185');
|
|
|
|
// Restore confirm setting
|
|
if (prevConfirm !== null) localStorage.setItem('hyprcrd-confirm-delete', prevConfirm);
|
|
else localStorage.removeItem('hyprcrd-confirm-delete');
|
|
});
|
|
|
|
it('Ctrl+Z after delete preserves parent_id', async () => {
|
|
const parent = await fetch('api/cards', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf() },
|
|
body: JSON.stringify({ title: '__test_undo_del_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_undo_del_child__', body: '', color: '#38bdf8', parent_id: parent.id }),
|
|
}).then(r => r.json());
|
|
await window._reloadAll?.();
|
|
await sleep(200);
|
|
|
|
const prevConfirm = localStorage.getItem('hyprcrd-confirm-delete');
|
|
localStorage.setItem('hyprcrd-confirm-delete', '0');
|
|
|
|
// Delete the child (it may not be directly visible, delete via API + push undo manually)
|
|
// Simpler: delete via API and test if undo restores parent_id
|
|
// Actually we need to use the context menu to get the undo pushed
|
|
// The child might be inside a group view — let's just test via the undo module directly
|
|
|
|
// Import undo and test directly
|
|
const { pushCardDelete, snapshotCard, undo, canUndo } = await import('../modules/undo.js');
|
|
const snap = { ...child, parent_id: parent.id };
|
|
pushCardDelete('test-del', snap);
|
|
await fetch(`api/cards/${child.id}`, { method: 'DELETE', headers: { 'X-CSRF-Token': csrf() } });
|
|
|
|
// Undo
|
|
await undo();
|
|
await sleep(300);
|
|
|
|
const cards = await fetch('api/cards').then(r => r.json());
|
|
const restored = cards.find(c => c.title === '__test_undo_del_child__');
|
|
expect(restored).toExist();
|
|
expect(restored.parent_id).toBe(parent.id);
|
|
|
|
if (prevConfirm !== null) localStorage.setItem('hyprcrd-confirm-delete', prevConfirm);
|
|
else localStorage.removeItem('hyprcrd-confirm-delete');
|
|
});
|
|
});
|
|
|
|
describe('Undo: Bucket Change', () => {
|
|
afterEach(() => closeAll());
|
|
|
|
it('Ctrl+Z reverts bucket assignment via context menu', async () => {
|
|
const bucket = await fetch('api/buckets', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf() },
|
|
body: JSON.stringify({ name: '__test_undo_bucket__' }),
|
|
}).then(r => r.json());
|
|
|
|
const res = await fetch('api/cards', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf() },
|
|
body: JSON.stringify({ title: '__test_undo_bkt__', body: '', color: '#38bdf8' }),
|
|
}).then(r => r.json());
|
|
await window._reloadAll?.();
|
|
await sleep(200);
|
|
|
|
const cardEl = document.querySelector(`[data-id="${res.id}"]`);
|
|
if (!cardEl) return;
|
|
|
|
// Original: no bucket
|
|
let card = (await fetch(`api/cards?ids=${res.id}`).then(r => r.json())).find(c => c.id == res.id);
|
|
expect(card.bucket_id).toBe(null);
|
|
|
|
// Assign bucket via context menu
|
|
rightClick(cardEl);
|
|
await sleep(150);
|
|
click($$('#ctx-items [data-action="bucket"]')[0]);
|
|
await sleep(100);
|
|
const bktBtn = $$('#ctx-buckets [data-ctx-bucket]').find(b => b.dataset.ctxBucket == bucket.id);
|
|
if (bktBtn) click(bktBtn);
|
|
await sleep(300);
|
|
|
|
card = (await fetch(`api/cards?ids=${res.id}`).then(r => r.json())).find(c => c.id == res.id);
|
|
expect(card.bucket_id).toBe(bucket.id);
|
|
|
|
// Undo
|
|
ctrlZ();
|
|
await sleep(400);
|
|
|
|
card = (await fetch(`api/cards?ids=${res.id}`).then(r => r.json())).find(c => c.id == res.id);
|
|
expect(card.bucket_id).toBe(null);
|
|
});
|
|
});
|
|
|
|
describe('Undo: Multiple Undos', () => {
|
|
afterEach(() => closeAll());
|
|
|
|
it('multiple Ctrl+Z reverts in stack order', async () => {
|
|
const res = await fetch('api/cards', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf() },
|
|
body: JSON.stringify({ title: '__test_undo_multi__', body: '', color: '#38bdf8' }),
|
|
}).then(r => r.json());
|
|
await window._reloadAll?.();
|
|
await sleep(200);
|
|
|
|
const cardEl = document.querySelector(`[data-id="${res.id}"]`);
|
|
if (!cardEl) return;
|
|
|
|
// Change 1: color to pink
|
|
rightClick(cardEl);
|
|
await sleep(150);
|
|
click($$('#ctx-items [data-action="color"]')[0]);
|
|
await sleep(100);
|
|
click(document.querySelector('#ctx-color-grid [data-ctx-color="#fb7185"]'));
|
|
await sleep(300);
|
|
|
|
// Change 2: color to green
|
|
const cardEl2 = document.querySelector(`[data-id="${res.id}"]`);
|
|
rightClick(cardEl2);
|
|
await sleep(150);
|
|
click($$('#ctx-items [data-action="color"]')[0]);
|
|
await sleep(100);
|
|
click(document.querySelector('#ctx-color-grid [data-ctx-color="#4ade80"]'));
|
|
await sleep(300);
|
|
|
|
let card = (await fetch(`api/cards?ids=${res.id}`).then(r => r.json())).find(c => c.id == res.id);
|
|
expect(card.color).toBe('#4ade80');
|
|
|
|
// Undo 1: back to pink
|
|
ctrlZ();
|
|
await sleep(400);
|
|
card = (await fetch(`api/cards?ids=${res.id}`).then(r => r.json())).find(c => c.id == res.id);
|
|
expect(card.color).toBe('#fb7185');
|
|
|
|
// Undo 2: back to original blue
|
|
ctrlZ();
|
|
await sleep(400);
|
|
card = (await fetch(`api/cards?ids=${res.id}`).then(r => r.json())).find(c => c.id == res.id);
|
|
expect(card.color).toBe('#38bdf8');
|
|
});
|
|
});
|
|
|
|
describe('Undo: No-Op When Empty', () => {
|
|
it('Ctrl+Z with empty stack does nothing', async () => {
|
|
const { canUndo } = await import('../modules/undo.js');
|
|
// Clear stack by undoing everything
|
|
while (canUndo()) {
|
|
ctrlZ();
|
|
await sleep(200);
|
|
}
|
|
// Now Ctrl+Z should be harmless
|
|
const cardsBefore = await fetch('api/cards').then(r => r.json());
|
|
ctrlZ();
|
|
await sleep(200);
|
|
const cardsAfter = await fetch('api/cards').then(r => r.json());
|
|
expect(cardsAfter.length).toBe(cardsBefore.length);
|
|
});
|
|
});
|