"""Exercise the standalone samples and current report component using loopback only.""" 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-local-acceptance-20261109' OUT=WORK/'browser' OUT.mkdir(exist_ok=True) class Handler(http.server.SimpleHTTPRequestHandler): def log_message(self,*args): pass def do_GET(self): p=urlparse(self.path).path if not p.startswith(('/src/','/lib/','/docs/workshop-skill-local-acceptance-20261109/')): 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))) thread=threading.Thread(target=server.serve_forever,daemon=True);thread.start() base='http://127.0.0.1:'+str(server.server_port) profile=Path(tempfile.mkdtemp(prefix='ksf-local-browser-')) edge=Path('C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe') proc=subprocess.Popen([str(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=[];network=[] try: deadline=time.monotonic()+20 while not (profile/'DevToolsActivePort').exists(): assert time.monotonic(){const end=Date.now()+18000;const tick=()=>{try{if('+code+'){resolve(true);return;}}catch{}if(Date.now()>end)reject(new Error("local 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://*']}) preview=base+'/docs/workshop-skill-local-acceptance-20261109/'+quote('工作坊成果与回复样张.html') call('Page.navigate',{'url':preview});wait('document.querySelectorAll("#steps button").length===13') preview_checks=[];report_checks=[] for width,height,name in [(1440,1000,'desktop'),(390,844,'mobile')]: 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() reply=ev('({mode:document.querySelector("#reply-mode").getAttribute("aria-pressed"),finding:document.querySelector(".finding p").innerText,tableRows:document.querySelectorAll("#report-body tbody tr").length})') assert reply['mode']=='true' and reply['finding'] and reply['tableRows']>0,reply ev('document.querySelector("#report-mode").click()');settle() r=ev('({id:OUT[selected].id,width:innerWidth,scroll:document.documentElement.scrollWidth,sections:document.querySelectorAll("#report-body .report-section").length,expected:OUT[selected].sections.length,minBodyFont:Math.min(...[...document.querySelectorAll("#report-body td,#report-body p")].map(e=>parseFloat(getComputedStyle(e).fontSize))),tables:document.querySelectorAll("#report-body table").length})') assert r['scroll']<=width and r['sections']==r['expected'] and r['minBodyFont']>=14,r preview_checks.append({'viewport':name,**r}) if index in (0,10):ev('scrollTo(0,document.querySelector("#sample").getBoundingClientRect().top+scrollY-12)');settle();shot('preview-'+name+'-'+r['id']) for target in ['real','checks']: ev(f'document.querySelector("#tab-{target}").click()');settle() assert ev('document.documentElement.scrollWidth')<=width assert ev(f'!document.querySelector("#{target}").hidden') if target=='real':ev('scrollTo(0,0)');settle();shot('real-rewrite-'+name) ev('document.querySelector("#tab-samples").click()') # Exercise the existing application report component with the local public-board projection. call('Page.navigate',{'url':base+'/docs/workshop-skill-local-acceptance-20261109/report-render-harness.html'}) wait('window.localReady===true') samples=json.loads((WORK/'sample-outputs.json').read_text(encoding='utf-8'))['outputs'] for width,height,name in [(1440,1000,'desktop'),(390,844,'mobile')]: call('Emulation.setDeviceMetricsOverride',{'width':width,'height':height,'deviceScaleFactor':1,'mobile':width<500});settle() for m in samples: 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):"");}const r=v.shadowRoot;return {text:walk(r),tables:r.querySelectorAll("table").length,sections:r.querySelectorAll("article.ch").length,width:innerWidth,scroll:document.documentElement.scrollWidth};})()') for s in m['sections']: assert s['title'] in result['text'],(name,m['id'],s['title'],result['text'][:600]) 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'],'tables':result['tables'],'chapters':result['sections'],'width':width,'scrollWidth':result['scroll'],'allSectionContentRendered':True}) if m['id'] in ('D1-01','D2-06'):ev('scrollTo(0,0)');settle();shot('current-report-'+name+'-'+m['id']) external=[u for u in network if u.startswith(('http://','https://')) and not u.startswith(base+'/')] assert not external,external assert not errors,errors result={'ok':True,'previewModuleViewportChecks':len(preview_checks),'currentRendererModuleViewportChecks':len(report_checks),'preview':preview_checks,'currentRenderer':report_checks,'tabsAndReplyReportSwitchesVerified':True,'mobileWidth':390,'desktopWidth':1440,'allPreviewBodyTextAtLeast14px':True,'externalRequests':external,'browserExceptions':errors,'scope':'loopback static files and local in-memory board data; no production requests or writes'} (WORK/'browser-acceptance.json').write_text(json.dumps(result,ensure_ascii=False,indent=2)+'\n',encoding='utf-8') print(json.dumps({k:result[k] for k in ['ok','previewModuleViewportChecks','currentRendererModuleViewportChecks','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()