"""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