"""Render and smoke-check the delivered local HTML using a temporary headless browser.""" from pathlib import Path import base64 import json import shutil import subprocess import tempfile import time import urllib.request import websocket ASSETS = Path(__file__).resolve().parent ROOT = ASSETS.parent PAGE = ROOT / 'Week7_Stata代码详解.html' browser = Path(r'C:\Program Files\Google\Chrome\Application\chrome.exe') profile = Path(tempfile.mkdtemp(prefix='browser_profile_', dir=ASSETS)).resolve() stderr = (ASSETS / 'browser_stderr.log').open('w', encoding='utf-8') process = subprocess.Popen([ str(browser), '--headless=new', '--disable-gpu', '--no-sandbox', '--disable-gpu-sandbox', '--no-first-run', '--no-default-browser-check', '--disable-extensions', '--disable-sync', '--disable-background-networking', '--disable-component-update', '--remote-debugging-port=0', '--user-data-dir=' + str(profile), 'about:blank' ], stdout=subprocess.DEVNULL, stderr=stderr, creationflags=subprocess.CREATE_NO_WINDOW) socket = None errors = [] events = [] seq = 0 def command(method, params=None): global seq seq += 1 message_id = seq socket.send(json.dumps({'id': message_id, 'method': method, 'params': params or {}})) while True: response = json.loads(socket.recv()) if response.get('method') == 'Runtime.exceptionThrown': errors.append(response['params']) if response.get('method'): events.append(response) if response.get('id') == message_id: if 'error' in response: raise RuntimeError(response['error']) return response.get('result', {}) def evaluate(expression): response = command('Runtime.evaluate', {'expression': expression, 'returnByValue': True, 'awaitPromise': True}) if 'exceptionDetails' in response: raise RuntimeError(response['exceptionDetails']) return response.get('result', {}).get('value') def settle(): evaluate('new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)))') def screenshot(name): settle() result = command('Page.captureScreenshot', {'format': 'png', 'captureBeyondViewport': False}) (ASSETS / name).write_bytes(base64.b64decode(result['data'])) try: port_file = profile / 'DevToolsActivePort' deadline = time.monotonic() + 25 while not port_file.exists() and time.monotonic() < deadline: if process.poll() is not None: raise RuntimeError('Browser exited before opening DevTools') time.sleep(.1) port = port_file.read_text().splitlines()[0] opener = urllib.request.build_opener(urllib.request.ProxyHandler({})) with opener.open(f'http://127.0.0.1:{port}/json/list', timeout=5) as response: targets = json.load(response) target = next(item for item in targets if item['type'] == 'page') socket = websocket.create_connection(target['webSocketDebuggerUrl'], timeout=20, suppress_origin=True) command('Page.enable') command('Runtime.enable') command('Page.addScriptToEvaluateOnNewDocument', {'source': 'window.matchMediaOriginal=window.matchMedia;'}) command('Emulation.setDeviceMetricsOverride', {'width': 1440, 'height': 1000, 'deviceScaleFactor': 1, 'mobile': False}) command('Page.navigate', {'url': PAGE.as_uri()}) deadline = time.monotonic() + 20 while time.monotonic() < deadline: if evaluate('document.readyState') == 'complete': break time.sleep(.1) evaluate('document.fonts.ready.then(()=>true)') evaluate("document.documentElement.style.scrollBehavior='auto'; true") checks = evaluate('''(()=>({ title:document.title, sections:document.querySelectorAll('main section').length, codeBlocks:document.querySelectorAll('.code-block').length, anchorsValid:[...document.querySelectorAll('a[href^="#"]')].every(a=>document.getElementById(a.hash.slice(1))), imageLoaded:[...document.images].every(i=>i.complete && i.naturalWidth>0), viewport:innerWidth, documentWidth:document.documentElement.scrollWidth, externalResources:performance.getEntriesByType('resource').filter(r=>/^https?:/.test(r.name)).length, codeTextMatches:[...document.querySelectorAll('.code-block')].every(b=>[...b.querySelectorAll('.src')].map(s=>s.textContent).join('\\n')===new TextDecoder().decode(Uint8Array.from(atob(b.dataset.source),c=>c.charCodeAt(0)))) }))()''') assert checks['sections'] == 20 assert checks['codeBlocks'] == 17 assert checks['anchorsValid'] and checks['imageLoaded'] and checks['codeTextMatches'] assert checks['externalResources'] == 0 assert checks['documentWidth'] <= checks['viewport'] screenshot('preview_desktop.png') evaluate("document.getElementById('audit').scrollIntoView(); true") screenshot('preview_code.png') evaluate("document.querySelector('#plot figure').scrollIntoView(); true") screenshot('preview_figure.png') # Exercise the print event without launching a print UI. evaluate("window.__printClicked=false; window.print=()=>{window.__printClicked=true};document.getElementById('print-btn').click();true") assert evaluate('window.__printClicked') is True # Clipboard is mocked to verify payload; browser permission itself is not asserted. evaluate("window.__copiedText=null;Object.defineProperty(navigator,'clipboard',{configurable:true,value:{writeText:async text=>{window.__copiedText=text}}});document.querySelector('.copy-btn').click();true") settle() copied = evaluate('window.__copiedText') assert copied == '\n'.join((ROOT / 'week7_analysis.do').read_text(encoding='utf-8-sig').splitlines()[:8]) evaluate("document.getElementById('copy-status').textContent='';true") command('Emulation.setDeviceMetricsOverride', {'width': 390, 'height': 844, 'deviceScaleFactor': 1, 'mobile': True}) evaluate('scrollTo({top:0,behavior:"instant"});true') settle() mobile = evaluate('({viewport:innerWidth,documentWidth:document.documentElement.scrollWidth,mobileNav:getComputedStyle(document.querySelector(".mobile-nav")).display,sidebar:getComputedStyle(document.querySelector(".sidebar")).display})') assert mobile['documentWidth'] <= mobile['viewport'] assert mobile['mobileNav'] != 'none' and mobile['sidebar'] == 'none' screenshot('preview_mobile.png') evaluate('document.querySelector(".mobile-nav summary").click();true') assert evaluate('document.querySelector(".mobile-nav").open') is True evaluate('''document.querySelector(".mobile-nav a[href='#reshape']").click();true''') settle() assert evaluate('document.querySelector(".mobile-nav").open') is False assert evaluate('location.hash') == '#reshape' screenshot('preview_mobile_code.png') # Check the long plot command fits in its code container at narrow width. narrow = evaluate('''(()=>{const b=document.querySelector('#plot .code-block');return {width:b.clientWidth,scrollWidth:b.scrollWidth,viewport:innerWidth,documentWidth:document.documentElement.scrollWidth}})()''') assert narrow['scrollWidth'] <= narrow['width'] + 1 assert narrow['documentWidth'] <= narrow['viewport'] # Emulate print CSS to check navigation and action buttons disappear. command('Emulation.setEmulatedMedia', {'media': 'print'}) assert evaluate('getComputedStyle(document.querySelector(".back-top")).display') == 'none' command('Emulation.setEmulatedMedia', {'media': ''}) assert not errors, errors result = {'status': 'passed', 'desktop': checks, 'mobile': mobile, 'long_code_mobile': narrow, 'runtime_exceptions': errors, 'copy_payload_verified_with_mock_clipboard': True, 'print_action_verified_with_mock_handler': True, 'mobile_navigation_verified': True, 'print_css_verified': True, 'screenshots': ['preview_desktop.png', 'preview_code.png', 'preview_figure.png', 'preview_mobile.png', 'preview_mobile_code.png']} (ASSETS / 'browser_verification.json').write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding='utf-8') print(json.dumps(result, ensure_ascii=True, indent=2)) finally: if socket: try: command('Browser.close') except Exception: pass socket.close() try: process.wait(timeout=8) except subprocess.TimeoutExpired: process.terminate() process.wait(timeout=5) stderr.close() # Delete only the unique profile created by this script, inside its own assets directory. assert profile.parent == ASSETS.resolve() and profile.name.startswith('browser_profile_') shutil.rmtree(profile, ignore_errors=True)