Week7-2: explain week7_analysis.do
This commit is contained in:
13 files changed
+1674
No files matched your search
@@ -0,0 +1,163 @@
|
||||
"""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)
|
||||
Reference in new issue
Block a user