"""Browser regression for presentation, motion preferences and existing research controls. Run against a built exhibition preview; this check never submits inference jobs. PREVIEW_URL and CHROMIUM_PATH select an existing server/browser. """ import json import os from pathlib import Path from playwright.sync_api import sync_playwright, expect OUT = Path('artifacts/refinements-20261110') OUT.mkdir(parents=True, exist_ok=True) BASE = os.environ.get('PREVIEW_URL', 'http://127.0.0.1:7860') errors = [] requests_failed = [] checks = [] def scroll(page, selector): page.locator(selector).evaluate("e=>window.scrollTo({top:e.getBoundingClientRect().top+scrollY-100,behavior:'instant'})") page.wait_for_timeout(350) def no_overflow(page, label): assert page.evaluate('document.documentElement.scrollWidth <= innerWidth'), label + ' overflow' checks.append(label) print('PASS:', label, flush=True) with sync_playwright() as p: browser = p.chromium.launch(executable_path=os.environ.get('CHROMIUM_PATH'), args=[ '--no-sandbox', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader']) context = browser.new_context(viewport={'width': 1440, 'height': 1000}) page = context.new_page() page.on('pageerror', lambda error: errors.append(str(error))) page.on('response', lambda response: requests_failed.append(f'{response.status} {response.url}') if response.status >= 400 else None) inference_requests = [] page.on('request', lambda request: inference_requests.append(request.url) if request.method == 'POST' and '/api/' in request.url else None) page.goto(BASE, wait_until='networkidle') page.evaluate('document.fonts.ready') expect(page.locator('[data-render-state="ready"]')).to_have_count(2, timeout=30000) expect(page.locator('.hero-scene')).to_have_attribute('data-running', 'true') expect(page.locator('.site-header .ouroboros-mark')).to_be_visible() expect(page.locator('.hero-brand-lockup .ouroboros-mark')).to_be_visible() page.wait_for_timeout(1100) page.screenshot(path=str(OUT / 'desktop-hero.png')) for label, direction in [('Design →', 'design'), ('← Describe', 'describe'), ('Learn together', 'joint')]: page.get_by_role('group', name='Hero learning direction', exact=True).get_by_role('button', name=label, exact=True).click() expect(page.locator('.hero-scene')).to_have_attribute('data-direction', direction) # Path animation actually advances, then freezes when paused. path = page.locator('.design-path .path-stream') before = path.evaluate('e=>getComputedStyle(e).strokeDashoffset') page.wait_for_timeout(200) assert path.evaluate('e=>getComputedStyle(e).strokeDashoffset') != before page.get_by_role('button', name='Pause page animations', exact=True).click() expect(page.locator('html')).to_have_attribute('data-motion', 'paused') page.wait_for_timeout(150) frozen = path.evaluate('e=>getComputedStyle(e).strokeDashoffset') page.wait_for_timeout(200) after_pause = path.evaluate('e=>getComputedStyle(e).strokeDashoffset') assert after_pause == frozen, (frozen, after_pause, path.evaluate('e=>getComputedStyle(e).animationPlayState')) expect(page.locator('.hero-scene')).to_have_attribute('data-running', 'false') page.reload(wait_until='networkidle') expect(page.get_by_role('button', name='Resume page animations', exact=True)).to_be_visible() scroll(page, '#evidence') assert page.locator('.bar-track>span').first.evaluate('e=>getComputedStyle(e).transform') == 'none' checks.append('Animation pause, persisted preference and visible charts while paused') page.get_by_role('button', name='Resume page animations', exact=True).click() scroll(page, '#dual-learning') expect(page.locator('.hero-scene')).to_have_attribute('data-running', 'false') expect(page.locator('nav a[href="#dual-learning"]')).to_have_attribute('aria-current', 'location') print('PASS: global motion and hero controls', flush=True) dual = page.locator('#dual-learning') # Every illustration is a function of scroll position, including on rewind. expected = json.loads(Path('public/data/core-example.json').read_text()) stage = page.locator('.story-stage') def at(value): page.locator('.story-scroll').evaluate("""(e,p)=>{const stage=e.querySelector('.story-stage'); const offset=parseFloat(getComputedStyle(stage).top); window.scrollTo({top:e.getBoundingClientRect().top+scrollY-offset+(e.offsetHeight-stage.offsetHeight)*p,behavior:'instant'}); }""", value) page.wait_for_function('(p)=>Math.abs(Number(document.querySelector(".story-stage").dataset.progress)-p)<.0002', arg=value) page.wait_for_timeout(220) for name, value, beat in [('start',0,0),('encoding',.09,0),('projection',.23,1),('residues',.475,2),('reverse',.665,3),('words',.815,4),('joint',1,5)]: at(value) expect(stage).to_have_attribute('data-beat', str(beat)) no_overflow(page, 'story ' + name) page.screenshot(path=str(OUT / f'desktop-{name}.png')) at(.385) halfway = page.locator('.dual-scene-svg').inner_html() arrived = page.locator('[data-output="protein"] [data-arrived="true"]').count() assert 0 < arrived < 72, arrived at(.475) assert page.locator('[data-output="protein"] [data-arrived="true"]').count() == 72 assert page.locator('[data-output="protein"] [data-letter]').evaluate_all('els=>els.map(e=>e.dataset.letter).join("")') == expected['generatedProtein'][:72] at(.385) assert page.locator('.dual-scene-svg').inner_html() == halfway, 'Rewinding must reproduce the same complete SVG state' page.wait_for_timeout(400) assert page.locator('.dual-scene-svg').inner_html() == halfway, 'An idle scene must not animate' assert page.locator('[data-kind="hidden-state"] text').count() == 0 assert page.locator('.ds-count').all_text_contents() == ['×24'] * 4 stage.get_by_role('button', name='Pause scene', exact=True).click() frozen = stage.get_attribute('data-progress') page.mouse.wheel(0, 350) page.wait_for_timeout(300) assert stage.get_attribute('data-progress') == frozen # Explicit navigation is useful even while paused, and uses the same timeline. stage.get_by_role('button', name='06 Learn together', exact=False).click() expect(stage).to_have_attribute('data-beat', '5') stage.get_by_role('button', name='Resume scene', exact=True).click() at(.7) page.get_by_role('button', name='Pause page animations', exact=True).click() frozen = stage.get_attribute('data-progress') page.mouse.wheel(0, 200) page.wait_for_timeout(300) assert stage.get_attribute('data-progress') == frozen page.get_by_role('button', name='Resume page animations', exact=True).click() page.get_by_role('slider', name='Explanation progress').focus() page.keyboard.press('End') expect(stage).to_have_attribute('data-beat', '5') page.keyboard.press('Home') expect(stage).to_have_attribute('data-beat', '0') scroll(page, '#dataset') page.wait_for_timeout(300) frozen = stage.get_attribute('data-progress') page.mouse.wheel(0, 180) page.wait_for_timeout(400) assert stage.get_attribute('data-progress') == frozen, 'Offscreen scene must stop' at(.815) stage.get_by_role('button', name='Read all steps', exact=False).click() expect(dual).to_have_attribute('data-story-mode', 'reading') assert dual.locator('.reading-step').count() == 6 dual.get_by_role('button', name='Return to scroll experience', exact=False).click() expect(dual).to_have_attribute('data-story-mode', 'scroll') page.locator('.story-archive summary').click() assert page.locator('[data-archive="generated-protein"]').inner_text() == expected['generatedProtein'] assert page.locator('[data-archive="reference-protein"]').inner_text() == expected['protein'] assert page.locator('[data-archive="generated-text"]').inner_text() == expected['generatedText'] page.locator('.story-archive summary').click() checks.append('Six scroll beats, exact rewind, idle/offscreen freeze, local/global pause, keyboard/chapters, independent archived inputs and full outputs') scroll(page, '#dataset') assert page.locator('.key-rail button').count() == 29 page.locator('.key-rail button').last.click() expect(page.locator('.key-rail button').last).to_have_attribute('aria-pressed', 'true') scroll(page, '#atlas') expect(page.locator('#atlas .atlas-case')).to_have_count(8) page.locator('#atlas .atlas-case').first.click() expect(page.locator('#atlas .atlas-preview-story')).to_contain_text('SHOWCASE 01') page.screenshot(path=str(OUT / 'desktop-atlas.png')) scroll(page, '#petase') petase = page.locator('#petase') expect(page.locator('.fold-story')).to_have_attribute('data-fold-state', 'playing', timeout=15000) slider = page.get_by_role('slider', name='Sequence to predicted structure') slider.fill('100') expect(slider).to_have_value('100') assert page.locator('.fold-canvas-wrap canvas').evaluate('e=>e.width>0&&e.height>0') expect(page.locator('.fold-story')).to_have_attribute('data-fold-state', 'paused') page.get_by_role('button', name='Replay structure transformation').click() expect(page.locator('.fold-story')).to_have_attribute('data-fold-state', 'playing') slider.fill('100') page.screenshot(path=str(OUT / 'desktop-fold.png')) checks.append('DE2 backbone loads, plays, scrubs, pauses and replays') assert petase.locator('.case-tabs button').count() == 10 petase.locator('.case-tabs button').nth(1).click() expect(petase.locator('[data-render-state="ready"]')).to_have_count(1, timeout=30000) petase.locator('.case-tabs button').first.click() expect(petase.locator('[data-render-state="ready"]')).to_have_count(1, timeout=30000) petase.get_by_role('button', name='Color by confidence', exact=True).click() expect(petase.locator('.confidence-legend')).to_be_visible() petase.locator('.residue-list button').nth(42).click() page.keyboard.press('ArrowRight') expect(petase.locator('.residue-list button').nth(43)).to_be_focused() petase.get_by_role('button', name='Try this function in the lab', exact=False).click() expect(page.locator('#model-input')).not_to_have_value('') expect(page.locator('#playground')).to_contain_text('Original LoRA generation prompt') assert page.locator('#playground .generation-result').count() == 0 scroll(page, '#evidence') evidence = page.locator('#evidence') evidence.get_by_role('button', name='Keyword recovery', exact=True).click() expect(evidence.locator('.chart-selection')).to_contain_text('n = 476') evidence.get_by_role('button', name='Protein → Text', exact=True).click() evidence.get_by_role('button', name='ROUGE-L', exact=True).click() assert evidence.locator('#benchmark-set').count() == 0 expect(evidence.locator('.benchmark-dataset')).to_contain_text('Swiss-Prot time-cutoff') assert '100 pairs' not in evidence.inner_text() assert 'Hard' not in evidence.inner_text() assert evidence.locator('.bar-row').count() > 1 page.wait_for_timeout(1100) page.screenshot(path=str(OUT / 'desktop-evidence.png')) checks.append('29 annotations, 8 Atlas cases, PETase structure/residues, prompt reuse, benchmark filters') no_overflow(page, '1440px desktop') # Ensure paused preferences on the homepage do not strand the standalone Atlas. page.get_by_role('button', name='Pause page animations', exact=True).click() page.goto(BASE + '/atlas?id=UniRef50_A0A9J7N2Q8', wait_until='networkidle') page.get_by_role('button', name='Resume page animations', exact=True).click() expect(page.locator('.atlas-structure [data-render-state="ready"]')).to_have_count(1, timeout=30000) expect(page.locator('.atlas-description')).to_be_visible() no_overflow(page, 'Standalone Atlas') for width in [1280, 1024, 820, 768, 390, 320]: page.set_viewport_size({'width': width, 'height': 844}) page.goto(BASE, wait_until='networkidle') page.wait_for_timeout(900) no_overflow(page, f'{width}px homepage') if width in [390, 320]: page.screenshot(path=str(OUT / f'mobile-{width}-hero.png')) scroll(page, '.hero-scene') page.screenshot(path=str(OUT / f'mobile-{width}-scene.png')) for section in ['dual-learning', 'dataset', 'atlas', 'petase', 'evidence', 'playground']: scroll(page, '#' + section) no_overflow(page, f'{width}px {section}') if width == 390 and section in ['dual-learning', 'atlas', 'petase']: page.screenshot(path=str(OUT / f'mobile-{section}.png')) if width <= 1100: expect(page.locator('#dual-learning')).to_have_attribute('data-story-mode', 'reading') assert page.locator('.reading-step').count() == 6 for step in range(1, 7): scroll(page, f'#dual-step-{step}') no_overflow(page, f'{width}px reading step {step}') if width == 390: page.screenshot(path=str(OUT / f'mobile-step-{step}.png')) if width <= 780: page.get_by_role('button', name='Toggle navigation', exact=True).click() expect(page.locator('#main-navigation')).to_be_visible() page.keyboard.press('Escape') expect(page.locator('#main-navigation')).not_to_be_visible() expect(page.get_by_role('button', name='Toggle navigation')).to_be_focused() page.get_by_role('button', name='Toggle navigation', exact=True).click() page.locator('nav a[href="#dataset"]').click() expect(page.locator('#main-navigation')).not_to_be_visible() # System preference is respected immediately and when changed at runtime. page.emulate_media(reduced_motion='reduce') page.goto(BASE, wait_until='networkidle') expect(page.locator('html')).to_have_attribute('data-motion', 'paused') expect(page.get_by_role('button', name='Animations off: reduced motion preference')).to_be_disabled() expect(page.locator('.hero-scene')).to_have_attribute('data-running', 'false') assert page.locator('.path-stream').first.evaluate('e=>getComputedStyle(e).animationName') == 'none' scroll(page, '#dual-learning') expect(page.locator('#dual-learning')).to_have_attribute('data-story-mode', 'reading') assert page.locator('.reading-step').count() == 6 assert page.locator('.story-stage').count() == 0 page.set_viewport_size({'width':1440,'height':1000}) expect(page.locator('#dual-learning')).to_have_attribute('data-story-mode', 'reading') scroll(page, '#evidence') assert page.locator('.bar-track>span').first.evaluate('e=>getComputedStyle(e).transform') == 'none' page.emulate_media(reduced_motion='no-preference') expect(page.get_by_role('button', name='Pause page animations')).to_be_enabled() checks.append('Reduced motion at load and runtime; all outputs and charts remain readable') assert not errors, errors assert not requests_failed, requests_failed assert not inference_requests, inference_requests browser.close() report = {'checks': checks, 'page_errors': errors, 'http_errors': requests_failed, 'inference_jobs_submitted': len(inference_requests)} (OUT / 'browser-report.json').write_text(json.dumps(report, indent=2) + '\n') print(json.dumps(report, indent=2))