Files
dictia-public/tests/test_marketing_secondary_pages.py
Allison 03f6e56f04 feat(marketing): section interactive 'Comment ça marche' (réacteur DictIA cyclant 6 features)
Ajoute une nouvelle section interactive sous les 6 fonctionnalités
(préservées intégralement) reproduisant le composant React
dictai-narrative.tsx en CSS pur + Alpine.js — sans Framer Motion ni
autre lib JS.

- Réacteur central holographique : 3 anneaux concentriques rotatifs
  (15 s / 22 s / 30 s) + 8 particules orbitales (cyan/blue/fuchsia)
  + wordmark DictIA glow pulsant
- Auto-cycle Alpine.js entre 6 features (1.6 s) avec pause au
  hover/focus et reprise au leave/blur
- Panneau feature active avec aria-live='polite' pour annonce
  lecteur d'écran (Transcription · Diarisation · 99+ langues ·
  Exports · Utilisateurs illimités · Partage & Classement)
- Card 'IA intégrée Mistral 7B LOCAL' avec 3 bullets souveraineté
- Spec list cliquable / hover déclenchant feature dans réacteur
- Layout responsive grid 2 cols desktop, stack mobile
- prefers-reduced-motion désactive rings + orbites + auto-cycle
- Position : APRÈS '6 fonctionnalités', AVANT 'Intégrations'
- Sub-nav reste à 4 ancres (sous-partie visuelle de Fonctionnalités)
- Tests : nouveau test_fonctionnalites_how_it_works_reactor_section
  valide structure, contenu canonique, a11y et Alpine bindings

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 09:09:40 -04:00

387 lines
15 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&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():
"""New 'Comment ça marche' interactive reactor section (post-6-features, pre-integrations).
Validates structure (heading + reactor + spec list + Mistral card), 6 cycling features,
canonical content, and a11y signals (aria-labelledby + aria-live status panel).
"""
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)
for feat in ['Transcription', 'Diarisation', '99+ langues', 'Exports',
'Utilisateurs illimités', 'Partage &amp; Classement']:
assert feat in body, f"Missing cycling feature: {feat}"
# Reactor visual: rings, orbits, wordmark, Auto badge
assert 'reactor-ring' in body and 'ring-outer' in body and 'ring-mid' in body and 'ring-inner' in body
assert 'orbit orbit-1' in body and 'orbit orbit-8' in body, "Missing 8 orbital particles"
assert '>DictIA<' in body, "Missing reactor centre wordmark"
# Mistral 7B IA intégrée card with 3 canonical bullets
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
# Alpine reactor data + auto-cycle + hover/focus stop logic
assert "active: \"Transcription\"" in body
assert 'isHovered' in body
assert 'setActive(feat)' in body
assert 'resumeCycle()' in body
# Hover, focus + blur listeners on each list item
assert '@mouseenter="setActive(feat)"' in body
assert '@mouseleave="resumeCycle()"' in body
assert '@focus="setActive(feat)"' in body
assert '@blur="resumeCycle()"' in body
# 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"
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