Files
retire-planner/audit/appshell_capture.py
T

140 lines
7.7 KiB
Python

"""App-shell round-4 capture: desktop 1280x900 EN+FR (province + tall vision step + results)
for the pixel-diff baseline, and mobile 320/360/390 EN+FR quiz shots + geometry
(btn-next / panel / .opt rects, coverage, doc scroll) for the before/after record.
Usage: python3 audit/appshell_capture.py BASE OUTDESK OUTMOB TAG
e.g. python3 audit/appshell_capture.py http://127.0.0.1:8899/ audit/appshell-baseline audit/appshell-before before
"""
import json, os, sys
from playwright.sync_api import sync_playwright
BASE = sys.argv[1].rstrip('/') + '/'
OUTD = sys.argv[2]
OUTM = sys.argv[3]
TAG = sys.argv[4]
os.makedirs(OUTD, exist_ok=True)
os.makedirs(OUTM, exist_ok=True)
CHROME = '/home/ubuntu/.cache/ms-playwright/chromium-1243/chrome-linux64/chrome'
LS = 'retire-planner-v1'
SINGLE = {
"province": "ON", "age": 45, "household": "single",
"partner": {"age": 45, "income": 0, "rrsp": 0, "tfsa": 0, "nonreg": 0, "monthlyContribution": 0},
"income": 85000, "rrsp": 250000, "tfsa": 60000,
"nonreg": 40000, "monthlyContribution": 800, "employerMatchMonthly": 0,
"retireAge": 65, "spendingAnnual": 60000,
"cppStartAge": 65, "oasStartAge": 65,
"pensionAnnual": 12000, "rentalAnnual": 0, "otherAnnual": 0,
"realReturnPre": 3.5, "realReturnPost": 2.5, "inflation": 2.0, "lifeExp": 95,
"interests": ["dock", "travel"]
}
COUPLE = dict(SINGLE, household="couple",
partner={"age": 43, "income": 62000, "rrsp": 120000, "tfsa": 30000, "nonreg": 15000, "monthlyContribution": 300})
DEVICES = [
('iphone14', 390, 844, 3.0, 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1'),
('pixel', 360, 800, 2.75, 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36'),
('se', 320, 700, 2.0, 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1'),
]
# steps to capture on mobile: province (grid, 13 opts) + vision (interests chips, last step of 14)
MSTEPS = [('province', 0), ('vision', 13)]
GEO_JS = """() => {
const r = e => { const b = e.getBoundingClientRect(); return {l:+b.left.toFixed(1), t:+b.top.toFixed(1), r:+b.right.toFixed(1), b:+b.bottom.toFixed(1), w:+b.width.toFixed(1), h:+b.height.toFixed(1)}; };
const g = s => document.querySelector(s);
const inter = (a, b) => Math.max(0, Math.min(a.r, b.r) - Math.max(a.l, b.l)) * Math.max(0, Math.min(a.b, b.b) - Math.max(a.t, b.t));
const next = g('#btn-next'), panel = g('#panel'), hdr = g('.hdr'), sb = g('#step-body');
const nr = next ? r(next) : null, pr = panel ? r(panel) : null, hr = hdr ? r(hdr) : null;
const opts = [...document.querySelectorAll('#step-body .opt')].map(e => ({txt: e.textContent.trim().slice(0, 22), rect: r(e)}));
const coverage = nr ? opts.filter(o => inter(nr, o.rect) > 0).map(o => ({
txt: o.txt, optAreaPct: +(inter(nr, o.rect) / (o.rect.w * o.rect.h) * 100).toFixed(1), opt: o.rect})) : [];
return {
vw: window.innerWidth, vh: window.innerHeight,
docSH: document.documentElement.scrollHeight, docCH: document.documentElement.clientHeight,
bodySH: document.body.scrollHeight, winScrollY: window.scrollY,
hdr: hr, next: nr, panel: pr,
stepBody: sb ? {...r(sb), scrollTop: +sb.scrollTop.toFixed(1), scrollH: sb.scrollHeight, clientH: sb.clientHeight} : null,
nOpts: opts.length, lastOpt: opts.length ? opts[opts.length - 1].rect : null,
coverage,
lang: document.documentElement.lang,
panelPos: panel ? getComputedStyle(panel).position : null,
nextPos: next ? getComputedStyle(next).position : null,
scrH: (() => { const e = g('#scr-quiz'); return e ? getComputedStyle(e).height : null; })(),
};
}"""
def set_state(page, screen, step, lang, a=None):
page.evaluate("""([ls, a, screen, step, lang]) => {
localStorage.setItem(ls, JSON.stringify({a: a, step: step, screen: screen, lang: lang}));
}""", [LS, a or SINGLE, screen, step, lang])
def shot(page, path, full=False):
page.screenshot(path=path, full_page=full)
print('saved', path)
geo = {}
with sync_playwright() as pw:
browser = pw.chromium.launch(executable_path=CHROME, headless=True,
args=['--no-sandbox', '--disable-dev-shm-usage'])
# ---------- desktop 1280x900 (pixel-diff set) ----------
ctx = browser.new_context(viewport={'width': 1280, 'height': 900}, device_scale_factor=1)
page = ctx.new_page()
page.goto(BASE, wait_until='networkidle')
for lang in ('en', 'fr'):
for tag, screen, step in (('quiz-province', 'quiz', 0), ('quiz-vision', 'quiz', 13), ('results', 'results', 13)):
set_state(page, screen, step, lang)
page.reload(wait_until='networkidle')
page.wait_for_timeout(1600) # charts + vision images settle
shot(page, os.path.join(OUTD, f'desktop-{lang}-{tag}.png'))
ctx.close()
# ---------- mobile 390/360/320 ----------
for dev, w, h, dsf, ua in DEVICES:
ctx = browser.new_context(viewport={'width': w, 'height': h}, device_scale_factor=dsf,
is_mobile=True, has_touch=True, user_agent=ua)
page = ctx.new_page()
page.goto(BASE, wait_until='networkidle')
for lang in ('en', 'fr'):
for stag, step in MSTEPS:
set_state(page, 'quiz', step, lang)
page.reload(wait_until='networkidle')
page.wait_for_timeout(700)
shot(page, os.path.join(OUTM, f'{dev}-{lang}-{stag}.png'))
geo[f'{dev}:{lang}:{stag}:s0'] = page.evaluate(GEO_JS)
if dev == 'iphone14':
# province step at scroll 0 (defect shot for FR is above); sheet expand test shots
set_state(page, 'quiz', 0, lang)
page.reload(wait_until='networkidle')
page.wait_for_timeout(500)
try:
page.click('.panel-h', timeout=3000)
page.wait_for_timeout(450)
shot(page, os.path.join(OUTM, f'{dev}-{lang}-sheet-open.png'))
geo[f'{dev}:{lang}:sheet-open'] = page.evaluate(GEO_JS)
page.click('#scrim', timeout=3000)
page.wait_for_timeout(350)
geo[f'{dev}:{lang}:sheet-collapsed'] = page.evaluate(
'() => ({open: document.querySelector("#panel").classList.contains("open"), scrim: getComputedStyle(document.querySelector("#scrim")).display})')
except Exception as e:
print('sheet test failed:', lang, e)
# intro + results at 390 full page (non-regression of non-quiz screens)
set_state(page, 'intro', 0, lang)
page.reload(wait_until='networkidle')
page.wait_for_timeout(700)
shot(page, os.path.join(OUTM, f'{dev}-{lang}-intro.png'), full=True)
geo[f'{dev}:{lang}:intro'] = page.evaluate('() => ({docSH: document.documentElement.scrollHeight, vh: innerHeight})')
set_state(page, 'results', 13, lang)
page.reload(wait_until='networkidle')
page.wait_for_timeout(1600)
shot(page, os.path.join(OUTM, f'{dev}-{lang}-results.png'), full=True)
geo[f'{dev}:{lang}:results'] = page.evaluate('() => ({docSH: document.documentElement.scrollHeight, vh: innerHeight})')
ctx.close()
browser.close()
with open(os.path.join(OUTM, f'geo-{TAG}.json'), 'w') as f:
json.dump(geo, f, indent=1, ensure_ascii=False)
print('captured', len(geo), 'measurement points ->', os.path.join(OUTM, f'geo-{TAG}.json'))
print('CAPTURE-OK')