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:
@@ -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'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éposé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ç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}"
|
||||
|
||||
Reference in New Issue
Block a user