PS pension module: optional federal PSSA engine + UI (PSPC-accurate, EN/FR)
This commit is contained in:
1 parent
fe62022a32
commit
583b5ee2f8
14 files changed
+1991
-27
No files matched your search
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""PS pension module UI audit — https://retire.krisforbes.ca in EN + FR at 320/360/390 + 1280.
|
||||
Drives localStorage state (PS answers), captures screenshots + geometry at the PS quiz steps
|
||||
and the results card. Checks:
|
||||
- no horizontal document overflow (scrollWidth <= innerWidth + 1)
|
||||
- #step-body is the scroller; #btn-next + #panel bottom cluster fully in viewport
|
||||
- #ps-card table fits the viewport; sources links present
|
||||
Writes audit/ps-pension/geo.json + *.png, prints PS-AUDIT-OK/FAIL.
|
||||
"""
|
||||
import json, os, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
OUT = 'audit/ps-pension'
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
CHROME = '/usr/bin/google-chrome'
|
||||
BASE = 'https://retire.krisforbes.ca/'
|
||||
LS = 'retire-planner-v1'
|
||||
|
||||
ANS = {
|
||||
"province": "ON", "age": 60, "retireAge": 60, "household": "single",
|
||||
"partner": {"age": 60, "income": 0, "rrsp": 0, "tfsa": 0, "nonreg": 0, "monthlyContribution": 0},
|
||||
"income": 0, "rrsp": 100000, "tfsa": 30000, "nonreg": 20000,
|
||||
"monthlyContribution": 0, "employerMatchMonthly": 0,
|
||||
"spendingAnnual": 50000, "cppStartAge": 65, "oasStartAge": 65,
|
||||
"pensionAnnual": 0, "rentalAnnual": 0, "otherAnnual": 0,
|
||||
"psPension": True, "psJoinYear": 2000, "psServiceYears": 30, "psAvgSalary": 100000,
|
||||
"psOccupation": "it", "psItLevel": "IT-02", "psItStep": 1,
|
||||
"realReturnPre": 3.5, "realReturnPost": 2.5, "inflation": 2.0, "lifeExp": 95,
|
||||
"interests": ["dock", "travel"],
|
||||
}
|
||||
STEPS = { # visible step index (single household, module ON): 20 steps
|
||||
'ps-toggle': 11, 'ps-join': 12, 'ps-service': 13, 'ps-salary': 14,
|
||||
'ps-occ': 15, 'ps-it': 16, 'extra-note': 17,
|
||||
}
|
||||
DEVICES = [
|
||||
('iphone14', 390, 844, 3.0),
|
||||
('pixel', 360, 800, 2.75),
|
||||
('se', 320, 700, 2.0),
|
||||
('desktop', 1280, 900, 1.0),
|
||||
]
|
||||
|
||||
GEO_JS = """() => {
|
||||
const de = document.documentElement;
|
||||
const out = {
|
||||
docOverflowX: de.scrollWidth - window.innerWidth,
|
||||
stepBodyScroll: (() => { const s = document.getElementById('step-body'); return s ? (s.scrollHeight > s.clientHeight) : null; })(),
|
||||
btnNextInView: (() => { const b = document.getElementById('btn-next'); if (!b) return null; const r = b.getBoundingClientRect(); return r.bottom <= window.innerHeight + 1 && r.top >= -1; })(),
|
||||
};
|
||||
const card = document.getElementById('ps-card');
|
||||
if (card && !card.hidden) {
|
||||
const r = card.getBoundingClientRect();
|
||||
const t = card.querySelector('table');
|
||||
out.psCard = { visible: getComputedStyle(card).display !== 'none', width: Math.round(r.width),
|
||||
fits: (t ? t.scrollWidth : 0) <= window.innerWidth + 1,
|
||||
links: card.querySelectorAll('a').length };
|
||||
}
|
||||
return out;
|
||||
}"""
|
||||
|
||||
def main():
|
||||
results, fails = [], 0
|
||||
with sync_playwright() as pw:
|
||||
browser = pw.chromium.launch(executable_path=CHROME)
|
||||
for lang in ('en', 'fr'):
|
||||
for name, w, h, dpr in DEVICES:
|
||||
ctx = browser.new_context(viewport={'width': w, 'height': h}, device_scale_factor=dpr,
|
||||
is_mobile=(w < 800), has_touch=(w < 800))
|
||||
page = ctx.new_page()
|
||||
page.goto(BASE, wait_until='domcontentloaded')
|
||||
page.evaluate("([ls, st]) => localStorage.setItem(ls, JSON.stringify(st))",
|
||||
[LS, {"a": ANS, "step": 0, "screen": "quiz", "lang": lang}])
|
||||
for shot, idx in STEPS.items():
|
||||
page.evaluate("([ls, st]) => localStorage.setItem(ls, JSON.stringify(st))",
|
||||
[LS, {"a": ANS, "step": idx, "screen": "quiz", "lang": lang}])
|
||||
page.reload(wait_until='domcontentloaded')
|
||||
page.wait_for_timeout(350)
|
||||
geo = page.evaluate(GEO_JS)
|
||||
png = f"{OUT}/{lang}-{name}-{shot}.png"
|
||||
page.screenshot(path=png)
|
||||
bad = [k for k in ('docOverflowX',) if geo[k] > 1]
|
||||
if geo.get('btnNextInView') is False: bad.append('btnNext')
|
||||
rec = {"lang": lang, "device": name, "shot": shot, "geo": geo, "ok": not bad, "bad": bad}
|
||||
results.append(rec); fails += 1 if bad else 0
|
||||
if not bad: print(f" ok {lang} {name:9s} {shot}")
|
||||
else: print(f" FAIL {lang} {name:9s} {shot} {bad} {geo}")
|
||||
# results screen with PS card
|
||||
page.evaluate("([ls, st]) => localStorage.setItem(ls, JSON.stringify(st))",
|
||||
[LS, {"a": ANS, "step": 0, "screen": "results", "lang": lang}])
|
||||
page.reload(wait_until='domcontentloaded')
|
||||
page.wait_for_timeout(900)
|
||||
geo = page.evaluate(GEO_JS)
|
||||
page.evaluate("() => { const c = document.getElementById('ps-card'); if (c) c.scrollIntoView(); }")
|
||||
page.wait_for_timeout(200)
|
||||
page.screenshot(path=f"{OUT}/{lang}-{name}-results-pscard.png")
|
||||
bad = []
|
||||
if geo.get('docOverflowX', 0) > 1: bad.append('overflowX')
|
||||
pc = geo.get('psCard')
|
||||
if not pc or not pc.get('visible'): bad.append('psCardHidden')
|
||||
elif not pc.get('fits'): bad.append('psCardWide')
|
||||
elif pc.get('links', 0) < 5: bad.append('links<5')
|
||||
rec = {"lang": lang, "device": name, "shot": "results-pscard", "geo": geo, "ok": not bad, "bad": bad}
|
||||
results.append(rec); fails += 1 if bad else 0
|
||||
if not bad: print(f" ok {lang} {name:9s} results-pscard")
|
||||
else: print(f" FAIL {lang} {name:9s} results-pscard {bad} {geo}")
|
||||
ctx.close()
|
||||
browser.close()
|
||||
with open(f'{OUT}/geo.json', 'w') as f:
|
||||
json.dump(results, f, indent=1)
|
||||
print(f"{len(results)} shots, {fails} failures")
|
||||
print("PS-AUDIT-OK" if fails == 0 else "PS-AUDIT-FAIL")
|
||||
sys.exit(1 if fails else 0)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in new issue
Block a user