10 améliorations cinématiques sur la section interactive : PHONE FRAME RÉALISTE - Bezel double border + glow ring externe pulsant - Notch (Dynamic Island stylisé) + speaker grille + camera dot - Status bar mobile (9:41, signal/wifi/batterie SVG) - Drop shadow dramatique + inner shadow encastrée MIC + SOUND WAVES - 3 ripples concentriques émanant du mic (sound-ring keyframes) MODE 1 TRANSCRIPTION - Header REC indicator pulsant (rouge) + nom fichier - Waveform animée (12 bars hauteurs randomisées) - Barre progress double avec gradient + glow MODE 2 DIARISATION - Header conversation : 3 avatars empilés overlapping - Bubbles avec timestamps (09:0X) + box-shadow - Typing indicator (3 dots) entre messages MODE 3 LANGUES - Header DÉTECTION AUTOMATIQUE avec dot pulsant - Ripple wave depuis le centre (rippleDelay calculé par distance) - 5 langues highlight aléatoire glow brièvement - Counter live FR · EN · ES · ... + 99+ détectées MODE 4 EXPORTS - Grid 4×2 organisé (au lieu de wrap aléatoire) - Files détaillés avec mini-pages (3 lignes texte) - Subtitle '7 FORMATS PRÊTS' + checkmark vert MODE 5 USERS - Counter centré 01 → 20 (font-mono black) - 5 USER_COLORS variations (purple/cyan/green/blue/amber) - Connecting lines SVG pointillés vers centre MODE 6 SHARE - Breadcrumb 'Mes dossiers › Réunions' - Toolbar mini (search, filter, sort) - Rows structurées avec 3-dots action BOTTOM ZONE - 6 boutons agrandis (30+px) avec labels + tab indicator border 2px - AUTO pill (badge dot pulse) / Manuel countdown bar 4.5s CARD INFO - Icon container 32×32 + badge top-right + hover lift RIGHT PANEL IA - Brain dans cercle 40×40 gradient + glow - Metrics row : 0ms latence · 100% privé · 24/7 - Sovereignty bullets : icon dans rounded box GLOBAL - Section background : grid pattern + 2 floating orbs blur - Connecting line SVG cyan→violet phone↔IA panel - Header section : eyebrow gradient pill + stats row (6/99+/0) + CTA démo - Crossfade transitions modes (scale 0.96 → 1) ACCESSIBILITÉ - prefers-reduced-motion désactive toutes animations - @media (max-width: 767px) désactive éléments décoratifs CPU-intensive - aria-pressed, aria-live polite, focus-visible préservés - 30 nouveaux keyframes CSS scopés Tests : 1 existant + 1 enrichi (53 lignes, 22 nouvelles assertions) Tests pass : 30/30 sur how-it-works (les 2 fails conformite sont pré-existants).
462 lines
19 KiB
Python
462 lines
19 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"
|
|
|
|
|
|
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
|