Files
flowdeck/e2e/templates_picker.spec.js
bruno fc8548194a
FlowDeck CI / lint (push) Failing after 1m32s
FlowDeck CI / test (push) Failing after 27m50s
FlowDeck CI / docker (push) Skipped
feat(templates): refonte complète des templates façon Notion + vues/agents-skills
Templates (v7.71.x) :

- registre unifié \	emplates\ (migrations 48-49) + TemplateService.instantiate unique (UI, API v2, agent, scheduler)

- sélecteur (pilule page vide, menu •••, commande /template), gestionnaire /templates, menu New ▾, From template, base inline dans un document

- 141 presets système (59 pages, 42 bases, 15 blocs, 25 lignes), titre auto depuis le template, variables title réservée

- récurrences RRULE + scheduler dédupliqué, agent apply_template/list_templates, API /api/templates + /api/v2/fd-templates

- correctifs : bouton Templates, centrage fenêtre, filtres CSP, flux de création, variable title

- tests : tests/test_fd_templates.py (19) et e2e/templates_picker.spec.js (8)

Inclut le travail déjà présent dans le working tree (vues Notion : view_query/view_aggregate/form_projection/geocoding, property_types, database_table, docs agents-skills) et ignore .playwright-mcp/.
2026-10-10 18:52:19 -04:00

187 lines
9.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 || '[email protected]';
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 —');
});
});