Audit complet des fonctionnalités réelles DictIA (manuel utilisateur + composants production) puis restructuration de l'animation phone container en 4 catégories logiques regroupant 12 sous-modes (6 historiques + 6 nouveaux). CATÉGORIES (bottom tab bar 4 boutons + sub-mode dots indicator) : - Capture (b2 cyan) : Transcription, Recording live, Recherche IA - Transformation IA (b1) : Diarisation, 99+ langues, Résumé+actions, Chat IA - Distribution (b3 fuchsia) : Exports, Intégrations Hub, Partage, Users - Gouvernance (b1 blue) : Audit trail, Conformité Loi 25 + 9 ordres pros NOUVEAUX MODES IMPLÉMENTÉS : - Mode 7 Recording live : minuteur 99s + waveform 24 bars random + 3 boutons sources (mic/système/combiné) selon manuel utilisation v1.0 - Mode 8 Recherche sémantique : query typed + 3 résultats highlight RAG - Mode 9 Résumé + actions : décisions/actions stagger + extraction ICS - Mode 10 Intégrations : hub central DictIA + 8 logos en orbite (Word, Outlook, Teams, Notion, Obsidian, Zapier, Make, n8n) + lignes connexion SVG - Mode 11 Audit trail : 6 events horodatés (INFO/AUTH/PROC/READ/EXP/SHARE) + badge consentement tracé immutable (Loi 25 art. 8) - Mode 12 Conformité Loi 25 : 6 badges (Loi 25/96/EFVP CAI/MCN/AGPL/0 Cloud Act US) + 9 ordres pros (Barreau, CNQ, CPA, ChAD, OACIQ, CMQ, OIIQ, OPQ, OEQ) REFACTOR Alpine dictiaDashboard() : - FEATURES étendu de 7 à 13 entrées (idx 0-12) - CATEGORIES array avec submodes[], iconPath, color, subtitle - activeCategory + handleCategorySelect(ci) en plus de handleManualSelect(i) - Auto-cycle 1100ms entre sous-modes ; switch catégorie quand fin atteinte - Right grid 3×6 → 2×2 categories cards (preview sub-modes dots) - Bottom tab bar 6 modes → 4 catégories (icons larger 18px) + sub-mode dots - Mobile pills par catégorie (au lieu de par mode) Préservé : palette brand-b1/b2/b3 stricte, phone shell statique 280×580, WCAG AA, prefers-reduced-motion, eyebrow text-brand-navy, IA Mistral card, section integrations, architecture, conformité-resume. Tests : 9/9 passent dans test_fonctionnalites_*. Assertions ajoutées pour les 4 catégories + 6 nouveaux modes + handleCategorySelect + 1100ms cycle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
507 lines
22 KiB
Python
507 lines
22 KiB
Python
"""Tests for the secondary marketing pages (A-2.8)."""
|
||
import os
|
||
import sys
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
os.environ.setdefault('SQLALCHEMY_DATABASE_URI', 'sqlite:///:memory:')
|
||
os.environ.setdefault('SECRET_KEY', 'test-secret-key')
|
||
|
||
from src.app import app # noqa: E402
|
||
|
||
|
||
# === /tarifs ===
|
||
|
||
def test_tarifs_route_returns_200():
|
||
client = app.test_client()
|
||
response = client.get('/tarifs')
|
||
assert response.status_code == 200, "GET /tarifs must return 200"
|
||
|
||
|
||
def test_tarifs_extends_marketing_base():
|
||
client = app.test_client()
|
||
body = client.get('/tarifs').data.decode('utf-8')
|
||
assert '<!DOCTYPE html>' in body
|
||
assert 'lang="fr-CA"' in body
|
||
assert '/static/css/marketing.css' in body
|
||
assert '/static/js/alpine.min.js' in body
|
||
|
||
|
||
def test_tarifs_has_h1_with_anchor():
|
||
client = app.test_client()
|
||
body = client.get('/tarifs').data.decode('utf-8')
|
||
assert 'page-title' in body
|
||
assert '<h1' in body and 'choisissez votre infrastructure' in body
|
||
|
||
|
||
def test_tarifs_renders_4_pricing_cards_v7():
|
||
"""Tarifs page renders the v7.0 3 Cloud forfaits + DictIA LOCAL dedicated block + Pro+ chip."""
|
||
client = app.test_client()
|
||
body = client.get('/tarifs').data.decode('utf-8')
|
||
for tier in ['Cloud BASIC', 'Cloud ESSENTIEL', 'Cloud PRO', 'DictIA LOCAL']:
|
||
assert tier in body
|
||
# Canonical NBSP prices (v7.0)
|
||
assert '189 $' in body
|
||
assert '349 $' in body
|
||
assert '549 $' in body
|
||
assert '485 $' in body # Cloud Pro onboarding
|
||
assert '5 998 $' in body # DictIA Local An 1
|
||
# 3 Cloud forfaits use checkout slugs
|
||
assert 'href="/checkout/cloud-basic"' in body
|
||
assert 'href="/checkout/cloud-essentiel"' in body
|
||
assert 'href="/checkout/cloud-pro"' in body
|
||
# DictIA LOCAL has its own dedicated block with a contact CTA (no /checkout slug)
|
||
assert '/contact?plan=dictia-local' in body, "Missing DictIA LOCAL block contact CTA"
|
||
assert 'Vous en êtes' in body or 'Vous en êtes' in body, \
|
||
"Missing DictIA LOCAL block headline 'Vous en êtes propriétaire'"
|
||
assert 'Serveur DictIA' in body, "Missing DictIA LOCAL block server visual mockup label"
|
||
# Pro+ chip with /contact link
|
||
assert 'Pro+' in body
|
||
assert '/contact?pro-plus=1' in body
|
||
|
||
|
||
def test_tarifs_comparison_matrix_v7():
|
||
"""v7.0 comparison matrix has 4 columns + 10 rows."""
|
||
client = app.test_client()
|
||
body = client.get('/tarifs').data.decode('utf-8')
|
||
assert 'matrix-title' in body
|
||
assert '<caption class="sr-only">' in body
|
||
assert 'scope="col"' in body
|
||
assert 'scope="row"' in body
|
||
# v7.0 row keywords (matches the rows in tarifs.html)
|
||
for kw in ['Hébergement', 'GPU', 'Capacité audio', 'Stockage', 'Utilisateurs',
|
||
'Diarisation pyannote', 'Loi 25', 'SLA', 'Délai']:
|
||
assert kw in body, f"Missing matrix row keyword: {kw}"
|
||
|
||
|
||
def test_tarifs_pricing_faq_v7():
|
||
"""v7.0 tarifs FAQ has 7 questions (added DictIA Local + Cloud Pro onboarding + Pro+ explanations)."""
|
||
client = app.test_client()
|
||
body = client.get('/tarifs').data.decode('utf-8')
|
||
assert 'tarifs-faq-title' in body
|
||
for i in range(1, 8):
|
||
assert f'tarifs-faq-panel-{i}' in body, f"Missing tarifs FAQ panel {i}"
|
||
# Alpine accordion bindings
|
||
assert body.count('x-data="{ open: false }"') >= 7
|
||
# Each accordion button has focus-visible (WCAG 2.4.7/2.4.11)
|
||
assert 'focus-visible:outline-2' in body
|
||
|
||
|
||
def test_tarifs_uses_oqlf_typography():
|
||
client = app.test_client()
|
||
body = client.get('/tarifs').data.decode('utf-8')
|
||
assert 'TPS 5 %' in body
|
||
assert 'TVQ 9,975 %' in body
|
||
assert 'Loi 25' in body
|
||
# No double-escape
|
||
assert '&nbsp;' not in body, "Pricing macro / safe filter regression"
|
||
|
||
|
||
# === /fonctionnalites ===
|
||
|
||
def test_fonctionnalites_route_returns_200():
|
||
client = app.test_client()
|
||
response = client.get('/fonctionnalites')
|
||
assert response.status_code == 200
|
||
|
||
|
||
def test_fonctionnalites_extends_marketing_base():
|
||
client = app.test_client()
|
||
body = client.get('/fonctionnalites').data.decode('utf-8')
|
||
assert '<!DOCTYPE html>' in body
|
||
assert 'lang="fr-CA"' in body
|
||
assert '/static/css/marketing.css' in body
|
||
|
||
|
||
def test_fonctionnalites_h1_present():
|
||
client = app.test_client()
|
||
body = client.get('/fonctionnalites').data.decode('utf-8')
|
||
assert 'page-title' in body
|
||
assert 'rester' in body or 'restant chez soi' in body
|
||
|
||
|
||
def test_fonctionnalites_renders_6_bento_cards():
|
||
client = app.test_client()
|
||
body = client.get('/fonctionnalites').data.decode('utf-8')
|
||
assert 'features-title' in body
|
||
# 6 watermark numbers
|
||
for n in ['01', '02', '03', '04', '05', '06']:
|
||
assert f'>{n}<' in body
|
||
# 6 feature anchors
|
||
for kw in ['WhisperX', 'Diarisation', 'Mistral 7B', 'RAG local', 'DOCX, PDF, SRT', 'Outlook, Teams']:
|
||
assert kw in body
|
||
|
||
|
||
def test_fonctionnalites_how_it_works_reactor_section():
|
||
"""'Comment ça marche' interactive section — fidèle reproduction du composant
|
||
DashboardHolographique (Website-Sanity/dictai-narrative.tsx).
|
||
|
||
Validates structure (phone container + 6 modes + IA Mistral card + 6 feature grid),
|
||
auto-cycle + manual click logic, canonical content, and a11y signals.
|
||
"""
|
||
client = app.test_client()
|
||
body = client.get('/fonctionnalites').data.decode('utf-8')
|
||
|
||
# Section heading + canonical phrasing
|
||
assert 'how-it-works-title' in body, "Missing how-it-works section anchor"
|
||
assert 'COMMENT ÇA MARCHE' in body, "Missing canonical eyebrow"
|
||
assert 'Du fichier au résumé' in body, "Missing canonical H2 phrasing"
|
||
assert 'en temps réel' in body
|
||
assert 'Survolez une fonctionnalité pour voir la machine en action' in body
|
||
|
||
# 6 cycling features (canonical names — appears in FEATURES JS array)
|
||
for feat in ['Transcription', 'Diarisation', '99+ langues', 'Exports',
|
||
'Utilisateurs illimités']:
|
||
assert feat in body, f"Missing cycling feature: {feat}"
|
||
# Partage & Classement (avec & littéral dans le JS)
|
||
assert "'Partage & Classement'" in body, "Missing Partage & Classement feature"
|
||
|
||
# Phone container : Alpine root + features + Mic pulsing + DictIA logo
|
||
assert 'dictiaDashboard()' in body, "Missing dictiaDashboard Alpine root"
|
||
assert 'dictia-mic-pulse' in body, "Missing pulsing Mic animation class"
|
||
assert 'dictia-logo-nom.png' in body, "Missing DictIA logo image"
|
||
|
||
# 6 modes uniques (chacun a son sub-data function ou sa signature unique)
|
||
assert 'trModeData()' in body, "Missing Mode 1 (Transcription) data function"
|
||
assert 'reunion-jan14.mp3' in body, "Missing transcription upload filename"
|
||
assert 'diaModeData()' in body, "Missing Mode 2 (Diarisation) data function"
|
||
assert 'Sophie' in body and 'Marc' in body and 'Julie' in body, "Missing diarisation speakers"
|
||
assert 'langModeData()' in body, "Missing Mode 3 (Langues) data function"
|
||
assert '99+ langues détectées' in body, "Missing langues subtitle"
|
||
assert 'expModeData()' in body, "Missing Mode 4 (Exports) data function"
|
||
assert 'usersModeData()' in body, "Missing Mode 5 (Users) data function"
|
||
assert 'iaModeData()' in body, "Missing Mode 0 (IA chat) data function"
|
||
assert 'MISTRAL 7B · LOCAL' in body, "Missing IA chat header label"
|
||
|
||
# Mode 6 (Share) : folders + tags + files
|
||
assert 'CR-Réunion-Jan14' in body, "Missing share file name"
|
||
assert '#Confidentiel' in body, "Missing share tag"
|
||
assert 'partage sécurisé' in body, "Missing share footer text"
|
||
|
||
# IA Mistral 7B premium card avec 3 bullets souveraineté
|
||
assert 'Mistral 7B' in body
|
||
assert 'IA intégrée' in body
|
||
assert 'Données hébergées sur VOS serveurs' in body
|
||
assert 'Zéro connexion OpenAI' in body
|
||
assert 'Inférence hors-ligne' in body
|
||
assert 'ia-ambient-glow' in body, "Missing IA ambient glow animation"
|
||
assert 'ia-brain-glow' in body, "Missing IA brain glow animation"
|
||
assert 'ia-local-badge' in body, "Missing LOCAL badge pulsing animation"
|
||
|
||
# Auto-cycle + manual select logic
|
||
assert 'startAutoCycle' in body, "Missing auto-cycle function"
|
||
assert 'handleManualSelect' in body, "Missing manual select handler"
|
||
assert '1100' in body, "Missing 1100ms sub-mode cycle interval (refonte 2026-04-29)"
|
||
assert '4500' in body, "Missing 4500ms manual reset timer"
|
||
# Refonte 2026-04-29 : 4 catégories × 12 sous-modes
|
||
assert 'CATEGORIES' in body, "Missing CATEGORIES array (refonte 4 catégories)"
|
||
assert 'activeCategory' in body, "Missing activeCategory state"
|
||
assert 'handleCategorySelect' in body, "Missing category click handler"
|
||
for cat_title in ['Capture', 'Transformation IA', 'Distribution', 'Gouvernance']:
|
||
assert cat_title in body, f"Missing category : {cat_title}"
|
||
# Nouveaux modes 7-12
|
||
assert 'recModeData' in body, "Missing Mode 7 (Recording live)"
|
||
assert 'searchModeData' in body, "Missing Mode 8 (Recherche sémantique)"
|
||
assert 'summaryModeData' in body, "Missing Mode 9 (Résumé + actions)"
|
||
assert 'integModeData' in body, "Missing Mode 10 (Intégrations Hub)"
|
||
assert 'auditModeData' in body, "Missing Mode 11 (Audit trail)"
|
||
assert 'loi25ModeData' in body, "Missing Mode 12 (Conformité Loi 25)"
|
||
# Mode panels content keywords
|
||
assert 'ENREGISTREMENT LIVE' in body, "Missing recording header"
|
||
assert 'RECHERCHE IA SÉMANTIQUE' in body, "Missing semantic search header"
|
||
assert 'RÉSUMÉ EXÉCUTIF' in body, "Missing summary header"
|
||
assert 'INTÉGRATIONS HUB' in body, "Missing integrations hub header"
|
||
assert 'AUDIT TRAIL' in body, "Missing audit trail header"
|
||
assert 'CONFORMITÉ QUÉBEC' in body, "Missing conformity header"
|
||
assert 'ÉVÉNEMENT DÉTECTÉ' in body, "Missing ICS event detection"
|
||
assert '9 ordres professionnels' in body, "Missing 9 ordres listing"
|
||
for ordre in ['Barreau', 'CNQ', 'CPA', 'OIIQ']:
|
||
assert ordre in body, f"Missing ordre pro: {ordre}"
|
||
# Auto / Manual status text
|
||
assert 'Auto' in body, "Missing Auto status indicator"
|
||
assert 'Auto reprend bientôt' in body, "Missing manual mode hint"
|
||
|
||
# Accessibility: aria-live status panel, prefers-reduced-motion guard
|
||
assert 'aria-live="polite"' in body
|
||
assert 'prefers-reduced-motion' in body, "Reduced-motion guard missing"
|
||
assert 'aria-pressed' in body, "Missing aria-pressed on feature buttons"
|
||
|
||
# ── HYPER PRO polish refactor 2026-04-29 ── ----------------------------
|
||
# Phone shell : bezel + notch + speaker + status bar
|
||
assert 'dictia-phone-shell' in body, "Missing phone shell class"
|
||
assert 'dictia-notch' in body, "Missing notch element"
|
||
assert 'dictia-statusbar' in body, "Missing mobile-style status bar"
|
||
assert '9:41' in body, "Missing 9:41 status bar time"
|
||
assert 'dictia-sound-ring' in body, "Missing sound ring animation"
|
||
assert 'dictia-phone-glow-ring' in body, "Missing phone external glow ring"
|
||
|
||
# Mode 1 — REC indicator + waveform
|
||
assert 'REC' in body, "Missing transcription REC indicator"
|
||
assert 'dictia-rec-dot' in body, "Missing REC pulsing dot"
|
||
assert 'dictia-wave-bar' in body, "Missing waveform animation"
|
||
|
||
# Mode 2 — typing indicator + timestamps + stacked avatars
|
||
assert 'dictia-typing-dot' in body, "Missing typing indicator dots"
|
||
assert 'Réunion · 3 participants' in body, "Missing diarisation header"
|
||
|
||
# Mode 3 — DÉTECTION AUTOMATIQUE header + ripple from center + counter
|
||
assert 'DÉTECTION AUTOMATIQUE' in body, "Missing langues detection header"
|
||
assert 'rippleDelay' in body, "Missing ripple delay function"
|
||
assert 'HIGHLIGHTS' in body, "Missing langues highlights array"
|
||
|
||
# Mode 4 — header EXPORTS + checkmark
|
||
assert 'EXPORTS DISPONIBLES' in body, "Missing exports header"
|
||
assert '7 FORMATS PRÊTS' in body, "Missing exports checkmark subtitle"
|
||
|
||
# Mode 5 — counter centered + USER_COLORS
|
||
assert 'UTILISATEURS' in body, "Missing users counter header"
|
||
assert 'USER_COLORS' in body, "Missing users color variations"
|
||
|
||
# Mode 6 — breadcrumb + toolbar
|
||
assert 'Mes dossiers' in body, "Missing share breadcrumb"
|
||
|
||
# BOTTOM zone — feature buttons + AUTO pill + countdown
|
||
assert 'featureShortLabel' in body, "Missing feature short labels function"
|
||
assert 'dictia-auto-pulse' in body, "Missing auto pill pulse"
|
||
assert 'dictia-countdown-fill' in body, "Missing manuel countdown bar"
|
||
|
||
# Right panel — IA card metrics + Brain in 40x40 circle
|
||
assert 'MISTRAL 7B' in body, "Missing Mistral badge label"
|
||
assert 'latence' in body, "Missing latency metric label"
|
||
|
||
# Section background + connecting line
|
||
assert 'dictia-bg-grid' in body, "Missing background grid pattern"
|
||
assert 'dictia-bg-orbs' in body, "Missing floating orbs"
|
||
assert 'dictia-connecting-line' in body, "Missing connecting line phone↔IA"
|
||
|
||
# Stats row in section header (refonte 2026-04-29 : 4 catégories · 6 modules)
|
||
assert 'modules' in body, "Missing stats row 'modules'"
|
||
assert 'catégories' in body, "Missing stats row 'catégories' (refonte)"
|
||
assert 'cloud' in body, "Missing stats row 'cloud'"
|
||
assert 'Voir une démo' in body, "Missing demo CTA link"
|
||
|
||
# ── HYPER PRO polish 2026-04-29 (audit purge non-brand hex + uniformization) ──
|
||
# Brand canonical palette ONLY in JS color tables (no purple-600/cyan-700/blue-700/...)
|
||
js_section = body[body.find("function dictiaDashboard()"):body.find("</script>", body.find("function dictiaDashboard()"))]
|
||
forbidden_hex = ['#9333ea', '#1d4ed8', '#a21caf', '#0e7490', '#0891b2',
|
||
'#1e40af', '#67e8f9', '#93c5fd', '#f5d0fe', '#e879f9',
|
||
'#A78BFA', '#22D3EE', '#6B9FFF', '#34D399', '#F59E0B',
|
||
'#7C3AED', '#5B21B6', '#065F46', '#1C3A5E']
|
||
for hx in forbidden_hex:
|
||
assert hx not in body, f"Forbidden non-brand hex {hx} found in fonctionnalites — must purge"
|
||
|
||
# Inner screen seam (effet "écran encastré")
|
||
assert 'dictia-phone-screen-seam' in body, "Missing inner screen seam (encastré effect)"
|
||
# Uniform feature info card — bg-brand-navy SOLIDE (extension visuelle du phone, WCAG AA garanti sur section claire) + accent border-left
|
||
assert 'dictia-feature-card rounded-xl px-4 py-3 relative bg-brand-navy' in body, \
|
||
"Missing solid bg-brand-navy on feature info card (must be dark, not transparent on light bg)"
|
||
# Sovereignty bullets : icon dans cercle 20×20 brand-b3/[0.15]
|
||
assert 'bg-brand-b3/[0.15]' in body, "Missing brand-b3 circle bg on sovereignty bullets"
|
||
# Performance metrics : 3 cells with grad-text for 0ms / 24/7
|
||
assert 'grad-text' in body, "Missing grad-text on metrics"
|
||
|
||
|
||
def test_fonctionnalites_export_formats_section():
|
||
client = app.test_client()
|
||
body = client.get('/fonctionnalites').data.decode('utf-8')
|
||
assert 'exports-title' in body
|
||
for ext in ['DOCX', 'PDF', 'SRT', 'VTT', 'TXT', 'JSON', 'MD']:
|
||
# Each format has its own card with the .ext as a heading
|
||
assert f'>{ext}<' in body, f"Missing export format card: {ext}"
|
||
|
||
|
||
def test_fonctionnalites_integrations_grid():
|
||
client = app.test_client()
|
||
body = client.get('/fonctionnalites').data.decode('utf-8')
|
||
assert 'integrations-title' in body
|
||
for name in ['Microsoft Word', 'Microsoft Outlook', 'Microsoft Teams',
|
||
'Notion', 'Obsidian', 'Zapier', 'Make', 'n8n']:
|
||
assert name in body, f"Missing integration: {name}"
|
||
# Trademark disclaimer
|
||
assert 'marques de leurs propriétaires' in body or 'marques de leurs propriétaires' in body
|
||
|
||
|
||
def test_fonctionnalites_tech_specs_6_items():
|
||
client = app.test_client()
|
||
body = client.get('/fonctionnalites').data.decode('utf-8')
|
||
assert 'specs-title' in body
|
||
for spec_keyword in ['Modèle ASR', 'pyannote', 'Mistral 7B', 'Flask', 'WAV, MP3', 'québécois']:
|
||
assert spec_keyword in body, f"Missing tech spec keyword: {spec_keyword}"
|
||
|
||
|
||
def test_fonctionnalites_uses_oqlf_typography():
|
||
client = app.test_client()
|
||
body = client.get('/fonctionnalites').data.decode('utf-8')
|
||
# NBSP entities
|
||
assert '95 %+' in body, "WhisperX precision NBSP entity"
|
||
# No double-escape
|
||
assert '&nbsp;' not in body
|
||
|
||
|
||
# === Cross-page checks ===
|
||
|
||
def test_secondary_pages_in_main_nav():
|
||
"""Header nav links to /tarifs and /fonctionnalites — verify both pages now respond."""
|
||
client = app.test_client()
|
||
for url in ['/tarifs', '/fonctionnalites']:
|
||
# Each page should self-link in its own nav (consistency)
|
||
body = client.get(url).data.decode('utf-8')
|
||
assert 'href="/tarifs"' in body and 'href="/fonctionnalites"' in body, \
|
||
f"Page {url} must include nav links to both new pages"
|
||
|
||
|
||
def test_secondary_pages_have_canonical_meta():
|
||
"""Both pages must have canonical URL + OG metadata via base.html."""
|
||
client = app.test_client()
|
||
for url in ['/tarifs', '/fonctionnalites']:
|
||
body = client.get(url).data.decode('utf-8')
|
||
assert 'rel="canonical"' in body
|
||
assert 'og:type' in body
|
||
assert 'twitter:card' in body
|
||
|
||
|
||
# === /conformite ===
|
||
|
||
def test_conformite_route_returns_200():
|
||
client = app.test_client()
|
||
response = client.get('/conformite')
|
||
assert response.status_code == 200
|
||
|
||
|
||
def test_conformite_extends_base_with_h1():
|
||
client = app.test_client()
|
||
body = client.get('/conformite').data.decode('utf-8')
|
||
assert '<!DOCTYPE html>' in body
|
||
assert 'lang="fr-CA"' in body
|
||
assert 'page-title' in body
|
||
assert body.count('<h1') == 1, "Must have exactly one H1"
|
||
|
||
|
||
def test_conformite_4_pillars_present():
|
||
client = app.test_client()
|
||
body = client.get('/conformite').data.decode('utf-8')
|
||
assert 'pillars-title' in body
|
||
# 4 pillars (same anchors as landing's conformité section)
|
||
pillar_keywords = ['OVH Beauharnois', 'LPRPSP', 'LGGRI', 'AGPL']
|
||
for kw in pillar_keywords:
|
||
assert kw in body, f"Conformité pillar missing: {kw}"
|
||
# Soft hedges (LPC art. 219)
|
||
assert 'Mapp' in body, "Must use 'Mappé' (not 'Certifié')"
|
||
assert 'selon le périmètre' in body or 'selon le périmètre' in body, \
|
||
"SOC 2 must be hedged"
|
||
|
||
|
||
def test_conformite_loi25_3_articles():
|
||
client = app.test_client()
|
||
body = client.get('/conformite').data.decode('utf-8')
|
||
assert 'loi25-title' in body
|
||
# 3 LPRPSP articles
|
||
for art in ['Art. 3.3', 'Art. 3.5', 'Art. 14']:
|
||
assert art in body, f"Conformité missing article: {art}"
|
||
# Article topics
|
||
for topic in ['EFVP', 'Audit trail', 'Consentement']:
|
||
assert topic in body, f"Conformité missing Loi 25 topic: {topic}"
|
||
# I-1 fix: badge must use solid bg-brand-navy (not grad-bg) — WCAG AA
|
||
# text-on-grad-bg only achieves ~2:1 contrast on the cyan/green portion
|
||
assert 'bg-brand-navy text-white text-xs font-black' in body, \
|
||
"Loi 25 badges must use solid navy background for WCAG AA contrast (not grad-bg)"
|
||
# The grad-bg should NOT appear with the small badge text combo
|
||
assert 'grad-bg text-white text-xs font-black' not in body, \
|
||
"Removed grad-bg + small white text combo (WCAG AA fail)"
|
||
|
||
|
||
def test_conformite_agpl_section_with_external_links():
|
||
client = app.test_client()
|
||
body = client.get('/conformite').data.decode('utf-8')
|
||
assert 'agpl-title' in body
|
||
assert 'AGPL' in body
|
||
# External Gitea link with rel=noopener
|
||
assert 'gitea.innova-ai.ca/Innova-AI/dictia-public' in body
|
||
assert 'rel="noopener"' in body
|
||
# Link to GNU AGPL v3 official text
|
||
assert 'gnu.org/licenses/agpl-3.0' in body
|
||
|
||
|
||
def test_conformite_uses_oqlf_typography():
|
||
client = app.test_client()
|
||
body = client.get('/conformite').data.decode('utf-8')
|
||
assert 'Loi 25' in body
|
||
assert 'AGPL v3' in body
|
||
assert 'art. 3.5' in body or 'Art. 3.5' in body
|
||
assert '&nbsp;' not in body, "Macro | safe regression"
|
||
|
||
|
||
# === /contact ===
|
||
|
||
def test_contact_route_returns_200():
|
||
client = app.test_client()
|
||
response = client.get('/contact')
|
||
assert response.status_code == 200
|
||
|
||
|
||
def test_contact_route_does_not_accept_post_yet():
|
||
"""POST handler is B-2.x territory — for now, /contact is GET-only."""
|
||
client = app.test_client()
|
||
response = client.post('/contact', data={'name': 'test'})
|
||
assert response.status_code == 405, "POST must return 405 until B-2.x form handler ships"
|
||
|
||
|
||
def test_contact_extends_base_with_h1():
|
||
client = app.test_client()
|
||
body = client.get('/contact').data.decode('utf-8')
|
||
assert '<!DOCTYPE html>' in body
|
||
assert 'page-title' in body
|
||
assert body.count('<h1') == 1
|
||
|
||
|
||
def test_contact_3_methods_email_phone_address():
|
||
client = app.test_client()
|
||
body = client.get('/contact').data.decode('utf-8')
|
||
assert 'methods-title' in body
|
||
# Email
|
||
assert 'href="mailto:info@dictia.ca"' in body
|
||
# Phone (tel: link)
|
||
assert 'href="tel:+15819968471"' in body
|
||
assert '(581) 996-8471' in body
|
||
# Address (postal code)
|
||
assert 'G0S 1K0' in body
|
||
assert 'Inverness' in body
|
||
assert '<address' in body
|
||
|
||
|
||
def test_contact_6_subject_shortcuts():
|
||
"""Pre-filled mailto subject shortcuts for common requests."""
|
||
client = app.test_client()
|
||
body = client.get('/contact').data.decode('utf-8')
|
||
assert 'shortcuts-title' in body
|
||
# 6 shortcut anchors with mailto + subject
|
||
expected_subjects = [
|
||
'Pr%C3%A9-inscription%20DictIA',
|
||
'Devis%20multi-sites',
|
||
'Demande%20de%20d%C3%A9monstration',
|
||
'Dossier%20conformit%C3%A9%20Loi%2025',
|
||
'Partenariat',
|
||
'Question%20m%C3%A9dia',
|
||
]
|
||
for subj in expected_subjects:
|
||
assert subj in body, f"Missing mailto subject shortcut: {subj}"
|
||
# Each shortcut is a focusable link with focus-visible
|
||
assert 'focus-visible:outline-2' in body
|
||
assert 'focus-visible:outline-brand-b1' in body
|
||
|
||
|
||
def test_contact_pre_launch_form_disclaimer():
|
||
"""Until B-2.x ships the form handler, the page must clearly disclose mailto-only."""
|
||
client = app.test_client()
|
||
body = client.get('/contact').data.decode('utf-8')
|
||
assert 'Formulaire en ligne' in body
|
||
assert 'lancement' in body or 'printemps' in body, \
|
||
"Must indicate online form is coming at launch"
|
||
|
||
|
||
def test_contact_uses_oqlf_typography():
|
||
client = app.test_client()
|
||
body = client.get('/contact').data.decode('utf-8')
|
||
assert '2 jours' in body
|
||
assert '9 h' in body
|
||
assert '&nbsp;' not in body
|