"""Browser verification for the presentation layout and counter-free sweep.""" from pathlib import Path import sys,json,math sys.path.insert(0,'/home/node/local/carbon-a-render/python') from playwright.sync_api import sync_playwright R=Path(__file__).parent BROWSER='/opt/pw-browsers/chromium_headless_shell-1234/chrome-headless-shell-linux64/chrome-headless-shell' ARGS=['--no-sandbox','--enable-webgl','--use-gl=angle','--use-angle=swiftshader','--enable-unsafe-swiftshader','--disable-renderer-backgrounding','--disable-background-timer-throttling','--disable-backgrounding-occluded-windows'] def check(url=None,headers=None): with sync_playwright() as p: browser=p.chromium.launch(executable_path=BROWSER,args=ARGS) page=browser.new_page(viewport={'width':1500,'height':1050},extra_http_headers=headers or {}) errors=[];page.on('pageerror',lambda e:errors.append(str(e))) page.goto(url or (R/'index.html').as_uri(),wait_until='domcontentloaded') page.wait_for_function('window.POC && !document.querySelector("#loading")',timeout=60000) assert page.evaluate('POC.assemblies')==33588 assert page.locator('#animate-sweep,.sweep-meter,#seen-species,#seen-assemblies,#seen-loci').count()==0 assert page.locator('.resource-links a').all_text_contents()==['Model↗','Database↗','Explorer↗','Paper↗','Blog↗'] assert page.locator('h1').inner_text()=='Carbon-A: Discovering Genes Across the Tree of Life' assert page.locator('.wordmark em').evaluate('e=>getComputedStyle(e).color')=='rgb(41, 233, 154)' assert page.locator('#view-dialog').is_hidden() resources=page.locator('.resources').bounding_box();tree=page.locator('.stage-wrap').bounding_box() assert resources['x']+resources['width']<=tree['x'] assert tree['height']>880 and abs(tree['y']+tree['height']-1050)<2 assert page.evaluate('Math.abs(stage.getBoundingClientRect().x+screen([0,0])[0]-innerWidth/2)')<1 assert page.locator('#links-title').inner_text()=='Links:' buttons=[a.bounding_box() for a in page.locator('.resource-links a').all()] assert max(b['width'] for b in buttons)-min(b['width'] for b in buttons)<1 assert all(a['y']+a['height']buttons[-1]['y']+buttons[-1]['height'] page.screenshot(path=str(R/'presentation-desktop.png')) # Hover previews a strand and its details; clicking pins that preview. # The next click anywhere clears without selecting a replacement. def point(i,radius=62): return page.evaluate("""([i,radius])=>{const q=screen(polar(radius,leaves[i].angle)),b=stage.getBoundingClientRect();return [b.x+q[0],b.y+q[1]]}""",[i,radius]) page.mouse.move(*point(12345));page.wait_for_function('hoveredAssembly?.i===12345') assert page.evaluate('selected.i===12345 && pinnedAssembly===null') assert page.locator('#selection').is_visible() assert page.locator('#sel-state').text_content()=='Preview · click strand to keep' assert page.locator('#sel-id').inner_text()==page.evaluate('leaves[12345].accession') page.mouse.move(*point(25000));page.wait_for_function('hoveredAssembly?.i===25000') page.mouse.move(0,0);page.wait_for_function('selected===null') assert page.locator('#selection').is_hidden() page.mouse.move(*point(12345,730));page.wait_for_function('hoveredAssembly!==null') assert page.evaluate('selected===hoveredAssembly && pinnedAssembly===null') # Highlights use the sampled WebGL strand, including on hover. assert page.evaluate('''()=>{const moves=[],lines=[],move=fx.moveTo,line=fx.lineTo;fx.moveTo=(...p)=>{moves.push(p);move.apply(fx,p)};fx.lineTo=(...p)=>{lines.push(p);line.apply(fx,p)};try{drawInteraction()}finally{fx.moveTo=move;fx.lineTo=line}const a=selected,expected=[];for(let k=a.geometryStart+4;k{const r=stage.getBoundingClientRect(),ic=iconHits[0],bin=locusBins.find(b=>b.known&&b.value>10000),q=screen(polar(R+BAR_GAP+4,leaves[Math.floor(bin.mid)].angle));return [[r.x+ic.x,r.y+ic.y],[r.x+q[0],r.y+q[1]]]}""") for xy in targets: page.mouse.click(*xy);assert page.evaluate('selected!==null') assert page.evaluate('zoom')==1 and page.evaluate('locusMode')=='5' page.locator('#sel-name').click();assert page.evaluate('selected===null') page.mouse.dblclick(*point(12345,730));assert page.evaluate('zoom')==1 assert page.evaluate('selected===null') # Wheel zoom and pan have an always-accessible reset on the graphic. page.mouse.move(*point(10000));page.mouse.wheel(0,-450) page.wait_for_function('zoom>1 && !document.querySelector("#home").hidden') page.locator('#home').click();page.wait_for_function('zoom===1 && pan.x===0 && pan.y===0') page.locator('#home').wait_for(state='hidden') box=page.locator('#stage').bounding_box() page.mouse.move(box['x']+20,box['y']+30);page.mouse.down() page.mouse.move(box['x']+75,box['y']+60,steps=4);page.mouse.up() page.wait_for_function('!document.querySelector("#home").hidden') assert page.evaluate('selected===null') page.locator('#home').click();assert page.evaluate('pan.x===0 && pan.y===0') # Search is accessible through the dialog; selecting a result closes it. page.locator('#view-options').click();page.locator('#search').fill('GCA_000001405.29') page.locator('#results button').first.click() assert page.locator('#view-dialog').is_hidden() assert page.locator('#sel-id').inner_text()=='GCA_000001405.29' assert page.evaluate('zoom')==1 assert page.locator('#sel-files').get_attribute('href').endswith('/GCA_000001405.29') page.locator('#sel-download').click();compile(page.locator('#data-code').inner_text(),'download_example','exec') assert page.evaluate('selected===null') # the info action runs before clearing page.locator('#data-close').click() page.locator('#view-options').click();page.locator('#scope').select_option('1');page.locator('#locus-mode').select_option('20') assert page.evaluate('mode')==1 and page.evaluate('locusMode')=='20' page.locator('#scope').select_option('0');page.locator('#locus-mode').select_option('5');page.locator('#reset').click();page.locator('#view-close').click() # Space inside a search input must never start animation. page.locator('#view-options').click();page.locator('#search').focus();page.keyboard.press('Space') assert not page.evaluate('sweep.running') page.locator('#view-close').click();page.locator('#theme-toggle').click();assert page.evaluate('theme')=='light' page.locator('#theme-toggle').click() # Test capture from a fresh load, matching the recording entry point. # Headless Chromium may stop compositor frames after repeated modal closures. page.reload(wait_until='domcontentloaded') page.wait_for_function('window.POC && !document.querySelector("#loading")',timeout=60000) # Keyboard playback, smooth full turn, exact highlighted geometry and no UI counters. page.evaluate('''()=>{document.activeElement.blur();window.framesSeen=[];window.treeRedraws=0;const oldDraw=gl.drawArrays.bind(gl);gl.drawArrays=(...a)=>{treeRedraws++;oldDraw(...a)};const oldUpdate=updateSweep;updateSweep=t=>{oldUpdate(t);framesSeen.push({t:performance.now(),p:sweep.progress,angle:sweep.angle,selected:selected?.i??null,info:document.querySelector('#sel-id').textContent,expected:selected?.accession})}}''') page.keyboard.press('Space');page.wait_for_function('sweep.running && sweep.progress>.1') assert page.locator('#selection').is_visible() try: page.wait_for_function('sweep.status==="complete"',timeout=12000) except Exception: print('sweep failure',page.evaluate('({sweep,hidden:document.hidden,frames:window.framesSeen?.length,focus:document.activeElement.tagName,now:performance.now(),redraws:treeRedraws,pending,zoom})'),errors,flush=True);page.screenshot(path=str(R/'sweep-failure.png'));print('after screenshot',page.evaluate('sweep'),flush=True);raise result=page.evaluate('({frames:framesSeen,redraws:treeRedraws,angle:sweep.angle,selected:selected?.i,pinned:pinnedAssembly?.i,zoom})') frames=result['frames'];duration=frames[-1]['t']-frames[1]['t'] assert 3900<=duration<4700,duration assert len(frames)>35,len(frames) assert result['redraws']<6,result['redraws'] assert abs(result['angle']-1.5*math.pi)<1e-9 assert result['selected']==33587 and result['pinned']==33587 and result['zoom']==1 assert all(f['info']==f['expected'] for f in frames), 'Info must match the highlighted strand every frame' assert page.locator('#selection').is_visible() page.screenshot(path=str(R/'sweep-info.png')) assert all(a['angle']<=b['angle'] for a,b in zip(frames,frames[1:])) # Space toggles playback off and freezes the current strand and info. # Repeats from holding the key do not toggle, scroll or activate a button. page.keyboard.down('Space');page.wait_for_function('sweep.running && sweep.progress>.1') page.keyboard.down('Space');assert page.evaluate('sweep.running') page.keyboard.up('Space');page.keyboard.press('Space') stopped=page.evaluate('({running:sweep.running,status:sweep.status,progress:sweep.progress,angle:sweep.angle,id:selected?.i,pin:pinnedAssembly?.i})') assert not stopped['running'] and stopped['status']=='stopped' assert .1.1') page.locator('#sel-download').focus();page.keyboard.press('Space') assert not page.evaluate('sweep.running') and page.evaluate('pinnedAssembly===selected && selected!==null') assert page.locator('#data-dialog').is_hidden() assert page.locator('#selection').is_visible() page.evaluate('document.activeElement.blur()') page.keyboard.press('Space');page.wait_for_function('sweep.progress>.1 && sweep.running');page.keyboard.press('Escape') assert not page.evaluate('sweep.running') and page.evaluate('selected===null') # A phone layout has no horizontal overflow and retains all resource buttons. page.evaluate('reset()');page.mouse.move(0,0);page.set_viewport_size({'width':390,'height':844}) page.wait_for_timeout(150) assert page.evaluate('document.documentElement.scrollWidth')==390 resources=page.locator('.resources').bounding_box();tree=page.locator('.stage-wrap').bounding_box() assert tree['width']==390 and tree['height']>=660 assert page.evaluate('Math.abs(stage.getBoundingClientRect().x+screen([0,0])[0]-innerWidth/2)')<1 assert resources['y']>=tree['y']+tree['height'] assert all(x.is_visible() for x in page.locator('.resource-links a').all()) page.screenshot(path=str(R/'presentation-mobile.png'),full_page=True) page.mouse.click(*point(11000));assert page.locator('#selection').is_visible() page.locator('h1').click();assert page.evaluate('selected===null') # Capture mode must retain the info box too. page.evaluate("document.documentElement.dataset.recording='true';POC.startSweep()") page.wait_for_function('sweep.progress>.1 && sweep.running') assert page.locator('#selection').is_visible() page.locator('h1').click() assert page.evaluate('selected===null && !sweep.running') assert page.locator('#selection').is_hidden() assert not errors,errors browser.close() return {'sweep_duration_ms':round(duration),'sweep_frames':len(frames),'background_redraws':result['redraws'],'layout':'desktop and mobile passed','hover_preview_and_exact_geometry':'passed','click_selection_and_anywhere_deselection':'passed','space_toggle_and_focused_button_stop':'passed','clicks_do_not_zoom':'passed','visible_home_reset':'passed','sweep_info_matches_strand':'passed','search_and_downloads':'passed','resource_links':5,'counter_and_button':'removed','errors':errors} if __name__=='__main__': result=check();(R/'presentation-verification.json').write_text(json.dumps(result,indent=2));print(json.dumps(result))