const { test, expect } = require('@playwright/test'); const FD_BASE = process.env.FD_BASE_URL || 'http://localhost:8080'; const USER = process.env.FD_USER || 'e2e@flowdeck.local'; const PASS = process.env.FD_PASS || 'e2e-secret-123'; async function login(page) { await page.goto(`${FD_BASE}/auth/login?provider=local`); await page.fill('#email', USER); await page.fill('#password', PASS); await page.click('.btn-primary'); await page.waitForURL('**/workspaces', { timeout: 15000 }); } async function setupWorkspace(page, name) { return await page.evaluate(async (n) => { const csrf = (document.cookie.match(/csrf_token=([^;]+)/) || [])[1] || ''; const r = await fetch('/api/workspaces', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf }, body: JSON.stringify({ name: n }), }); const ws = await r.json(); await fetch(`/api/workspaces/${ws.id}/select`, { method: 'POST' }); return ws; }, name); } async function createFromTemplate(page, key, wsId) { return await page.evaluate(async ({ k, id }) => { const csrf = (document.cookie.match(/csrf_token=([^;]+)/) || [])[1] || ''; const r = await fetch('/board/api/page-templates/0/use', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf }, body: JSON.stringify({ key: k, workspace_id: id }), }); return await r.json(); }, { k: key, id: wsId }); } test.describe('Template page editor', () => { test('to-do list: block ids remain valid after realtime sync', async ({ page }) => { await login(page); const ws = await setupWorkspace(page, 'Edit Repro WS ' + Date.now()); const tpl = await createFromTemplate(page, 'todo_list', ws.id); console.log('created page', tpl); await page.goto(`${FD_BASE}/pages/${tpl.id}`); await page.waitForSelector('#_blocksCt [data-bid]', { timeout: 15000 }); await page.waitForTimeout(3000); // let WS sync land const bids = await page.$$eval('#_blocksCt [data-bid]', (els) => els.map((e) => e.getAttribute('data-bid')) ); console.log('BIDS:', JSON.stringify(bids)); expect(bids.length).toBeGreaterThan(0); expect(bids).not.toContain('undefined'); expect(new Set(bids).size).toBe(bids.length); }); test('weekly report: date chip survives editing', async ({ page }) => { await login(page); const ws = await setupWorkspace(page, 'Edit Repro WS2 ' + Date.now()); const tpl = await createFromTemplate(page, 'weekly_report', ws.id); await page.goto(`${FD_BASE}/pages/${tpl.id}`); await page.waitForSelector('#_blocksCt [data-bid]', { timeout: 15000 }); await page.waitForTimeout(3000); // Type in the first heading then read back the saved content token const h1 = page.locator('#_blocksCt .block-content').first(); await h1.click(); await page.keyboard.press('End'); await page.keyboard.type(' (edited)'); await page.waitForTimeout(2500); // autosave const stored = await page.evaluate(async (pid) => { const r = await fetch('/board/api/pages/' + pid); return (await r.json()).content; }, tpl.id); console.log('STORED:', stored); expect(stored).toContain('fddate'); }); test('to-do list: typing + Enter keeps order and does not duplicate', async ({ page }) => { await login(page); const ws = await setupWorkspace(page, 'Edit Repro WS3 ' + Date.now()); const tpl = await createFromTemplate(page, 'todo_list', ws.id); await page.goto(`${FD_BASE}/pages/${tpl.id}`); await page.waitForSelector('#_blocksCt .block-todo [contenteditable][data-bid]', { timeout: 15000 }); await page.waitForTimeout(2500); // let WS sync land const first = page.locator('#_blocksCt .block-todo [contenteditable][data-bid]').first(); await first.click(); await page.keyboard.type('Task A'); await page.keyboard.press('Enter'); await page.waitForTimeout(250); // editor refocuses the new block after render await page.keyboard.type('Task B'); await page.waitForTimeout(2500); // autosave const stored = await page.evaluate(async (pid) => { const r = await fetch('/board/api/pages/' + pid); return JSON.parse((await r.json()).content); }, tpl.id); const texts = stored.map((b) => b.content); console.log('TODO STORED:', JSON.stringify(texts)); // heading + 3 to_do (template) + 1 inserted by Enter expect(texts).toEqual(['To-do', 'Task A', 'Task B', '', '']); const ids = stored.map((b) => b.id); expect(new Set(ids).size).toBe(ids.length); expect(ids).not.toContain(undefined); }); test('legacy id-less page is repaired on load (no undefined ids / duplicates)', async ({ page }) => { await login(page); const ws = await setupWorkspace(page, 'Edit Repro Legacy ' + Date.now()); // create an empty page, then write id-less blocks directly to simulate a // page created before block ids were persisted (old templates). const pid = await page.evaluate(async (wsId) => { const csrf = (document.cookie.match(/csrf_token=([^;]+)/) || [])[1] || ''; const r = await fetch('/api/local-workspace/items', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf }, body: JSON.stringify({ name: 'Legacy', type: 'page', workspace_id: wsId }), }); const d = await r.json(); await fetch('/board/api/pages/' + d.id + '/blocks', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf }, body: JSON.stringify({ title: 'Legacy', blocks: [ { type: 'heading_1', content: 'To-do' }, { type: 'to_do', content: '', checked: false }, { type: 'to_do', content: '', checked: false }, ], }), }); return d.id; }, ws.id); await page.goto(`${FD_BASE}/pages/${pid}`); await page.waitForSelector('#_blocksCt [data-bid]', { timeout: 15000 }); await page.waitForTimeout(3000); // let realtime sync land const bids = await page.$$eval('#_blocksCt [data-bid]', (els) => els.map((e) => e.getAttribute('data-bid')) ); console.log('LEGACY BIDS:', JSON.stringify(bids)); expect(bids.length).toBe(3); expect(bids).not.toContain('undefined'); expect(new Set(bids).size).toBe(bids.length); }); });