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:
1 parent
94a3c1ba0e
commit
8ecf3e94a6
6 files changed
+260
-25
No files matched your search
@@ -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."""
|
||||
|
||||
Reference in new issue
Block a user