140 lines
7.7 KiB
Python
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')
|