UI-tests und fixes aus tests
This commit is contained in:
1 parent
73da8fb849
commit
968d980be1
46 files changed
+3853
-61
No files matched your search
@@ -0,0 +1,333 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user