"""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(50getComputedStyle(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 80100,'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;i100}') 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())