feat(marketing): A-2.7b témoignages placeholder + FAQ accordion + CTA + JSON-LD

- Pre-launch testimonials section: 3 placeholder cards (avocat, CPA, municipal)
  with persona icons + 'Témoignage à venir' label — NO fabricated quotes
  (LPC art. 219). Expected publication mai-juin 2026 from T-4.1 interviews.
- FAQ accordion: 7 verifiable Q&A using Alpine.js core (x-data + x-show +
  built-in x-transition; NO x-collapse plugin). Each item has @click toggle,
  :aria-expanded, aria-controls, role="region" panel, focusable button.
- Schema.org FAQPage JSON-LD inline at end of FAQ section — striptags +
  replace(' ', ' ') to normalize entities for Google FAQ rich result.
- CTA final: 'Réservez votre pré-inscription' (mailto + #tarifs anchor),
  cosmic orbs to mirror Hero (page closure), ghost variant secondary button.
- Inline TESTIMONIALS and FAQ Python lists in src/marketing/routes.py
  (no PyYAML dep — YAGNI; T-4.1 can introduce it when real data warrants).
- 8 new tests covering testimonials placeholders, forbidden fake names,
  7 FAQ panels, Alpine bindings, JSON-LD schema, CTA wording, route data.
This commit is contained in:
Allison
2026-04-27 19:52:36 -04:00
parent 31fada46d4
commit 824ea638de
4 changed files with 324 additions and 1 deletions

View File

@@ -621,3 +621,120 @@ def test_comparatif_check_marks_consistently_mean_good():
assert 'Exposé au Cloud Act' not in body, "Row 2 must be reworded to positive convention"
# Specifically check Teams gets ✗ for the territoriality criterion (was ⚠ before)
assert '✗ Soumis Cloud Act' in body, "Teams must show ✗ for non-Loi-25-compliant transfer"
def test_testimonials_section_present_with_placeholder_cards():
"""Témoignages section present with 3 placeholder cards (no fabricated quotes)."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
assert 'testimonials-title' in body
assert 'Premiers cabinets pilotes' in body
# 3 personas
assert 'Cabinet juridique pilote' in body
assert 'Cabinet CPA pilote' in body
assert 'Municipalit' in body and 'pilote' in body
# No fabricated quotes — must say "Témoignage à venir"
assert 'Témoignage à venir' in body or 'Témoignage à venir' in body, \
"Pre-launch testimonials must show 'Témoignage à venir' (LPC art. 219)"
# Expected publication months
assert 'Mai 2026' in body
assert 'Juin 2026' in body
def test_testimonials_use_personas_not_fake_names():
"""Testimonials must NOT contain fabricated names (Me Tremblay, Mme Bouchard, etc.)."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
forbidden_names = ['Me Tremblay', 'Mme Bouchard', 'M. Lefebvre',
'Cabinet Pilote A', 'Cabinet Pilote B', 'Municipalité Pilote C']
for name in forbidden_names:
assert name not in body, f"Forbidden fabricated testimonial name: {name}"
def test_faq_section_with_7_questions():
"""FAQ section present with 7 questions."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
assert 'faq-title' in body
# 7 panel IDs (loop.index is 1-indexed)
for i in range(1, 8):
assert f'id="faq-panel-{i}"' in body, f"Missing FAQ panel {i}"
# Question topic anchors
topics = ['Loi', 'Cloud et DictIA on-premise', 'fran', 'audiences', 'formats',
'résilie', 'AGPL']
for topic in topics:
assert topic in body, f"FAQ missing topic anchor: {topic}"
def test_faq_alpine_accordion_bindings():
"""FAQ uses Alpine.js x-data + @click + :aria-expanded for accessible accordion."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
# 7 x-data="{ open: false }" instances
assert body.count('x-data="{ open: false }"') == 7, \
"FAQ must have 7 independent Alpine accordion items"
# Each toggle button has @click and :aria-expanded
assert body.count('@click="open = !open"') == 7
assert body.count(':aria-expanded="open.toString()"') == 7
# Use built-in x-transition (NOT x-collapse plugin which is not bundled)
assert 'x-collapse' not in body, "Must NOT use x-collapse plugin (not loaded — use x-transition)"
assert 'x-transition.opacity' in body, "FAQ panels must use built-in x-transition"
# aria-controls links button to panel
assert 'aria-controls="faq-panel-1"' in body
def test_faq_jsonld_schema_present():
"""FAQ section embeds Schema.org FAQPage JSON-LD for SEO/GEO."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
# Inline JSON-LD script
assert '<script type="application/ld+json">' in body
assert '"@type": "FAQPage"' in body
assert '"@type": "Question"' in body
assert '"@type": "Answer"' in body
# NBSP must be normalized to plain space in JSON-LD (Google would otherwise parse '&nbsp;')
assert '&nbsp;' not in body[body.find('"FAQPage"'):body.find('</script>', body.find('"FAQPage"'))], \
"JSON-LD must not contain raw '&nbsp;' entities — strip them server-side"
def test_cta_final_section():
"""CTA final section with mailto pré-inscription + ghost button to #tarifs."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
assert 'cta-title' in body
assert 'pré-inscription' in body or 'pr&eacute;-inscription' in body
# mailto with subject
assert 'href="mailto:info@dictia.ca?subject=Pr%C3%A9-inscription%20DictIA"' in body or \
'href="mailto:info@dictia.ca?subject=Pré-inscription%20DictIA"' in body, \
"CTA must have mailto with subject prefilled"
# Anchor link to existing #tarifs section
assert 'href="#tarifs"' in body, "Secondary CTA must anchor to pricing"
# Ghost variant button
assert 'border-white/[0.08]' in body # ghost button class
def test_cta_final_uses_safe_pre_launch_wording():
"""CTA must use 'Réservez votre pré-inscription' not 'Achetez maintenant' (pre-launch)."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
forbidden = ['Achetez maintenant', 'Acheter maintenant', 'Acheter immédiatement',
'Acheter dès aujourd', 'Disponible immédiatement']
for phrase in forbidden:
assert phrase not in body, f"Forbidden pre-launch CTA: {phrase}"
def test_routes_passes_testimonials_and_faq_to_template():
"""marketing.routes.landing() must pass testimonials and faq to render_template."""
# Import the module to verify the data lists exist
from src.marketing import routes
assert hasattr(routes, 'TESTIMONIALS'), "Module must define TESTIMONIALS list"
assert hasattr(routes, 'FAQ'), "Module must define FAQ list"
assert len(routes.TESTIMONIALS) == 3, "Must have 3 placeholder testimonials"
assert len(routes.FAQ) == 7, "Must have 7 FAQ entries"
# Each testimonial must NOT contain a 'quote' field (no fabricated quotes pre-launch)
for t in routes.TESTIMONIALS:
assert 'quote' not in t, "Pre-launch testimonials must not contain quote fields"
assert 'persona' in t and 'placeholder_label' in t and 'expected' in t
# Each FAQ entry must have 'q' and 'a'
for item in routes.FAQ:
assert 'q' in item and 'a' in item