"""Check independent case replies, full reports and current renderer on loopback.""" from pathlib import Path import base64 import functools import http.server import json import subprocess import tempfile import threading import time from urllib.parse import quote, urlparse from urllib.request import urlopen import websocket ROOT = Path(__file__).resolve().parents[1] WORK = ROOT / 'docs/workshop-skill-retest-20261109-r2' OUT = WORK / 'browser' OUT.mkdir(exist_ok=True) class Handler(http.server.SimpleHTTPRequestHandler): def log_message(self, *args): pass def do_GET(self): if not urlparse(self.path).path.startswith(('/src/', '/lib/', '/docs/workshop-skill-retest-20261109-r2/')): self.send_error(404) return super().do_GET() def list_directory(self, path): self.send_error(404) return None server = http.server.ThreadingHTTPServer(('127.0.0.1', 0), functools.partial(Handler, directory=str(ROOT))) threading.Thread(target=server.serve_forever, daemon=True).start() base = 'http://127.0.0.1:' + str(server.server_port) profile = Path(tempfile.mkdtemp(prefix='ksf-retest-browser-')) edge = 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe' proc = subprocess.Popen([edge, '--headless=new', '--disable-gpu', '--remote-debugging-port=0', '--disable-background-networking', '--disable-component-update', '--disable-sync', '--disable-extensions', '--no-first-run', '--no-default-browser-check', '--user-data-dir=' + str(profile), 'about:blank'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, creationflags=subprocess.CREATE_NO_WINDOW) seq = 0 ws = None errors = [] requests = [] try: deadline = time.monotonic() + 20 while not (profile / 'DevToolsActivePort').exists(): assert time.monotonic() < deadline and proc.poll() is None time.sleep(.1) port = (profile / 'DevToolsActivePort').read_text().splitlines()[0] page = next(p for p in json.load(urlopen('http://127.0.0.1:' + port + '/json/list', timeout=5)) if p['type'] == 'page') ws = websocket.create_connection(page['webSocketDebuggerUrl'], timeout=30, suppress_origin=True, http_no_proxy=['127.0.0.1', 'localhost']) def call(method, params=None): global seq seq += 1 rid = seq ws.send(json.dumps({'id': rid, 'method': method, 'params': params or {}})) while True: msg = json.loads(ws.recv()) if msg.get('id') == rid: assert 'error' not in msg, msg return msg.get('result', {}) if msg.get('method') == 'Runtime.exceptionThrown': errors.append(msg['params']) if msg.get('method') == 'Network.requestWillBeSent': requests.append(msg['params']['request']['url']) def ev(code, promise=False): r = call('Runtime.evaluate', {'expression': code, 'returnByValue': True, 'awaitPromise': promise}) assert 'exceptionDetails' not in r, r return r['result'].get('value') def wait(code): return ev('new Promise((resolve,reject)=>{const end=Date.now()+18000;const tick=()=>{try{if(' + code + '){resolve(true);return;}}catch{}if(Date.now()>end)reject(new Error("retest page timeout"));else setTimeout(tick,70)};tick()})', True) def settle(): return ev('new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r)))', True) def shot(name): (OUT / (name + '.png')).write_bytes(base64.b64decode(call('Page.captureScreenshot', {'format': 'png', 'captureBeyondViewport': False})['data'])) call('Page.enable') call('Runtime.enable') call('Network.enable') call('Network.setBlockedURLs', {'urls': ['https://*']}) call('Page.navigate', {'url': base + '/docs/workshop-skill-retest-20261109-r2/' + quote('独立案例复测.html')}) wait('document.querySelectorAll("#steps button").length===13') preview_checks = [] report_checks = [] viewports = [(1440, 1000, 'desktop'), (390, 844, 'mobile'), (360, 800, 'narrow')] for width, height, name in viewports: call('Emulation.setDeviceMetricsOverride', {'width': width, 'height': height, 'deviceScaleFactor': 1, 'mobile': width < 500}) settle() for index in range(13): if width < 500: ev(f'(()=>{{const s=document.querySelector("#mobile-stage");s.value="{index}";s.dispatchEvent(new Event("change",{{bubbles:true}}));}})()') else: ev(f'document.querySelectorAll("#steps button")[{index}].click()') settle() if index == 0: ev('scrollTo(0,0)') settle() first_screen = ev('({leadTop:document.querySelector(".lead").getBoundingClientRect().top,height:innerHeight})') assert first_screen['leadTop'] < height - 80, (name, first_screen) shot('initial-' + name) reply = ev('({text:document.querySelector("#report-body").innerText,bodyFont:Math.min(...[...document.querySelectorAll("#report-body p,#report-body li,#report-body td")].map(e=>parseFloat(getComputedStyle(e).fontSize))),scroll:document.documentElement.scrollWidth,mode:document.querySelector("#reply-mode").getAttribute("aria-pressed")})') assert len(reply['text']) > 60 and reply['bodyFont'] >= 15 and reply['scroll'] <= width and reply['mode'] == 'true', (index, reply) if index in (0, 10): ev('scrollTo(0,document.querySelector("#sample").getBoundingClientRect().top+scrollY-12)') settle() shot('reply-' + name + '-' + str(index)) ev('document.querySelector("#report-mode").click()') settle() r = ev('({id:OUT[selected].id,scroll:document.documentElement.scrollWidth,width:innerWidth,sections:document.querySelectorAll(".report-section").length,expected:OUT[selected].sections.length,allBodyFonts:Math.min(...[...document.querySelectorAll("#report-body p,#report-body td")].map(e=>parseFloat(getComputedStyle(e).fontSize))),text:document.querySelector("#report-body").innerText})') assert r['scroll'] <= width and r['sections'] == r['expected'] and r['allBodyFonts'] >= 15, r source = ev('OUT[selected].sections') for s in source: assert s['title'] in r['text'], (r['id'], s['title']) if s['kind'] == 'table': for row in s['data']['rows']: for cell in row: assert cell in r['text'], (r['id'], cell) else: assert s['text'] in r['text'], (r['id'], s['title']) supplemental = ev('OUT[selected].supplementalReports||[]') for sup in supplemental: ev('document.querySelector("details[data-supplement="+CSS.escape(' + json.dumps(sup['id']) + ')+"]").open=true') text = ev('document.querySelector("details[data-supplement="+CSS.escape(' + json.dumps(sup['id']) + ')+"]").innerText') for s in sup['sections']: assert s['title'] in text, (sup['id'], s['title']) if s['kind'] == 'table': for row in s['data']['rows']: for cell in row: assert cell in text, (sup['id'], cell) else: assert s['text'] in text, (sup['id'], s['title']) assert ev('document.documentElement.scrollWidth') <= width preview_checks.append({k: v for k, v in r.items() if k != 'text'} | {'viewport': name, 'replyReadable': True}) if index in (6, 10): ev('scrollTo(0,document.querySelector("#sample").getBoundingClientRect().top+scrollY-12)') settle() shot('full-' + name + '-' + r['id']) for target in ['candidates', 'checks']: ev(f'document.querySelector("#tab-{target}").click()') settle() assert ev(f'!document.querySelector("#{target}").hidden&&document.documentElement.scrollWidth<=innerWidth') if target == 'candidates': candidates = ev('DATA.samples.candidates') for c in candidates: ev('document.querySelector("details[data-candidate="+CSS.escape(' + json.dumps(c['key']) + ')+"]").open=true') text = ev('document.querySelector("details[data-candidate="+CSS.escape(' + json.dumps(c['key']) + ')+"]").innerText') for s in c['reportSections']: assert s['title'] in text, (c['key'], s['title']) if s['kind'] == 'table': for row in s['data']['rows']: for cell in row: assert cell in text, (c['key'], cell) else: assert s['text'] in text, (c['key'], s['title']) ev('document.querySelector("details[data-candidate="+CSS.escape(' + json.dumps(c['key']) + ')+"]").open=false') svg = ev('(()=>{const s=document.querySelector("#costchart svg"),v=s.viewBox.baseVal;return [...s.querySelectorAll("text")].map(t=>{const b=t.getBBox();return {text:t.textContent,x:b.x,end:b.x+b.width,y:b.y,bottom:b.y+b.height,vw:v.width,vh:v.height};});})()') assert all(t['x'] >= 0 and t['end'] <= t['vw'] and t['y'] >= 0 and t['bottom'] <= t['vh'] for t in svg), svg ev('scrollTo(0,0)') settle() shot('candidates-' + name) ev('document.querySelector("#costchart").scrollIntoView({block:"center"})') settle() shot('cost-' + name) ev('document.querySelector("#tab-samples").click()') call('Page.navigate', {'url': base + '/docs/workshop-skill-retest-20261109-r2/report-render-harness.html'}) wait('window.localReady===true') sample_data = json.loads((WORK / 'sample-outputs.json').read_text(encoding='utf-8')) outputs = sample_data['outputs'] + sample_data['supplementalOutputs'] + sample_data['cardOutputs'] for width, height, name in viewports: call('Emulation.setDeviceMetricsOverride', {'width': width, 'height': height, 'deviceScaleFactor': 1, 'mobile': width < 500}) for m in outputs: ev('window.localSet(' + json.dumps(m['artifactId']) + ')', True) settle() result = ev('(()=>{const v=document.querySelector("view-report");function walk(n){if(n.nodeType===3)return n.parentElement?.getClientRects().length?n.textContent:"";if(n.nodeType===1&&(n.matches("style,script,noscript,[hidden]")||getComputedStyle(n).display==="none"))return "";return [...n.childNodes].map(walk).join(" ")+(n.shadowRoot?walk(n.shadowRoot):"");}return {text:walk(v.shadowRoot),width:innerWidth,scroll:document.documentElement.scrollWidth};})()') for s in m['sections']: assert s['title'] in result['text'], (name, m['id'], s['title']) if s['kind'] == 'table': for row in s['data']['rows']: for cell in row: assert cell in result['text'], (name, m['id'], cell) else: assert s['text'] in result['text'], (name, m['id'], s['title']) assert result['scroll'] <= width, (name, m['id'], result['scroll']) report_checks.append({'viewport': name, 'moduleId': m['id'], 'allCoreContentRendered': True, 'pageOverflow': False}) if m['id'] == 'D2-06': ev('scrollTo(0,0)') settle() shot('current-report-' + name) external = [u for u in requests if u.startswith(('http://', 'https://')) and not u.startswith(base + '/')] assert not external and not errors, (external, errors) receipt = {'ok': True, 'previewModuleViewportChecks': len(preview_checks), 'currentRendererModuleViewportChecks': len(report_checks), 'totalChecks': len(preview_checks) + len(report_checks), 'viewports': [v[0] for v in viewports], 'preview': preview_checks, 'currentRenderer': report_checks, 'supplementalReportsExpandedAndChecked': 3, 'candidateFullReportsExpandedAndChecked': 5, 'replyMinimumFontPx': 15, 'externalRequests': external, 'browserExceptions': errors, 'scope': 'loopback preview and current report component; no production calls'} (WORK / 'browser-acceptance.json').write_text(json.dumps(receipt, ensure_ascii=False, indent=2) + '\n', encoding='utf-8') print(json.dumps({k: receipt[k] for k in ['ok', 'totalChecks', 'viewports', 'externalRequests', 'browserExceptions']}, ensure_ascii=False)) finally: if ws: ws.close() proc.terminate() try: proc.wait(timeout=10) except subprocess.TimeoutExpired: proc.kill() server.shutdown() server.server_close()