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:
@@ -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 ' ')
|
||||
assert ' ' not in body[body.find('"FAQPage"'):body.find('</script>', body.find('"FAQPage"'))], \
|
||||
"JSON-LD must not contain raw ' ' 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é-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
|
||||
|
||||
Reference in New Issue
Block a user