| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243 |
- """Frozen r8 browser acceptance: innovation evidence, Agent copy and existing research flows."""
- import argparse
- import base64
- from datetime import datetime, timezone
- import json
- from pathlib import Path
- import subprocess
- import tempfile
- import time
- from urllib.request import Request, urlopen
- import websocket
- ROOT = Path(__file__).resolve().parents[1]
- def main():
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('--url', default='http://127.0.0.1:4183')
- parser.add_argument('--out', default='work/industry-mainline-handoff-20261107-r1/browser')
- args = parser.parse_args()
- out = (ROOT / args.out).resolve()
- out.mkdir(parents=True, exist_ok=True)
- profile = Path(tempfile.mkdtemp(prefix='innovation-edge-', dir=out))
- browser = subprocess.Popen(['C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe', '--headless=new',
- '--disable-gpu', '--disable-background-networking', '--disable-component-update', '--disable-extensions',
- '--no-first-run', '--remote-debugging-port=0', '--user-data-dir=' + str(profile), 'about:blank'],
- stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, creationflags=subprocess.CREATE_NO_WINDOW)
- serial, ws, events = 0, None, []
- report = {'ok': False, 'checkedAt': datetime.now(timezone.utc).isoformat(), 'url': args.url,
- 'checks': [], 'layouts': [], 'responseOverrides': 0, 'paidCalls': 0, 'productionWrites': 0}
- try:
- end = time.monotonic() + 20
- while not (profile / 'DevToolsActivePort').exists():
- assert time.monotonic() < end and browser.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')) if p['type'] == 'page')
- ws = websocket.create_connection(page['webSocketDebuggerUrl'], timeout=45, suppress_origin=True, http_no_proxy=['127.0.0.1'])
- def call(method, params=None):
- nonlocal serial
- serial += 1
- ws.send(json.dumps({'id': serial, 'method': method, 'params': params or {}}))
- while True:
- d = json.loads(ws.recv())
- if d.get('id') == serial:
- assert 'error' not in d, d
- return d.get('result', {})
- events.append(d)
- def ev(code):
- d = call('Runtime.evaluate', {'expression': code, 'returnByValue': True, 'awaitPromise': True})
- assert 'exceptionDetails' not in d, d
- return d['result'].get('value')
- view = 'document.querySelector("kf-app")?.renderRoot?.querySelector("view-industry")'
- def action(code): return ev('(()=>{const p=' + view + ';' + code + '})()')
- def ready(tab='insights'):
- ev('new Promise((resolve,reject)=>{let end=Date.now()+30000;function tick(){const p=' + view +
- ';if(p && p._tab===' + json.dumps(tab) + ' && !p._loading && p._data){p.updateComplete.then(()=>resolve(true),reject);return}'
- 'if(p?._error)reject(Error(p._error));else if(Date.now()>end)reject(Error("timeout"));else setTimeout(tick,70)}tick()})')
- def navigate(path):
- call('Page.navigate', {'url': args.url + path})
- ready()
- def change(field, value):
- action('const s=p.renderRoot.querySelector(' + json.dumps('select[data-filter=' + field + ']') + ');s.value=' + json.dumps(value) + ';s.dispatchEvent(new Event("change"));return true')
- ev('new Promise(r=>setTimeout(r,80))')
- ready()
- def shot(name):
- (out / name).write_bytes(base64.b64decode(call('Page.captureScreenshot', {'format': 'png', 'captureBeyondViewport': False})['data']))
- def check(name): report['checks'].append(name)
- call('Runtime.enable'); call('Page.enable')
- call('Emulation.setFocusEmulationEnabled', {'enabled': True})
- call('Browser.grantPermissions', {'origin': args.url, 'permissions': ['clipboardReadWrite', 'clipboardSanitizedWrite']})
- call('Emulation.setDeviceMetricsOverride', {'width': 1440, 'height': 1050, 'deviceScaleFactor': 1, 'mobile': False})
- navigate('/industry')
- assert action('return p._filters.category') == 'banmian'
- scope = action('return p._data.scope')
- assert (scope['products'], scope['reviewRows'], scope['distinctReadablePairs'], scope['eligibleRows']) == (837, 693, 429, 49), scope
- report['scope'] = scope
- check('default-banmian-real-scope-and-independent-quality-counts')
- assert action('return !p.renderRoot.querySelector("details.maintenance").open')
- assert action('return p.renderRoot.querySelectorAll(".finding-card").length') == 3
- check('innovation-first-maintenance-collapsed')
- ev('new Promise(r=>setTimeout(r,1000))')
- report['representativeImages'] = action('return [...p.renderRoot.querySelectorAll("library-product-image")].filter(x=>x.image?.thumbnail).length')
- assert report['representativeImages'] > 0
- shot('innovation-1440.png')
- for index, target in enumerate(['directions', 'supply', 'voice', 'prices']):
- action('p.renderRoot.querySelectorAll(".insight-jumps button")[' + str(index) + '].click();return true')
- top = action('return p.renderRoot.querySelector("#insight-' + target + '").getBoundingClientRect().top')
- assert 0 <= top <= 40, (target, top)
- check('section-navigation-scrolls-to-the-actual-analysis-sections')
- assert action('return !p.renderRoot.querySelector("[data-export]") && !!p.renderRoot.querySelector(".p-head [data-copy-agent]")')
- action('p.renderRoot.querySelector("[data-select-direction=seasoning]").click();return true')
- assert action('return p._discussionIds.length') == 1
- call('Page.reload')
- ready()
- assert action('return p._discussionIds.length') == 1
- check('discussion-restores-on-refresh-within-the-same-scope-and-version')
- action('p.renderRoot.querySelector("[data-copy-agent]").click();return true')
- copied = ev('navigator.clipboard.readText()').replace('\r\n', '\n')
- assert all(x in copied for x in ('研究范围', '判断标准', '待核条件', '原文依据', '资料版本', 'ksf-voc-library', 'source_record_id'))
- assert copied == action('return p._prepareInsights()')
- (out / 'copied-agent-analysis.txt').write_text(copied, encoding='utf-8')
- check('top-right-real-clipboard-copy-has-scope-complete-record-ids-and-database-instructions')
- action('p.renderRoot.querySelector("[data-evidence=seasoning]").click();return true')
- ev('new Promise(r=>setTimeout(r,80))'); ready()
- assert action('return p._data.evidence.theme') == 'seasoning'
- assert action('return p.renderRoot.querySelectorAll(".evidence-item").length') == 12
- assert action('return p.renderRoot.activeElement?.id') == 'insight-evidence'
- shot('innovation-evidence-1440.png')
- action('p.renderRoot.querySelector(".evidence-panel .task-paging button:last-child").click();return true')
- ev('new Promise(r=>setTimeout(r,80))'); ready()
- assert action('return p._data.evidence.page') == 2
- action('p.renderRoot.querySelector("[data-close-evidence]").click();return true')
- ev('new Promise(r=>setTimeout(r,80))'); ready()
- assert action('return p._discussionIds.length') == 1
- check('real-theme-evidence-pagination-and-keyboard-focus')
- action('p._scrollInsight("directions");p.renderRoot.querySelector("[data-insight-focus=value]").click();return true')
- ev('new Promise(r=>setTimeout(r,80))'); ready()
- assert action('return p._data.themes.every(t=>t.id==="value")')
- assert action('return p._data.scope.products') == 837
- assert action('return p._discussionIds.length') == 1
- assert 0 <= action('return p.renderRoot.querySelector("#insight-directions").getBoundingClientRect().top') < 100
- action('p.renderRoot.querySelector("[data-select-direction=value]").click();return true')
- assert action('return p._discussionIds.length') == 2
- action('p.renderRoot.querySelector("[data-copy-agent]").click();return true')
- copied = ev('navigator.clipboard.readText()').replace('\r\n', '\n')
- assert '已选2个方向' in copied
- assert '保留风味,让调味更容易按偏好控制' in copied and '把价格接受与实际体验价值一起验证' in copied
- (out / 'copied-two-directions.txt').write_text(copied, encoding='utf-8')
- check('visual-focus-keeps-scope-scroll-position-and-multiple-selected-directions')
- action('p.renderRoot.querySelector(".copy-menu>summary").click();const s=p.renderRoot.querySelector("[data-copy-mode]");s.value="current";s.dispatchEvent(new Event("change"));return true')
- action('p.renderRoot.querySelector("[data-preview-agent]").click();return true')
- ev('new Promise(r=>requestAnimationFrame(r))')
- assert action('return p.renderRoot.querySelector(".agent-copy-dialog").open')
- assert action('return p.renderRoot.querySelector("[data-agent-text]").value===p._prepareInsights()')
- assert action('return !p.renderRoot.querySelector("[data-agent-text]").value.includes("方向1:保留风味")')
- shot('agent-copy-preview-1440.png')
- call('Input.dispatchKeyEvent', {'type': 'rawKeyDown', 'key': 'Escape', 'code': 'Escape', 'windowsVirtualKeyCode': 27, 'nativeVirtualKeyCode': 27})
- call('Input.dispatchKeyEvent', {'type': 'keyUp', 'key': 'Escape', 'code': 'Escape', 'windowsVirtualKeyCode': 27, 'nativeVirtualKeyCode': 27})
- ev('new Promise(r=>setTimeout(r,80))')
- assert action('return !p.renderRoot.querySelector(".agent-copy-dialog").open')
- check('copy-scope-preview-and-native-keyboard-dialog')
- call('Browser.grantPermissions', {'origin': args.url, 'permissions': []})
- assert ev('navigator.permissions.query({name:"clipboard-write"}).then(p=>p.state)') == 'denied'
- action('p.renderRoot.querySelector("[data-copy-agent]").click();return true')
- ev('new Promise((resolve,reject)=>{const end=Date.now()+5000;function tick(){const p=' + view + ';if(p.renderRoot.querySelector(".agent-copy-dialog").open){resolve(true);return}if(Date.now()>end)reject(Error("clipboard fallback timeout"));else setTimeout(tick,50)}tick()})')
- assert action('return p.renderRoot.querySelector(".agent-copy-dialog").open')
- assert action('const t=p.renderRoot.querySelector("[data-agent-text]");return t.selectionEnd-t.selectionStart===t.value.length')
- action('p.renderRoot.querySelector(".agent-copy-dialog").close();return true')
- call('Browser.grantPermissions', {'origin': args.url, 'permissions': ['clipboardReadWrite', 'clipboardSanitizedWrite']})
- check('clipboard-denial-opens-selected-text-for-manual-copy')
- action('const s=p.renderRoot.querySelector("[data-copy-mode]");s.value="all";s.dispatchEvent(new Event("change"));return true')
- ev('new Promise(r=>requestAnimationFrame(r))')
- assert action('return p._prepareInsights().includes("当前范围全部方向")')
- assert action('return p._discussionIds.length') == 2
- check('all-directions-copy-scope-keeps-existing-selection')
- change('category', 'soup_noodle')
- assert action('return p._discussionIds.length') == 0
- assert action('return p._data.scope.products') == 508
- change('brand', 'ksf')
- assert action('return p._data.brands.every(b=>b.brand==="ksf")')
- change('platform', 'jd')
- change('shopId', '1000084104')
- assert action('return p._data.representativeProducts.every(r=>r.productId.startsWith("jd:"))')
- change('platform', 'tmall')
- assert action('return !p.routeQuery.shopId')
- check('category-brand-platform-shop-propagate-without-cross-scope-evidence')
- navigate('/industry?category=banmian&brand=samyang')
- assert action('return p._data.scope.products') == 90
- assert action('return p._data.scope.reviewRows') == 0
- assert action('return p._data.opportunities.length') == 0
- check('real-no-reviews-brand-never-invents-innovation-hypotheses')
- navigate('/industry?category=banmian&brand=not-in-library')
- assert action('return p._data.scope.products') == 0
- assert action('return !!p.renderRoot.querySelector(".insight-empty")')
- check('empty-scope-not-confused-with-no-market-demand')
- navigate('/industry?category=banmian')
- action('const b=p.renderRoot.querySelectorAll(".insight-products button");b[0].click();b[1].click();return true')
- assert action('return p._compareIds.length') == 2
- action('p.renderRoot.querySelector(".section-title.compact a").click();return true')
- ev('new Promise((resolve,reject)=>{let end=Date.now()+20000;function tick(){const p=document.querySelector("kf-app")?.renderRoot?.querySelector("view-library");const c=p?.renderRoot?.querySelector("library-product-compare");if(c && !c._loading && c._items?.length===2 && c._items.every(x=>x.detail)){resolve(true);return}if(Date.now()>end)reject(Error("comparison timeout"));else setTimeout(tick,80)}tick()})')
- check('representative-products-open-existing-real-comparison')
- navigate('/industry?category=banmian&insightTheme=seasoning')
- for width in [1440, 1024, 768, 390, 320]:
- call('Emulation.setDeviceMetricsOverride', {'width': width, 'height': 1050, 'deviceScaleFactor': 1, 'mobile': False})
- ev('window.scrollTo(0,0);new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r)))')
- metrics = ev('({requested:' + str(width) + ',innerWidth,clientWidth:document.documentElement.clientWidth,scrollWidth:document.documentElement.scrollWidth})')
- assert metrics['requested'] == metrics['innerWidth']
- assert metrics['clientWidth'] == metrics['scrollWidth'], metrics
- report['layouts'].append(metrics)
- if width in (1440, 390, 320):
- shot('innovation-' + str(width) + '.png')
- action('p._scrollInsight("directions");return true')
- shot('innovation-directions-' + str(width) + '.png')
- if width in (1440, 390, 320):
- action('p._scrollInsight("supply");return true')
- shot('innovation-supply-' + str(width) + '.png')
- action('p.renderRoot.querySelector(".insight-products").scrollIntoView({block:"start"});return true')
- ev('new Promise(r=>setTimeout(r,350))')
- decoded = action('return [...p.renderRoot.querySelectorAll("library-product-image")].filter(x=>{const i=x.renderRoot.querySelector("img");return i?.complete && i.naturalWidth>0}).length')
- assert decoded == report['representativeImages'], (width, decoded)
- shot('innovation-products-' + str(width) + '.png')
- action('p._scrollInsight("prices");return true')
- shot('innovation-prices-' + str(width) + '.png')
- if width <= 390:
- assert action('return [...p.renderRoot.querySelectorAll(".price-table td")].every(x=>x.scrollWidth<=x.clientWidth)'), width
- if width == 390:
- action('p._scrollInsight("evidence");return true')
- shot('innovation-evidence-390.png')
- check('five-exact-viewports-with-no-page-horizontal-overflow')
- for tab in ['overview', 'listings', 'reviews', 'policies', 'rankings', 'plan', 'contract']:
- call('Page.navigate', {'url': args.url + '/industry?tab=' + tab})
- ready(tab)
- check('seven-existing-research-and-maintenance-tabs-retained')
- call('Page.navigate', {'url': args.url + '/industry?insightTheme=seasoning&insightPage=99999'})
- ev('new Promise((resolve,reject)=>{let end=Date.now()+20000;function tick(){const p=' + view + ';if(p?._error){resolve(true);return}if(Date.now()>end)reject(Error("error-state timeout"));else setTimeout(tick,70)}tick()})')
- action('p.renderRoot.querySelector("[data-reset-insights]").click();return true')
- ev('new Promise(r=>setTimeout(r,80))'); ready()
- check('invalid-evidence-page-shows-recoverable-error-not-zero')
- report['consoleExceptions'] = [e for e in events if e['method'] == 'Runtime.exceptionThrown']
- assert not report['consoleExceptions'], report['consoleExceptions']
- report['ok'] = True
- except Exception:
- if ws:
- report['failureState'] = action('return {url:location.href,tab:p?._tab,query:p?.routeQuery,loading:p?._loading,error:p?._error,keys:Object.keys(p?._data||{})}')
- shot('failure.png')
- raise
- finally:
- report['consoleExceptions'] = [e for e in events if e['method'] == 'Runtime.exceptionThrown']
- if ws: ws.close()
- browser.terminate()
- try: browser.wait(timeout=10)
- except subprocess.TimeoutExpired: browser.kill(); browser.wait(timeout=10)
- (out / 'verification.json').write_text(json.dumps(report, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
- print(json.dumps(report, ensure_ascii=False, indent=2))
- if __name__ == '__main__': main()
|