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:
Allison
2026-04-27 19:05:36 -04:00
parent 0ae4053faa
commit 7d67b64ddc
4 changed files with 48 additions and 11 deletions

View File

@@ -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&#39;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&nbsp;%' 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}"