feat(marketing): A-2.7a footer 4-col + comparatif table + conformité 4 pillars

- Replace placeholder _footer.html with full 4-column footer (Brand, Produit,
  Légal, Compte) — added aria-labelledby landmark, sr-only headings, address
  block, tel: link, external-link new-tab announcements, NBSP per OQLF
- Add Comparatif section: DictIA vs MS Teams vs Otter.ai vs Whisper local
  on 6 criteria. Sourced (politiques publiques + grilles officielles 2026-04-27),
  trademark disclaimer, responsive table with sr-only caption + scope attrs
- Add Conformité forteresse section: 4 pillars (OVH Beauharnois, mapped Loi 25
  LPRPSP refs, compatible Cadre IA LGGRI, AGPL v3 verifiable Gitea link)
- Soft hedges throughout: 'mappé', 'conçu avec', 'compatible' (no 'certifié',
  'endossé', 'reconnu' — LPC art. 219 / Competition Act s. 52 hygiene)
- 11 new tests covering footer landmarks, comparatif sourcing, conformité
  hedges, WCAG /70 contrast, and forbidden competitive claim regressions

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Allison
2026-04-27 19:21:39 -04:00
parent 7d67b64ddc
commit 0d69fcd034
4 changed files with 400 additions and 17 deletions

View File

