scripts/test-browser-language.py 1#!/usr/bin/env python32"""Verify actual browser language UI, WASM protocol, and output-mutation rejection."""3import argparse, base64, hashlib, importlib.util, json, os, shutil, signal, subprocess, tempfile, time, urllib.request4from pathlib import Path5ROOT=Path(__file__).resolve().parents[1]6spec=importlib.util.spec_from_file_location('browser_smoke',ROOT/'scripts/smoke-browser.py');browser=importlib.util.module_from_spec(spec);spec.loader.exec_module(browser)7 8def main():9 p=argparse.ArgumentParser(description=__doc__);p.add_argument('--url',required=True);p.add_argument('--chrome',default='/usr/bin/google-chrome');p.add_argument('--output',type=Path,default=ROOT/'release/comparison/revision-language-browser');a=p.parse_args();a.output.mkdir(parents=True,exist_ok=True)10 with tempfile.TemporaryDirectory(prefix='laymesh-language-browser-',ignore_cleanup_errors=True) as profile:11 chrome=subprocess.Popen([a.chrome,'--headless=new','--no-sandbox','--disable-gpu','--no-proxy-server','--remote-debugging-port=0','--remote-allow-origins=*','--user-data-dir='+profile,'about:blank'],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,start_new_session=True)12 try:13 active=Path(profile)/'DevToolsActivePort';deadline=time.monotonic()+2514 while not active.exists():15 if time.monotonic()>deadline:raise RuntimeError('Chromium startup timeout')16 time.sleep(.1)17 port=int(active.read_text().splitlines()[0]);opener=urllib.request.build_opener(urllib.request.ProxyHandler({}));pages=json.load(opener.open(f'http://127.0.0.1:{port}/json'));cdp=browser.CDP(next(p['webSocketDebuggerUrl'] for p in pages if p['type']=='page'));cdp.call('Page.enable');cdp.call('Runtime.enable');cdp.call('Log.enable');cdp.call('Emulation.setDeviceMetricsOverride',{'width':1440,'height':1000,'deviceScaleFactor':1,'mobile':False});cdp.call('Page.navigate',{'url':a.url})18 def wait(expression):19 return cdp.evaluate('new Promise((resolve,reject)=>{const end=Date.now()+12000;function poll(){const result=('+expression+');if(result)return resolve(result);if(Date.now()>end)return reject(Error("Timed out: "+'+json.dumps(expression)+'));setTimeout(poll,40)}poll()})')20 wait("document.querySelector('.example-module')?.dataset.liveState==='success'");cdp.evaluate("document.querySelector('[data-source-mode=source]').click()");wait("!!document.querySelector('.cm-content')")21 def key(name,code,modifiers=2):22 for kind in ['keyDown','keyUp']:cdp.call('Input.dispatchKeyEvent',{'type':kind,'key':name,'code':{32:'Space',9:'Tab',27:'Escape'}.get(code,'Key'+name.upper()),'modifiers':modifiers,'windowsVirtualKeyCode':code})23 def replace(source):24 cdp.evaluate("document.querySelector('.cm-content').focus()");key('a',65);cdp.call('Input.insertText',{'text':source})25 def tooltip(selector,needle):return wait('(()=>{const t=document.querySelector('+json.dumps(selector)+')?.textContent;return t?.includes('+json.dumps(needle)+')?t:null})()')26 ui=[]27 for label,source in [('many candidates','page=canvas(size=(30,20))\nr=rect('),('one candidate','page=canvas(size=(30,20))\nr=rect(si')]:28 replace(source)29 candidates=tooltip('.cm-tooltip-autocomplete','size')30 details=wait("document.querySelector('.cm-completionInfo')?.textContent")31 cdp.evaluate('new Promise(resolve=>setTimeout(resolve,450))')32 assert not cdp.evaluate("!!document.querySelector('.signature-help')"),label+' opened full call help while typing'33 ui.append({'action':label+' while typing','candidates':candidates,'details':details,'full_signature':False})34 if label=='one candidate':35 shot=cdp.call('Page.captureScreenshot',{'format':'png'});(a.output/'completion-one-candidate.png').write_bytes(base64.b64decode(shot['data']))36 replace('## @')37 doc_tags=tooltip('.cm-tooltip-autocomplete','@param')38 cdp.evaluate('new Promise(resolve=>setTimeout(resolve,450))')39 assert not cdp.evaluate("!!document.querySelector('.signature-help')"),'documentation tags opened full call help'40 ui.append({'action':'documentation tags while typing','candidates':doc_tags,'full_signature':False})41 for label,source,expected in [42 ('physical units','page=canvas(size=(30,20))\ntext(font_size=','pt'),43 ('style classes','style { .title {color:red;} }\ntext(class="ti','title'),44 ('factory return types','## @returns {plot} A plot.\nfunction makeplot() {return plot(size=(20,20))}\np=makeplot()\np.','line')]:45 replace(source);key(' ',32);text=tooltip('.cm-tooltip-autocomplete',expected);ui.append({'action':label,'tooltip':text})46 replace('page=canvas(size=(30,20))\ntext(content="hello",font_size=');key(' ',32,10);ui.append({'action':'signature help','tooltip':tooltip('.signature-help','font_size')})47 def hover(offset):48 point=cdp.evaluate('(async()=>{const base=new URL("live/",document.querySelector("script[src$=\\\"app.js\\\"]").src),{EditorView}=await import(new URL("vendor/@codemirror/view/index.js",base));const view=EditorView.findFromDOM(document.querySelector(".cm-editor"));view.contentDOM.scrollIntoView({block:"center"});const p=view.coordsAtPos('+str(offset)+');return {x:p.left+2,y:(p.top+p.bottom)/2}})()');cdp.call('Input.dispatchMouseEvent',{'type':'mouseMoved',**point})49 def editor():50 return cdp.evaluate(r'''(async()=>{const base=new URL('live/',document.querySelector('script[src$="app.js"]').src),{EditorView}=await import(new URL('vendor/@codemirror/view/index.js',base));return EditorView.findFromDOM(document.querySelector('.cm-editor')).state.doc.toString()})()''')51 bare='page=canvas(size=(80mm,60mm))\npage.add(line(length=40mm,start_cap=ro'52 replace(bare);key(' ',32);tooltip('.cm-tooltip-autocomplete','round');key('Tab',9,0)53 wait("document.querySelector('.cm-content').textContent.includes('start_cap=round')")54 assert editor()==bare[:-2]+'round'55 ui.append({'action':'Named option completion inserts the ordinary predefined variable without quotes','passed':True})56 quoted='# 中文 😀\npage=canvas(size=(80mm,60mm))\npage.add(line(length=40mm,start_cap="roxx"),anchor=top_left)'57 replace(quoted);key('Escape',27,0)58 cdp.evaluate(r'''(async()=>{const base=new URL('live/',document.querySelector('script[src$="app.js"]').src),{EditorView}=await import(new URL('vendor/@codemirror/view/index.js',base));const view=EditorView.findFromDOM(document.querySelector('.cm-editor'));view.dispatch({selection:{anchor:view.state.doc.toString().indexOf('roxx')+2}});view.focus()})()''')59 key(' ',32);tooltip('.cm-tooltip-autocomplete','round');key('Tab',9,0)60 wait("document.querySelector('.cm-content').textContent.includes('start_cap=\"round\"')")61 assert editor()==quoted.replace('roxx','round')62 ui.append({'action':'Quoted completion replaces the full suffix and preserves quotes after Chinese and emoji text','passed':True})63 variable_source='page=canvas(size=(80mm,60mm))\npage.add(line(length=40mm,start_cap=round),anchor=top_left)'64 replace(variable_source);hover(variable_source.index('round')+2)65 variable_hover=tooltip('.cm-tooltip-hover','round: string = "round"')66 assert 'line_cap' not in variable_hover67 ui.append({'action':'Predefined variable hover shows only its ordinary string type and value','tooltip':variable_hover})68 hover(variable_source.index('start_cap')+3);parameter_hover=tooltip('.cm-tooltip-hover','line_cap')69 assert all(value in parameter_hover for value in ['butt','round','square'])70 ui.append({'action':'Parameter hover explains all cap choices and the inherited default','tooltip':parameter_hover})71 replace('page=canvas(size=(80mm,60mm))\npage.add(line(length=40mm),anchor=');key(' ',32)72 anchor_choices=tooltip('.cm-tooltip-autocomplete','top_left');assert 'self' in anchor_choices73 ui.append({'action':'Anchor value completion offers the nine-point variables and self selectors','tooltip':anchor_choices})74 replace('page=canvas(size=(30,20))');hover(8);ui.append({'action':'Chinese hover documentation','tooltip':tooltip('.cm-tooltip-hover','创建页面')})75 replace('# 😀\nstyle {text {font-size:12;}}');wait("!!document.querySelector('.cm-lintRange-error')");hover(18);ui.append({'action':'LCSS diagnostic underline','underline':True,'tooltip':tooltip('.cm-tooltip-lint','font_size')})76 # Web popover: closing commits one guarded editor edit.77 color_source='page=canvas(size=(80,60))\npage.add(rect(size=(20,10),fill="#0072b290"))'78 replace(color_source);wait("!!document.querySelector('.laymesh-color-swatch')")79 cdp.evaluate("document.querySelector('.laymesh-color-swatch').click()")80 wait("document.querySelector('.laymesh-color-panel input[aria-label=HEX]')?.value==='#0072b290'")81 wait("document.querySelector('.laymesh-color-panel')?.dataset.valid==='true'")82 cdp.evaluate(r"""(async()=>{const base=new URL('live/',document.querySelector('script[src$="app.js"]').src),{EditorView}=await import(new URL('vendor/@codemirror/view/index.js',base));globalThis.pickerInitialDoc=EditorView.findFromDOM(document.querySelector('.cm-editor')).state.doc;})()""")83 cdp.evaluate("document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}))")84 wait("!document.querySelector('.laymesh-color-panel')")85 assert cdp.evaluate(r"""(async()=>{const base=new URL('live/',document.querySelector('script[src$="app.js"]').src),{EditorView}=await import(new URL('vendor/@codemirror/view/index.js',base));return EditorView.findFromDOM(document.querySelector('.cm-editor')).state.doc===globalThis.pickerInitialDoc;})()""")86 ui.append({'action':'Unchanged close leaves the editor document and undo history untouched','passed':True})87 cdp.evaluate("document.querySelector('.laymesh-color-swatch').click()")88 wait("document.querySelector('.laymesh-color-panel')?.dataset.valid==='true'")89 # Drafts stay in the same focused input; raw precision is independent of90 # the rounded display after a tab switch. Invalid input cannot be applied.91 def input_text(selector,value):92 cdp.evaluate('document.querySelector('+json.dumps(selector)+').focus()');key('a',65);cdp.call('Input.insertText',{'text':value})93 cdp.evaluate("window.pickerRed=document.querySelector('.laymesh-color-channel input[type=number]')")94 input_text('.laymesh-color-channel input[type=number]','256')95 wait("document.querySelector('.laymesh-color-panel')?.dataset.valid==='false'&&document.querySelector('.laymesh-color-status').textContent.length>0")96 assert cdp.evaluate("pickerRed.isConnected&&document.activeElement===pickerRed&&pickerRed.value==='256'")97 input_text('.laymesh-color-channel input[type=number]','20.123456789')98 wait("document.querySelector('.laymesh-color-panel')?.dataset.valid==='true'")99 assert cdp.evaluate("pickerRed.value==='20.123456789'")100 cdp.evaluate("document.querySelector('[data-space=hsv]').click();document.querySelector('[data-space=rgb]').click()")101 assert cdp.evaluate("Math.abs(Number(document.querySelector('.laymesh-color-channel input[type=number]').dataset.raw)-20.123456789)<1e-10")102 input_text('input[aria-label=HEX]','#12')103 wait("document.querySelector('.laymesh-color-panel')?.dataset.valid==='false'")104 assert cdp.evaluate("document.querySelector('input[aria-label=HEX]').value==='#12'")105 input_text('input[aria-label=HEX]','#0072b290')106 wait("document.querySelector('.laymesh-color-panel')?.dataset.valid==='true'")107 assert editor()==color_source108 ui.append({'action':'Invalid channels and unfinished HEX preserve focused drafts; space switching preserves raw precision','passed':True})109 # Pointer input on the actual two-dimensional picker, with no source edits.110 point=cdp.evaluate("(()=>{const r=document.querySelector('.laymesh-color-sv').getBoundingClientRect();return {x:r.left+r.width*.7,y:r.top+r.height*.3}})()")111 cdp.call('Input.dispatchMouseEvent',{'type':'mouseMoved',**point})112 cdp.call('Input.dispatchMouseEvent',{'type':'mousePressed','button':'left','clickCount':1,**point})113 cdp.call('Input.dispatchMouseEvent',{'type':'mouseReleased','button':'left','clickCount':1,**point})114 wait("document.querySelector('.laymesh-color-panel')?.dataset.valid==='true'&&document.querySelector('input[aria-label=HEX]').value!=='#0072b290'")115 assert cdp.evaluate("document.querySelector('.cm-content').textContent.includes('#0072b290')")116 cdp.evaluate("(()=>{const input=document.querySelector('input[aria-label=HEX]');input.focus();input.value='#0072b290';input.dispatchEvent(new Event('input',{bubbles:true}))})()")117 wait("document.querySelector('.laymesh-color-panel')?.dataset.valid==='true'&&Math.abs(Number(document.querySelector('[data-channel=alpha]').value)-144/255*100)<0.001")118 wait("document.querySelector('.laymesh-color-channel input[type=range]').style.getPropertyValue('--track').includes('linear-gradient')")119 ui.append({'action':'Mosaic SV pointer input, colored tracks, HEX-alpha synchronization and preview-only edits','passed':True})120 cdp.evaluate("document.querySelector('[data-space=hsv]').click();const input=document.querySelector('[data-channel=alpha]');input.value='40';input.dispatchEvent(new Event('input',{bubbles:true}));document.querySelector('.laymesh-color-output select').value='hsv'")121 wait("document.querySelector('.laymesh-color-panel')?.dataset.valid==='true'")122 assert not cdp.evaluate("document.querySelector('.cm-content').textContent.includes('hsv(')")123 wait("[...document.querySelectorAll('.laymesh-color-channel input[type=range]')].every(input=>input.style.getPropertyValue('--track').includes('linear-gradient'))")124 shot=cdp.call('Page.captureScreenshot',{'format':'png'});(a.output/'color-panel-desktop.png').write_bytes(base64.b64decode(shot['data']))125 cdp.evaluate("document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}))")126 wait("!document.querySelector('.laymesh-color-panel')")127 assert cdp.evaluate("document.querySelector('.cm-content').textContent.includes('hsv(')&&document.querySelector('.cm-content').textContent.includes('/ 0.4')")128 key('z',90);wait("document.querySelector('.cm-content').textContent.includes('#0072b290')")129 ui.append({'action':'Color swatch, HSV and alpha, preview, close commit and one-step undo','passed':True})130 cdp.evaluate("document.querySelector('.laymesh-color-swatch').click()")131 wait("document.querySelector('.laymesh-color-panel')?.dataset.valid==='true'")132 cdp.evaluate("document.querySelector('[data-space=oklch]').click()")133 wait("!!document.querySelector('.laymesh-color-channel')")134 wait("[...document.querySelectorAll('.laymesh-color-channel input[type=range]')].every(input=>input.style.getPropertyValue('--track').includes('linear-gradient'))")135 cdp.call('Emulation.setDeviceMetricsOverride',{'width':390,'height':844,'deviceScaleFactor':1,'mobile':True})136 shot=cdp.call('Page.captureScreenshot',{'format':'png'});(a.output/'color-panel-mobile.png').write_bytes(base64.b64decode(shot['data']))137 cdp.evaluate("document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}))")138 wait("!document.querySelector('.laymesh-color-panel')")139 assert cdp.evaluate("document.querySelector('.cm-content').textContent.includes('#0072b290')")140 cdp.call('Emulation.setDeviceMetricsOverride',{'width':1440,'height':1000,'deviceScaleFactor':1,'mobile':False})141 if cdp.evaluate("document.documentElement.dataset.theme!=='dark'"):cdp.evaluate("document.querySelector('.theme-toggle').click()")142 wait("document.documentElement.dataset.theme==='dark'")143 cdp.evaluate("document.querySelector('.laymesh-color-swatch').click()")144 wait("document.querySelector('.laymesh-color-panel')?.dataset.valid==='true'")145 wait("[...document.querySelectorAll('.laymesh-color-channel input[type=range]')].every(input=>input.style.getPropertyValue('--track').includes('linear-gradient'))")146 shot=cdp.call('Page.captureScreenshot',{'format':'png'});(a.output/'color-panel-dark.png').write_bytes(base64.b64decode(shot['data']))147 cdp.evaluate(r'''(async()=>{const base=new URL('live/',document.querySelector('script[src$="app.js"]').src),{EditorView}=await import(new URL('vendor/@codemirror/view/index.js',base));const view=EditorView.findFromDOM(document.querySelector('.cm-editor'));view.dispatch({changes:{from:view.state.doc.length,insert:'\n# concurrent edit'}});})()''')148 cdp.evaluate("document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}))")149 tooltip('.laymesh-color-status','文档已变化')150 assert cdp.evaluate("document.querySelector('.cm-content').textContent.includes('#0072b290')&&document.querySelector('.cm-content').textContent.includes('concurrent edit')")151 cdp.evaluate("document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}))")152 ui.append({'action':'OKLCH tab, mobile layout, unchanged close and stale-document refusal','passed':True})153 # The same Worker envelope used by the editor checks UTF-16, localization,154 # aliases, in-memory dependency replacement, and error recovery.155 protocol=cdp.evaluate(r'''(async()=>{156 const base=new URL('live/',document.querySelector('script[src$="app.js"]').src),w=new Worker(new URL('language-worker.js',base),{type:'module'});let seq=0;const pending=new Map();w.onmessage=({data})=>{pending.get(data.id)?.(data);pending.delete(data.id)};157 const query=(files,file,method,offset=0,locale='en')=>new Promise((resolve,reject)=>{const id=++seq,t=setTimeout(()=>reject(Error('Language worker timeout')),8000);pending.set(id,d=>{clearTimeout(t);d.error?reject(Error(d.error)):resolve(d.result)});w.postMessage({id,files,file,method,offset,locale})});158 const clone=v=>JSON.parse(JSON.stringify(v)),checks=[],faults=[];159 function verify(name,actual,predicate,mutations){if(!predicate(actual))throw Error(name+': '+JSON.stringify(actual));checks.push(name);for(const [label,mutate] of mutations){const broken=clone(actual);mutate(broken);if(predicate(broken))throw Error('Mutation survived '+name+': '+label);faults.push({check:name,mutation:label,rejected:true})}}160 try{161 const file='/main.lay',source='# 😀\nstyle {text {nonsense:1pt;}}\npage=canvas(size=(10deg,20))\nt=text(font_family=["sans",3])\nr=rect(border_width=true)',diagnostics=await query({[file]:source},file,'diagnostics');162 const from=source.indexOf('nonsense');verify('LCSS/types UTF16 diagnostics',diagnostics,d=>Array.isArray(d)&&d.map(v=>v.code).join(',')==='E_LCSS,E_UNIT,E_TYPE,E_TYPE'&&d[0].from===from&&d[0].to>from,[['drop diagnostic',d=>d.shift()],['wrong UTF16 offset',d=>d[0].from++],['missing code',d=>delete d[0].code]]);163 const unit='text(font_size=',units=await query({[file]:unit},file,'completions',unit.length);verify('unit completion envelope',units,d=>Array.isArray(d)&&['pt','mm','cm','auto'].every(label=>d.some(o=>o.label===label&&typeof o.apply==='string'&&o.from===unit.length)),[['remove pt',d=>d.splice(d.findIndex(o=>o.label==='pt'),1)],['missing insertion',d=>delete d.find(o=>o.label==='pt').apply]]);164 const lib='/lib.lay',caller='import {card as tile} from "./lib.lay"\ntile(',library='## @lang zh-CN\n## 中文面板。\n## @param title - 中文标题。\n## @returns {group} 组合。\n## @lang en\n## English panel.\n## @param title - English title.\n## @returns {group} Group.\nexport function card(title, size=(40,25)) {return group()}';165 for(const [locale,expected]of [['zh-CN','中文面板'],['en','English panel']]){const s=await query({[file]:caller,[lib]:library},file,'signature',caller.length,locale);verify('localized signature '+locale,s,d=>d&&d.summary?.includes(expected)&&d.name==='tile'&&d.parameters?.[0]?.name==='title'&&typeof d.parameters[0].description==='string',[['wrong locale text',d=>d.summary='wrong'],['missing parameter list',d=>delete d.parameters]])}166 const hover=await query({[file]:'page=canvas(size=(30,20))'},file,'hover',8,'en');verify('hover source range',hover,d=>d?.contents?.includes('Create a page')&&d.from===5&&d.to===11,[['wrong source range',d=>d.from=0],['missing documentation',d=>delete d.contents]]);167 const edited=await query({[file]:caller,[lib]:'## Changed unsaved dependency.\nexport function card(buffer=2) {return buffer}'},file,'signature',caller.length);if(!edited.label.includes('buffer=2'))throw Error('Unsaved module was not used');const missing=await query({[file]:caller},file,'signature',caller.length);if(missing!==null)throw Error('Deleted module survived');const recovered=await query({[file]:caller,[lib]:library},file,'signature',caller.length);if(!recovered.label.includes('title'))throw Error('Module recovery failed');checks.push('unsaved dependency -> missing -> restored state');168 const invalid='# 😀\n😀',bad=await query({[file]:invalid},file,'diagnostics');if(bad[0].from!==5||bad[0].to!==7)throw Error('Unicode diagnostic conversion failed');const restored=await query({[file]:'page=canvas(size=(30,20))'},file,'diagnostics');if(restored.length)throw Error('Unicode failure recovery failed');checks.push('Unicode syntax error -> valid recovery');169 return {checks,output_mutations:faults,product_code_mutation:false};170 }finally{w.terminate()}171 })()''')172 # Verify document language reaches the real help widget after page navigation.173 english=a.url.replace('/examples/','/en/examples/');cdp.call('Page.navigate',{'url':english});wait("document.documentElement.lang==='en'&&document.querySelector('.example-module')?.dataset.liveState==='success'");cdp.evaluate("document.querySelector('[data-source-mode=source]').click()");wait("!!document.querySelector('.cm-content')");replace('page=canvas(size=(30,20))');hover(8);ui.append({'action':'English hover documentation','tooltip':tooltip('.cm-tooltip-hover','Create a page')})174 shot=cdp.call('Page.captureScreenshot',{'format':'png'});(a.output/'language-ui.png').write_bytes(base64.b64decode(shot['data']))175 errors=[e for e in cdp.events if e.get('method')=='Runtime.exceptionThrown' or e.get('method')=='Log.entryAdded' and e['params']['entry']['level']=='error'];assert not errors,errors176 result={'status':'passed','artifact_sha256':browser.provenance(__file__),'ui':ui,'wasm_protocol':protocol,'console_errors':errors,'node_on_path':bool(shutil.which('node')),'npm_on_path':bool(shutil.which('npm')),'wasm_sha256':hashlib.sha256((ROOT/'site/dist/site/live/wasm/laymesh_wasm_bg.wasm').read_bytes()).hexdigest()};(a.output/'verification.json').write_text(json.dumps(result,ensure_ascii=False,indent=2)+'\n');print(json.dumps(result,ensure_ascii=False))177 except Exception:178 if 'cdp' in locals():179 print(json.dumps(cdp.evaluate("({tooltips:[...document.querySelectorAll('.cm-tooltip')].map(e=>({class:e.className,text:e.textContent})),editor:document.querySelector('.cm-content')?.textContent})"),ensure_ascii=False),flush=True)180 print(json.dumps(cdp.evaluate(r'''(async()=>{const base=new URL('live/',document.querySelector('script[src$="app.js"]').src),{EditorView}=await import(new URL('vendor/@codemirror/view/index.js',base)),cm=await import(new URL('vendor/@codemirror/autocomplete/index.js',base)),core=await import(new URL('wasm/laymesh_wasm.js',base));await core.default();const view=EditorView.findFromDOM(document.querySelector('.cm-editor')),text=view.state.doc.toString(),pos=view.state.selection.main.head;return {source:text,selection:pos,completion_status:cm.completionStatus(view.state),backend:JSON.parse(core.language_query(JSON.stringify({'/main.lay':text}),'/main.lay','completions',pos,'zh-CN'))}})()'''),ensure_ascii=False),flush=True)181 shot=cdp.call('Page.captureScreenshot',{'format':'png'});(a.output/'failed-language-ui.png').write_bytes(base64.b64decode(shot['data']))182 raise183 finally:184 try:os.killpg(chrome.pid,signal.SIGTERM)185 except ProcessLookupError:pass186 try:chrome.wait(timeout=5)187 except subprocess.TimeoutExpired:os.killpg(chrome.pid,signal.SIGKILL);chrome.wait()188if __name__=='__main__':main()