Arrange tool, and a hold that is easier to find

An Arrange tool marks the corners of every image, Hide and own text
rectangle in blue and moves or resizes them by a plain drag, a press
elsewhere doing nothing.  The hold that does the same with any tool is
shorter (450 ms), with more slack, and shows all four corners.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ackandClaude Fable 5.1 committed 2026-10-14 00:01:45 +02:00
1 parent 94a3c1ba0e
commit 8ecf3e94a6
6 files changed
+260 -25

No files matched your search

+93
View File
@@ -1391,6 +1391,99 @@ class EndToEndTest(unittest.TestCase):
self.wait_until(lambda s: s["texts"].get("Typist") and s["texts"]["Typist"][0]["text"] == shown,
"the onlooker to see the same text")
def test_arrange_tool(self):
"""Andy, 2026-10-13: with the Arrange tool, blue corners mark every
image, Hide and own text rectangle, a drag inside one moves it and
a drag on a corner resizes it, at once, no hold; a press elsewhere
does nothing. A guest without the right sees only their own text."""
firefox = self.firefox
host_tab = firefox.current_tab()
firefox.go(self.address("Late"))
self.enter_name("Arranger", "start_session")
invitation = firefox.run("return document.getElementById('invitation').value;")
guest_tab = firefox.new_tab()
firefox.go(self.address("Student", join=invitation))
self.enter_name("Watcher", "join_session")
self.wait_until(lambda s: s["page"]["number"] == 1 and len(s["participants"]) == 2, "the watcher")
firefox.switch_to(host_tab)
cues = lambda: firefox.run("return [...document.querySelectorAll('.arrange_cue')].map((c) => c.className.split(' ')[1]);")
choose = lambda tool: firefox.run(f"document.querySelector('[data-tool={tool}]').click();")
rect = lambda state, i: [state["images"][i][k] for k in ("x", "y", "width", "height")]
firefox.run_until_done(LOAD_IMAGE, 300, 200, "png", 0, False)
placed = self.wait_until(lambda s: len(s["images"]) == 1 and s["image"]["ready"], "the image")
self.assertEqual(cues(), [], "no cues with the pen")
choose("arrange")
self.assertEqual(cues(), ["image"])
self.assertTrue(firefox.run("return document.getElementById('colors').hidden;"))
scale = firefox.run("return window.kokkoja.renderer.cssPixelsPerUnit;")
origin = placed["origin"]
px = lambda x: (x - origin[0]) * scale
py = lambda y: (y - origin[1]) * scale
# A drag inside the image moves it at once, and draws nothing.
before = rect(placed, 0)
firefox.drag("#page", px(before[0] + 150), py(before[1] + 100), px(before[0] + 150) + 200, py(before[1] + 100) + 100)
moved = self.wait_until(lambda s: s["images"][0]["x"] != before[0], "the image moved")
self.assertEqual(rect(moved, 0)[2:], before[2:], "the same size")
self.assertAlmostEqual(moved["images"][0]["x"], before[0] + 200 / scale, delta=2)
self.assertAlmostEqual(moved["images"][0]["y"], before[1] + 100 / scale, delta=2)
self.assertEqual(moved["layers"]["Arranger"], [], "no stroke")
# A drag on its bottom-right corner scales it, the shape kept, the
# top-left corner fixed.
now = rect(moved, 0)
firefox.drag("#page", px(now[0] + now[2]) - 3, py(now[1] + now[3]) - 3, px(now[0] + now[2]) + 150, py(now[1] + now[3]) - 3)
scaled = self.wait_until(lambda s: s["images"][0]["width"] != now[2], "the image scaled")
self.assertEqual(rect(scaled, 0)[:2], now[:2], "the top-left corner stays")
self.assertAlmostEqual(scaled["images"][0]["width"] / scaled["images"][0]["height"], 300 / 200, places=2)
self.assertGreater(scaled["images"][0]["width"], now[2] + 100 / scale)
# A drag on empty paper does nothing at all.
firefox.drag("#page", px(100), py(1800), px(600), py(1900))
time.sleep(0.5)
after = self.state()
self.assertEqual(after["layers"]["Arranger"], [])
self.assertEqual(rect(after, 0), rect(scaled, 0))
# A text rectangle of one's own and a Hide get cues too; a corner
# drag widens the text box, a drag inside moves the Hide.
choose("text")
firefox.run(TEXT_GESTURE, 0.1, 0.6, 0.3, 0.7)
firefox.type_into(".text_field", "Box")
choose("hide")
firefox.run(TEXT_GESTURE, 0.5, 0.6, 0.7, 0.7)
with_all = self.wait_until(lambda s: len(s["hides"]) == 1 and s["texts"]["Arranger"] and s["texts"]["Arranger"][0]["text"] == "Box",
"the box and the Hide")
choose("arrange")
self.assertEqual(sorted(cues()), ["hide", "image", "text"])
box = with_all["texts"]["Arranger"][0]
firefox.drag("#page", px(box["x"] + box["width"]) - 3, py(box["y"] + box["height"]) - 3,
px(box["x"] + box["width"]) + 100, py(box["y"] + box["height"]) + 40)
widened = self.wait_until(lambda s: s["texts"]["Arranger"][0]["width"] > box["width"] + 80 / scale, "the box widened")
self.assertEqual((widened["texts"]["Arranger"][0]["x"], widened["texts"]["Arranger"][0]["y"]), (box["x"], box["y"]))
hide = with_all["hides"][0]
firefox.drag("#page", px(hide[0] + hide[2] / 2), py(hide[1] + hide[3] / 2),
px(hide[0] + hide[2] / 2) - 100, py(hide[1] + hide[3] / 2) + 50)
hide_moved = self.wait_until(lambda s: s["hides"][0][0] != hide[0], "the Hide moved")
self.assertEqual(hide_moved["hides"][0][2:], hide[2:])
self.assertEqual(len(cues()), 3)
# Undo takes the Hide's move back, as it does a hold's.
firefox.run("document.getElementById('undo').click();")
self.wait_until(lambda s: s["hides"][0] == hide, "the Hide back by Undo")
# The watcher, without the right, is shown only their own text.
firefox.switch_to(guest_tab)
self.wait_until(lambda s: len(s["images"]) == 1 and len(s["hides"]) == 1, "the page at the watcher")
choose("arrange")
self.assertEqual(cues(), [])
choose("text")
firefox.run(TEXT_GESTURE, 0.1, 0.8, 0.3, 0.9)
firefox.type_into(".text_field", "Mine")
choose("arrange")
self.assertEqual(cues(), ["text"])
def test_text_being_typed_stays_on_the_page_left(self):
"""Andy, 2026-10-06: a guest was typing when the host pressed New,
and the text reappeared on the new page."""