aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_layout_measured.py
blob: 91f1ed0f94696e0836a3db89a492400c1b89b734 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
"""
Layouts measured in a browser instead of read out of the stylesheet.

`test_layout_responsive.py` says, in its own docstring, that a layout cannot be
measured here because there is no browser in the suite. There is one now —
Chrome is what the video work has been verified against — and the difference
matters: `width: 330px` in a rule tells you nothing about whether the thing
lands on the screen. That depends on where its anchor sits, which depends on
everything to its right.

The transfers panel is the case that proved it. 330 px wide, anchored to the
right edge of its button — but that button is not at the right edge of the
screen, the bell and the user menu come after it. Measured before the fix:

    320 px viewport -> panel at -138..192, 138 px off the left
    360 px          -> -98..232
    412 px          -> -46..284

What is cut off is the left-hand side, which is where the file names are, so
what was left on screen was a strip of progress bars belonging to nothing —
reported as "on mobile you only see the right-hand side, without the content".

Every assertion here is a rectangle, not a declaration.
"""

import json
import shutil
import subprocess
import textwrap
from pathlib import Path

import pytest

HARNESS = Path(__file__).parent / "harness" / "layout_probe.py"
STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static"

pytestmark = pytest.mark.skipif(
    shutil.which("google-chrome") is None or not (STATIC / "style.css").exists(),
    reason="Chrome or the SPA stylesheet is not available")

# The nav as `Nav()` renders it: the transfers widget is not last — the bell and
# the user menu follow it, which is the whole reason the panel hangs off.
NAV = textwrap.dedent("""
    <nav class="nav">
      <div class="nav-left">
        <button class="nav-hamburger">&#9776;</button>
        <a class="nav-brand" href="#/">MeshBay</a>
      </div>
      <div class="nav-right">
        <div class="transfer-wrap">
          <button class="nav-notif transfer-btn">&#8595;</button>
          <div class="transfer-panel">
            <div class="transfer-head">Transfers<button class="btn-secondary">Clear</button></div>
            <div class="transfer-item">
              <div class="transfer-line">
                <span class="transfer-kind">&#8595;</span>
                <span class="transfer-name">S03E01. Salt and Sea, Fire and Blood.mp4</span>
                <button class="transfer-cancel">&#10005;</button>
              </div>
              <div class="dl-progress"><div class="dl-fill" style="width:42%"></div></div>
              <div class="transfer-meta"><span>210 MB / 493 MB</span><span>3.1 MB/s</span></div>
            </div>
          </div>
        </div>
        <a class="nav-notif" href="#/">&#128276;</a>
        <div class="user-menu"><button class="nav-btn">someone</button></div>
      </div>
    </nav>
""")


WIDTHS = [320, 360, 412, 768, 1024]
SELECTORS = [".transfer-panel", ".transfer-name"]


@pytest.fixture(scope="module")
def measured(tmp_path_factory):
    """One browser for every width, because launching one apiece cost the
    suite three minutes."""
    fragment = tmp_path_factory.mktemp("layout") / "fragment.html"
    fragment.write_text(NAV)
    proc = subprocess.run(
        ["python3", str(HARNESS), ",".join(str(w) for w in WIDTHS),
         str(fragment), *SELECTORS],
        capture_output=True, text=True, timeout=180)
    assert proc.returncode == 0, f"probe failed: {proc.stdout}{proc.stderr}"
    out = json.loads(proc.stdout)
    assert "error" not in out, f"no measurement: {out}"
    for w in WIDTHS:
        assert out[str(w)]["viewport"]["w"] == w, (
            f"asked for {w} px and measured at {out[str(w)]['viewport']['w']}")
    return out


def _box(measured, width: int, selector: str) -> dict:
    return measured[str(width)]["boxes"][selector]


@pytest.mark.parametrize("width", [320, 360, 412])
def test_the_transfers_panel_fits_a_phone(measured, width):
    """The reported defect, as a rectangle."""
    box = _box(measured, width, ".transfer-panel")
    assert box is not None, "the panel did not render"
    assert box["offLeft"] == 0, (
        f"{box['offLeft']} px of the panel is off the left of a {width} px "
        "screen — and the left is where the file names are")
    assert box["offRight"] == 0, (
        f"{box['offRight']} px of the panel is off the right of a {width} px screen")


@pytest.mark.parametrize("width", [320, 360])
def test_the_file_name_gets_room_to_be_read(measured, width):
    """Fitting on screen is not the same as being legible.

    A panel could satisfy the test above by being narrow enough to show
    nothing. The name is the one part a viewer needs.
    """
    box = _box(measured, width, ".transfer-name")
    assert box is not None and box["width"] >= 180, (
        f"the file name has {box['width'] if box else 0} px on a {width} px "
        "screen, which is not enough to tell two downloads apart")


def test_the_desktop_panel_is_untouched(measured):
    """The fix is a media query, and it must stay inside it."""
    box = _box(measured, 1024, ".transfer-panel")
    assert box["width"] == 330, (
        f"the desktop panel is now {box['width']} px — the mobile rule has "
        "escaped its breakpoint")
    assert box["offLeft"] == 0 and box["offRight"] == 0


def test_it_does_not_stretch_across_a_tablet(measured):
    """Pinned to both edges, a panel would be 750 px wide at 768."""
    box = _box(measured, 768, ".transfer-panel")
    assert box["width"] <= 440, (
        f"{box['width']} px of panel on a tablet, which is a list of two "
        "filenames stretched over most of the screen")


@pytest.mark.parametrize("width", [320, 360, 412])
def test_the_page_does_not_scroll_sideways(measured, width):
    """The other half of "it fits": nothing pushed the document wider."""
    r = measured[str(width)]
    assert r["docScrollW"] <= r["viewport"]["w"], (
        f"the document scrolls to {r['docScrollW']} px on a {width} px screen")