verify-expanded-review-browser.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. """Verify published review text on the public board and local preview, without a token."""
  2. import base64
  3. import datetime as dt
  4. import json
  5. from pathlib import Path
  6. import sqlite3
  7. import subprocess
  8. import tempfile
  9. import time
  10. from urllib.parse import quote
  11. from urllib.request import urlopen
  12. import websocket
  13. ROOT = Path(__file__).resolve().parents[1]
  14. OUT = ROOT / 'docs/industry-product-reviews-20261106/round2'
  15. SHOTS = OUT / 'screenshots'
  16. SHOTS.mkdir(parents=True, exist_ok=True)
  17. evidence = json.loads((OUT / 'production-readback.json').read_text(encoding='utf-8'))
  18. assert evidence['ok'] and evidence['verifiedReviews'] == 512
  19. samples = []
  20. for folder in sorted((ROOT / 'work/product-reviews/round2/intake').glob('batch-*')):
  21. with sqlite3.connect((folder / 'staging.sqlite').resolve().as_uri() + '?mode=ro', uri=True) as c:
  22. c.row_factory = sqlite3.Row
  23. pid = c.execute('SELECT product_id FROM reviews ORDER BY length(content) DESC,id LIMIT 1').fetchone()[0]
  24. rows = [dict(r) for r in c.execute('SELECT id,content FROM reviews WHERE product_id=?', (pid,))]
  25. samples.append({'batch': folder.name, 'productId': pid, 'reviews': rows})
  26. profile = Path(tempfile.mkdtemp(prefix='published-reviews-', dir=ROOT / 'work/library-preview'))
  27. process = subprocess.Popen(['C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe', '--headless=new',
  28. '--disable-gpu', '--remote-debugging-port=0', '--disable-background-networking', '--disable-extensions',
  29. '--no-first-run', '--user-data-dir=' + str(profile), 'about:blank'], stdout=subprocess.DEVNULL,
  30. stderr=subprocess.DEVNULL, creationflags=subprocess.CREATE_NO_WINDOW)
  31. ws = None
  32. serial = 0
  33. events = []
  34. report = {'ok': False, 'checkedAt': dt.datetime.now(dt.timezone.utc).isoformat(),
  35. 'expectedGeneration': evidence['generation'], 'tokenProvided': False,
  36. 'publicBoardConnectionVerified': False, 'localPreviewVerified': False,
  37. 'productionWrites': 0, 'paidCalls': 0, 'sites': []}
  38. try:
  39. deadline = time.monotonic() + 20
  40. while not (profile / 'DevToolsActivePort').exists():
  41. assert time.monotonic() < deadline and process.poll() is None
  42. time.sleep(.1)
  43. port = (profile / 'DevToolsActivePort').read_text().splitlines()[0]
  44. page = next(p for p in json.load(urlopen('http://127.0.0.1:' + port + '/json/list', timeout=5)) if p['type'] == 'page')
  45. ws = websocket.create_connection(page['webSocketDebuggerUrl'], timeout=40, suppress_origin=True,
  46. http_no_proxy=['127.0.0.1', 'localhost'])
  47. def call(method, params=None):
  48. global serial
  49. serial += 1
  50. ws.send(json.dumps({'id': serial, 'method': method, 'params': params or {}}))
  51. while True:
  52. response = json.loads(ws.recv())
  53. if response.get('id') == serial:
  54. assert 'error' not in response, response
  55. return response.get('result', {})
  56. events.append(response)
  57. def ev(expression):
  58. result = call('Runtime.evaluate', {'expression': expression, 'returnByValue': True, 'awaitPromise': True})
  59. assert 'exceptionDetails' not in result, result
  60. return result['result'].get('value')
  61. def wait(expression):
  62. ev('new Promise((resolve,reject)=>{const end=Date.now()+30000;const tick=()=>{try{if(' + expression +
  63. '){resolve(true);return}}catch{}if(Date.now()>end)reject(new Error("browser wait timeout"));'
  64. 'else setTimeout(tick,70)};tick()})')
  65. V = 'document.querySelector("kf-app")?.renderRoot.querySelector("view-library")'
  66. P = V + '?.renderRoot.querySelector("library-product-detail")'
  67. def action(body):
  68. return ev('(()=>{const p=' + P + ';' + body + '})()')
  69. def shot(name):
  70. data = call('Page.captureScreenshot', {'format': 'png', 'captureBeyondViewport': False})
  71. (SHOTS / (name + '.png')).write_bytes(base64.b64decode(data['data']))
  72. call('Page.enable')
  73. call('Runtime.enable')
  74. call('Network.enable')
  75. for label, base in [('public', 'https://kangshifu.fmode.cn'), ('local', 'http://127.0.0.1:4181')]:
  76. site = {'baseUrl': base, 'products': [], 'screenshots': []}
  77. for index, sample in enumerate(samples):
  78. call('Emulation.setDeviceMetricsOverride', {'width': 1440, 'height': 1100, 'deviceScaleFactor': 1, 'mobile': False})
  79. path = '/library/products/' + quote(sample['productId'], safe='') + '?panel=reviews'
  80. call('Page.navigate', {'url': base + path})
  81. wait(P + '?.panel==="reviews" && !' + P + '._loading && ' + P + '._data')
  82. assert ev(V + '._summary.generation') == evidence['generation']
  83. default_scope = action('return p.renderRoot.querySelector("select[aria-label=商品评价范围]").value;')
  84. default_count = action('return p._data.totalMatched;')
  85. if default_scope != 'audit':
  86. action('const select=p.renderRoot.querySelector("select[aria-label=商品评价范围]");'
  87. 'select.value="audit";select.dispatchEvent(new Event("change",{bubbles:true}));')
  88. wait(P + '?.query.relatedQuality==="audit" && !' + P + '._loading && ' + P + '._data?.effectiveQuery.quality==="audit"')
  89. snapshot = action('return {generation:p._data.generation,total:p._data.totalMatched,'
  90. 'rows:Array.from(p.renderRoot.querySelectorAll("article.review")).map(el=>({'
  91. 'id:el.dataset.childId,productId:el.dataset.productId,'
  92. 'content:el.querySelector("p.full-review").textContent}))};')
  93. expected = {r['id']: r['content'] for r in sample['reviews']}
  94. assert snapshot['generation'] == evidence['generation']
  95. assert snapshot['total'] == len(expected) == len(snapshot['rows']), snapshot
  96. assert {r['id']: r['content'] for r in snapshot['rows']} == expected, sample['productId']
  97. assert all(r['productId'] == sample['productId'] for r in snapshot['rows'])
  98. layouts = []
  99. for width in [1440, 390]:
  100. call('Emulation.setDeviceMetricsOverride', {'width': width, 'height': 1100, 'deviceScaleFactor': 1, 'mobile': width < 600})
  101. ev('new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)))')
  102. assert ev('document.documentElement.scrollWidth<=innerWidth'), (label, width)
  103. layouts.append({'width': width, 'noDocumentOverflow': True})
  104. if index == 0:
  105. name = label + '-published-reviews-' + str(width)
  106. shot(name)
  107. site['screenshots'].append('screenshots/' + name + '.png')
  108. site['products'].append({'batch': sample['batch'], 'productId': sample['productId'],
  109. 'url': base + path, 'defaultScope': default_scope,
  110. 'defaultReviewCount': default_count, 'verifiedVisibleReviews': len(expected),
  111. 'allVisibleTextMatched': True, 'layouts': layouts})
  112. print(label + ' ' + sample['batch'] + ': visible review text verified', flush=True)
  113. report['sites'].append(site)
  114. if label == 'public':
  115. report['publicBoardConnectionVerified'] = True
  116. else:
  117. report['localPreviewVerified'] = True
  118. failures = [e['params']['exceptionDetails'].get('text') for e in events if e.get('method') == 'Runtime.exceptionThrown']
  119. report['consoleExceptions'] = failures
  120. assert not failures, failures
  121. responses = [e['params']['response'] for e in events if e.get('method') == 'Network.responseReceived'
  122. and e['params']['response']['url'].endswith('/api/library')]
  123. report['libraryHttpStatuses'] = sorted({int(r['status']) for r in responses})
  124. assert responses and report['libraryHttpStatuses'] == [200]
  125. report['ok'] = True
  126. finally:
  127. if ws:
  128. ws.close()
  129. process.terminate()
  130. try:
  131. process.wait(timeout=8)
  132. except subprocess.TimeoutExpired:
  133. process.kill()
  134. (OUT / 'published-browser-verification.json').write_text(json.dumps(report, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
  135. print(json.dumps(report, ensure_ascii=False, indent=2))