| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143 |
- """Check local report reading, responsive layout and actual links in Edge."""
- 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
- from urllib.request import urlopen
- import websocket
- OUT = Path(__file__).resolve().parent
- SCREENSHOTS = OUT / 'browser'
- SCREENSHOTS.mkdir(exist_ok=True)
- class Handler(http.server.SimpleHTTPRequestHandler):
- def log_message(self, *args):
- pass
- def list_directory(self, path):
- self.send_error(404)
- return None
- def main():
- server = http.server.ThreadingHTTPServer(('127.0.0.1', 0), functools.partial(Handler, directory=str(OUT)))
- 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-report-design-'))
- 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)
- ws = None
- seq = 0
- errors, requests, checks = [], [], []
- 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=20, suppress_origin=True, http_no_proxy=['127.0.0.1', 'localhost'])
- def call(method, params=None):
- nonlocal seq
- seq += 1
- rid = seq
- ws.send(json.dumps({'id': rid, 'method': method, 'params': params or {}}))
- while True:
- message = json.loads(ws.recv())
- if message.get('id') == rid:
- assert 'error' not in message, message
- return message.get('result', {})
- if message.get('method') == 'Runtime.exceptionThrown':
- errors.append(message['params'])
- if message.get('method') == 'Network.requestWillBeSent':
- requests.append(message['params']['request']['url'])
- def ev(code, promise=False):
- result = call('Runtime.evaluate', {'expression': code, 'returnByValue': True, 'awaitPromise': promise})
- assert 'exceptionDetails' not in result, result
- return result['result'].get('value')
- def settle():
- ev('new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r)))', True)
- def screenshot(name):
- settle()
- shot = call('Page.captureScreenshot', {'format': 'png', 'captureBeyondViewport': False})
- (SCREENSHOTS / name).write_bytes(base64.b64decode(shot['data']))
- call('Page.enable')
- call('Runtime.enable')
- call('Network.enable')
- call('Network.setBlockedURLs', {'urls': ['https://*']})
- url = base + '/' + quote('拌面研究报告形式样稿.html')
- call('Page.navigate', {'url': url})
- 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)
- assert ev('document.querySelectorAll("#products tbody tr").length') == 4
- assert ev('document.querySelectorAll(".task").length') == 3
- assert ev('document.querySelectorAll("a[href^=https]").length') == 2
- assert ev('document.body.innerText.includes("未新增消费者研究")')
- assert ev('document.body.innerText.includes("不排序品牌体验")')
- assert not ev('/茶决决|COMPEX|Aroma Diffuser|18\\.7%|31\\.2%/.test(document.body.innerText)')
- assert ev('[...document.querySelectorAll(".anchors a")].every(a=>document.querySelector(a.getAttribute("href")))')
- text_before = ev('document.querySelector("main").textContent')
- for width, height, label in [(1440, 1000, 'desktop'), (390, 844, 'mobile'), (360, 800, 'narrow')]:
- call('Emulation.setDeviceMetricsOverride', {'width': width, 'height': height, 'deviceScaleFactor': 1, 'mobile': width < 500})
- for mode in ('full', 'brief'):
- ev('document.querySelector(' + json.dumps('[data-set-mode="' + mode + '"]') + ').click()')
- settle()
- 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'};})()''')
- assert result['width'] == width and result['scroll'] <= width, result
- assert result['mode'] == mode and result['selectedButtons'] == 1 and result['minTextSize'] >= 14, result
- assert result['detailVisible'] == (mode == 'full'), result
- assert ev('[...document.querySelectorAll(".anchors a")].filter(a=>a.getClientRects().length).every(a=>getComputedStyle(document.querySelector(a.getAttribute("href"))).display!=="none")')
- if width < 500 and mode == 'full':
- assert result['tableWidth'] > result['tableContainerWidth'] > 0, result
- assert ev('(()=>{const e=document.querySelector(".table-scroll");e.scrollLeft=e.scrollWidth;return e.scrollLeft>0})()')
- ev('document.querySelector(".table-scroll").scrollLeft=0')
- assert ev('document.querySelector("main").textContent') == text_before
- checks.append(result | {'viewport': label})
- if label in ('desktop', 'mobile'):
- ev('scrollTo(0,0)')
- screenshot(label + '-' + mode + '.png')
- if mode == 'full':
- for section in ('products', 'research'):
- ev('document.querySelector("#' + section + '").scrollIntoView({behavior:"instant",block:"start"})')
- screenshot(label + '-' + section + '.png')
- call('Emulation.setEmulatedMedia', {'media': 'print'})
- assert ev('getComputedStyle(document.querySelector("[data-detail]")).display') != 'none'
- assert ev('getComputedStyle(document.querySelector("#sources")).display') != 'none'
- call('Emulation.setEmulatedMedia', {'media': ''})
- assert not errors, errors
- external = [request for request in requests if not request.startswith(base + '/') and not request.startswith('data:')]
- assert not external, external
- receipt = {'ok': True, 'scope': 'local report form, interaction and content boundaries; not consumer or production validation',
- 'browser': 'Edge headless', 'renderChecks': len(checks), 'checks': checks,
- 'viewports': [1440, 390, 360], 'modes': ['full', 'brief'],
- 'pageHorizontalOverflow': False, 'sourceAnchorLinks': 2,
- 'readingModesPreserveUnderlyingText': True, 'printRetainsCoreSections': True,
- 'runtimeErrors': errors, 'externalAutomaticRequests': external,
- 'screenshotsManuallyReviewed': False, 'actualResearchRun': False, 'remoteInstalled': False}
- (OUT / 'browser-checks.json').write_text(json.dumps(receipt, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
- print(json.dumps({k: v for k, v in receipt.items() if k != 'checks'}, ensure_ascii=False))
- finally:
- if ws:
- ws.close()
- proc.terminate()
- proc.wait(timeout=10)
- server.shutdown()
- server.server_close()
- if __name__ == '__main__':
- main()
|