Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1777,6 +1777,13 @@ document listener, and an `aria-expanded` control fires nothing anywhere. Both
keep that state in an attribute, so one `MutationObserver` over `open` and
`aria-expanded` repaints for both, and `shadowStage` hands it each root.

State, and not the write that carries it: the watch compares each record against
the attribute's current value and repaints only where the two differ. The paint
restates both attributes on the controls it owns, so a watch reading every record
as news repaints for its own writing, and the page runs at its refresh rate with
nobody touching it. Nothing on screen says so — what said it was the suite, whose
every browser test paid for it until the run went over its bound.

### Standing somewhere

Focus is the reader's current place. `focused` follows it through declared
Expand Down
12 changes: 11 additions & 1 deletion plugins/leaf/skills/leaf/assets/leaf.js
Original file line number Diff line number Diff line change
Expand Up @@ -460,11 +460,21 @@ function receiveState(...args) {

// Where a disclosure keeps which way it stands, in both spellings. Declared up here
// because `shadowStage` calls it, far above the key line it repaints for.
const disclosureWatch = new MutationObserver(() => paintHere());
// A write that says what the attribute already said is not a disclosure changing, and
// taking it for one closes a loop: paintCoreControls paints `aria-expanded` on the key
// line's More control, so every paint scheduled the next one and the page repainted for
// as long as it was open. Reading the old value is what tells the two apart. A real
// toggle still arrives, including one that lands back where it started, because the
// record for its return leg carries the other value.
const disclosureWatch = new MutationObserver((records) => {
if (records.some((r) => r.target.getAttribute(r.attributeName) !== r.oldValue))
paintHere();
});
const watchDisclosures = (root) =>
disclosureWatch.observe(root, {
subtree: true,
attributeFilter: ["open", "aria-expanded"],
attributeOldValue: true,
});
createShadowStage(watchDisclosures);

Expand Down
45 changes: 45 additions & 0 deletions tests/test_render_navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2958,6 +2958,51 @@ def test_the_walk_reaches_more_and_goes_on_after_the_line_has_repainted(browser,
page.close()


def test_a_page_at_rest_repaints_the_key_line_only_when_the_state_moves(browser, serve):
"""A repaint that schedules the next one is a loop no surface reports.

`paintCoreControls` runs inside `paintHere` and writes what the More control
currently says, `aria-expanded` among it. The runtime watches `open` and
`aria-expanded` over the whole document, because those two attributes are how both
spellings of a disclosure keep which way they stand, and it repaints the line for
either. So the paint delivered its own write back to itself and asked for another
frame, and the page went on repainting for as long as it was open — every browser
test on every page paying for it, which is where it showed: the nightly suite ran
half again as long and the run went over its bound with a fifth of the tests unread.

Nothing on screen says so, which is why the reading is the page's own frames against
its own state applications. Every application repaints the line and says so through
`lf-actions`, the heartbeat's re-application of state the page already holds
included, so a line that repaints more often than the state moves is repainting for
a reason the page has not got."""
page, errors = open_page(browser, serve(NOTED_PAGE, comments=2))
page.evaluate(
"""() => {
const probe = { frames: 0, paints: 0, applied: 0 };
window.__lfProbe = probe;
document.addEventListener("lf-actions", () => { probe.applied += 1; });
new MutationObserver(() => { probe.paints += 1; }).observe(
document.querySelector(".lf-keyline"),
{ attributes: true, childList: true, subtree: true },
);
const tick = () => { probe.frames += 1; requestAnimationFrame(tick); };
requestAnimationFrame(tick);
}"""
)
# The window is the page's own frames rather than a duration: a loop of this shape
# repaints once per frame whatever the machine's speed, so counting frames is what
# makes the contrast the same size on a loaded runner as on a desk.
page.wait_for_function("() => window.__lfProbe.frames >= 90")
probe = page.evaluate("() => window.__lfProbe")

assert probe["paints"] <= probe["applied"] + 1, (
"the key line repainted without the state moving over "
f"{probe['frames']} frames: {probe}"
)
assert errors == []
page.close()


def test_escape_backs_out_from_a_control_nothing_is_typed_into(browser, serve):
"""A scope takes the keys it uses, so a control that has no Escape of its own
leaves the rung standing behind it. The banner's version chooser swallowed it,
Expand Down
Loading