feat(r3): compare — two picked rels side by side, linked stepping, synced pan, flag the winner
GET /b/{name}/compare with the conjunction 404 (containment AND the review
ring), both sides recorded as seen, view state (side, link) mapped from a
closed set onto every link, side-keyed regions, and back=compare in
_mark_redirect. compare.html: two stages sharing one set of rows, the strip
as picker (the side active now), linked and per-side stepping, X/L/Z/A/B/C
keys under the review's guards, synced pan by fraction with an echo guard,
per-side blur reveals, JS-off parity.
The stage machinery moves out of view.html into _stage_js.html
(BoothMode.bind, BoothStage.attach), shared by the review and compare. The
review gains a Compare control and a C key. At phone width a full top bar
wraps.
Tables: r2c's 15 stage rows re-pointed to _stage_js.html; r2b's phone
top-bar row re-anchored (the wrap made it vacuous alone); new r3.toml. The
contract records the wrap, equal stages and C on the compare page.
This commit is contained in:
@@ -0,0 +1,451 @@
|
||||
"""R3 — compare, in a real DOM.
|
||||
|
||||
Contract: docs/contracts/r3_compare.contract.md. A TestClient can prove what
|
||||
the server answers; it cannot prove that two stages sit side by side, that a
|
||||
pan on one lands the other on the same crop, or that an in-place save keeps
|
||||
the active side. The harness is test_flow_browser's (a real uvicorn, a real
|
||||
offline Chromium), and like it this SKIPS, never fails, without a browser.
|
||||
"""
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).parent.parent))
|
||||
|
||||
from test_flow_browser import _png, browser, live # noqa: E402,F401 (fixtures)
|
||||
|
||||
|
||||
def _pics(root: pathlib.Path, pics: dict, name: str = "g") -> pathlib.Path:
|
||||
b = root / name
|
||||
b.mkdir()
|
||||
for rel, (w, h) in pics.items():
|
||||
p = b / rel
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_bytes(_png(w, h))
|
||||
return b
|
||||
|
||||
|
||||
def _open(page, url):
|
||||
"""Load a compare page and wait until every picture on a stage decoded."""
|
||||
page.goto(url, wait_until="networkidle")
|
||||
page.wait_for_function("""[...document.querySelectorAll('.cmp-side .vstage img')]
|
||||
.every(i => i.complete && i.naturalWidth > 0)""")
|
||||
page.wait_for_timeout(150)
|
||||
|
||||
|
||||
_BOXES = """() => [...document.querySelectorAll('.cmp-side .vstage')].map(s => {
|
||||
const b = s.getBoundingClientRect();
|
||||
return {side: s.dataset.side, l: b.left, r: b.right, t: b.top, b: b.bottom, h: b.height};
|
||||
})"""
|
||||
|
||||
|
||||
def test_two_stages_side_by_side_wide_and_stacked_narrow(browser, live):
|
||||
"""The tracer. At 1440 A and B share one row, A on the left, each about
|
||||
half the body. At 390 they stack, A above B, each at most 45vh tall."""
|
||||
base, root = live
|
||||
from booth.app import set_blurred
|
||||
b = _pics(root, {"a.png": (800, 600), "b.png": (800, 600)})
|
||||
(b / "a.png.txt").write_text("a caption on A only, " * 8)
|
||||
set_blurred(b, "b.png", True) # the bar carries Reveal all too: its fullest
|
||||
got = {}
|
||||
for w, h in ((1440, 900), (390, 844)):
|
||||
page = browser.new_page(viewport={"width": w, "height": h})
|
||||
_open(page, f"{base}/b/g/compare?a=a.png&b=b.png")
|
||||
got[w] = (page.evaluate(_BOXES), h,
|
||||
page.evaluate("document.documentElement.scrollWidth - document.documentElement.clientWidth"),
|
||||
# and nothing squeezed: every top-bar control one line, none
|
||||
# crushed narrower than its own content (a flex item shrinks
|
||||
# before it overflows — the Fit | 1:1 toggle went to 2px)
|
||||
page.evaluate("""() => { const c = [...document.querySelectorAll(
|
||||
'.vbar button, .vbar .vbtn, .vbar .vtoggle')].filter(e => e.offsetParent);
|
||||
return [Math.max(...c.map(e => e.getBoundingClientRect().height)),
|
||||
Math.max(...c.map(e => e.scrollWidth - e.clientWidth))]; }"""))
|
||||
page.close()
|
||||
(a, b), _, over, tallest = got[1440]
|
||||
assert (a["side"], b["side"]) == ("a", "b")
|
||||
assert abs(a["t"] - b["t"]) <= 1 and a["r"] <= b["l"], (a, b)
|
||||
assert a["r"] - a["l"] > 1440 * 0.4 and b["r"] - b["l"] > 1440 * 0.4, (a, b)
|
||||
# the SAME stage for both, or Fit draws one smaller: A's caption must not
|
||||
# take its height from A's stage alone
|
||||
assert abs(a["h"] - b["h"]) <= 1 and abs(a["b"] - b["b"]) <= 1, (a, b)
|
||||
assert over <= 0 and tallest[0] <= 40 and tallest[1] <= 1, (over, tallest)
|
||||
(a, b), vh, over, tallest = got[390]
|
||||
assert a["b"] <= b["t"], ("A above B", a, b)
|
||||
assert a["h"] <= 0.45 * vh + 1 and b["h"] <= 0.45 * vh + 1, (a, b)
|
||||
assert a["h"] > 100 and b["h"] > 100, (a, b)
|
||||
assert over <= 0, "the compare page scrolls sideways at phone width"
|
||||
assert tallest[0] <= 40, ("a top-bar control squeezed into a stack", tallest)
|
||||
assert tallest[1] <= 1, ("a top-bar control crushed narrower than its content", tallest)
|
||||
|
||||
|
||||
_IMGS = """() => [...document.querySelectorAll('.cmp-side .vstage img')].map(i => {
|
||||
const b = i.getBoundingClientRect();
|
||||
return [Math.round(b.width), Math.round(b.height), i.naturalWidth, i.naturalHeight];
|
||||
})"""
|
||||
|
||||
|
||||
def test_one_mode_for_both_and_for_the_review(browser, live):
|
||||
"""`Z` switches BOTH stages to 1:1 and stores it as the review's own
|
||||
preference: the review then opens in 1:1, and Z back is Fit for both."""
|
||||
base, root = live
|
||||
_pics(root, {"a.png": (1600, 1200), "b.png": (1400, 1000)})
|
||||
ctx = browser.new_context(viewport={"width": 1440, "height": 900})
|
||||
page = ctx.new_page()
|
||||
_open(page, f"{base}/b/g/compare?a=a.png&b=b.png")
|
||||
fit = page.evaluate(_IMGS)
|
||||
page.keyboard.press("z")
|
||||
page.wait_for_timeout(150)
|
||||
one = page.evaluate(_IMGS)
|
||||
# both larger than their stages, so both offer the grab at once — not at
|
||||
# the next resize
|
||||
grab = page.evaluate("[...document.querySelectorAll('.cmp-side .vstage')].map(s => getComputedStyle(s).cursor)")
|
||||
stored = page.evaluate("localStorage.getItem('booth.fit')")
|
||||
pressed = page.locator("#btn-one").get_attribute("aria-pressed")
|
||||
page.goto(f"{base}/b/g/view?f=a.png", wait_until="networkidle")
|
||||
review_one = page.evaluate("document.documentElement.classList.contains('stage-one')")
|
||||
page.goto(f"{base}/b/g/compare?a=a.png&b=b.png", wait_until="networkidle")
|
||||
page.keyboard.press("Z")
|
||||
back = page.evaluate("[document.documentElement.classList.contains('stage-one'), localStorage.getItem('booth.fit')]")
|
||||
ctx.close()
|
||||
assert all([w, h] != [nw, nh] for w, h, nw, nh in fit), fit
|
||||
assert all([w, h] == [nw, nh] for w, h, nw, nh in one), one
|
||||
assert grab == ["grab", "grab"], grab
|
||||
assert stored == "one" and pressed == "true" and review_one is True, (stored, pressed, review_one)
|
||||
assert back == [False, None], back
|
||||
|
||||
|
||||
_SCROLLS = """() => [...document.querySelectorAll('.cmp-side .vstage')].map(s => [s.scrollLeft, s.scrollTop])"""
|
||||
|
||||
|
||||
def _one_to_one(page, url):
|
||||
"""Open a compare in 1:1 (the stored preference, applied before paint)."""
|
||||
page.add_init_script("try { localStorage.setItem('booth.fit', 'one'); } catch (e) {}")
|
||||
_open(page, url)
|
||||
|
||||
|
||||
def _center(page, side):
|
||||
box = page.locator(f'.cmp-side[data-side="{side}"] .vstage').bounding_box()
|
||||
return box["x"] + box["width"] / 2, box["y"] + box["height"] / 2
|
||||
|
||||
|
||||
def test_synced_pan_lands_on_the_same_crop(browser, live):
|
||||
"""Two pictures of one size, larger than the stage, in 1:1. A drag on A of
|
||||
(+80, +60) scrolls BOTH by (-80, -60): the same pixels under the same
|
||||
point. A wheel on B moves A with it. After a second of idle neither has
|
||||
moved on its own (no sync loop)."""
|
||||
base, root = live
|
||||
_pics(root, {"a.png": (3000, 3000), "b.png": (3000, 3000)})
|
||||
page = browser.new_page(viewport={"width": 1440, "height": 900})
|
||||
_one_to_one(page, f"{base}/b/g/compare?a=a.png&b=b.png")
|
||||
page.evaluate("document.querySelector('.cmp-side[data-side=\"a\"] .vstage').scrollTo(500, 500)")
|
||||
page.wait_for_timeout(200)
|
||||
start = page.evaluate(_SCROLLS)
|
||||
cx, cy = _center(page, "a")
|
||||
page.mouse.move(cx, cy); page.mouse.down(); page.mouse.move(cx + 80, cy + 60, steps=6); page.mouse.up()
|
||||
page.wait_for_timeout(200)
|
||||
dragged = page.evaluate(_SCROLLS)
|
||||
bx, by = _center(page, "b")
|
||||
page.mouse.move(bx, by)
|
||||
page.mouse.wheel(0, 300)
|
||||
page.wait_for_timeout(600)
|
||||
wheeled = page.evaluate(_SCROLLS)
|
||||
page.wait_for_timeout(1000)
|
||||
idle = page.evaluate(_SCROLLS)
|
||||
page.close()
|
||||
assert start == [[500, 500], [500, 500]], start
|
||||
assert dragged == [[420, 440], [420, 440]], dragged
|
||||
assert wheeled[1][1] > 440 and wheeled[0] == wheeled[1], wheeled
|
||||
assert idle == wheeled, (wheeled, idle)
|
||||
|
||||
|
||||
_RANGES = """() => [...document.querySelectorAll('.cmp-side .vstage')].map(s =>
|
||||
[s.scrollWidth - s.clientWidth, s.scrollHeight - s.clientHeight])"""
|
||||
|
||||
# The first y near `from` on B whose trip B -> A -> B does not come back to
|
||||
# itself when each scroll lands on a whole pixel: where a sync that re-synced
|
||||
# its own echo would walk B off the spot it was put on.
|
||||
_LOSSY = """([from, rA, rB]) => {
|
||||
for (let y = from; y < from + 200; y++)
|
||||
if (Math.round(Math.round(y / rB * rA) / rA * rB) !== y) return y;
|
||||
return from;
|
||||
}"""
|
||||
|
||||
|
||||
def test_synced_pan_by_fraction_for_different_sizes(browser, live):
|
||||
"""A 2000px and a 3000px picture. One at its middle puts the other at ITS
|
||||
middle; one at 25% of its range puts the other at 25% of ITS range — not
|
||||
at the same pixel offset. The equal-size test cannot see a fraction bug
|
||||
(equal overflow makes offsets and fractions coincide); this one can. And a
|
||||
side put somewhere STAYS there: the sync never echoes back and walks it."""
|
||||
base, root = live
|
||||
_pics(root, {"a.png": (2000, 2000), "b.png": (3000, 3000)})
|
||||
page = browser.new_page(viewport={"width": 1440, "height": 900})
|
||||
_one_to_one(page, f"{base}/b/g/compare?a=a.png&b=b.png")
|
||||
(rax, ray), (rbx, rby) = page.evaluate(_RANGES)
|
||||
a = '.cmp-side[data-side="a"] .vstage'
|
||||
b = '.cmp-side[data-side="b"] .vstage'
|
||||
page.evaluate("([s, x, y]) => document.querySelector(s).scrollTo(x, y)", [a, round(rax / 2), round(ray / 2)])
|
||||
page.wait_for_timeout(250)
|
||||
middle = page.evaluate(_SCROLLS)
|
||||
y = page.evaluate(_LOSSY, [round(rby / 4), ray, rby])
|
||||
x = page.evaluate(_LOSSY, [round(rbx / 4), rax, rbx])
|
||||
page.evaluate("([s, x, y]) => document.querySelector(s).scrollTo(x, y)", [b, x, y])
|
||||
page.wait_for_timeout(250)
|
||||
quarter = page.evaluate(_SCROLLS)
|
||||
page.wait_for_timeout(1000)
|
||||
idle = page.evaluate(_SCROLLS)
|
||||
page.close()
|
||||
assert rbx > rax > 0 and rby > ray > 0, (rax, ray, rbx, rby)
|
||||
assert abs(middle[1][0] - rbx / 2) <= 1 and abs(middle[1][1] - rby / 2) <= 1, (middle, rbx, rby)
|
||||
assert abs(quarter[0][0] / rax - x / rbx) * rax <= 1, (quarter, x)
|
||||
assert abs(quarter[0][1] / ray - y / rby) * ray <= 1, (quarter, y)
|
||||
assert quarter[0][0] < x - 100, "A must sit at ITS 25%, not at B's pixel offset"
|
||||
assert quarter[1] == [x, y] and idle == quarter, (x, y, quarter, idle)
|
||||
|
||||
|
||||
def test_a_flags_A_in_place_and_the_stages_survive(browser, live):
|
||||
"""`A` flags A in place: no navigation, A's control and label show the
|
||||
flag, B's do not, and both stages are the SAME nodes — a save swaps the
|
||||
regions and never a stage (a playing track would restart)."""
|
||||
from booth.marks import marks_for
|
||||
base, root = live
|
||||
b = _pics(root, {"a.png": (800, 600), "b.png": (800, 600)})
|
||||
page = browser.new_page(viewport={"width": 1440, "height": 900})
|
||||
_open(page, f"{base}/b/g/compare?a=a.png&b=b.png")
|
||||
page.evaluate("""() => { window.__noReload = 1;
|
||||
window.__stages = [...document.querySelectorAll('.cmp-side .vstage')]; }""")
|
||||
page.keyboard.press("a")
|
||||
page.wait_for_selector("#cmp-flag-a.is-flagged", timeout=10000)
|
||||
got = page.evaluate("""() => ({
|
||||
reload: window.__noReload !== 1,
|
||||
same: [...document.querySelectorAll('.cmp-side .vstage')].every((s, i) => s === window.__stages[i]),
|
||||
a: document.querySelector('[data-region="label-a"]').textContent,
|
||||
b: document.querySelector('[data-region="label-b"]').textContent,
|
||||
bflag: document.getElementById('cmp-flag-b').classList.contains('is-flagged'),
|
||||
})""")
|
||||
flagged = [m.target for m in marks_for(b) if m.shape == "flag"]
|
||||
# the save REPLACED the buttons: each key must find the fresh one
|
||||
page.keyboard.press("b")
|
||||
page.wait_for_selector("#cmp-flag-b.is-flagged", timeout=10000)
|
||||
page.keyboard.press("a")
|
||||
page.wait_for_selector("#cmp-flag-a:not(.is-flagged)", timeout=10000)
|
||||
after = sorted(m.target for m in marks_for(b) if m.shape == "flag")
|
||||
reloaded = page.evaluate("window.__noReload !== 1")
|
||||
page.close()
|
||||
assert not got["reload"] and got["same"], got
|
||||
assert "flagged" in got["a"] and "flagged" not in got["b"] and not got["bflag"], got
|
||||
assert flagged == ["a.png"] and after == ["b.png"] and not reloaded, (flagged, after, reloaded)
|
||||
|
||||
|
||||
def _bakeoff(root: pathlib.Path) -> pathlib.Path:
|
||||
"""sindra-bakeoff's shape: two lanes, m and r, the same scenes and seeds,
|
||||
laid out as two parallel runs in sorted order — no pairing rule needed."""
|
||||
scenes = ("dock-s11", "forge-s23", "marsh-s37", "tower-s42")
|
||||
return _pics(root, {f"{lane}-{i}-{sc}.png": (400, 300)
|
||||
for lane in ("m", "r") for i, sc in enumerate(scenes, 1)})
|
||||
|
||||
|
||||
def _pair(page) -> tuple[str, str]:
|
||||
from urllib.parse import parse_qs, urlsplit
|
||||
q = parse_qs(urlsplit(page.url).query)
|
||||
return q["a"][0], q["b"][0]
|
||||
|
||||
|
||||
def _press_and_wait(page, key):
|
||||
with page.expect_navigation(wait_until="networkidle"):
|
||||
page.keyboard.press(key)
|
||||
|
||||
|
||||
def test_linked_arrow_walks_a_bakeoff(browser, live):
|
||||
"""m#1 against r#1, then `→` three times: every pair is the same scene and
|
||||
seed in the two lanes."""
|
||||
base, root = live
|
||||
_bakeoff(root)
|
||||
page = browser.new_page(viewport={"width": 1440, "height": 900})
|
||||
_open(page, f"{base}/b/g/compare?a=m-1-dock-s11.png&b=r-1-dock-s11.png")
|
||||
pairs = [_pair(page)]
|
||||
for _ in range(3):
|
||||
_press_and_wait(page, "ArrowRight")
|
||||
pairs.append(_pair(page))
|
||||
page.close()
|
||||
assert [(a[2:], b[2:]) for a, b in pairs] == [(a[2:], a[2:]) for a, _ in pairs], pairs
|
||||
assert [a[0] + b[0] for a, b in pairs] == ["mr"] * 4, pairs
|
||||
assert pairs[-1] == ("m-4-tower-s42.png", "r-4-tower-s42.png"), pairs
|
||||
|
||||
|
||||
def test_unlinked_moves_only_the_active_side_and_the_strip_picks_it(browser, live):
|
||||
"""`L` unlinks: `→` moves only B, and a SECOND `→` still moves only B (the
|
||||
state survived the navigation). A strip click replaces the active side.
|
||||
`X` swaps the active side, the reticle follows, and it survives a step."""
|
||||
base, root = live
|
||||
_bakeoff(root)
|
||||
page = browser.new_page(viewport={"width": 1440, "height": 900})
|
||||
_open(page, f"{base}/b/g/compare?a=m-1-dock-s11.png&b=r-1-dock-s11.png")
|
||||
page.keyboard.press("l")
|
||||
linked = page.locator("#cmp-link").get_attribute("aria-pressed")
|
||||
_press_and_wait(page, "ArrowRight")
|
||||
one = _pair(page)
|
||||
_press_and_wait(page, "ArrowRight")
|
||||
two = _pair(page)
|
||||
with page.expect_navigation(wait_until="networkidle"):
|
||||
page.locator('.film-f[data-rel="m-3-marsh-s37.png"]').click()
|
||||
picked = _pair(page)
|
||||
page.keyboard.press("x")
|
||||
active = page.evaluate("""() => [document.querySelector('.cmp-side.is-active').dataset.side,
|
||||
[...document.querySelectorAll('.film-f.is-active')].map(f => f.dataset.rel)]""")
|
||||
_press_and_wait(page, "ArrowRight")
|
||||
stepped = _pair(page)
|
||||
after = page.evaluate("""() => [document.querySelector('.cmp-side.is-active').dataset.side,
|
||||
getComputedStyle(document.querySelector('.cmp-side.is-active > .cmp-stagewrap'), '::after').backgroundImage !== 'none',
|
||||
getComputedStyle(document.querySelector('.cmp-side:not(.is-active) > .cmp-stagewrap'), '::after').backgroundImage !== 'none']""")
|
||||
page.close()
|
||||
assert linked == "false", linked
|
||||
assert one == ("m-1-dock-s11.png", "r-2-forge-s23.png"), one
|
||||
assert two == ("m-1-dock-s11.png", "r-3-marsh-s37.png"), two
|
||||
assert picked == ("m-1-dock-s11.png", "m-3-marsh-s37.png"), picked
|
||||
assert active == ["a", ["m-1-dock-s11.png"]], active
|
||||
assert stepped == ("m-2-forge-s23.png", "m-3-marsh-s37.png"), stepped
|
||||
assert after == ["a", True, False], after
|
||||
|
||||
|
||||
_FILTERS = """() => [...document.querySelectorAll('.cmp-side .vstage img')].map(i => getComputedStyle(i).filter)"""
|
||||
_REVEALS = """() => [...document.querySelectorAll('.cmp-reveal')].map(b => getComputedStyle(b).display !== 'none')"""
|
||||
|
||||
|
||||
def test_blur_is_honest_on_both_sides(browser, live):
|
||||
"""A blurred side IS blurred — the computed filter, not just a class. Its
|
||||
own reveal lifts it and leaves the other side blurred. Reveal all lifts
|
||||
both, and stands both per-side reveals down."""
|
||||
from booth.app import set_blurred
|
||||
base, root = live
|
||||
b = _pics(root, {"a.png": (800, 600), "b.png": (800, 600)})
|
||||
set_blurred(b, "a.png", True)
|
||||
set_blurred(b, "b.png", True)
|
||||
page = browser.new_page(viewport={"width": 1440, "height": 900})
|
||||
_open(page, f"{base}/b/g/compare?a=a.png&b=b.png")
|
||||
before = page.evaluate(_FILTERS)
|
||||
shown = page.evaluate(_REVEALS)
|
||||
page.locator('.cmp-reveal[data-side="b"]').click()
|
||||
page.wait_for_timeout(400) # the filter transition
|
||||
own = page.evaluate(_FILTERS)
|
||||
page.locator('.cmp-reveal[data-side="b"]').click()
|
||||
page.locator("[data-reveal-all]").click()
|
||||
page.wait_for_timeout(400)
|
||||
everything = page.evaluate(_FILTERS)
|
||||
stood_down = page.evaluate(_REVEALS)
|
||||
page.close()
|
||||
assert all("blur(" in f for f in before), before
|
||||
assert shown == [True, True], shown
|
||||
assert "blur(" in own[0] and "blur(" not in own[1], own
|
||||
assert all("blur(" not in f for f in everything), everything
|
||||
assert stood_down == [False, False], stood_down
|
||||
|
||||
|
||||
def test_a_save_keeps_the_active_side(browser, live):
|
||||
"""Make A active (a press on its stage), then flag B in place. The save
|
||||
swaps the strip and the labels, and A is STILL the active side — on its
|
||||
stage and on the strip — and a strip click after the swap replaces A."""
|
||||
base, root = live
|
||||
_bakeoff(root)
|
||||
page = browser.new_page(viewport={"width": 1440, "height": 900})
|
||||
_open(page, f"{base}/b/g/compare?a=m-1-dock-s11.png&b=r-1-dock-s11.png")
|
||||
ax, ay = _center(page, "a")
|
||||
page.mouse.click(ax, ay)
|
||||
page.evaluate("window.__noReload = 1")
|
||||
page.keyboard.press("b")
|
||||
page.wait_for_selector("#cmp-flag-b.is-flagged", timeout=10000)
|
||||
kept = page.evaluate("""() => ({
|
||||
reload: window.__noReload !== 1,
|
||||
side: document.querySelector('.cmp-side.is-active').dataset.side,
|
||||
strip: [...document.querySelectorAll('.film-f.is-active')].map(f => f.dataset.rel),
|
||||
url: location.search,
|
||||
})""")
|
||||
with page.expect_navigation(wait_until="networkidle"):
|
||||
page.locator('.film-f[data-rel="m-2-forge-s23.png"]').click()
|
||||
picked = _pair(page)
|
||||
side = page.evaluate("document.querySelector('.cmp-side.is-active').dataset.side")
|
||||
# the active side lives in the URL: `X` rewrites it in place, so a reload
|
||||
# shows the side that was active, not the default
|
||||
page.keyboard.press("x")
|
||||
page.reload(wait_until="networkidle")
|
||||
reloaded = page.evaluate("document.querySelector('.cmp-side.is-active').dataset.side")
|
||||
page.close()
|
||||
assert reloaded == "b", reloaded
|
||||
assert not kept["reload"] and kept["side"] == "a", kept
|
||||
assert kept["strip"] == ["m-1-dock-s11.png"] and "side=a" in kept["url"], kept
|
||||
assert picked == ("m-2-forge-s23.png", "r-1-dock-s11.png") and side == "a", (picked, side)
|
||||
|
||||
|
||||
def test_without_js_every_judgment_and_step_still_works(browser, live):
|
||||
"""INV-4. Scripts off: the pair renders in two Fit stages, the step and
|
||||
strip links navigate, both flag forms are there and a flag lands back on
|
||||
the same pair. Nothing judgment-bearing hides behind a script."""
|
||||
from booth.marks import marks_for
|
||||
base, root = live
|
||||
b = _bakeoff(root)
|
||||
ctx = browser.new_context(java_script_enabled=False, viewport={"width": 1440, "height": 900})
|
||||
page = ctx.new_page()
|
||||
page.goto(f"{base}/b/g/compare?a=m-1-dock-s11.png&b=r-1-dock-s11.png", wait_until="networkidle")
|
||||
fit = page.evaluate("document.documentElement.classList.contains('stage-one')")
|
||||
hidden = [page.locator(s).is_visible() for s in ("#cmp-link", "#vtoggle")]
|
||||
forms = page.locator(".cmp-flag form").count()
|
||||
page.locator('a[data-step="both-next"]').click()
|
||||
page.wait_for_load_state("networkidle")
|
||||
both = _pair(page)
|
||||
page.locator('a[data-step="a-prev"]').click()
|
||||
page.wait_for_load_state("networkidle")
|
||||
a_back = _pair(page)
|
||||
page.locator('.film-f[data-rel="m-4-tower-s42.png"]').click()
|
||||
page.wait_for_load_state("networkidle")
|
||||
strip = _pair(page)
|
||||
page.locator("#cmp-flag-b").click()
|
||||
page.wait_for_load_state("networkidle")
|
||||
landed = _pair(page)
|
||||
shows = page.locator("#cmp-flag-b").get_attribute("class")
|
||||
ctx.close()
|
||||
assert fit is False and hidden == [False, False] and forms == 2, (fit, hidden, forms)
|
||||
assert both == ("m-2-forge-s23.png", "r-2-forge-s23.png"), both
|
||||
assert a_back == ("m-1-dock-s11.png", "r-2-forge-s23.png"), a_back
|
||||
assert strip == ("m-1-dock-s11.png", "m-4-tower-s42.png"), strip
|
||||
assert landed == strip and "is-flagged" in shows, (landed, shows)
|
||||
assert [m.target for m in marks_for(b) if m.shape == "flag"] == ["m-4-tower-s42.png"]
|
||||
|
||||
|
||||
def test_the_keys_keep_the_reviews_guards_and_c_toggles_the_view(browser, live):
|
||||
"""C3: a held modifier makes every key inert; Space on a focused control
|
||||
presses it and never steps, and from nowhere in particular it steps
|
||||
(Shift+Space back). `C` in the review opens compare against the next
|
||||
item; `Esc` (or `C`) in compare returns to the review of A."""
|
||||
base, root = live
|
||||
_bakeoff(root)
|
||||
page = browser.new_page(viewport={"width": 1440, "height": 900})
|
||||
_open(page, f"{base}/b/g/compare?a=m-2-forge-s23.png&b=r-2-forge-s23.png")
|
||||
start = page.url
|
||||
for key in ("Control+ArrowRight", "Alt+ArrowRight", "Meta+ArrowRight", "Control+x", "Alt+l"):
|
||||
page.keyboard.press(key)
|
||||
page.wait_for_timeout(300)
|
||||
inert = (page.url == start,
|
||||
page.evaluate("document.querySelector('.cmp-side.is-active').dataset.side"),
|
||||
page.locator("#cmp-link").get_attribute("aria-pressed"))
|
||||
page.locator("#cmp-link").focus()
|
||||
page.keyboard.press(" ")
|
||||
page.wait_for_timeout(300)
|
||||
pressed = (page.url.split("?")[1], page.locator("#cmp-link").get_attribute("aria-pressed"))
|
||||
page.locator("#cmp-link").press(" ") # linked again
|
||||
page.evaluate("document.activeElement.blur()")
|
||||
_press_and_wait(page, " ")
|
||||
fwd = _pair(page)
|
||||
_press_and_wait(page, "Shift+ ")
|
||||
back = _pair(page)
|
||||
_press_and_wait(page, "Escape")
|
||||
esc = page.url
|
||||
_press_and_wait(page, "c")
|
||||
c_review = _pair(page)
|
||||
_press_and_wait(page, "C")
|
||||
c_back = page.url
|
||||
page.close()
|
||||
assert inert == (True, "b", "true"), inert
|
||||
assert pressed == ("a=m-2-forge-s23.png&b=r-2-forge-s23.png&link=0", "false"), pressed
|
||||
assert fwd == ("m-3-marsh-s37.png", "r-3-marsh-s37.png") and back == ("m-2-forge-s23.png", "r-2-forge-s23.png"), (fwd, back)
|
||||
assert esc.endswith("/b/g/view?f=m-2-forge-s23.png"), esc
|
||||
assert c_review == ("m-2-forge-s23.png", "m-3-marsh-s37.png"), c_review
|
||||
assert c_back.endswith("/b/g/view?f=m-2-forge-s23.png"), c_back
|
||||
Reference in New Issue
Block a user