- /tarifs page: extends base.html, reuses pricing_card + button macros, shows the 3 forfaits with NBSP-formatted CAD prices, an 8-row deep-dive comparison matrix (DictIA 8 vs DictIA 16 vs DictIA Cloud), 5 tarification FAQ items (frais cachés, migration, GPU, taxes TPS/TVQ, plans annuels) with Alpine accordion + focus-visible WCAG 2.2 AA, CTA section - /fonctionnalites page: extends base.html, reuses bento_card macro, re-renders the 6 features with same content as landing's bento section for consistency, adds dedicated 7-format export grid + 8-integration grid (with trademark disclaimer) + 6 tech specs section (Whisper/pyannote /Mistral/stack/audio/langues), CTA section - routes.py: add /tarifs and /fonctionnalites routes (passes FAQ to /tarifs for the tarification accordion; preserves existing landing(), TESTIMONIALS, FAQ data structures unchanged) - tests/test_marketing_secondary_pages.py: NEW test file (16 tests covering routes 200, base.html inheritance, H1 anchors, 3 pricing cards, comparison matrix, tarifs FAQ accordion, OQLF typography, 6 bento + 7 exports + 8 integrations + 6 tech specs sections, canonical meta) - All sections respect WCAG 2.2 AA, FlexiHub design discipline, LPC art. 219 hygiene (sourcing dates, trademark disclaimer, hedged claims, NBSP)
179 lines
6.4 KiB
Python
179 lines
6.4 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_3_pricing_cards():
|
|
client = app.test_client()
|
|
body = client.get('/tarifs').data.decode('utf-8')
|
|
for tier in ['DictIA 8', 'DictIA 16', 'DictIA Cloud']:
|
|
assert tier in body
|
|
# Canonical NBSP prices
|
|
assert '3 450 $' in body
|
|
assert '5 750 $' in body
|
|
assert '369 $' in body
|
|
assert 'href="/checkout/dictia-8"' in body
|
|
assert 'href="/checkout/dictia-16"' in body
|
|
assert 'href="/checkout/dictia-cloud"' in body
|
|
|
|
|
|
def test_tarifs_comparison_matrix_8_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
|
|
# 8 row keywords
|
|
for kw in ['Hébergement', 'GPU', 'Volume audio', 'Utilisateurs',
|
|
'Diarisation', 'Mistral 7B local', 'Q&R', 'Délai']:
|
|
assert kw in body, f"Missing matrix row keyword: {kw}"
|
|
|
|
|
|
def test_tarifs_pricing_faq_5_questions():
|
|
client = app.test_client()
|
|
body = client.get('/tarifs').data.decode('utf-8')
|
|
assert 'tarifs-faq-title' in body
|
|
for i in range(1, 6):
|
|
assert f'tarifs-faq-panel-{i}' in body, f"Missing tarifs FAQ panel {i}"
|
|
# Alpine accordion bindings
|
|
assert body.count('x-data="{ open: false }"') >= 5
|
|
# 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_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"
|
|
assert 'GPU 8 Go RTX' not in body # Bento card calls don't use 8 Go RTX (that's pricing)
|
|
assert 'Q&R' in body, "French Q&R (not Q&A)"
|
|
# 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
|