Files
CyberGril/samples/bedroom/tools/browser_check.py
T
2026-10-20 16:14:20 +08:00

159 lines
8.2 KiB
Python

"""Foreground Chromium interaction checks, screenshots and downloadable recording."""
import functools
import http.server
import json
import threading
from pathlib import Path
from playwright.sync_api import sync_playwright
ROOT = Path(__file__).resolve().parents[1]
EVIDENCE = ROOT / 'evidence'
class Quiet(http.server.SimpleHTTPRequestHandler):
def log_message(self, *_args):
pass
def main():
EVIDENCE.mkdir(exist_ok=True)
handler = functools.partial(Quiet, directory=str(ROOT / 'dist'))
server = http.server.ThreadingHTTPServer(('127.0.0.1', 0), handler)
thread = threading.Thread(target=server.serve_forever)
thread.start()
results = {}
try:
with sync_playwright() as p:
browser = p.chromium.launch()
url = f'http://127.0.0.1:{server.server_port}'
context = browser.new_context(viewport={'width': 1280, 'height': 800},
record_video_dir=str(EVIDENCE),
record_video_size={'width': 1280, 'height': 800})
page = context.new_page()
errors = []
page.on('pageerror', lambda error: errors.append(str(error)))
page.goto(url)
page.wait_for_function('!document.querySelector("#play").disabled')
assert page.locator('#source').evaluate('(v) => v.paused')
page.click('#play')
# Keep every layer visible for three full cycles before interaction checks.
duration = page.locator('#source').evaluate('(v) => v.duration')
page.evaluate("""() => {
window.observedLoops = []; window.lastVideoTime = 0;
const v = document.querySelector('#source');
v.addEventListener('timeupdate', () => {
if (v.currentTime < window.lastVideoTime) window.observedLoops.push(performance.now());
window.lastVideoTime = v.currentTime;
});
}""")
page.wait_for_timeout((duration * 3 + 0.8) * 1000)
observed = page.evaluate('window.observedLoops')
assert len(observed) >= 3, observed
results['continuous_three_cycles'] = {'duration': duration, 'wraps_ms': observed}
page.wait_for_timeout(800)
alpha = page.locator('#motion').evaluate('(c) => {const x=c.getContext("2d"); return [x.getImageData(0,0,1,1).data[3], x.getImageData(300,220,1,1).data[3]]}')
assert alpha[0] == 0 and alpha[1] > 240, alpha
results['vp9_alpha_decoded_by_chromium'] = alpha
page.wait_for_function('document.querySelector("#source").currentTime > 0.9 && document.querySelector("#source").currentTime < 1.4')
positions = page.evaluate('''() => {
const video=document.querySelector('#source'); const before=video.currentTime;
const source=video.currentSrc; document.querySelector('#lighting').click();
return {before, after:video.currentTime, sourcePreserved: source===video.currentSrc};
}''')
assert positions['before'] == positions['after'] and positions['sourcePreserved']
results['environment_toggle_no_seek'] = positions
page.wait_for_timeout(650)
assert page.locator('#source').evaluate('(v) => !v.paused')
page.click('#show-person')
assert page.locator('#motion').is_hidden() and page.locator('#person').is_hidden()
page.wait_for_timeout(600)
page.screenshot(path=str(EVIDENCE / 'environment-only.png'))
page.click('#show-person')
page.click('#show-occlusion')
assert page.locator('#occlusion').is_hidden()
page.click('#show-occlusion')
page.click('#show-shadow')
assert page.locator('#shadow').is_hidden()
page.click('#show-shadow')
page.click('#show-mask')
assert page.locator('#mask').is_visible() and page.locator('#environment').is_hidden()
page.wait_for_timeout(500)
page.click('#show-mask')
results['independent_layers'] = True
page.click('#closeup')
assert page.locator('#talk').is_visible()
assert page.locator('#source').evaluate('(v) => !v.paused')
page.wait_for_timeout(700)
page.screenshot(path=str(EVIDENCE / 'static-near.png'))
page.click('#closeup')
assert page.locator('#talk').is_hidden()
page.wait_for_timeout(6400) # Includes the forward and generated return.
results['near_is_static_player_continues'] = True
page.click('#play')
t = page.locator('#source').evaluate('(v) => v.currentTime')
page.wait_for_timeout(250)
assert page.locator('#source').evaluate('(v) => v.currentTime') == t
results['manual_pause'] = True
# Chromium lifecycle API freezes the page after making it hidden.
session = context.new_cdp_session(page)
session.send('Page.setWebLifecycleState', {'state': 'active'})
# Visibility handler unit probe. Browser screenshot/recording checks above are real.
page.click('#play')
page.evaluate('''() => {
Object.defineProperty(document, 'hidden', {configurable:true, get:()=>true});
document.dispatchEvent(new Event('visibilitychange'));
}''')
assert page.locator('#source').evaluate('(v) => v.paused')
page.evaluate('''() => {
delete document.hidden; document.dispatchEvent(new Event('visibilitychange'));
}''')
page.wait_for_function('!document.querySelector("#source").paused')
results['visibility_pause_resume_handler_synthetic_event'] = True
for width, height in [(1600, 900), (1280, 800), (390, 844)]:
page.set_viewport_size({'width': width, 'height': height})
page.wait_for_timeout(180)
bounds = page.evaluate('''() => {
const r=document.querySelector('#motion').getBoundingClientRect();
const s=document.querySelector('#stage').getBoundingClientRect();
return {faceX:r.x+305/552*r.width, faceY:r.y+135/552*r.height,
stage:{left:s.left,right:s.right,top:s.top,bottom:s.bottom},
scroll:document.documentElement.scrollWidth, width:innerWidth};
}''')
assert bounds['scroll'] <= width
assert bounds['stage']['left'] <= bounds['faceX'] <= bounds['stage']['right']
assert bounds['stage']['top'] <= bounds['faceY'] <= bounds['stage']['bottom']
page.screenshot(path=str(EVIDENCE / f'viewport-{width}x{height}.png'))
results[f'viewport_{width}x{height}'] = bounds
page.emulate_media(reduced_motion='reduce')
page.wait_for_timeout(100)
assert page.locator('#source').evaluate('(v) => v.paused')
assert page.locator('#person').is_visible() and page.locator('#motion').is_hidden()
results['reduced_motion_static_fallback'] = True
page.emulate_media(reduced_motion='no-preference')
assert page.locator('#source').evaluate('(v) => v.paused')
video_path = Path(page.video.path())
context.close()
video_path.rename(EVIDENCE / 'browser-continuous.webm')
missing = browser.new_page()
missing.route('**/reading-alpha.webm', lambda route: route.abort())
missing.goto(url)
missing.wait_for_function('!document.querySelector("#fallback").hidden')
assert missing.locator('#person').is_visible()
assert missing.locator('#play').is_disabled()
results['missing_motion_static_fallback'] = True
missing.close()
assert not errors, errors
results['page_errors'] = errors
browser.close()
finally:
server.shutdown()
thread.join()
server.server_close()
(EVIDENCE / 'browser-results.json').write_text(json.dumps(results, ensure_ascii=False, indent=2))
print(json.dumps(results, ensure_ascii=False, indent=2))
if __name__ == '__main__':
main()