Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 7 additions & 0 deletions plugins/leaf/skills/leaf/assets/leaf.js
Original file line number Diff line number Diff line change
Expand Up @@ -1239,6 +1239,13 @@ function merge(sections, { title, when, at, claims, rows }) {
* gated by its own row `when`, and calls paintKeys() when the state moves — a grab is
* Enter on an already-focused grip, so no focus event would repaint the line.
*
* The same duty is owed for state a widget writes that moves a row it never declared.
* `.lf-dragging` is half of `unrecordedGesture()`, so a widget wearing it moves core's
Comment thread
leaf-agent marked this conversation as resolved.
Outdated
* `z`, and the two edges of a pointer drag went unpainted for as long as the duty read
* as being about a widget's own rows: the line offered `undo` for the whole of a drag,
* over a press the dispatcher was already refusing. Whoever moves the state paints,
* whosever row reads it.
*
* Registering at upgrade rather than at module load is what keeps the reference honest:
* every x-upgrade module loads on every page, so a scope declared at the top level is help
* for a widget the page hasn't got. The scope leaves with its element; there is no
Expand Down
12 changes: 12 additions & 0 deletions plugins/leaf/skills/leaf/bundled/widgets/lf-board.js
Original file line number Diff line number Diff line change
Expand Up @@ -373,9 +373,21 @@ customElements.define(
} else this.#cancel();
}
this.classList.add("lf-dragging");
// The class is half of the runtime's own `z` liveness, and the drag it
// covers is a whole gesture rather than a frame: the focus paint landed
// on the mousedown, and `fallbackTolerance` fires this after it, so
// nothing else repaints until the drop. Without this the line goes on
// offering `undo` for as long as the reader holds the card, over a press
// the dispatcher refuses.
paintKeys();
},
onEnd: (evt) => {
this.classList.remove("lf-dragging");
// Painted before the branches below, because the one that returns early
// sends nothing: a card dropped where it was picked up takes the class
// off with nothing following it. Where a send does follow, the frame
// this coalesces to lands after #send has stated what is in flight.
paintKeys();
const sup = this.#superseded;
this.#superseded = null;
// The *draggable* indexes, which count cards; Sortable's plain
Expand Down
72 changes: 72 additions & 0 deletions tests/test_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,22 @@ def _traffic(page):
return page.lf_traffic


def _painted_line(page):
"""The key line as the gesture just made left it.

`paintHere` coalesces to an animation frame, so a read taken straight after the
state moves reads the frame before the paint. Consuming the frame is what makes
the read once rather than a poll: the line repaints on its own often enough — a
version poll, a focus move, news arriving — that an assertion re-asking through
`expect` reports whichever later paint the page happened to make, and passes on a
gesture that painted nothing at all.
"""
page.evaluate(
"() => new Promise(done => requestAnimationFrame(() => requestAnimationFrame(done)))"
)
return page.locator(".lf-keyline").inner_text()


def _until(page, fact, wanted):
"""Block until `fact` holds of the page's traffic.

Expand Down Expand Up @@ -13667,6 +13683,62 @@ def hold(route):
page.close()


def test_a_pointer_drag_stops_the_line_offering_the_press_it_refuses(browser, serve):
"""`.lf-dragging` is half of the `z` liveness the runtime declares, and a pointer
drag is a whole gesture rather than a frame: the focus paint lands on the
mousedown, `fallbackTolerance` fires the drag's start after it, and on a quiet
board nothing repaints between the pick-up and the drop. So unpainted, the line
goes on offering `undo` for as long as the reader holds the card, over a press the
dispatcher is already refusing. The drop is the same gap read backwards: a card
put down where it was picked up takes the class off and returns before #send, so
there is no send downstream to paint in its place."""
page, errors = open_page(browser, serve(BOARD_PAGE))
grip = page.locator("#card-heater .lf-grip")
grip.focus()
for key in ["Enter", "ArrowRight", "Enter"]:
page.keyboard.press(key)
round_trip(page)
# The move carries a FLIP, and a box measured across it is a box the card has
# already left: the press lands on the grip at that instant and the pointer is
# somewhere else by the mousemove after it, so the drag never starts and the
# failure arrives as the assertion below timing out on a page nobody dragged.
page.wait_for_function(
"() => document.getElementById('card-heater').getAnimations().length === 0"
)
expect(page.locator(".lf-keyline")).to_contain_text("undo")
sent = _traffic(page).sends

# The card the keyboard move left the grip focused on, so the mousedown lands on
# an already-focused control and fires no focusin — the paint under test is the
# only one that could clear the offer. Held inside the column it now has to
# itself, so the drop reorders nothing and onEnd returns before #send.
box = grip.bounding_box()
start = (
math.floor(box["x"] + box["width"] / 2),
math.floor(box["y"] + box["height"] / 2),
)
page.mouse.move(*start)
page.mouse.down()
page.mouse.move(start[0], start[1] + 24, steps=8) # past fallbackTolerance
page.wait_for_selector("lf-board.lf-dragging") # the gesture is live in the page
# Read once, on the frame the paint coalesces to, rather than through `expect`:
# a poll two seconds out repaints the line whatever this drag did, so an
# assertion that re-asks passes on the poll and says nothing about the edge.
assert "undo" not in _painted_line(page), (
"the line offered a press the dispatcher refuses for the length of a drag"
)

page.mouse.up()
assert page.locator("lf-board.lf-dragging").count() == 0
assert "undo" in _painted_line(page), (
"the drop that sent nothing left the line refusing a press that is live"
)
assert _traffic(page).sends == sent, "the drop that moved nothing sent a move"
expect(page.locator("#col-done #card-heater")).to_have_count(1)
assert errors == []
page.close()


def test_z_walks_back_through_gestures_rather_than_toggling_one(browser, serve):
"""The walk steps past what it has already taken and reaches the gesture before
it — the edit here, whose authored text comes back with its paragraphs, where
Expand Down