Refactor mécanique strict : - Purge 100% des hex non-brand (#0891b2, #a21caf, #e879f9, #1d4ed8, #9333ea, #f5d0fe, #67e8f9, #1e40af, #93c5fd, #9CA3AF, #0e7490, #EF4444 capital) → mapping vers brand-b1/b2/b3 - Standardisation tailles : 0 inline font-size, text-[9px/10px/11px] uniquement (purge 5.5/6/6.5/7/7.5/8/8.5 arbitraires) - 0 font-family inline (utilise font-sans/font-mono Tailwind) Polish device : - Inner screen seam (effet "écran encastré dans bezel") - Notch : intègre speaker grille 3 dots + camera dot dans la dynamic island - Status bar : vraie batterie 80% fill, vrai WiFi 3 arcs concentriques + dot, signal 4 bars croissantes - Logo DictIA 92×28 plus grand (opacité 85%) Polish modes : - Mode 1 : header compact mic+filename+REC, waveform 16 bars symétriques, file card MP3 redesign avec corner fold - Mode 2 : avatars empilés 18×18 avec bordure white/15, bubbles max-width 80%, timestamps text-[9px] - Mode 3 : grille langues text-[10px] line-height 18px, padding 8px, palette stricte b1/b2/b3 - Mode 4 : grid 4×2 cards 42×50, drop staggered 90ms, palette stricte b1/b2/b3 + dc2626 PDF + 374151 TXT - Mode 5 : header counter Inter font-black text-base, connecting lines opacité 0.18 - Mode 6 : breadcrumb compact, toolbar 4 icons, hover row highlight, palette b1/b2/b3 - Mode 0 : chat bubbles uniformisés text-[10px], footer shield emerald (sécurité) Polish right panel IA : - Brain 40×40 cercle gradient brand-b3 (déjà OK) - Badges Mistral 7B (b3 bg) + LOCAL (emerald bg) - 3 metrics : 0ms grad-text · 100% emerald · 24/7 grad-text font-black text-lg - Sovereignty bullets : icon dans cercle 20×20 rounded-full bg-brand-b3/[0.15] - Padding p-5 généreux Polish feature info card sous phone : - Background uniforme bg-white/[0.06] + border-white/[0.10] - Border-left 3px accent activeColor (style tab indicator) - Icon container 32×32 rounded-md - Badge top-right text-[10px] tracking-wider Polish bottom tab bar : - Buttons 34×42, gap-1 serré - Active : bottom border 2px + scale icon 1.15 + drop-shadow color - Labels text-[9px] uppercase tracking-wider - AUTO pill : px-2.5 py-0.5 rounded-full bg-emerald/12 Tests : - +6 assertions polish (forbidden hex purge, screen seam, white/0.06, brand-b3/[0.15], grad-text) - 9/9 fonctionnalites tests pass - 29/29 marketing tests pass (2 conformite failures pré-existantes baseline) Build : npm run build:css → static/css/marketing.css régénéré pour les nouvelles classes arbitraires Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
481 lines
20 KiB
Python
481 lines
20 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 '900' in body, "Missing 900ms cycle interval"
|
||
assert '4500' in body, "Missing 4500ms manual reset timer"
|
||
# 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
|
||
assert 'modules' in body, "Missing stats row 'modules'"
|
||
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-white/[0.06] + accent border-left
|
||
assert 'bg-white/[0.06]' in body, "Missing uniform white/0.06 bg on feature card"
|
||
# 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
|