Files
jacuzzi/tests/smoke_privacy.py
Simon 1b38f9d9e7 Blur the page whenever the window isn't in front
Losing focus -- another tab, another window, another app -- now puts the
whole page behind a blur, so an app switcher's snapshot or a glance over
the shoulder gives nothing away. It comes back on focus, on the tab
becoming visible again, and on the first touch or key, which lands on the
veil rather than on a page nobody could see.

Painted as a backdrop filter on a fixed overlay above everything the app
can show: a filter on the page itself would make every fixed element
position against the wrong box.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPZFnNdHbPGDTqQUNiE4ZN
2026-09-22 08:30:20 +00:00

142 lines
5.5 KiB
Python

#!/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())