Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions dbuild/screenshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ def secure_origin(url: str) -> str:
return f"{u.scheme}://{u.netloc}"


def frame_is_blank(png: bytes) -> bool:
"""True when the frame is one flat color (verify's blank test). Without
scikit-image nothing counts as blank, as before."""
try:
from io import BytesIO

from skimage import io as skio

from dbuild.verify import is_blank
except ImportError:
return False
return is_blank(skio.imread(BytesIO(png)))


def capture(url: str, output: str, timeout: int = 30, min_wait: int = 0, secure_context: bool = False) -> bool:
"""Capture a screenshot of *url* and save to *output*.

Expand Down Expand Up @@ -101,12 +115,15 @@ def capture(url: str, output: str, timeout: int = 30, min_wait: int = 0, secure_
)
stable = False
while time.time() - start_time < max_stability_wait:
current_screen = driver.get_screenshot_as_base64()
current_screen = driver.get_screenshot_as_png()
elapsed = time.time() - start_time
if last_screen and current_screen == last_screen and elapsed >= min_wait:
print(f"UI stabilized after {elapsed:.2f}s", file=sys.stderr)
stable = True
break
# An SPA paints a blank page before its first render; two blank
# frames match, but that is not the UI settling.
if (last_screen and current_screen == last_screen
and elapsed >= min_wait and not frame_is_blank(current_screen)):
print(f"UI stabilized after {elapsed:.2f}s", file=sys.stderr)
stable = True
break
last_screen = current_screen
time.sleep(0.5)

Expand Down
80 changes: 80 additions & 0 deletions tests/test_screenshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,83 @@ def test_secure_context_read_from_config():

assert _parse_test_config({"cit": {"port": 5006, "secure_context": True}}).secure_context is True
assert _parse_test_config({"cit": {"port": 5006}}).secure_context is False


def _png(pattern: bool) -> bytes:
import io

np = pytest.importorskip("numpy")
Image = pytest.importorskip("PIL.Image")
a = np.full((32, 32, 3), 128, dtype=np.uint8)
if pattern:
a[::4] = 0
b = io.BytesIO()
Image.fromarray(a).save(b, "PNG")
return b.getvalue()


def test_frame_is_blank():
pytest.importorskip("skimage")
from dbuild.screenshot import frame_is_blank

assert frame_is_blank(_png(False))
assert not frame_is_blank(_png(True))


class _FakeDriver:
def __init__(self, frames):
self.frames = frames
self.shots = 0
self.saved = None

def set_page_load_timeout(self, t):
pass

def execute_cdp_cmd(self, *a):
pass

def get(self, url):
pass

def execute_script(self, s):
return "complete"

def get_screenshot_as_png(self):
f = self.frames[min(self.shots, len(self.frames) - 1)]
self.shots += 1
return f

def save_screenshot(self, path):
self.saved = self.frames[min(self.shots - 1, len(self.frames) - 1)]

def quit(self):
pass


def _capture(monkeypatch, frames):
import dbuild.screenshot as s

clock = [0.0]
monkeypatch.setattr(s.time, "time", lambda: clock[0])
monkeypatch.setattr(s.time, "sleep", lambda n: clock.__setitem__(0, clock[0] + n))
d = _FakeDriver(frames)
monkeypatch.setattr(s.webdriver, "Chrome", lambda **kw: d)
monkeypatch.setattr(s, "Service", lambda **kw: None)
assert s.capture("http://10.88.0.4:8080/", "/nonexistent.png")
return d


def test_blank_frames_are_not_stable(monkeypatch, capsys):
# nocodb on CI: the SPA's blank first paint matched itself and was shot.
pytest.importorskip("skimage")
blank, ui = _png(False), _png(True)
d = _capture(monkeypatch, [blank, blank, blank, ui, ui])
assert d.saved == ui
assert "UI stabilized" in capsys.readouterr().err


def test_page_that_stays_blank_times_out(monkeypatch, capsys):
pytest.importorskip("skimage")
d = _capture(monkeypatch, [_png(False)])
assert d.saved == _png(False)
assert "did not stabilize" in capsys.readouterr().err
Loading