| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210 |
- """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()
|