starmap / scripts /browser_display_check.py
Agent Manager
Expand sky catalogues and consolidate telescope controls
6ba5297
Raw History Blame Contribute Delete
9.82 kB
"""Exercise themes, faint-star selection, 10 Hz painting and the curved Observer view."""
import asyncio,math,os,statistics
from pathlib import Path
from playwright.async_api import async_playwright
from PIL import Image
OUTPUT=Path(os.environ.get('SIDEREAL_TEST_OUTPUT','/tmp/sidereal-display-check'))
OUTPUT.mkdir(parents=True,exist_ok=True)
async def main():
async with async_playwright() as p:
browser=await p.chromium.launch(headless=True,executable_path=os.environ.get('CHROMIUM_EXECUTABLE','/opt/pw-browsers/chromium-1234/chrome-linux64/chrome'),args=['--no-sandbox'])
page=await browser.new_page(viewport={'width':1440,'height':1000})
errors=[];page.on('pageerror',lambda e:errors.append(str(e)))
await page.add_init_script("""
localStorage.setItem('sidereal-site',JSON.stringify({name:'Bern',lat:46.948,lon:7.4474,elevation:540}));
window.drawTimes=[];window.starDots=[];
const clear=CanvasRenderingContext2D.prototype.clearRect,arc=CanvasRenderingContext2D.prototype.arc;
CanvasRenderingContext2D.prototype.clearRect=function(...args){if(!this.canvas.classList.contains('map-overlay')){window.drawTimes.push(performance.now());window.drawTimes=window.drawTimes.slice(-500);window.starDots=[];}return clear.apply(this,args)};
CanvasRenderingContext2D.prototype.arc=function(x,y,r,...rest){if(r>=.85&&r<=1.2&&!this.canvas.classList.contains('map-overlay'))window.starDots.push({x,y,r,color:this.fillStyle});return arc.call(this,x,y,r,...rest)};
""")
base=os.environ.get('SIDEREAL_TEST_URL','http://127.0.0.1:7866')
if 'hf.space' in base:
async def auth(route):await route.continue_(headers={**route.request.headers,'Authorization':'Bearer '+os.environ['HF_TOKEN']})
await page.route('https://lvwerra-sidereal.hf.space/**',auth)
await page.goto(base,wait_until='networkidle')
date=page.get_by_label('Observing date and time in Europe/Zurich')
await date.fill('2026-09-06T22:00')
await page.get_by_label('Search sky objects').fill('Vega')
await page.locator('.search-results button').first.click()
slider=page.get_by_label('Faintest displayed star magnitude')
await slider.fill('14')
await page.get_by_role('button',name='ZOOM FOR FAINT STARS',exact=False).click()
await page.wait_for_function("document.querySelector('.deep-star-status')?.textContent.includes('Dense field') || document.querySelector('.deep-star-status')?.textContent.includes('Gaia field loaded')",timeout=45000)
initial=await page.locator('.sky-canvas').bounding_box()
await page.mouse.move(initial['x']+initial['width']/2,initial['y']+initial['height']/2)
await page.mouse.wheel(0,math.log(.2)/.0015)
await page.wait_for_function("document.querySelector('.deep-star-status')?.textContent.includes('Gaia field loaded')",timeout=45000)
await page.wait_for_timeout(100)
dots=await page.evaluate('window.starDots')
assert len(dots)>100 and min(d['r'] for d in dots)>1.1
assert any(d['color']=='#62665c' for d in dots)
await page.screenshot(path=str(OUTPUT/'faint-light.png'))
await page.get_by_role('button',name='Switch to dark mode',exact=True).click()
assert await page.locator('html').get_attribute('data-theme')=='dark'
assert await page.evaluate('getComputedStyle(document.documentElement).backgroundColor')=='rgb(20, 21, 19)'
assert await page.evaluate("window.starDots.some(p=>p.color==='#c4c8bb')")
await page.wait_for_timeout(200)
await page.screenshot(path=str(OUTPUT/'faint-dark.png'))
host=page.locator('.sky-canvas');box=await host.bounding_box()
await page.mouse.move(box['x']+5,box['y']+60)
assert 'crosshair' in await host.evaluate('(e)=>getComputedStyle(e).cursor')
selected=False
for dot in dots[::max(1,len(dots)//40)]:
if not(50<dot['x']<box['width']-50 and 50<dot['y']<box['height']-50):continue
x,y=box['x']+dot['x'],box['y']+dot['y']
await page.mouse.move(x,y)
tooltip=page.get_by_role('tooltip')
if await tooltip.count() and 'Gaia DR3' in await tooltip.inner_text():
assert 'crosshair' in await host.evaluate('(e)=>getComputedStyle(e).cursor')
name=await tooltip.locator('strong').inner_text()
await page.mouse.down()
assert await host.evaluate('(e)=>getComputedStyle(e).cursor')!='grabbing'
await page.mouse.up()
assert await page.locator('.object-heading h2').inner_text()==name
selected=True;break
assert selected
await page.mouse.move(box['x']+box['width']/2,box['y']+box['height']/2)
await page.mouse.down()
await page.mouse.move(box['x']+box['width']/2+50,box['y']+box['height']/2+20,steps=4)
assert 'crosshair' in await host.evaluate('(e)=>getComputedStyle(e).cursor')
await page.mouse.up()
assert await host.evaluate('(e)=>getComputedStyle(e).cursor')!='grabbing'
await page.get_by_role('button',name='ALL SKY',exact=True).click()
await page.get_by_role('button',name='Play time',exact=True).click()
await page.wait_for_timeout(500)
await page.evaluate('window.drawTimes=[]')
await page.wait_for_timeout(3000)
draws=await page.evaluate('window.drawTimes')
median=statistics.median(b-a for a,b in zip(draws,draws[1:]))
assert 26<=len(draws)<=35,(len(draws),median)
assert 80<median<125,median
await page.get_by_role('button',name='Pause time',exact=True).click()
await page.wait_for_timeout(500)
await page.evaluate('window.drawTimes=[]')
await page.wait_for_timeout(350)
assert not await page.evaluate('window.drawTimes')
await page.reload(wait_until='networkidle')
assert await page.locator('html').get_attribute('data-theme')=='dark'
await slider.fill('6.5')
await date.fill('2026-09-06T14:00')
await page.wait_for_timeout(300)
await page.get_by_label('Search sky objects').fill('Sun')
await page.locator('.search-results button').first.click()
await page.get_by_role('button',name='OBSERVER VIEW',exact=True).click()
await page.wait_for_function("document.querySelector('.terrain-controls span')?.textContent === ''",timeout=30000)
await page.screenshot(path=str(OUTPUT/'personal-dark.png'))
await page.get_by_role('button',name='Switch to light mode',exact=True).click()
await page.wait_for_timeout(200)
await page.screenshot(path=str(OUTPUT/'personal-light.png'))
# The Sun's actual half-degree limb must remain round in a close personal view.
await page.get_by_role('button',name='Switch to dark mode',exact=True).click()
for _ in range(6):await page.get_by_role('button',name='Zoom in',exact=True).click()
await page.get_by_role('button',name='CENTRE IN SKY VIEW',exact=True).click()
await page.wait_for_timeout(100)
await host.screenshot(path=str(OUTPUT/'personal-sun.png'))
im=Image.open(OUTPUT/'personal-sun.png').convert('RGB')
points=[(x,y) for y in range(im.height) for x in range(im.width) if im.getpixel((x,y))==(236,236,218)]
assert len(points)>100,'Sun pixels missing'
width=max(x for x,y in points)-min(x for x,y in points)+1
height=max(y for x,y in points)-min(y for x,y in points)+1
assert abs(width/height-1)<.05,(width,height)
# Moon selection, cursor-anchored zoom and the simulated field use the same projection.
await date.fill('2026-09-06T08:00')
await page.wait_for_timeout(300)
await page.get_by_label('Search sky objects').fill('Moon')
await page.locator('.search-results button').first.click()
async def moon_bounds():
await page.wait_for_timeout(150)
await host.screenshot(path=str(OUTPUT/'personal-moon.png'))
im=Image.open(OUTPUT/'personal-moon.png').convert('RGB')
points=[(x,y) for y in range(im.height) for x in range(im.width) if im.getpixel((x,y))==(155,163,146)]
assert len(points)>100,'Moon pixels missing'
left,right=min(x for x,y in points),max(x for x,y in points)
top,bottom=min(y for x,y in points),max(y for x,y in points)
assert abs((right-left+1)/(bottom-top+1)-1)<.05,(left,right,top,bottom)
return ((left+right)/2,(top+bottom)/2)
moon=await moon_bounds()
box=await host.bounding_box()
await page.mouse.move(box['x']+moon[0]+60,box['y']+moon[1]+50)
await page.mouse.down()
await page.mouse.move(box['x']+moon[0]+120,box['y']+moon[1]+70,steps=5)
await page.mouse.up()
moon=await moon_bounds()
await page.mouse.move(box['x']+moon[0],box['y']+moon[1])
await page.mouse.wheel(0,-140)
after=await moon_bounds()
assert abs(after[0]-moon[0])<1.5 and abs(after[1]-moon[1])<1.5,(moon,after)
await page.get_by_role('button',name='TELESCOPE',exact=True).click()
await page.get_by_role('button',name='CONNECT SIMULATOR',exact=False).click()
await page.get_by_role('button',name='SLEW TO OBJECT',exact=False).click()
await page.wait_for_function("document.querySelector('.sim-label')?.textContent === 'SIMULATING TRACKING'",timeout=45000)
await page.get_by_role('button',name='ZOOM TO FIELD',exact=False).click()
await page.wait_for_timeout(200)
assert await page.locator('.map-overlay').evaluate('(c)=>{const d=c.getContext("2d").getImageData(0,0,c.width,c.height).data;let n=0;for(let i=3;i<d.length;i+=4)if(d[i])n++;return n>100}')
await page.screenshot(path=str(OUTPUT/'personal-telescope.png'))
await page.get_by_role('button',name='DISCONNECT SIMULATOR',exact=False).click()
for width in [820,390,320]:
await page.set_viewport_size({'width':width,'height':844})
assert not await page.evaluate('document.documentElement.scrollWidth>innerWidth')
assert await page.get_by_role('button',name='Switch to light mode',exact=True).is_visible()
await page.screenshot(path=str(OUTPUT/f'dark-mobile-{width}.png'))
assert not errors,errors
print(f'PASSED {base}: faint-star size/contrast; light/dark canvas; persistent theme; forced crosshair; {len(draws)} redraws in 3 seconds, median {median:.1f} ms; pause; curved terrain; round Sun and Moon; cursor zoom; simulated field; responsive layout. Errors: {errors}')
await browser.close()
asyncio.run(main())