File size: 13,281 Bytes
e5675eb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b3a9cc3
e5675eb
62181a5
e5675eb
 
b3a9cc3
 
 
b084019
b3a9cc3
 
 
 
 
e5675eb
c3828f6
 
fbc4cf6
 
c3828f6
 
 
 
 
 
 
 
 
 
 
 
 
fbc4cf6
 
c3828f6
 
fbc4cf6
 
c3828f6
fbc4cf6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e5675eb
 
 
 
 
fbc4cf6
e5675eb
 
fbc4cf6
 
e5675eb
 
 
 
 
 
 
 
 
 
 
 
 
fbc4cf6
e5675eb
fbc4cf6
e5675eb
 
 
fbc4cf6
e5675eb
 
 
 
 
fbc4cf6
 
 
 
e5675eb
c3828f6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e5675eb
fbc4cf6
e5675eb
 
 
 
b3a9cc3
 
b084019
b3a9cc3
e5675eb
b3a9cc3
fbc4cf6
 
 
 
 
 
e34c5a8
 
 
e5675eb
 
c3828f6
e5675eb
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
"""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))