#!/usr/bin/env python3 """The privacy veil: the page blurs whenever this window is not the one in front. Run against a locally running backend: backend/main.py & .venv/bin/python tests/smoke_privacy.py Headless Chromium keeps every tab visible and focused -- opening a second tab and bringing it to the front changes neither, and neither does freezing the page over CDP -- so there is no way to genuinely send this window to the back. The transitions are therefore dispatched by hand, which tests what this change owns -- what the page does when the browser says it has gone away -- and not what the browser does with a backgrounded tab. """ import sys from playwright.sync_api import sync_playwright BASE = "http://127.0.0.1:5000/" VEILED = """() => { const veil = document.getElementById('privacy-veil'); return !!veil && veil.classList.contains('is-on'); }""" OPACITY = """() => getComputedStyle(document.getElementById('privacy-veil')).opacity""" # What a tap in the middle of the window would land on. UNDER_FINGER = """() => { const el = document.elementFromPoint(innerWidth / 2, innerHeight / 2); return el ? el.id || el.className || el.tagName : null; }""" # Neither a second tab nor CDP can put this window in the background, so the # two halves of a tab switch are announced the way the browser would announce # them. HIDE = """() => { Object.defineProperty(document, 'visibilityState', { configurable: true, get: () => 'hidden' }); document.dispatchEvent(new Event('visibilitychange')); }""" SHOW = """() => { Object.defineProperty(document, 'visibilityState', { configurable: true, get: () => 'visible' }); document.dispatchEvent(new Event('visibilitychange')); }""" # The highest z-index anywhere in the app's own stylesheet, veil excluded: the # veil has to beat the player and the reels feed, not just the page behind them. HIGHEST_OTHER = """() => { let top = 0; for (const sheet of document.styleSheets) { let rules; try { rules = sheet.cssRules; } catch (err) { continue; } for (const rule of rules || []) { if (!rule.selectorText || rule.selectorText.includes('privacy-veil')) continue; const z = parseInt(rule.style && rule.style.zIndex, 10); if (!isNaN(z) && z > top) top = z; } } return top; }""" class Checks: def __init__(self): self.failed = 0 def ok(self, label, condition, detail=""): mark = "PASS" if condition else "FAIL" if not condition: self.failed += 1 print(f" [{mark}] {label}" + (f" -- {detail}" if detail and not condition else "")) def main(): c = Checks() with sync_playwright() as p: browser = p.chromium.launch(args=[ "--no-sandbox", "--disable-dev-shm-usage", "--disable-gpu", "--renderer-process-limit=1", "--js-flags=--max-old-space-size=512", ]) page = browser.new_page(viewport={"width": 430, "height": 930}) crashes = [] page.on("pageerror", lambda e: crashes.append(str(e))) page.goto(BASE, wait_until="load") page.wait_for_function("() => window.App && App.privacy && App.privacy.isVeiled", timeout=30000) page.wait_for_timeout(300) print("\nthe window in front") c.ok("nothing is veiled", page.evaluate(VEILED) is False) c.ok("and the veil is out of the way", page.evaluate(OPACITY) == "0") c.ok("so taps reach the page", page.evaluate(UNDER_FINGER) != "privacy-veil", str(page.evaluate(UNDER_FINGER))) print("\nthe tab going to the back") page.evaluate(HIDE) c.ok("the page goes behind the blur", page.evaluate(VEILED) is True) c.ok("fully, and at once", page.evaluate(OPACITY) == "1") c.ok("with nothing underneath reachable", page.evaluate(UNDER_FINGER) == "privacy-veil", str(page.evaluate(UNDER_FINGER))) c.ok("and it sits above everything the app can show", page.evaluate("() => parseInt(getComputedStyle(document.getElementById('privacy-veil')).zIndex, 10)") > page.evaluate(HIGHEST_OTHER), str(page.evaluate(HIGHEST_OTHER))) print("\ncoming back to it") page.evaluate(SHOW) page.wait_for_timeout(300) c.ok("the blur lifts", page.evaluate(VEILED) is False) print("\nlosing focus without leaving the tab -- another window, say") page.evaluate("() => window.dispatchEvent(new Event('blur'))") c.ok("the page goes behind the blur too", page.evaluate(VEILED) is True) page.evaluate("() => window.dispatchEvent(new Event('focus'))") c.ok("and comes back when focus does", page.evaluate(VEILED) is False) print("\na touch on the veil") page.evaluate("() => window.dispatchEvent(new Event('blur'))") c.ok("veiled to start with", page.evaluate(VEILED) is True) page.evaluate("""() => document.getElementById('privacy-veil') .dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))""") c.ok("lifts it rather than pressing what is underneath", page.evaluate(VEILED) is False) print("\nthe tab going away altogether") page.evaluate("() => window.dispatchEvent(new Event('pagehide'))") c.ok("leaves it blurred behind us", page.evaluate(VEILED) is True) c.ok("nothing threw along the way", not crashes, str(crashes[:2])) browser.close() print(f"\n{'OK' if not c.failed else 'FAILED'}: {c.failed} check(s) failed") return 1 if c.failed else 0 sys.exit(main())