@@ -439,3 +439,165 @@ def test_pricing_cta_url_no_double_slash():
for slug in ['dictia-8', 'dictia-16', 'dictia-cloud']:
assert f'href="/checkout/{slug}"' in body, f"Missing single-slash href for {slug}"
assert f'href="/checkout//{slug}"' not in body, f"Double-slash regression for {slug}"
def test_footer_has_4_columns_with_aria_labels():
"""Full footer has 4 columns (Brand, Produit, Légal, Compte) with proper landmarks."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
# Footer landmark with accessible name
assert 'footer-heading' in body, "Footer must have an accessible heading"
assert 'aria-label="Produit"' in body
assert 'aria-label="L' in body and 'gal"' in body # Légal (handles entity-encoded é)
assert 'aria-label="Compte"' in body
# Address with tel + mailto
assert '<address' in body
assert 'href="tel:+15819968471"' in body
assert 'href="mailto:info@dictia.ca"' in body
def test_footer_links_complete():
"""Footer must link to all 4 product pages, 5 legal pages, and account flows."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
# Product
for url in ['/fonctionnalites', '/tarifs', '/conformite', '/blog']:
assert f'href="{url}"' in body, f"Footer missing product link {url}"
# Legal
for url in ['/legal/conditions', '/legal/confidentialite', '/legal/cookies',
'/legal/remboursement', '/legal/accessibilite']:
assert f'href="{url}"' in body, f"Footer missing legal link {url}"
# Account
for url in ['/login', '/signup', '/contact']:
assert f'href="{url}"' in body, f"Footer missing account link {url}"
# External AGPL source link
assert 'gitea.innova-ai.ca/Innova-AI/dictia-public' in body
assert 'rel="noopener"' in body, "External links must use rel=noopener"
def test_footer_external_link_screen_reader_hint():
"""External link to Gitea has sr-only hint indicating new tab opens."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
# The "↗" arrow should be aria-hidden + sr-only fallback
assert "s'ouvre dans un nouvel onglet" in body or "s&#39;ouvre dans un nouvel onglet" in body, \
"External link must announce new tab opening to screen readers"
def test_footer_uses_wcag_safe_text_on_dark():
"""Footer text uses text-white/70 minimum (WCAG AA on bg-brand-navy2)."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
# No regression to weaker /40 or /50 in footer area
# Check the footer block specifically
footer_start = body.find('<footer')
footer_end = body.find('</footer>') + len('</footer>')
footer_html = body[footer_start:footer_end]
assert 'text-white/70' in footer_html, "Footer text must use /70 opacity for WCAG AA"
# Negative regression
assert 'text-white/40' not in footer_html, "Footer must not regress to /40 opacity"
assert 'text-white/50' not in footer_html, "Footer must not regress to /50 opacity"
def test_comparatif_section_present():
"""Comparatif section is present after Pricing with table + sourcing footnote."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
assert 'comparatif-title' in body
assert 'COMPARATIF' in body
assert 'DictIA face aux solutions cloud' in body
# Sourcing footnote (LPC art. 219 hygiene)
assert 'sources publiques' in body, "Must disclose sources for competitor claims"
assert '2026-04-27' in body, "Must date the comparison"
# Trademark disclaimer
assert 'marques déposées' in body or 'marques d&eacute;pos&eacute;es' in body, \
"Trademark disclaimer required for competitor names"
def test_comparatif_table_has_4_competitors_and_6_criteria():
"""Comparatif table lists DictIA + 3 competitors over 6 criteria rows."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
# Column headers
for col in ['DictIA', 'MS Teams Premium', 'Otter.ai Business', 'Whisper local']:
assert col in body, f"Comparatif missing column: {col}"
# 6 criteria (extract by their distinctive phrasing)
criteria_keywords = [
'Conforme Loi', # row 1
'Cloud Act', # row 2
'Large-v3 fine-tun', # row 3 (escaped or raw)
'Diarisation', # row 4
'utilisateur/mois', # row 5
'Audit trail' # row 6
]
for kw in criteria_keywords:
assert kw in body, f"Comparatif missing criterion containing: {kw}"
def test_comparatif_uses_responsive_overflow_scroll():
"""Comparatif table wraps in overflow-x-auto for narrow viewports + has accessible caption."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
assert 'overflow-x-auto' in body
# Caption is sr-only but mandatory for table accessibility
assert '<caption class="sr-only">' in body
# Scope attributes on column and row headers
assert 'scope="col"' in body
assert 'scope="row"' in body
def test_conformite_section_present():
"""Conformité forteresse section is present with 4 pillar cards."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
assert 'conformite-title' in body
assert 'CONFORMIT' in body and 'FORTERESSE' in body
# Soft hedge: "conçue avec" (not "certifiée par")
assert 'conçue avec' in body or 'con&ccedil;ue avec' in body, \
"Must use soft hedge 'conçue avec' (LPC art. 219)"
def test_conformite_4_pillars():
"""Conformité has 4 pillars: hébergement, Loi 25, Cadre IA, AGPL."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
pillar_keywords = [
'OVH Beauharnois', # pillar 1
'LPRPSP', # pillar 2 (Loi 25 reference)
'LGGRI', # pillar 3 (Cadre IA reference)
'AGPL' # pillar 4
]
for kw in pillar_keywords:
assert kw in body, f"Conformité missing pillar reference: {kw}"
# Soft hedges (LPC art. 219)
assert 'Mapp' in body, "Must use 'Mappé' (not 'Certifié')"
# Citation/contact for verification
assert 'info@dictia.ca' in body
def test_conformite_uses_wcag_safe_text_on_dark():
"""Conformité card text uses text-white/80 minimum on bg-brand-navy."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
# Within the conformite section block specifically
section_start = body.find('id="conformite-title"')
# Find next </section>
section_end = body.find('</section>', section_start)
section_html = body[section_start:section_end]
assert 'text-white/80' in section_html or 'text-white/70' in section_html, \
"Conformité must use /70+ on dark for WCAG AA"
def test_no_unverifiable_competitor_claims():
"""Comparatif must NOT contain unhedged percentage claims about competitors (LPC art. 219)."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
# Forbidden patterns: bold quantitative claims like '5 stars', '100% accurate', 'X% precision'
# We allow our own '95%+' (already hedged with methodology footnote elsewhere)
forbidden_phrases = [
'Otter.ai a 100', # No claims about Otter accuracy
'Teams a 99', # No claims about Teams accuracy
'50% moins cher', # No comparative pricing without verification
]
for phrase in forbidden_phrases:
assert phrase not in body, f"Forbidden competitive claim: {phrase}"