carbon-a-release / check_presentation.py
lvwerra's picture
lvwerra HF Staff
Centre the tree on the full page while retaining the left links column
b084019 verified
Raw History Blame Contribute Delete
13.3 kB
"""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']<b['y'] for a,b in zip(buttons,buttons[1:]))
assert page.locator('.strand-legend').bounding_box()['y']>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<a.geometryEnd;k+=8)expected.push(screen([hitData[k],hitData[k+1]]));return JSON.stringify(lines.slice(1))===JSON.stringify(expected)&&JSON.stringify(moves[1])===JSON.stringify(screen([hitData[a.geometryStart],hitData[a.geometryStart+1]]))}''')
page.mouse.move(*point(12345));page.wait_for_function('hoveredAssembly?.i===12345')
page.mouse.click(*point(12345));assert page.evaluate('pinnedAssembly.i')==12345
assert page.locator('#selection').is_visible() and page.evaluate('zoom')==1
page.mouse.move(*point(25000));page.wait_for_timeout(80)
assert page.evaluate('pinnedAssembly.i===12345 && selected===pinnedAssembly')
page.mouse.click(*point(25000));assert page.evaluate('selected===null')
assert page.locator('#selection').is_hidden()
page.wait_for_timeout(100);assert page.evaluate('selected===null')
page.mouse.click(*point(25000));assert page.evaluate('pinnedAssembly.i')==25000
page.locator('h1').click();assert page.evaluate('selected===null')
# Plain strands, icons and binned bars all use selection, without zoom or
# changing bar mode. Clicking info-box text also deselects.
targets=[point(12345,730)]
targets+=page.evaluate("""()=>{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<stopped['progress']<1 and stopped['id']==stopped['pin']
assert page.locator('#selection').is_visible()
page.wait_for_timeout(250)
assert page.evaluate('({running:sweep.running,status:sweep.status,progress:sweep.progress,angle:sweep.angle,id:selected?.i,pin:pinnedAssembly?.i})')==stopped
# Playback can be stopped while a button holds focus, without triggering it.
page.keyboard.press('Space');page.wait_for_function('sweep.running && sweep.progress>.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))