exec(open('ui_test.py').read().split("with sync_playwright() as pw:")[0])
aid2 = sql("SELECT id FROM agents WHERE agent_code='DEMO2'")
with sync_playwright() as pw:
    b = pw.chromium.launch()
    for label, (W, H, mob) in {'desktop': (1440, 900, False), 'phone': (390, 844, True)}.items():
        print('== ' + label)
        ctx = b.new_context(viewport={'width': W, 'height': H}, is_mobile=mob, has_touch=mob); p, errs = login(ctx)
        # Overview
        p.goto(BASE + '#/agents/' + aid2 + '/overview'); p.wait_for_selector('.hero2 .hero-art')
        ok(p.eval_on_selector('.hero2 .hero-art', 'e => e.getBoundingClientRect().width') > 150, 'overview: hero art shows')
        ok(p.eval_on_selector_all('.ov-stat', 'e => e.length') == 6 and p.query_selector('.pc2.now'), 'overview: 6 pipeline tiles, current phase highlighted')
        ok(p.eval_on_selector_all('.ov-kv', 'e => e.length') >= 9 and p.query_selector('.fams .fam'), 'overview: profile rows with icons and family on file')
        ok(noscroll(p), 'overview: no sideways scroll')
        # System
        p.goto(BASE + '#/agents/' + aid2 + '/system'); p.wait_for_selector('.sys-head')
        ok(p.query_selector('.phase.sys2.open .sys-side'), 'system: open phase shows the guidance panel')
        before = p.inner_text('#sysLine'); n0 = int(before.split(' ')[0])
        item = p.query_selector('.phase.sys2.open .item:not(.done)'); iid = item.get_attribute('data-item')
        item.click(); p.wait_for_timeout(1200)
        ok(p.inner_text('#sysLine').startswith(str(n0 + 1) + ' of'), 'system: checking a step updates the header', p.inner_text('#sysLine'))
        ok('left in this phase' in p.inner_text('.phase.sys2.open .sys-side') or 'complete' in p.inner_text('.phase.sys2.open .sys-side'), 'system: side panel recounts')
        p.click('.item[data-item="' + iid + '"]'); p.wait_for_timeout(1200)
        ok(p.inner_text('#sysLine').startswith(str(n0) + ' of'), 'system: unchecking puts it back')
        p.click('#sysCollapse'); ok(p.eval_on_selector_all('.phase.sys2.open', 'e => e.length') == 0, 'system: collapse all')
        p.click('#sysExpand'); ok(p.eval_on_selector_all('.phase.sys2.open', 'e => e.length') == p.eval_on_selector_all('.phase.sys2', 'e => e.length'), 'system: expand all')
        more = p.query_selector('.item [data-more]')
        if more:
            it = more.evaluate_handle("b => b.closest('.item')"); was = it.evaluate("e => e.classList.contains('done')")
            more.click(); p.wait_for_timeout(500)
            ok(it.evaluate("e => e.classList.contains('show-help')") and it.evaluate("e => e.classList.contains('done')") == was, 'system: › shows details without checking the step')
        ok(noscroll(p), 'system: no sideways scroll')
        # Onboarding
        p.goto(BASE + '#/agents/' + aid2 + '/onboarding'); p.wait_for_selector('.onb-road')
        ok(p.eval_on_selector_all('.rm-col', 'e => e.length') >= 4 and p.query_selector('.rm-col.now'), 'onboarding: roadmap columns, current phase highlighted')
        ok(p.query_selector('.hero2 h2 em') and 'Summit Financial Group' in p.inner_text('.hero2 h2 em'), 'onboarding: agency name in the accent color')
        col = p.query_selector('.rm-col.now'); c0 = col.query_selector('.rm-c').inner_text()
        step = col.query_selector('.rm-i:not(.on)'); step.click(); p.wait_for_timeout(1200)
        ok(col.query_selector('.rm-c').inner_text() != c0, 'onboarding: checking a step updates the count', (c0, col.query_selector('.rm-c').inner_text()))
        step.click(); p.wait_for_timeout(1200); ok(col.query_selector('.rm-c').inner_text() == c0, 'onboarding: and back')
        ok(noscroll(p), 'onboarding: no sideways scroll')
        # Pipelines
        p.goto(BASE + '#/agents/' + aid2 + '/pipeline?p=top100'); p.wait_for_timeout(1500)
        if mob:
            ok(p.eval_on_selector_all('.acc.open', 'e => e.length') == 1, 'phone: stages are an accordion, one open to start')
            closed = p.query_selector('.acc:not(.open) [data-acctoggle]'); sid = closed.get_attribute('data-acctoggle'); closed.click(); p.wait_for_timeout(300)
            ok(p.query_selector('.acc.open[data-acc="' + sid + '"]'), 'phone: tap a stage to open it')
            card = p.query_selector('.acc.open .pcard.crm'); cid = card.get_attribute('data-card')
            opts = card.eval_on_selector('select[data-mmove]', "s => [...s.options].filter(o => !o.disabled).map(o => o.value)")
            card.query_selector('select[data-mmove]').select_option(opts[0]); p.wait_for_timeout(1500)
            ok(sql(f"select stage_id from cards where id={cid}") == opts[0], 'phone: ⋯ moves a lead to another stage (saved)')
            ok(not p.query_selector('.rec2-h'), 'phone: moving does not open the lead')
        else:
            ok(p.eval_on_selector_all('.board.crm .col-h .dot', "e => new Set(e.map(x => getComputedStyle(x).backgroundColor)).size") >= 4, 'board: each stage has its own color')
            p.click('.col[data-stage] [data-colmenu]'); p.wait_for_selector('.col-menu')
            p.click('.col-menu [data-cm="select"]'); p.wait_for_timeout(400)
            ok(p.eval_on_selector_all('.pcard.crm.sel', 'e => e.length') >= 1 and p.query_selector('#pipeSelect.navy'), 'board: ⋯ → select everyone here')
            p.click('#pipeSelect'); p.wait_for_timeout(300)
            p.click('.col[data-stage] [data-colmenu]'); p.click('.col-menu [data-cm="add"]'); p.wait_for_selector('.mbox', timeout=5000); ok(True, 'board: ⋯ → add a lead here'); p.keyboard.press('Escape'); p.wait_for_timeout(300)
            ok(p.query_selector('.pcard.crm .line2'), 'board: occupation and city share a row')
        ok(noscroll(p), 'pipelines: no sideways scroll')
        ok(not errs, 'no script errors', errs[:3])
        ctx.close()
    b.close()
print(f"\n{sum(R)} passed, {len(R) - sum(R)} failed"); sys.exit(0 if all(R) else 1)
