| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344 |
- """Read-only browser acceptance against the real local industry library."""
- import argparse
- import base64
- import json
- from pathlib import Path
- import subprocess
- import tempfile
- import time
- from urllib.request import urlopen
- import websocket
- ROOT=Path(__file__).resolve().parents[1]
- WORK=ROOT/'work/library-preview'
- WORK.mkdir(parents=True,exist_ok=True)
- parser=argparse.ArgumentParser();parser.add_argument('--url',default='http://127.0.0.1:4181/library');args=parser.parse_args()
- profile=Path(tempfile.mkdtemp(prefix='browser-',dir=WORK))
- process=subprocess.Popen(['C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe','--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)
- serial=0;ws=None;events=[]
- try:
- deadline=time.monotonic()+20
- while not (profile/'DevToolsActivePort').exists():
- assert time.monotonic()<deadline and process.poll() is None
- time.sleep(.1)
- port=(profile/'DevToolsActivePort').read_text().splitlines()[0]
- page=next(x for x in json.load(urlopen('http://127.0.0.1:'+port+'/json/list',timeout=5)) if x['type']=='page')
- ws=websocket.create_connection(page['webSocketDebuggerUrl'],timeout=35,suppress_origin=True,http_no_proxy=['127.0.0.1','localhost'])
- def call(method,params=None):
- global serial
- serial+=1;rid=serial;ws.send(json.dumps(dict(id=rid,method=method,params=params or {})))
- while True:
- response=json.loads(ws.recv())
- if response.get('id')==rid:
- assert 'error' not in response,response
- return response.get('result',{})
- events.append(response)
- def evaluate(expression,promise=False):
- response=call('Runtime.evaluate',dict(expression=expression,returnByValue=True,awaitPromise=promise))
- assert 'exceptionDetails' not in response,response
- return response['result'].get('value')
- def wait(expression,timeout=25):
- evaluate('new Promise((resolve,reject)=>{const end=Date.now()+'+str(timeout*1000)+';const tick=()=>{try{if('+expression+'){resolve(true);return;}}catch{}if(Date.now()>end)reject(new Error("Timed out: "+'+json.dumps(expression)+'));else setTimeout(tick,60)};tick()})',True)
- def action(body):
- return evaluate('(()=>{const v=document.querySelector("kf-app").shadowRoot.querySelector("view-library");'+body+'})()')
- V='document.querySelector("kf-app")?.shadowRoot?.querySelector("view-library")'
- P=V+'.renderRoot.querySelector("library-product-detail")'
- def product_action(body):
- return evaluate('(()=>{const p='+P+';'+body+'})()')
- def product_ready():wait(P+' && !'+P+'._loading && ('+P+'.panel==="overview" || '+P+'._data)')
- def product_tab(panel):
- product_action('p.renderRoot.querySelector("#tab-'+panel+'").click();')
- wait(P+'.panel==='+json.dumps(panel)+' && !'+P+'._loading && ('+P+'.panel==="overview" || '+P+'._data)')
- def ready():wait(V+'?._summary && !'+V+'?._loading && ('+V+'?._data || '+V+'?._detail || '+V+'?._type==="reports")')
- def shot(name):
- image=call('Page.captureScreenshot',dict(format='png',captureBeyondViewport=False))
- (WORK/(name+'.png')).write_bytes(base64.b64decode(image['data']))
- def reader_surface():
- text=action('return v.renderRoot.textContent+"\\n"+Array.from(v.renderRoot.querySelectorAll("[title]")).map(el=>el.title).join("\\n");')
- forbidden=['本地资料库','资料库已连接','资料更新','更新记录','已入库','业务库','元数据','分类依据','品牌依据','品类依据','分类状态','待核对规则','SHA256','校验指纹','平台接口族','来源编号','来源文件','JSONL','数据版本','rule_title_','source_attribute']
- assert not any(token in text for token in forbidden),text[:1500]
- assert not action('return !!v.renderRoot.querySelector(".version,.updates,.fingerprints,.record-number");')
- assert not action('return !!v.renderRoot.querySelector("input:not([type=date]),form.search");')
- # Facts belong to the information panel; source actions must not repeat them.
- assert not action('return !!v.renderRoot.querySelector(".source .metadata,.source .source-title");')
- labels=action('return Array.from(v.renderRoot.querySelectorAll(".detail-grid .metadata dt")).map(el=>el.textContent);')
- assert len(labels)==len(set(labels)),labels
- call('Page.enable');call('Runtime.enable');call('Network.enable')
- call('Emulation.setDeviceMetricsOverride',dict(width=1440,height=1100,deviceScaleFactor=1,mobile=False))
- call('Page.navigate',dict(url=args.url));ready()
- assert action('return v._summary.counts.products;')==4639
- assert action('return v._data.totalMatched;')==4639
- assert action('return Array.from(v.renderRoot.querySelectorAll(".tree-link")).map(a=>new URL(a.href).searchParams.get("type"));')==['products','social','documents','reports']
- assert action('return Array.from(v.renderRoot.querySelectorAll(".stat")).map(a=>new URL(a.href).searchParams.get("type"));')==['products','social','documents','reports']
- reader_surface()
- assert action('return v.renderRoot.getElementById("advanced-filters").hidden;')
- assert action('return v.renderRoot.getElementById("coverage-insights").hidden;')
- assert action('return v.renderRoot.querySelector("a[data-record-id]").getBoundingClientRect().top;')<350
- assert action('return getComputedStyle(v.renderRoot.querySelector(".stats")).display;')=='none'
- assert action('return getComputedStyle(v.renderRoot.querySelector(".record-list")).gridTemplateColumns.split(" ").length;')>=3
- # Display preferences reveal existing data without changing the query or fetching again.
- resources=evaluate('performance.getEntriesByType("resource").length')
- action('v.renderRoot.querySelector("button[aria-label=列表视图]").click();')
- wait(V+'.renderRoot.querySelector(".record-list.list")')
- assert action('return v._data.records.length;')==20
- shot('library-list-view')
- action('v.renderRoot.querySelector("button[aria-label=卡片视图]").click();v.renderRoot.querySelector(".insights-toggle").click();')
- wait('!'+V+'.renderRoot.getElementById("coverage-insights").hidden')
- assert action('return v.renderRoot.querySelectorAll(".bar").length;')>0
- assert evaluate('performance.getEntriesByType("resource").length')==resources
- action('v.renderRoot.querySelector(".insights-toggle").click();v.renderRoot.querySelector(".advanced-toggle").click();')
- wait('!'+V+'.renderRoot.getElementById("advanced-filters").hidden')
- action('const i=v.renderRoot.querySelector("input[aria-label=日期起]");i.value="2026-11-01";i.dispatchEvent(new Event("change",{bubbles:true}));')
- wait(V+'.routeQuery.from==="2026-11-01" && !'+V+'._loading')
- action('v.renderRoot.querySelector(".advanced-toggle").click();')
- wait(V+'.renderRoot.getElementById("advanced-filters").hidden')
- assert action('return !!v.renderRoot.querySelector("button[aria-label^=移除日期起]");')
- action('v.renderRoot.querySelector("button[aria-label^=移除日期起]").click();')
- wait('!'+V+'.routeQuery.from && !'+V+'._loading')
- def select(label,value):
- selector=json.dumps('select[aria-label="'+label+'"]',ensure_ascii=False)
- action('const s=v.renderRoot.querySelector('+selector+');s.value='+json.dumps(value)+';s.dispatchEvent(new Event("change",{bubbles:true}));')
- wait(V+'._loading===true || '+V+'._data?.effectiveQuery.filters['+json.dumps({'平台':'platform','品牌':'brand','品类':'category'}.get(label,label))+']')
- ready()
- select('平台','jd')
- wait(V+'.routeQuery.platform==="jd" && !'+V+'._loading')
- select('品牌','ksf')
- wait(V+'.routeQuery.brand==="ksf" && !'+V+'._loading')
- select('品类','banmian')
- wait(V+'.routeQuery.category==="banmian" && !'+V+'._loading')
- scope=action('return {total:v._data.totalMatched,rows:v._data.records.map(r=>[r.platform,r.brand,r.category])};')
- assert scope['total']>0 and all(r==['jd','ksf','banmian'] for r in scope['rows']),scope
- assert action('return v.renderRoot.querySelectorAll(".filter-chip").length;')==3
- action('v.renderRoot.querySelector("button[aria-label^=移除品牌]").click();')
- wait('!'+V+'.routeQuery.brand && !'+V+'._loading')
- assert action('return [v.routeQuery.platform,v.routeQuery.category];')==['jd','banmian']
- select('品牌','ksf')
- wait(V+'.routeQuery.brand==="ksf" && !'+V+'._loading')
- shot('library-filtered-desktop')
- action('v.renderRoot.querySelector("a[data-record-id]").click();')
- wait(V+'._detail && !'+V+'._loading')
- assert action('return v._detail.record.source.archiveSha256.length;')==64
- assert action('return v._detail.relations.length;')==5
- product_ready()
- assert product_action('return p.product.id;')==action('return v._detail.record.id;')
- assert product_action('return p.renderRoot.querySelectorAll(".product-resources button").length;')==4
- product_tab('prices')
- assert product_action('return p._data.records.every(row=>row.product_id===p.product.id);')
- assert action('return v.routeQuery.brand;')=='ksf'
- product_tab('overview')
- reader_surface()
- action('navigator.clipboard.writeText=async text=>{window.__libraryCitation=text;};v.renderRoot.querySelector(".detail-actions .primary").click();')
- wait('!!window.__libraryCitation')
- citation=evaluate('window.__libraryCitation')
- assert '资料链接:' in citation and '原始页面:https://' in citation,citation
- assert not any(x in citation for x in ['SHA256','来源编号','来源文件','JSONL','rule_title_']),citation
- shot('library-product-detail')
- detail_url=evaluate('location.href')
- call('Page.reload');ready();assert evaluate('location.href')==detail_url
- assert action('return v._detail.record.id.startsWith("jd:");')
- action('v.renderRoot.querySelector(".breadcrumb a").click();')
- wait(V+'._data && !'+V+'._loading && !'+V+'.recordId')
- assert action('return v.routeQuery.brand;')=='ksf'
- # Category navigation and cards operate on URL state, not a local mock dataset.
- def go_type(entity):
- if entity in ['products','social','documents','reports']:
- action('Array.from(v.renderRoot.querySelectorAll(".tree-link")).find(a=>new URL(a.href).searchParams.get("type")==='+json.dumps(entity)+').click();')
- else:
- # Old bookmarked entity URLs remain readable, but have no standalone navigation.
- call('Page.navigate',dict(url=args.url+'?type='+entity))
- wait(V+'._type==='+json.dumps(entity)+' && !'+V+'._loading && ('+V+'._data || '+V+'._type==="reports")')
- # A product with many reviews validates isolated search, complete text, pagination and deep URLs.
- product_id='taobao:1040511334739'
- call('Page.navigate',dict(url=args.url+'/products/'+product_id+'?type=products&brand=ksf&panel=reviews&relatedQuality=audit'))
- ready();product_ready()
- assert product_action('return p._data.totalMatched;')==1000
- assert product_action('return p._data.records.every(row=>row.product_id===p.product.id);')
- assert product_action('return p.renderRoot.querySelectorAll(".review").length;')==20
- assert product_action('return !/SHA256|分类依据|rule_title_|source_attribute/.test(p.renderRoot.textContent);')
- shot('library-product-reviews')
- review_first=product_action('return p._data.records[0].record_id;')
- product_action('Array.from(p.renderRoot.querySelectorAll(".pagination button")).find(b=>b.textContent.includes("下一页")).click();')
- wait(P+'.query.relatedCursor && !'+P+'._loading && '+P+'._data')
- assert product_action('return p._data.records[0].record_id;')!=review_first
- assert product_action('return p._data.records.every(row=>row.product_id===p.product.id);')
- review_page_url=evaluate('location.href')
- call('Page.reload');ready();product_ready()
- assert evaluate('location.href')==review_page_url
- assert product_action('return p.query.relatedQuality;')=='audit'
- product_action('Array.from(p.renderRoot.querySelectorAll(".pagination button")).find(b=>b.textContent.includes("第一页")).click();')
- wait('!'+P+'.query.relatedCursor && !'+P+'._loading && '+P+'._data')
- assert product_action('return p._data.records[0].record_id;')==review_first
- assert product_action('return !p.renderRoot.querySelector("input:not([type=date]),form");')
- product_action('const i=p.renderRoot.querySelector("input[aria-label=商品资料日期起]");i.value="2099-01-01";i.dispatchEvent(new Event("change",{bubbles:true}));')
- wait(P+'.query.relatedFrom==="2099-01-01" && !'+P+'._loading && '+P+'._data?.totalMatched===0')
- assert product_action('return p.product.id;')==product_id
- product_action('Array.from(p.renderRoot.querySelectorAll("button")).find(b=>b.textContent.includes("清空当前筛选")).click();')
- wait('!'+P+'.query.relatedFrom && !'+P+'._loading && '+P+'._data')
- product_tab('prices')
- assert product_action('return p._data.records.every(row=>row.product_id===p.product.id);')
- assert product_action('return !!p.renderRoot.querySelector(".price-heading strong");')
- shot('library-product-prices')
- product_tab('specs')
- assert product_action('return p._data.records.every(row=>row.product_id===p.product.id);')
- product_tab('rankings')
- assert product_action('return p._data.records.every(row=>row.product_id===p.product.id);')
- # Failed tab reads stay in the product and must never be called empty data.
- call('Network.setBlockedURLs',dict(urls=['*/api/library']))
- product_action('p.renderRoot.querySelector("#tab-reviews").click();')
- wait(P+'._error && !'+P+'._loading')
- assert product_action('return !!p.renderRoot.querySelector("[role=alert]");')
- call('Network.setBlockedURLs',dict(urls=[]))
- product_action('p.renderRoot.querySelector(".error button").click();');product_ready()
- assert product_action('return p._data.records.every(row=>row.product_id===p.product.id);')
- action('v.renderRoot.querySelector(".breadcrumb a").click();')
- wait(V+'._data && !'+V+'._loading && !'+V+'.recordId')
- assert action('return v.routeQuery.brand;')=='ksf'
- assert not evaluate('new URL(location.href).searchParams.has("relatedQ") || new URL(location.href).searchParams.has("panel")')
- # Legacy per-product links now land inside the same product instead of a detached list.
- call('Page.navigate',dict(url=args.url+'?type=prices&productId='+product_id))
- wait(V+'.recordEntity==="products" && '+V+'._detail && !'+V+'._loading');product_ready()
- assert product_action('return p.panel;')=='prices'
- assert product_action('return p.product.id;')==product_id
- # Old search bookmarks shed keyword-bound cursors and preserve explicit filters.
- call('Page.navigate',dict(url=args.url+'?type=products&brand=ksf&q=旧搜索&cursor=old-search-cursor'))
- ready()
- assert action('return v.routeQuery.brand==="ksf" && !v.routeQuery.q && !v.routeQuery.cursor && !v._error;')
- call('Page.navigate',dict(url=args.url+'/products/'+product_id+'?type=products&panel=reviews&relatedQuality=audit&relatedQ=旧搜索&relatedCursor=old-review-cursor'))
- ready();product_ready()
- assert product_action('return p.product.id===p._data.records[0].product_id && !p.query.relatedQ && !p.query.relatedCursor;')
- call('Page.navigate',dict(url=args.url));ready()
- go_type('reviews')
- assert action('return v._data.totalMatched;')==771
- action('const s=v.renderRoot.querySelector("select[aria-label=查看口径]");s.value="audit";s.dispatchEvent(new Event("change",{bubbles:true}));')
- wait(V+'._data?.totalMatched===2360 && !'+V+'._loading')
- go_type('documents')
- assert action('return v._data.totalMatched;')==25
- shot('library-policy-browse')
- action('const s=v.renderRoot.querySelector("select[aria-label=政策状态]");s.value=s.options[1].value;s.dispatchEvent(new Event("change",{bubbles:true}));')
- wait(V+'.routeQuery.policyStatus && !'+V+'._loading')
- assert action('return v._data.totalMatched;')>0
- action('v.renderRoot.querySelector("a[data-record-id]").click();')
- wait(V+'._detail && !'+V+'._loading')
- assert action('return v._detail.record.full_text.length;')>100
- assert action('return v.renderRoot.querySelector(".detail-main").firstElementChild.id;')=='full_text'
- reader_surface()
- assert action('return !!v.renderRoot.querySelector(".source a[target=_blank]");')
- shot('library-policy-detail')
- call('Page.navigate',dict(url=args.url+'?type=clauses'));ready()
- assert action('return v._data.totalMatched;')==31
- action('v.renderRoot.querySelector("a[data-record-id]").click();')
- wait(V+'._detail && !'+V+'._loading')
- assert action('return v.renderRoot.textContent.includes("应用提示(非官方原话)");')
- assert action('return v._detail.record.parentDocument.title;')
- assert action('return v.renderRoot.querySelector(".source a[target=_blank]").href;').startswith('http')
- reader_surface()
- call('Page.navigate',dict(url=args.url+'?type=products'));ready()
- first=action('return v._data.records[0].record_id;')
- action('Array.from(v.renderRoot.querySelectorAll("button")).find(b=>b.textContent.includes("下一页")).click();')
- wait(V+'.routeQuery.cursor && !'+V+'._loading')
- assert action('return v._data.records[0].record_id;')!=first
- second=action('return v._data.records[0].record_id;')
- action('Array.from(v.renderRoot.querySelectorAll("button")).find(b=>b.textContent.includes("上一页")).click();')
- wait('!'+V+'.routeQuery.cursor && !'+V+'._loading')
- assert action('return v._data.records[0].record_id;')==first
- action('Array.from(v.renderRoot.querySelectorAll("button")).find(b=>b.textContent.includes("下一页")).click();')
- wait(V+'.routeQuery.cursor && !'+V+'._loading')
- assert action('return v._data.records[0].record_id;')==second
- call('Page.reload');ready();assert action('return !!v.routeQuery.cursor;')
- # Read all remaining categories, retaining a real zero-result state for absent evidence.
- for entity in ['social','prices','specs','rankings','coverage','reports']:
- go_type(entity)
- if entity!='reports':assert action('return v._data.totalMatched;')>0
- if entity=='prices':
- price=action('return {raw:v._data.records[0].price_minor,shown:v.renderRoot.querySelector(".price-fact strong").textContent};')
- assert price['shown']=='¥'+format(price['raw']/100,'.2f'),price
- assert action('return !v.renderRoot.querySelector(".record").textContent.includes("search_listing_snapshot");')
- shot('library-price-browse')
- if entity=='specs':
- assert action('return v._data.records[0].net_weight_g===null && !v.renderRoot.querySelector(".spec-facts").textContent.includes("净含量");')
- if entity=='coverage':
- assert action('return !v.renderRoot.querySelector(".record").textContent.includes("not_attempted_service_blocked");')
- reader_surface()
- if entity=='reports':
- action('const s=v.renderRoot.querySelector("select[aria-label=成果阶段]");s.value=s.options[1].value;s.dispatchEvent(new Event("change",{bubbles:true}));')
- wait(V+'.routeQuery.reportStage && !'+V+'._loading')
- assert action('return v.renderRoot.querySelectorAll(".report-record").length===v.db.artifacts.filter(r=>r.stageObj?.code===v.routeQuery.reportStage).length;')
- call('Page.reload');ready()
- assert action('return !!v.routeQuery.reportStage && !v.renderRoot.querySelector("input");')
- if entity!='reports':
- action('v.renderRoot.querySelector("a[data-record-id]").click();')
- wait(V+'._detail && !'+V+'._loading')
- reader_surface()
- action('v.renderRoot.querySelector(".breadcrumb a").click();')
- wait(V+'._data && !'+V+'._loading && !'+V+'.recordId')
- go_type('products')
- action('v.renderRoot.querySelector(".advanced-toggle").click();')
- wait('!'+V+'.renderRoot.getElementById("advanced-filters").hidden')
- action('const i=v.renderRoot.querySelector("input[aria-label=日期起]");i.value="2099-01-01";i.dispatchEvent(new Event("change",{bubbles:true}));')
- wait(V+'.routeQuery.from==="2099-01-01" && !'+V+'._loading')
- assert action('return v._data.totalMatched;')==0
- action('v.renderRoot.querySelector(".empty button").click();')
- wait('!'+V+'.routeQuery.from && !'+V+'._loading && '+V+'._data?.totalMatched===4639')
- # A failed connection must stay distinct from a genuine zero-result query.
- call('Network.setBlockedURLs',dict(urls=['*/api/library']))
- action('v.renderRoot.querySelector(".refresh").click();')
- wait(V+'._error && !'+V+'._loading')
- assert action('return !!v.renderRoot.querySelector("[role=alert]") && !v.renderRoot.querySelector(".empty");')
- call('Network.setBlockedURLs',dict(urls=[]))
- action('v.renderRoot.querySelector(".error button").click();')
- ready()
- call('Page.navigate',dict(url=args.url));ready()
- layouts=[]
- for width,height in [(1440,1100),(1024,1000),(390,844),(320,740)]:
- call('Emulation.setDeviceMetricsOverride',dict(width=width,height=height,deviceScaleFactor=1,mobile=width<700))
- time.sleep(.15)
- metrics=action('return {width:innerWidth,documentWidth:document.documentElement.scrollWidth,viewWidth:v.clientWidth,cards:v.renderRoot.querySelectorAll(".stat").length,records:v.renderRoot.querySelectorAll("a[data-record-id]").length,columns:getComputedStyle(v.renderRoot.querySelector(".record-list")).gridTemplateColumns.split(" ").length,resultTop:Math.round(v.renderRoot.querySelector("a[data-record-id]").getBoundingClientRect().top),visibleRecords:Array.from(v.renderRoot.querySelectorAll("a[data-record-id]")).filter(r=>r.getBoundingClientRect().bottom<innerHeight-70).length};')
- assert metrics['documentWidth']<=width,metrics
- assert metrics['cards']==4 and metrics['records']==20,metrics
- assert metrics['resultTop']<(350 if width>=1000 else 450),metrics
- if width==1440:assert metrics['columns']>=3 and metrics['visibleRecords']>=9,metrics
- if width==1024:assert metrics['columns']>=2,metrics
- shot('library-'+str(width));layouts.append(metrics)
- assert action('return getComputedStyle(v.renderRoot.querySelector(".stats")).display;')=='none'
- action('v.renderRoot.querySelector(".overview-toggle").click();')
- wait(V+'.renderRoot.querySelector(".overview-toggle").getAttribute("aria-expanded")==="true"')
- assert action('return getComputedStyle(v.renderRoot.querySelector(".stats")).display;')=='grid'
- action('v.renderRoot.querySelector(".overview-toggle").click();')
- # Product tabs scroll within their own row; long reviews fit the mobile viewport.
- call('Page.navigate',dict(url=args.url+'/products/'+product_id+'?type=products&panel=reviews&relatedQuality=audit'))
- ready();product_ready()
- assert evaluate('document.documentElement.scrollWidth<=innerWidth')
- assert product_action('return p.renderRoot.querySelector(".product-tabs").scrollWidth>p.renderRoot.querySelector(".product-tabs").clientWidth;')
- product_action('p.renderRoot.querySelector(".section-tools").scrollIntoView({block:"start"});')
- shot('library-product-reviews-mobile')
- product_tab('specs')
- assert evaluate('document.documentElement.scrollWidth<=innerWidth')
- call('Page.navigate',dict(url=args.url));ready()
- # New tab remains reachable on mobile, and active state covers detail routes.
- assert evaluate('document.querySelector("kf-app").shadowRoot.querySelector("kf-nav").shadowRoot.querySelector(".tabbar a[data-path=\\"/library\\"]").getAttribute("aria-current")')=='page'
- action('const s=v.renderRoot.querySelector("select[aria-label=资料分类]");s.value="documents";s.dispatchEvent(new Event("change",{bubbles:true}));')
- wait(V+'._type==="documents" && !'+V+'._loading && '+V+'._data?.totalMatched===25')
- action('v.renderRoot.querySelector(".filter").scrollIntoView({block:"start"});')
- shot('library-mobile-browse')
- action('v.renderRoot.querySelector("a[data-record-id]").click();')
- wait(V+'._detail && !'+V+'._loading')
- assert evaluate('document.documentElement.scrollWidth<=innerWidth')
- assert action('return getComputedStyle(v.renderRoot.getElementById("record-facts")).display;')=='none'
- action('v.renderRoot.querySelector(".facts-toggle").click();')
- wait(V+'.renderRoot.querySelector(".facts-toggle").getAttribute("aria-expanded")==="true"')
- assert action('return getComputedStyle(v.renderRoot.getElementById("record-facts")).display;')=='block'
- action('v.renderRoot.querySelector(".facts-toggle").click();')
- shot('library-mobile-detail')
- failures=[x for x in events if x.get('method')=='Runtime.exceptionThrown']
- assert not failures,failures
- result=dict(ok=True,scope=scope,layouts=layouts,tests=['product-catalog-navigation','real-data-counts','exact-filters','active-filter-removal','advanced-filters','display-mode-no-refetch','coverage-toggle','product-inline-panels','product-review-date-filter','no-search-controls','legacy-search-link-normalization','report-stage-filter','compact-layout','product-review-pagination-refresh','product-isolation','product-panel-error-recovery','legacy-product-link','deep-refresh','back-preserves-filters','review-quality','policy-prose-first','reader-source-without-internal-fields','reader-citation','clause-original-source','previous-next-pagination','zero-result-reset','connection-error-recovery','typed-price-spec-coverage-cards','legacy-category-details','mobile-overview','mobile-product-tabs','mobile-navigation','mobile-facts'],productionWrites=0,consoleExceptions=0)
- (WORK/'browser-validation.json').write_text(json.dumps(result,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
- print(json.dumps(result,ensure_ascii=False,indent=2))
- finally:
- if ws:ws.close()
- process.terminate()
- try:process.wait(timeout=8)
- except subprocess.TimeoutExpired:process.kill()
|