import { describe, it, afterEach, expect, $, click, type, sleep, closeAll, csrf } from './runner.js'; describe('Editor: Empty Title Prevention', () => { afterEach(() => closeAll()); it('save with empty title does nothing', async () => { const countBefore = (await fetch('api/cards').then(r => r.json())).length; click('#add-btn'); await sleep(200); // Leave title empty, type body const body = $('#editor-body'); body.value = 'body without title'; body.dispatchEvent(new Event('input', { bubbles: true })); await sleep(50); click('#modal-save-close'); await sleep(300); // Modal should still be open (save blocked) // OR card count unchanged const countAfter = (await fetch('api/cards').then(r => r.json())).length; expect(countAfter).toBe(countBefore); }); it('save with whitespace-only title does nothing', async () => { const countBefore = (await fetch('api/cards').then(r => r.json())).length; click('#add-btn'); await sleep(200); type('#inp-title', ' '); click('#modal-save-close'); await sleep(300); const countAfter = (await fetch('api/cards').then(r => r.json())).length; expect(countAfter).toBe(countBefore); }); }); describe('Editor: Ctrl+S Save', () => { afterEach(() => closeAll()); it('Ctrl+S saves card', async () => { click('#add-btn'); await sleep(200); type('#inp-title', '__test_ctrl_s__'); const body = $('#editor-body'); body.value = 'saved with ctrl+s'; body.dispatchEvent(new Event('input', { bubbles: true })); await sleep(50); // Ctrl+S (save only, don't close) document.dispatchEvent(new KeyboardEvent('keydown', { key: 's', ctrlKey: true, bubbles: true })); await sleep(300); const cards = await fetch('api/cards').then(r => r.json()); expect(cards.some(c => c.title === '__test_ctrl_s__')).toBe(true); }); }); describe('Editor: Tab from Title to Body', () => { afterEach(() => closeAll()); it('Tab moves focus from title to body', async () => { click('#add-btn'); await sleep(200); expect(document.activeElement).toBe($('#inp-title')); // Simulate Tab keydown $('#inp-title').dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true })); await sleep(100); expect(document.activeElement).toBe($('#editor-body')); }); }); describe('Editor: Update Existing Card', () => { afterEach(() => closeAll()); it('editing card updates title and body', async () => { const res = await fetch('api/cards', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf() }, body: JSON.stringify({ title: '__test_edit_update__', body: 'original', 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 and body type('#inp-title', '__test_edit_changed__'); const body = $('#editor-body'); body.value = 'changed body'; body.dispatchEvent(new Event('input', { bubbles: true })); await sleep(50); click('#modal-save-close'); await sleep(400); const updated = await fetch(`api/cards?ids=${res.id}`).then(r => r.json()); const card = updated.find(c => c.id == res.id); expect(card.title).toBe('__test_edit_changed__'); expect(card.body).toContain('changed body'); }); });