fix(marketing): pricing — honest ROI payback + capped sliders + URL hygiene
- ROI payback now returns raw months; template branches to 'moins d'un mois'
for sub-month paybacks and 'Payable dès la première année' when savings≤0
(was rounding up to 'Payback : 1 mois' for ~95% of slider combos)
- Cap sliders: users 1..25 (was 50), hours 0.5..4 (was 8) to keep displayed
savings in a defensible band (~8.8 M$/yr max instead of 35 M$)
- pricing_card href uses cta_url.rstrip('/') to avoid double-slash if caller
passes a trailing slash (preempts A-2.8 / B-2.7 regression)
- aria-live polite + aria-atomic on the savings paragraph so screen readers
announce slider updates
- Cleaner JS module pattern: single window.roiCalculator = function() {...}
- Tests updated for payback ternary; new tests for slider caps, aria-live,
and double-slash guard
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -386,7 +386,9 @@ def test_roi_calculator_present_with_alpine_bindings():
|
||||
assert 'x-model.number="rate"' in body
|
||||
# Live output bindings
|
||||
assert 'x-text="savings' in body
|
||||
assert 'x-text="\'Payback' in body
|
||||
assert 'payback === null' in body, "Payback display must use null sentinel branch"
|
||||
assert "moins d\\'un mois" in body or 'moins d'un mois' in body or "moins d'un mois" in body, \
|
||||
"Payback display must offer 'moins d'un mois' branch"
|
||||
# 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)
|
||||
@@ -403,3 +405,37 @@ def test_roi_calculator_script_loaded():
|
||||
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"
|
||||
|
||||
|
||||
def test_roi_calculator_sliders_capped_defensibly():
|
||||
"""Sliders capped: users<=25, hours<=4 (LPC art. 219 hygiene — no $35M screenshots)."""
|
||||
client = app.test_client()
|
||||
body = client.get('/').data.decode('utf-8')
|
||||
# Users slider max must be 25, not 50
|
||||
assert 'x-model.number="users"' in body
|
||||
assert 'max="25"' in body, "Users slider must cap at 25 (was 50 — too aggressive for marketing claim)"
|
||||
# Hours slider max must be 4, not 8
|
||||
assert 'x-model.number="hours"' in body
|
||||
assert 'max="4"' in body, "Hours slider must cap at 4 (was 8 — too aggressive)"
|
||||
|
||||
|
||||
def test_roi_savings_paragraph_has_aria_live():
|
||||
"""Savings <p> must announce updates to screen readers on slider change (aria-live polite)."""
|
||||
client = app.test_client()
|
||||
body = client.get('/').data.decode('utf-8')
|
||||
# The savings paragraph (the headline number) must be a polite live region
|
||||
assert 'aria-live="polite"' in body
|
||||
assert 'aria-atomic="true"' in body
|
||||
# Verify it's on the savings line, not somewhere unrelated
|
||||
# (The savings p is the only element with text-5xl in the section)
|
||||
assert 'text-5xl font-black grad-text' in body
|
||||
|
||||
|
||||
def test_pricing_cta_url_no_double_slash():
|
||||
"""pricing_card uses cta_url.rstrip('/') so href never has '//' (regression guard)."""
|
||||
client = app.test_client()
|
||||
body = client.get('/').data.decode('utf-8')
|
||||
# All 3 CTAs use the default cta_url='/checkout' (no trailing slash) — so /checkout/dictia-X
|
||||
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}"
|
||||
|
||||
Reference in New Issue
Block a user