// T0-3 — Régression du bouton « Templates » (page vide). // Avant correctif : la pilule « Templates » basculait `gsMoreOpen` (même flag // que « ••• ») et ouvrait le menu de création de base — aucun sélecteur. // Après : le clic ouvre TOUJOURS le sélecteur unifié (liste, état vide ou // erreur explicite — jamais un no-op silencieux, P5). 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 createEmptyPage(page) { return await page.evaluate(async () => { 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: 'Tpl regression ' + Date.now(), type: 'page' }), }); return await r.json(); }); } 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', headers: { 'X-CSRF-Token': csrf } }); return ws; }, name); } test.describe('Templates picker (T0-3)', () => { test('le clic sur la pilule Templates ouvre le sélecteur', async ({ page }) => { await login(page); const created = await createEmptyPage(page); await page.goto(`${FD_BASE}/pages/${created.id}`); await page.waitForSelector('#_blocksCt', { timeout: 15000 }); // La rangée « Get started with » est visible sur une page vide. const pill = page.locator('.get-started-toolbar .gs-pill', { hasText: 'Templates' }); await expect(pill).toBeVisible({ timeout: 10000 }); await pill.click(); // Le sélecteur unifié s'ouvre — liste, état vide OU erreur : jamais rien. const picker = page.getByTestId('tpl-picker'); await expect(picker).toBeVisible({ timeout: 10000 }); // Le catalogue contient les presets système (ou un état vide explicite). const items = page.getByTestId('tpl-item'); const n = await items.count(); if (n === 0) { await expect(picker.getByText(/Aucun template|Chargement impossible/)).toBeVisible(); } else { // Choisir le premier template et l'appliquer à la page courante. await items.first().click(); await page.getByTestId('tpl-apply').click(); // Effet visible : toast de confirmation (la page se remplit en place). await expect(page.locator('.sd-toast, .toast').first()).toBeVisible({ timeout: 15000 }); } }); test('le gestionnaire /templates liste le catalogue', async ({ page }) => { await login(page); await page.goto(`${FD_BASE}/templates`); await expect(page.getByRole('heading', { name: 'Templates' })).toBeVisible({ timeout: 15000 }); // Au moins les presets système, même pour un utilisateur non-admin. await expect(page.locator('.lib-container').getByText(/Note de réunion|Revue hebdo|Journal quotidien/).first()).toBeVisible({ timeout: 15000 }); }); test('les filtres Tous/page/blocks/row/database filtrent la liste', async ({ page }) => { await login(page); const created = await createEmptyPage(page); await page.goto(`${FD_BASE}/pages/${created.id}`); await page.waitForSelector('#_blocksCt', { timeout: 15000 }); await page.locator('.get-started-toolbar .gs-pill', { hasText: 'Templates' }).click(); await page.getByTestId('tpl-picker').waitFor({ state: 'visible', timeout: 10000 }); // Filtre « Bases » : ne liste que des bases. await page.locator('#fd-tpl-picker button', { hasText: /^Bases$/ }).click(); await expect(page.getByTestId('tpl-item').first()).toContainText('database', { timeout: 10000 }); // Filtre « Pages » : ne liste que des pages. await page.locator('#fd-tpl-picker button', { hasText: /^Pages$/ }).click(); await expect(page.getByTestId('tpl-item').first()).toContainText('page', { timeout: 10000 }); }); test('un template Database s’insère dans le document courant', async ({ page }) => { await login(page); const created = await createEmptyPage(page); await page.goto(`${FD_BASE}/pages/${created.id}`); await page.waitForSelector('#_blocksCt', { timeout: 15000 }); await page.locator('.get-started-toolbar .gs-pill', { hasText: 'Templates' }).click(); await page.getByTestId('tpl-picker').waitFor({ state: 'visible', timeout: 10000 }); await page.locator('#fd-tpl-picker button', { hasText: /^Bases$/ }).click(); await page.getByTestId('tpl-item').first().click(); await page.getByTestId('tpl-apply').click(); // Effet visible sans navigation : la base est insérée dans le document. await expect(page.locator('.sd-toast, .toast').first()).toContainText(/insérée|créé/i, { timeout: 15000 }); expect(page.url()).toContain(`/pages/${created.id}`); }); test('le bouton « New page » crée un document vide (pas le panneau template)', async ({ page }) => { await login(page); await setupWorkspace(page, 'NewPage ' + Date.now()); await page.goto(`${FD_BASE}/local-workspace`); await page.waitForTimeout(1500); const btn = page.locator('button[title="New page"]').first(); await expect(btn).toBeVisible({ timeout: 10000 }); await btn.click(); // Création directe → navigation vers l'éditeur, aucun panneau template. await page.waitForURL('**/pages/**', { timeout: 15000 }); expect(await page.locator('#fd-global-tpl-picker, .tpl-picker-overlay').count()).toBe(0); }); test('le menu New > From template ouvre le sélecteur unifié', async ({ page }) => { await login(page); await setupWorkspace(page, 'NewTpl ' + Date.now()); await page.goto(`${FD_BASE}/local-workspace`); await page.waitForTimeout(1500); await page.locator('.fd-new-btn').click(); await page.locator('.fd-new-pop-item', { hasText: /From template/ }).click(); const picker = page.locator('#fd-global-tpl-picker'); await expect(picker).toBeVisible({ timeout: 10000 }); // Fermeture explicite : le ✕ fonctionne (jamais un no-op). await picker.locator('[data-gtpl="close"]').click(); await expect(picker).toHaveCount(0, { timeout: 5000 }); }); test('la commande / template ouvre le sélecteur sans les pages', async ({ page }) => { await login(page); const created = await createEmptyPage(page); await page.goto(`${FD_BASE}/pages/${created.id}`); await page.waitForSelector('#_blocksCt .block-content', { timeout: 15000 }); const block = page.locator('#_blocksCt .block-content').first(); await block.click(); await page.waitForTimeout(300); await page.keyboard.type('/'); await page.waitForSelector('.slash-item', { timeout: 10000 }); await page.keyboard.type('template'); const item = page.locator('.slash-item[data-sid="template"]'); await expect(item).toBeVisible({ timeout: 10000 }); await item.click(); const picker = page.getByTestId('tpl-picker'); await expect(picker).toBeVisible({ timeout: 10000 }); // Filtres : base / ligne / blocs seulement — pas de « Pages ». const labels = await picker.locator('.fd-tpl-filter').allInnerTexts(); expect(labels).toEqual(expect.arrayContaining(['Bases', 'Lignes', 'Blocs'])); expect(labels).not.toContain('Pages'); // Aucun template de page représenté (genres base/ligne/blocs uniquement). const texts = await picker.getByTestId('tpl-item').allInnerTexts(); expect(texts.length).toBeGreaterThan(0); expect(texts.every((t) => !/page ·/.test(t))).toBeTruthy(); }); test('appliquer un template de page règle le titre sans variable', async ({ page }) => { await login(page); const created = await createEmptyPage(page); await page.goto(`${FD_BASE}/pages/${created.id}`); await page.waitForSelector('#_blocksCt .block-content', { timeout: 15000 }); await page.locator('.get-started-toolbar .gs-pill', { hasText: 'Templates' }).click(); const picker = page.getByTestId('tpl-picker'); await picker.waitFor({ state: 'visible', timeout: 10000 }); await picker.locator('.fd-tpl-filter', { hasText: /^Pages$/ }).click(); await picker.getByTestId('tpl-item').filter({ hasText: 'Compte rendu client' }).first().click(); await page.getByTestId('tpl-apply').click(); // Pas d'échec « variable requise », et le titre suit le template. const toast = page.locator('.sd-toast, .toast').first(); await expect(toast).toContainText(/remplie depuis/i, { timeout: 15000 }); await expect(page.locator('#_titleEl')).toContainText('CR —'); }); });