Files
dictia-public/tests/test_marketing_secondary_pages.py
Allison a14bcb9a1a fix(marketing): restaurer visibilité 12 fonctions dans 'Comment ça marche'
L'abstraction en 4 catégories (commit 8a7650f) cachait les 13 modes
derrière des regroupements ; l'utilisateur ne VOYAIT plus les 6 boutons
features originaux et percevait que des fonctions avaient disparu.

Refonte v2 :
- Bottom tab bar : 6 boutons FEATURES originaux 1-6 (Trans/Diari/Lang/Exp/Users/Part) — visibles, cliquables
- Right panel : grid 3×4 = 12 boutons FEATURES 1-12 (IA mode 0 reste accessible via Brain card)
- Mobile pills : 12 features scrollables horizontalement
- Auto-cycle : 1→12 skip 0, 1100ms each (~13s cycle complet)
- Manual click : isManual 4500ms reset puis reprend auto
- SUPPRESSION : CATEGORIES array + activeCategory state + handleCategorySelect + sub-mode dots indicator
- AJOUT : featureGridLabel() pour labels compacts (Recording/Recherche IA/Résumés/Intégrations/Audit trail/Conformité)

Préservé : phone shell statique, palette brand stricte b1/b2/b3, IA Mistral
card inchangée, 13 templates de modes (0-12) intacts, eyebrow brand-navy noir,
WCAG aria-labels + aria-pressed, prefers-reduced-motion guard.

Test adapté : assert featureGridLabel + 12 fonctions + labels grid.
2026-04-29 13:17:09 -04:00

508 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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&nbsp;$' in body
assert '349&nbsp;$' in body
assert '549&nbsp;$' in body
assert '485&nbsp;$' in body # Cloud Pro onboarding
assert '5&nbsp;998&nbsp;$' 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 &ecirc;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&nbsp;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&nbsp;5&nbsp;%' in body
assert 'TVQ&nbsp;9,975&nbsp;%' in body
assert 'Loi&nbsp;25' in body
# No double-escape
assert '&amp;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 (v2) : 12 fonctions visibles directement (1-12), IA mode 0 via Brain card.
# Bottom tab bar = 6 boutons originaux (Trans/Diari/Lang/Exp/Users/Part) ; right panel grid = 12 features.
assert 'featureGridLabel' in body, "Missing featureGridLabel (right grid 12 buttons)"
assert '12 fonctions' in body, "Missing '12 fonctions' badge in right panel header"
# Labels compacts grid right panel
for grid_label in ['Recording', 'Recherche IA', 'Résumés', 'Intégrations', 'Audit trail', 'Conformité']:
assert grid_label in body, f"Missing right grid label : {grid_label}"
# 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&nbsp;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 v2 2026-04-29 : 12 fonctions visibles)
assert 'modules' in body, "Missing stats row 'modules'"
assert 'fonctions' in body, "Missing stats row 'fonctions' (refonte v2)"
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&eacute;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&nbsp;%+' in body, "WhisperX precision NBSP entity"
# No double-escape
assert '&amp;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&eacute;rim&egrave;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.&nbsp;3.3', 'Art.&nbsp;3.5', 'Art.&nbsp;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&nbsp;25' in body
assert 'AGPL&nbsp;v3' in body
assert 'art.&nbsp;3.5' in body or 'Art.&nbsp;3.5' in body
assert '&amp;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)&nbsp;996-8471' in body
# Address (postal code)
assert 'G0S&nbsp;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&nbsp;jours' in body
assert '9&nbsp;h' in body
assert '&amp;nbsp;' not in body