| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115 |
- """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()<deadline and proc.poll() is None
- time.sleep(.1)
- port=(profile/'DevToolsActivePort').read_text().splitlines()[0]
- page=next(x for x in json.load(urlopen('http://127.0.0.1:'+port+'/json/list',timeout=5)) if x['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':network.append(msg['params']['request']['url'])
- def ev(code,await_promise=False):
- r=call('Runtime.evaluate',{'expression':code,'returnByValue':True,'awaitPromise':await_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("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()
|