"""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')