Files
flowdeck/tests/test_vues_notion.py
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

138 lines
5.9 KiB
Python

"""Vues Notion (doc architecture-vues-notion-flowdeck.md §2.4, §21).
Tests d'acceptation CU-Chart / CU-Timeline / CU-Map / CU-Feed /
CU-Dashboard / CU-Form + type place + migrations 44-47.
"""
from __future__ import annotations
def _make_base(c):
r = c.post('/db/api', json={'name': 'TodoNotion'})
assert r.status_code == 200, r.text
cid = r.json()['id']
c.post(f'/db/{cid}/properties/api', json={'name': 'Status', 'prop_type': 'select'})
c.post(f'/db/{cid}/properties/api', json={'name': 'Due', 'prop_type': 'date'})
c.post(f'/db/{cid}/properties/api', json={'name': 'Place', 'prop_type': 'place'})
# 4 Not started, 1 In progress, 1 Done (cf. CU-Chart §2.4)
for i, s in enumerate(['Not started'] * 4 + ['In progress', 'Done']):
c.post(f'/db/{cid}/pages/api', json={'title': f'tache {i}', 'properties': {'Status': s}})
# Une ligne datee 22 -> 27 octobre 2026 (CU-Timeline)
c.post(f'/db/{cid}/pages/api', json={'title': 'bruno', 'properties': {
'Status': 'In progress', 'Due': '2026-10-22...2026-10-27',
'Place': {'name': 'Beloeil', 'address': 'Beloeil, QC', 'lat': 45.56, 'lng': -73.20}}})
return cid
def test_migrations_44_47_applied(client):
from app.db import get_conn
with get_conn() as conn:
v = conn.execute("SELECT MAX(version) FROM schema_version").fetchone()[0]
assert v >= 47
for tbl in ('geocode_cache', 'dashboard_widgets', 'form_questions'):
assert conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name=?", (tbl,)).fetchone()
def test_place_type_validation(client):
c = client
cid = _make_base(c)
r = c.post(f'/db/{cid}/pages/api', json={'title': 'Bad', 'properties': {'Place': {'lat': 1, 'lng': 2}}})
assert r.status_code == 400
r = c.post(f'/db/{cid}/pages/api', json={'title': 'Bad2', 'properties': {
'Place': {'name': 'X', 'lat': 200, 'lng': 0}}})
assert r.status_code == 400
r = c.post(f'/db/{cid}/pages/api', json={'title': 'Addr', 'properties': {'Place': 'Montreal, QC, Canada'}})
assert r.status_code in (200, 201)
def test_cu_chart_donut_and_drilldown(client):
c = client
cid = _make_base(c)
r = c.post(f'/db/{cid}/views/save-as', json={'name': 'Chart', 'view_type': 'chart', 'config': {}})
vid = r.json()['id']
c.put(f"/db/views/{vid}/config", json={'chart_type': 'doughnut', 'group_by': 'Status',
'measure': {'kind': 'count'}, 'omit_zero': False})
r = c.post(f"/db/{cid}/views/{vid}/aggregate", json={})
assert r.status_code == 200
data = r.json()
assert data['total'] == 7
by_key = {g['key']: g for g in data['groups']}
assert by_key['Not started']['count'] == 4
assert abs(by_key['Not started']['percent'] - 4 / 7) < 0.01
r = c.get(f"/db/{cid}/views/{vid}/drilldown", params={'group': 'Not started'})
assert r.json()['count'] == 4
def test_cu_timeline_nodate_bucket(client):
c = client
cid = _make_base(c)
r = c.post(f'/db/{cid}/views/save-as', json={'name': 'TL', 'view_type': 'timeline', 'config': {}})
vid = r.json()['id']
c.put(f"/db/views/{vid}/config", json={'date_property': 'Due', 'scale': 'quarter'})
r = c.get(f"/db/{cid}/views/{vid}/rows", params={'limit': 50})
assert r.status_code == 200
assert r.json()['undated'] == 6 # 6 lignes sans date
def test_cu_map_ceiling_and_geocode_off(client):
c = client
cid = _make_base(c)
r = c.post(f'/db/{cid}/views/save-as', json={'name': 'Map', 'view_type': 'map', 'config': {'place_property': 'Place'}})
vid = r.json()['id']
r = c.get(f"/db/{cid}/views/{vid}/rows", params={'limit': 50})
assert r.status_code == 200
# Provider none par defaut : pas d'appel externe, recherche vide.
r = c.get('/db/geocode/search', params={'q': 'Beloeil, QC'})
assert r.status_code == 200
assert r.json()['suggestions'] == []
def test_cu_dashboard_global_filter(client):
c = client
cid = _make_base(c)
r = c.post(f'/db/{cid}/views/save-as', json={'name': 'Chart', 'view_type': 'chart',
'config': {'group_by': 'Status', 'measure': {'kind': 'count'}}})
cv = r.json()['id']
r = c.post(f'/db/{cid}/views/save-as', json={'name': 'Dash', 'view_type': 'dashboard', 'config': {}})
dv = r.json()['id']
r = c.put(f"/db/{cid}/views/{dv}/widgets", json={'widgets': [
{'row_index': 0, 'position': 0, 'source_collection_id': cid,
'source_view_id': cv, 'inline_config': {}}]})
assert r.status_code == 200
widgets = c.get(f"/db/{cid}/views/{dv}/widgets").json()['widgets']
assert len(widgets) == 1
r = c.get(f"/db/{cid}/views/{dv}/widgets/{widgets[0]['id']}/data")
assert r.status_code == 200
# 13 widgets refuses (plafond 12).
r = c.put(f"/db/{cid}/views/{dv}/widgets",
json={'widgets': [{'row_index': 0, 'position': i} for i in range(13)]})
assert r.status_code == 400
def test_cu_form_builder_projection(client):
c = client
cid = _make_base(c)
r = c.post(f'/db/{cid}/views/save-as', json={'name': 'Form', 'view_type': 'form', 'config': {}})
fv = r.json()['id']
r = c.put(f"/db/{cid}/views/{fv}/form", json={
'questions': [{'property_name': 'Status', 'position': 0, 'required': True}],
'config': {'title': 'Demande', 'sharing': {'audience': 'workspace', 'anonymous': False}}})
assert r.status_code == 200
assert r.json()['projected_fields'] == 1
r = c.get(f"/db/{cid}/views/{fv}/form")
assert len(r.json()['questions']) == 1
r = c.post(f"/db/{cid}/views/{fv}/form/preview-token")
assert r.json()['submittable'] is False
def test_ssr_compat_untouched(client):
"""Les rendus SSR restent servis (compat §7.6) : chart/timeline/feed/map."""
c = client
cid = _make_base(c)
for vt in ('chart', 'timeline', 'feed', 'map', 'form', 'gantt'):
r = c.get(f"/db/{cid}/view/{vt}")
assert r.status_code == 200, vt