feat(marketing): pricing 3 forfaits + ROI calculator Alpine.js
This commit is contained in:
@@ -300,3 +300,106 @@ def test_bento_renders_nbsp_entities_not_escaped():
|
||||
# Q&R card title: French ampersand must survive as & in HTML, not &
|
||||
assert 'Q&R' in body, "Q&R title should appear single-escaped"
|
||||
assert 'Q&R' not in body, "Q&R title must not be double-escaped"
|
||||
|
||||
|
||||
def test_pricing_section_present():
|
||||
"""Pricing section is present after bento section, with eyebrow + H2 + tax disclaimer."""
|
||||
client = app.test_client()
|
||||
body = client.get('/').data.decode('utf-8')
|
||||
assert 'pricing-title' in body, "Missing pricing section anchor"
|
||||
assert 'Choisissez votre formule' in body, "Missing pricing H2"
|
||||
# Tax disclaimer must be visible (LPC art. 219 — total cost transparency)
|
||||
assert 'TPS' in body and 'TVQ' in body, "Missing tax disclaimer (TPS/TVQ)"
|
||||
|
||||
|
||||
def test_pricing_3_tiers_with_canonical_amounts():
|
||||
"""Pricing has 3 tiers: DictIA 8 (3450/173), DictIA 16 (5750/201), DictIA Cloud (0/369)."""
|
||||
client = app.test_client()
|
||||
body = client.get('/').data.decode('utf-8')
|
||||
# Names
|
||||
for name in ['DictIA 8', 'DictIA 16', 'DictIA Cloud']:
|
||||
assert name in body, f"Missing pricing tier: {name}"
|
||||
# Canonical prices with NBSP per OQLF
|
||||
assert '3 450 $' in body, "Missing DictIA 8 setup price"
|
||||
assert '173 $' in body, "Missing DictIA 8 monthly price"
|
||||
assert '5 750 $' in body, "Missing DictIA 16 setup price"
|
||||
assert '201 $' in body, "Missing DictIA 16 monthly price"
|
||||
assert '369 $' in body, "Missing DictIA Cloud monthly price (canonical 369$)"
|
||||
|
||||
|
||||
def test_pricing_recommended_tier_is_dictia_16():
|
||||
"""DictIA 16 is the visually-recommended tier (RECOMMANDÉ badge + grad-bg frame)."""
|
||||
client = app.test_client()
|
||||
body = client.get('/').data.decode('utf-8')
|
||||
assert 'RECOMMAND' in body, "Missing RECOMMANDÉ badge"
|
||||
# The recommended tier wraps in grad-bg p-[1.5px] rounded-[20px] FlexiHub style
|
||||
assert 'grad-bg p-[1.5px] rounded-[20px]' in body, "Missing FlexiHub gradient frame on recommended tier"
|
||||
|
||||
|
||||
def test_pricing_cta_uses_reserver_pre_launch_wording():
|
||||
"""CTAs say 'Réserver' not 'Choisir' — pre-launch LPC art. 219 hygiene."""
|
||||
client = app.test_client()
|
||||
body = client.get('/').data.decode('utf-8')
|
||||
for slug in ['dictia-8', 'dictia-16', 'dictia-cloud']:
|
||||
assert f'href="/checkout/{slug}"' in body, f"Missing checkout link for {slug}"
|
||||
assert 'Réserver DictIA 8' in body or 'Réserver DictIA 8' in body, "CTA must use 'Réserver' wording (pre-launch)"
|
||||
|
||||
|
||||
def test_pricing_features_use_safe_filter_no_double_escape():
|
||||
"""Pricing card features piped through | safe — ' ' must render single-escaped, not double."""
|
||||
client = app.test_client()
|
||||
body = client.get('/').data.decode('utf-8')
|
||||
# GPU sizes use NBSP
|
||||
assert 'GPU 8 Go RTX' in body, "GPU 8 Go feature missing or NBSP double-escaped"
|
||||
assert 'GPU 16 Go RTX' in body, "GPU 16 Go feature missing or NBSP double-escaped"
|
||||
# Q&R card must use French Q&R, not English Q&A
|
||||
assert 'Q&R' in body, "DictIA 16 must mention Q&R (French), not Q&A (English)"
|
||||
assert 'Q&A' not in body, "Must use French Q&R consistently — no English Q&A"
|
||||
# Loi 25 with NBSP
|
||||
assert 'Conforme Loi 25' in body, "Conforme Loi 25 must use NBSP"
|
||||
# SLA must be hedged ('visé') not absolute claim
|
||||
assert 'SLA visé 99,9' in body, "SLA must be hedged 'visé' (pre-launch LPC art. 219 hygiene)"
|
||||
# Negative: NO double-escape
|
||||
assert ' ' not in body, "NBSP must not be double-escaped — | safe missing on pricing macro?"
|
||||
|
||||
|
||||
def test_pricing_uses_wcag_safe_text_on_white():
|
||||
"""Pricing card text uses text-brand-navy/70 or /80 minimum (WCAG AA on white)."""
|
||||
client = app.test_client()
|
||||
body = client.get('/').data.decode('utf-8')
|
||||
# No regression to weak opacities like /40 or /50 in pricing area
|
||||
assert 'text-brand-navy/70' in body
|
||||
# The features list uses /80 in our impl
|
||||
assert 'text-brand-navy/80' in body, "Feature text should use /80 for WCAG AA"
|
||||
|
||||
|
||||
def test_roi_calculator_present_with_alpine_bindings():
|
||||
"""ROI calculator section present with Alpine.js bindings + transparent hypotheses footnote."""
|
||||
client = app.test_client()
|
||||
body = client.get('/').data.decode('utf-8')
|
||||
assert 'CALCULATEUR ROI' in body
|
||||
assert 'roi-title' in body, "ROI calculator must have aria-labelledby anchor"
|
||||
assert 'x-data="roiCalculator()"' in body
|
||||
# Three sliders with x-model.number for type coercion
|
||||
assert 'x-model.number="users"' in body
|
||||
assert 'x-model.number="hours"' in body
|
||||
assert 'x-model.number="rate"' in body
|
||||
# Live output bindings
|
||||
assert 'x-text="savings' in body
|
||||
assert 'x-text="\'Payback' in body
|
||||
# Transparent hypothesis footnote — LPC art. 219 hygiene
|
||||
assert '80 %' in body and 'jours ouvrables' in body, "Missing transparent hypothesis footnote"
|
||||
# Sliders accessible (aria-label on each input)
|
||||
assert 'aria-label="Nombre d' in body
|
||||
|
||||
|
||||
def test_roi_calculator_script_loaded():
|
||||
"""roi_calculator.js loaded via {% block scripts %} (deferred after Alpine.js)."""
|
||||
client = app.test_client()
|
||||
body = client.get('/').data.decode('utf-8')
|
||||
assert '/static/js/roi_calculator.js' in body, "ROI script must be referenced"
|
||||
# Must come AFTER alpine.min.js in the document order
|
||||
alpine_pos = body.find('alpine.min.js')
|
||||
roi_pos = body.find('roi_calculator.js')
|
||||
assert alpine_pos != -1 and roi_pos != -1
|
||||
assert alpine_pos < roi_pos, "Alpine.js must load before roi_calculator.js"
|
||||
|
||||
Reference in New Issue
Block a user