verify_report_preview.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. """Check local report reading, responsive layout and actual links in Edge."""
  2. from pathlib import Path
  3. import base64
  4. import functools
  5. import http.server
  6. import json
  7. import subprocess
  8. import tempfile
  9. import threading
  10. import time
  11. from urllib.parse import quote
  12. from urllib.request import urlopen
  13. import websocket
  14. OUT = Path(__file__).resolve().parent
  15. SCREENSHOTS = OUT / 'browser'
  16. SCREENSHOTS.mkdir(exist_ok=True)
  17. class Handler(http.server.SimpleHTTPRequestHandler):
  18. def log_message(self, *args):
  19. pass
  20. def list_directory(self, path):
  21. self.send_error(404)
  22. return None
  23. def main():
  24. server = http.server.ThreadingHTTPServer(('127.0.0.1', 0), functools.partial(Handler, directory=str(OUT)))
  25. threading.Thread(target=server.serve_forever, daemon=True).start()
  26. base = 'http://127.0.0.1:' + str(server.server_port)
  27. profile = Path(tempfile.mkdtemp(prefix='ksf-report-design-'))
  28. edge = 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe'
  29. proc = subprocess.Popen([edge, '--headless=new', '--disable-gpu', '--remote-debugging-port=0',
  30. '--disable-background-networking', '--disable-component-update', '--disable-sync', '--disable-extensions',
  31. '--no-first-run', '--no-default-browser-check', '--user-data-dir=' + str(profile), 'about:blank'],
  32. stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, creationflags=subprocess.CREATE_NO_WINDOW)
  33. ws = None
  34. seq = 0
  35. errors, requests, checks = [], [], []
  36. try:
  37. deadline = time.monotonic() + 20
  38. while not (profile / 'DevToolsActivePort').exists():
  39. assert time.monotonic() < deadline and proc.poll() is None
  40. time.sleep(.1)
  41. port = (profile / 'DevToolsActivePort').read_text().splitlines()[0]
  42. page = next(p for p in json.load(urlopen('http://127.0.0.1:' + port + '/json/list', timeout=5)) if p['type'] == 'page')
  43. ws = websocket.create_connection(page['webSocketDebuggerUrl'], timeout=20, suppress_origin=True, http_no_proxy=['127.0.0.1', 'localhost'])
  44. def call(method, params=None):
  45. nonlocal seq
  46. seq += 1
  47. rid = seq
  48. ws.send(json.dumps({'id': rid, 'method': method, 'params': params or {}}))
  49. while True:
  50. message = json.loads(ws.recv())
  51. if message.get('id') == rid:
  52. assert 'error' not in message, message
  53. return message.get('result', {})
  54. if message.get('method') == 'Runtime.exceptionThrown':
  55. errors.append(message['params'])
  56. if message.get('method') == 'Network.requestWillBeSent':
  57. requests.append(message['params']['request']['url'])
  58. def ev(code, promise=False):
  59. result = call('Runtime.evaluate', {'expression': code, 'returnByValue': True, 'awaitPromise': promise})
  60. assert 'exceptionDetails' not in result, result
  61. return result['result'].get('value')
  62. def settle():
  63. ev('new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r)))', True)
  64. def screenshot(name):
  65. settle()
  66. shot = call('Page.captureScreenshot', {'format': 'png', 'captureBeyondViewport': False})
  67. (SCREENSHOTS / name).write_bytes(base64.b64decode(shot['data']))
  68. call('Page.enable')
  69. call('Runtime.enable')
  70. call('Network.enable')
  71. call('Network.setBlockedURLs', {'urls': ['https://*']})
  72. url = base + '/' + quote('拌面研究报告形式样稿.html')
  73. call('Page.navigate', {'url': url})
  74. ev('new Promise((resolve,reject)=>{const stop=Date.now()+10000;const poll=()=>{if(document.querySelectorAll("#products tbody tr").length===4)return resolve(true);if(Date.now()>stop)return reject(Error("load timeout"));setTimeout(poll,40)};poll()})', True)
  75. assert ev('document.querySelectorAll("#products tbody tr").length') == 4
  76. assert ev('document.querySelectorAll(".task").length') == 3
  77. assert ev('document.querySelectorAll("a[href^=https]").length') == 2
  78. assert ev('document.body.innerText.includes("未新增消费者研究")')
  79. assert ev('document.body.innerText.includes("不排序品牌体验")')
  80. assert not ev('/茶决决|COMPEX|Aroma Diffuser|18\\.7%|31\\.2%/.test(document.body.innerText)')
  81. assert ev('[...document.querySelectorAll(".anchors a")].every(a=>document.querySelector(a.getAttribute("href")))')
  82. text_before = ev('document.querySelector("main").textContent')
  83. for width, height, label in [(1440, 1000, 'desktop'), (390, 844, 'mobile'), (360, 800, 'narrow')]:
  84. call('Emulation.setDeviceMetricsOverride', {'width': width, 'height': height, 'deviceScaleFactor': 1, 'mobile': width < 500})
  85. for mode in ('full', 'brief'):
  86. ev('document.querySelector(' + json.dumps('[data-set-mode="' + mode + '"]') + ').click()')
  87. settle()
  88. result = ev('''(()=>{const shown=[...document.querySelectorAll('main p,main td,main th,main small')].filter(e=>e.getClientRects().length);return {width:innerWidth,scroll:document.documentElement.scrollWidth,mode:document.body.dataset.mode,selectedButtons:document.querySelectorAll('[aria-pressed=true]').length,minTextSize:Math.min(...shown.map(e=>parseFloat(getComputedStyle(e).fontSize))),tableContainerWidth:document.querySelector('.table-scroll').clientWidth,tableWidth:document.querySelector('table').scrollWidth,detailVisible:getComputedStyle(document.querySelector('[data-detail]')).display!=='none'};})()''')
  89. assert result['width'] == width and result['scroll'] <= width, result
  90. assert result['mode'] == mode and result['selectedButtons'] == 1 and result['minTextSize'] >= 14, result
  91. assert result['detailVisible'] == (mode == 'full'), result
  92. assert ev('[...document.querySelectorAll(".anchors a")].filter(a=>a.getClientRects().length).every(a=>getComputedStyle(document.querySelector(a.getAttribute("href"))).display!=="none")')
  93. if width < 500 and mode == 'full':
  94. assert result['tableWidth'] > result['tableContainerWidth'] > 0, result
  95. assert ev('(()=>{const e=document.querySelector(".table-scroll");e.scrollLeft=e.scrollWidth;return e.scrollLeft>0})()')
  96. ev('document.querySelector(".table-scroll").scrollLeft=0')
  97. assert ev('document.querySelector("main").textContent') == text_before
  98. checks.append(result | {'viewport': label})
  99. if label in ('desktop', 'mobile'):
  100. ev('scrollTo(0,0)')
  101. screenshot(label + '-' + mode + '.png')
  102. if mode == 'full':
  103. for section in ('products', 'research'):
  104. ev('document.querySelector("#' + section + '").scrollIntoView({behavior:"instant",block:"start"})')
  105. screenshot(label + '-' + section + '.png')
  106. call('Emulation.setEmulatedMedia', {'media': 'print'})
  107. assert ev('getComputedStyle(document.querySelector("[data-detail]")).display') != 'none'
  108. assert ev('getComputedStyle(document.querySelector("#sources")).display') != 'none'
  109. call('Emulation.setEmulatedMedia', {'media': ''})
  110. assert not errors, errors
  111. external = [request for request in requests if not request.startswith(base + '/') and not request.startswith('data:')]
  112. assert not external, external
  113. receipt = {'ok': True, 'scope': 'local report form, interaction and content boundaries; not consumer or production validation',
  114. 'browser': 'Edge headless', 'renderChecks': len(checks), 'checks': checks,
  115. 'viewports': [1440, 390, 360], 'modes': ['full', 'brief'],
  116. 'pageHorizontalOverflow': False, 'sourceAnchorLinks': 2,
  117. 'readingModesPreserveUnderlyingText': True, 'printRetainsCoreSections': True,
  118. 'runtimeErrors': errors, 'externalAutomaticRequests': external,
  119. 'screenshotsManuallyReviewed': False, 'actualResearchRun': False, 'remoteInstalled': False}
  120. (OUT / 'browser-checks.json').write_text(json.dumps(receipt, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
  121. print(json.dumps({k: v for k, v in receipt.items() if k != 'checks'}, ensure_ascii=False))
  122. finally:
  123. if ws:
  124. ws.close()
  125. proc.terminate()
  126. proc.wait(timeout=10)
  127. server.shutdown()
  128. server.server_close()
  129. if __name__ == '__main__':
  130. main()