diff --git a/skills/leaf/CLAUDE.md b/skills/leaf/CLAUDE.md index 2bfe985ee..e3a788fa3 100644 --- a/skills/leaf/CLAUDE.md +++ b/skills/leaf/CLAUDE.md @@ -1153,21 +1153,23 @@ into the words as a `` (BAKE), the highlight registry being script state no file can hold. The bar a selection or keyboard-selected item raises is `.lf-fab-bar`: the durable, -compact `.lf-fab-input` followed by one response ellipsis. Naming a target opens and -focuses that field immediately; it grows in place and never transfers text into a -second composer card. Enter sends and Shift-Enter inserts a newline. Tab changes the -same bar into Comment, Suggest when the anchor is a quote, and the layer's reaction -tokens. `.lf-response-control` keeps the field and every choice on one baseline with -one type, border, and elevation; the bar keeps its DOM owner and accessible name while -its contents change. Comment restores the field and Suggest restores it in -replacement-text mode. - -`showFab` places the bar; `openComposer` binds its field to the durable draft and focuses -it. `selectResponseTarget` does both for a keyboard item hint, and the ⌥ press uses -`focusTargetComment` on the same captured anchor. Automatic passage selection opens that -passage's own durable draft; these explicit Comment gestures carry unsent words onto the -new anchor. Submitted words still in flight remain owned by their original anchor, while -a later target starts clean and keeps focus. For a page +compact `.lf-fab-input` followed by one response ellipsis. An explicit item target opens +and focuses that field immediately. Selecting a passage opens the field without taking +focus or collapsing the browser selection; the reader can still copy the selection or use +its native context menu, then enter the field with Comment. The field grows in place and +never transfers text into a second composer card. Enter sends and Shift-Enter inserts a +newline. Tab changes the same bar into Comment, Suggest when the anchor is a quote, and +the layer's reaction tokens. `.lf-response-control` keeps the field and every choice on +one baseline with one type, border, and elevation; the bar keeps its DOM owner and +accessible name while its contents change. Comment restores the field and Suggest +restores it in replacement-text mode. + +`showFab` places the bar; `openComposer` binds its field to the durable draft and takes the +focus decision. `selectResponseTarget` focuses it for a keyboard item hint, and the ⌥ press +uses `focusTargetComment` on the same captured anchor. Automatic passage selection opens +that passage's own durable draft without moving focus; explicit Comment gestures carry +unsent words onto the new anchor. Submitted words still in flight remain owned by their +original anchor, while a later target starts clean and keeps focus. For a page target, `r` contributes Comment, Suggest where available, and the reaction Buttons to that target's existing Button options. Those temporary Buttons borrow the cluster's room and dock with it when necessary; they do not claim permanent rail width. A thread-local @@ -1923,6 +1925,10 @@ it owns the press, and stops. A `native` row runs and stops the scope walk but leaves that default intact. A focused widget may shadow a page key without either scope naming the other. +Leaf must not block standard platform or browser shortcuts. A handler prevents a default +only after a Leaf command owns the complete modified press; secondary clicks and the +native context menu remain the browser's too. + `claims` lists platform keys a scope consumes even when no registered row answers them. A text entry scope uses `takesLetters` and claims character keys plus the keys that edit that specific control: Enter, deletion, caret movement, Home/End, and page @@ -2562,12 +2568,13 @@ surface. Ordinary boxes send with `Mod+Enter`; the compact anchored composer pas textarea growth through `field-sizing: content`. Script does not measure or set textarea height. -The selection composer keeps its passage painted after focus moves into the textarea. +The selection composer keeps its passage painted after an explicit Comment gesture moves +focus into the textarea. Automatic passage selection leaves the native selection in place. Its `.lf-composer` wrapper contributes state and draft machinery through `display: contents`; only `.lf-fab-input` draws. `showComposer` states the whole visible -and focus outcome from `composerOpen`, `pendingAnchor`, and `fabAnchor`. Outside clicks -and Escape hide without discarding words. A successful send or an explicit draft close -discards the local record. +outcome from `composerOpen`, `pendingAnchor`, and `fabAnchor`; `openComposer`'s `focus` +option decides focus independently. Outside clicks and Escape hide without discarding +words. A successful send or an explicit draft close discards the local record. An accepted anchored comment opens its inline thread. When the reserved margin is too narrow, that thread may cover the page in its bounded card; it does not substitute the diff --git a/skills/leaf/assets/leaf.js b/skills/leaf/assets/leaf.js index dd32262da..38823defd 100644 --- a/skills/leaf/assets/leaf.js +++ b/skills/leaf/assets/leaf.js @@ -2140,10 +2140,7 @@ const COMPOSER = { does: "Close the composer, keeping the draft", line: "close — draft kept", promoteEscape: false, - run: () => { - hideComposer(); - showFab(null); - }, + run: dismissFab, }, ], }; diff --git a/skills/leaf/assets/runtime/composing/selection.js b/skills/leaf/assets/runtime/composing/selection.js index cd3d709a7..e70c89bff 100644 --- a/skills/leaf/assets/runtime/composing/selection.js +++ b/skills/leaf/assets/runtime/composing/selection.js @@ -183,8 +183,9 @@ export function createSelectionComposer(runtime, dependencies) { // control inside the response bar; the other composer controls stay hidden there. composer.style.display = open ? "contents" : "none"; composer.toggleAttribute("data-lf-open", open); - // The reader's own selection is gone by now — focusing a textarea drops it — so this - // mark is the only thing left pointing at the passage being quoted. + // An explicit Comment gesture focuses the textarea and drops the native selection, so + // this mark then becomes the durable pointer to the quoted passage. Automatic passage + // selection leaves both readings standing until the reader enters the field. paintAnchors(); paintHere(); } @@ -198,7 +199,12 @@ export function createSelectionComposer(runtime, dependencies) { function openComposer( anchor, text, - { suggest = false, about = designIsOn() ? "layer" : null, carry = false } = {}, + { + suggest = false, + about = designIsOn() ? "layer" : null, + carry = false, + focus = true, + } = {}, ) { closeReactions(); if (composerInput.value === seededQuote) composerInput.value = ""; @@ -234,7 +240,7 @@ export function createSelectionComposer(runtime, dependencies) { showComposer(true); showFab(anchor); syncComposer(); - composerInput.focus(); + if (focus) composerInput.focus(); watchComposer(); // Programmatic carrying fires no input event, so persist that one move explicitly. // An automatically opened empty field has no draft to save; its first edit does. diff --git a/skills/leaf/assets/runtime/composing/surface.js b/skills/leaf/assets/runtime/composing/surface.js index a825d4822..e9028406c 100644 --- a/skills/leaf/assets/runtime/composing/surface.js +++ b/skills/leaf/assets/runtime/composing/surface.js @@ -334,7 +334,11 @@ export function createSelectionSurface({ const sel = pageSelection(); const anchor = sel ? selectionAnchor(sel) : null; if (anchor?.quote.length >= MIN_QUOTE) { - openComment(anchor, ""); + // Selecting words is still the browser's gesture. Open Leaf's response field beside + // them without moving focus into it, so the live Selection remains available to Copy + // and the native context menu. An explicit Comment press uses the same field and + // focuses it through focusFabComment below. + openComment(anchor, "", { focus: false }); } else if (fabAnchor?.quote && !fabHoldsCapturedPassage()) showFab(null); } // Where the pointer stopped is not the question; where the selection is, is. The guard @@ -363,12 +367,12 @@ export function createSelectionSurface({ let actionPress = false; let targetActivation = false; let fabInputTakingFocus = false; - function openComment(...args) { + function openComment(anchor, text, options = {}) { // Chromium may collapse the native page Selection before dispatching the textarea's // focus event. Mark the handoff first so that intermediate selectionchange cannot // dismiss the durable anchor the composer is opening on. - fabInputTakingFocus = true; - return openComposer(...args); + fabInputTakingFocus = options.focus !== false; + return openComposer(anchor, text, options); } function fabHoldsCapturedPassage() { return fabInputTakingFocus || fabBar.contains(document.activeElement); diff --git a/tests/render_cases_navigation.py b/tests/render_cases_navigation.py index 573965ebc..bdf7fb3ff 100644 --- a/tests/render_cases_navigation.py +++ b/tests/render_cases_navigation.py @@ -794,6 +794,9 @@ def compose(page, passage, text=None): key is built from is the same in both tabs.""" page.locator(passage).scroll_into_view_if_needed() page.locator(passage).click(click_count=3) + expect(page.locator(".lf-fab-bar")).to_be_visible() + expect(page.locator(".lf-fab-input")).not_to_be_focused() + page.keyboard.press("c") expect(page.locator(".lf-fab-input")).to_be_focused() if text is not None: page.locator(".lf-fab-input").fill(text) diff --git a/tests/test_render_anchors.py b/tests/test_render_anchors.py index eceaa9c30..07286e0bf 100644 --- a/tests/test_render_anchors.py +++ b/tests/test_render_anchors.py @@ -440,10 +440,10 @@ def test_a_widgets_label_takes_a_comment_inside_the_control_it_labels(browser, s def test_a_selection_around_a_targets_buttons_does_not_deaden_them(browser, serve): - """A drag around a target opens the immediate field without deadening its Buttons. + """A drag around a target offers Comment without deadening its Buttons. - Focus collapses the browser's native selection, so the captured passage must remain - durable while the pointer path through `…` and a direct keyboard action both work.""" + The browser's native selection remains available while the pointer path through `…` + and a direct keyboard action both work.""" page, errors = open_page(browser, serve(SUGGESTION_PAGE)) # Across the two paragraphs, so the row deciding the first is inside the selection. start = page.locator("#replace").bounding_box() @@ -455,7 +455,7 @@ def test_a_selection_around_a_targets_buttons_does_not_deaden_them(browser, serv steps=16, ) assert "Refill" in pending_text(page) - expect(page.locator(".lf-fab-input")).to_be_focused() + expect(page.locator(".lf-fab-input")).not_to_be_focused() item = page.locator("[data-lf-for='sug-refill']").locator("xpath=..") item.locator(":scope > .lf-margin-more").click() @@ -659,8 +659,7 @@ def test_a_drag_released_mid_word_hugs_words_and_sentences(browser, serve): drag must stay in one rendered block; matching endpoints around a nested paragraph do not make the intervening blocks one sentence. - The reads use Leaf's pending highlight because focus moves into the immediate - comment field and therefore collapses the browser's native selection.""" + The reads use Leaf's pending highlight, the durable form of the native selection.""" page, errors = open_page( browser, serve( @@ -696,7 +695,7 @@ def spot(root, word, into): return page.evaluate(mid, {"root": root, "word": word, "into": into}) def captured(): - expect(page.locator(".lf-fab-input")).to_be_focused() + expect(page.locator(".lf-fab-input")).to_be_visible() quoted = composer_quote(page)["text"] return quoted[1:-1] @@ -755,6 +754,7 @@ def captured(): }""") page.keyboard.press("Shift") assert captured() == "ragra" + page.locator("#t").click() page.evaluate("""() => { const n = document.querySelector('#p').firstChild; @@ -2241,6 +2241,7 @@ def test_an_ambiguous_revised_passage_detaches_instead_of_guessing(browser, serv const fab = document.querySelector('.lf-fab-input'); if (fab.style.display !== 'block') return 'no button'; await new Promise(r => setTimeout(r, 40)); + fab.focus(); document.querySelector('.lf-composer textarea').value = 'is this idempotent?'; document.querySelector('.lf-composer textarea') .dispatchEvent(new Event('input', {bubbles: true})); @@ -2436,7 +2437,7 @@ def test_a_passage_longer_than_the_pattern_is_anchored_whole(browser, serve): steps=12, ) expect(page.locator(".lf-fab-input")).to_be_visible() - expect(page.locator(".lf-fab-input")).to_be_focused() + expect(page.locator(".lf-fab-input")).not_to_be_focused() expect(page.locator(".lf-composer")).to_be_visible() # The mark under the open composer is the selection, both ends of it — and on the @@ -2495,7 +2496,7 @@ def test_a_selection_of_the_whole_page_still_finds_its_passage(browser, serve): page.keyboard.press("ControlOrMeta+a") expect(page.locator(".lf-fab-input")).to_be_visible() - expect(page.locator(".lf-fab-input")).to_be_focused() + expect(page.locator(".lf-fab-input")).not_to_be_focused() expect(page.locator(".lf-composer")).to_be_visible() painted = page.evaluate( "() => [...(CSS.highlights.get('lf-pending') ?? [])]" @@ -2538,6 +2539,7 @@ def test_one_neighbour_is_not_enough_to_identify_a_revised_comment(browser, serv const fab = document.querySelector('.lf-fab-input'); if (fab.style.display !== 'block') return 'no button'; await new Promise(r => setTimeout(r, 40)); + fab.focus(); const box = document.querySelector('.lf-composer textarea'); box.value = 'does this hold?'; box.dispatchEvent(new Event('input', {bubbles: true})); @@ -3343,7 +3345,7 @@ def route(request): added.click(modifiers=["Alt"]) expect(page.locator(".lf-fab-bar")).to_be_visible() expect(page.locator(".lf-fab-input")).to_be_focused() - page.locator(".lf-composer textarea").fill("Review the whole added line.") + page.locator(".lf-fab-input").fill("Review the whole added line.") page.keyboard.press("Enter") round_trip(page) page.get_by_role("button", name=re.compile("^Threads")).click() @@ -3398,8 +3400,9 @@ def route(request): "endsAtTokenStart": True, }, selected expect(page.locator(".lf-fab-bar")).to_be_visible() - expect(page.locator(".lf-fab-input")).to_be_focused() - page.locator(".lf-composer textarea").fill("Review this expression.") + expect(page.locator("#lf-composer-quote")).to_contain_text("“request.token.id”") + expect(page.locator(".lf-fab-input")).not_to_be_focused() + page.locator(".lf-fab-input").fill("Review this expression.") page.keyboard.press("Enter") round_trip(page) expect(page.locator(".lf-thread .lf-quote").nth(1)).to_have_text( diff --git a/tests/test_render_drafts.py b/tests/test_render_drafts.py index 49a4dd7aa..e295464c1 100644 --- a/tests/test_render_drafts.py +++ b/tests/test_render_drafts.py @@ -77,7 +77,7 @@ def test_page_round_trip(browser, serve): page.evaluate("window.__leafJourneyDocument = 'held'") # Select the passage from the keyboard's path: a real Range, then the keyup - # the runtime watches for keyboard selections. The immediate field takes focus. + # the runtime watches for keyboard selections. Comment explicitly enters its field. page.evaluate("""() => { const r = document.createRange(); r.selectNodeContents(document.getElementById('intro')); @@ -88,6 +88,8 @@ def test_page_round_trip(browser, serve): page.wait_for_selector( ".lf-fab-input", state="visible" ) # the selection raised the button + expect(page.locator(".lf-fab-input")).not_to_be_focused() + page.keyboard.press("c") expect(page.locator(".lf-fab-input")).to_be_focused() page.wait_for_selector(".lf-composer", state="visible") page.locator(".lf-composer textarea").fill("Is 0041 idempotent?") @@ -916,13 +918,10 @@ def test_an_untouched_inline_reply_follows_but_an_emptied_draft_holds(browser, s def test_a_held_comment_send_leaves_the_passage_picked_out_behind_it(browser, serve): - """The same reading of a later gesture, for the other gesture a reader can have - standing. A comment's send ends by handing typing to the thread it became, and a - round trip is how long that step takes to arrive; focusing a box collapses whatever - the page had selected. So a reader who picked out their next passage while the send - was in the wire had it taken back — silently, because nothing re-decides the 💬 - until they gesture again, and the words in front of them simply stop being - something to comment on. + """A comment's send must not take a newer passage selection with its focus handoff. + + The newer selection remains native and keeps its response field available while the + earlier send becomes a thread behind it. Held rather than raced: the window is one request's flight, and a machine quick enough closes it before the next gesture. A loaded CI runner is not, and it said so @@ -941,8 +940,8 @@ def test_a_held_comment_send_leaves_the_passage_picked_out_behind_it(browser, se # The reader picks out their next passage while the first send is still in the wire. page.locator("#p2").click(click_count=3) expect(page.locator(".lf-fab-input")).to_be_visible() - expect(page.locator(".lf-fab-input")).to_be_focused() expect(page.locator(".lf-fab-input")).to_have_value("") + expect(page.locator(".lf-fab-input")).not_to_be_focused() held[0].continue_() page.unroute("**/api/event") @@ -957,7 +956,7 @@ def test_a_held_comment_send_leaves_the_passage_picked_out_behind_it(browser, se assert pending_text(page) == "A short second passage.", ( "the send's landing lost the passage the reader had picked out" ) - expect(page.locator(".lf-fab-input")).to_be_focused() + expect(page.locator(".lf-fab-input")).not_to_be_focused() expect(page.locator(".lf-composer")).to_be_visible() assert composer_quote(page)["text"].strip("“”") == "A short second passage." assert errors == [] @@ -976,12 +975,13 @@ def test_an_unsent_comment_stays_with_its_passage_when_another_is_selected( original = "These words belong to the first passage." page.locator("#p1").click(click_count=3) - expect(field).to_be_focused() + expect(field).to_be_visible() + expect(field).not_to_be_focused() field.fill(original) page.locator("#p2").click(click_count=3) - expect(field).to_be_focused() expect(field).to_have_value("") + expect(field).not_to_be_focused() assert ( page.evaluate( """() => Object.keys(localStorage) @@ -991,8 +991,8 @@ def test_an_unsent_comment_stays_with_its_passage_when_another_is_selected( ) page.locator("#p1").click(click_count=3) - expect(field).to_be_focused() expect(field).to_have_value(original) + expect(field).not_to_be_focused() assert errors == [] page.close() diff --git a/tests/test_render_navigation.py b/tests/test_render_navigation.py index d224aaa66..46bcb865b 100644 --- a/tests/test_render_navigation.py +++ b/tests/test_render_navigation.py @@ -3889,6 +3889,9 @@ def test_the_other_response_row_can_turn_the_compact_field_into_a_suggestion( page, errors = open_page(browser, serve(INLINE_PAGE)) page.locator("#p").click(click_count=3) box = page.locator(".lf-fab-input") + expect(page.locator(".lf-fab-bar")).to_be_visible() + expect(box).not_to_be_focused() + page.keyboard.press("c") expect(box).to_be_focused() expect(box).to_have_attribute("placeholder", re.compile(r"^Comment… .*⏎$")) @@ -3912,6 +3915,51 @@ def test_the_other_response_row_can_turn_the_compact_field_into_a_suggestion( page.close() +def test_a_passage_selection_keeps_native_copy_and_context_menu(browser, serve): + """Leaf may offer a response without taking the browser's selection gestures.""" + context = browser.new_context( + viewport={"width": 1200, "height": 900}, + permissions=["clipboard-read", "clipboard-write"], + ) + try: + page, errors = open_page(browser, serve(INLINE_PAGE), context=context) + paragraph = page.locator("#p") + paragraph.click(click_count=3) + expect(page.locator(".lf-fab-bar")).to_be_visible() + + selected = page.evaluate("() => getSelection().toString()") + assert "A paragraph carrying" in selected + is_mac = page.evaluate( + "() => /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent)" + ) + page.keyboard.press("Meta+c" if is_mac else "Control+c") + assert page.evaluate("() => navigator.clipboard.readText()") == selected + + page.evaluate( + """() => { + window.lfContextMenu = null; + document.addEventListener('contextmenu', event => { + setTimeout(() => { + window.lfContextMenu = { + prevented: event.defaultPrevented, + selection: getSelection().toString(), + }; + }); + }, {capture: true, once: true}); + }""" + ) + paragraph.click(button="right") + page.wait_for_function("() => window.lfContextMenu !== null") + assert page.evaluate("() => window.lfContextMenu") == { + "prevented": False, + "selection": selected, + } + assert errors == [] + page.close() + finally: + context.close() + + def test_focus_paint_releases_every_text_box_crossed_before_a_frame(browser, serve): """A synchronous input sync cannot hide an intermediate focus from repaint.""" page, errors = open_page(browser, serve(INLINE_PAGE, comments=2)) @@ -3938,10 +3986,10 @@ def test_focus_paint_releases_every_text_box_crossed_before_a_frame(browser, ser page.close() -def test_the_key_line_names_the_immediate_comment_and_its_other_responses( +def test_the_key_line_names_the_selected_comment_and_its_other_responses( browser, serve ): - """Targeting enters Comment immediately; the key line names send and the Tab exit.""" + """Comment enters a selected passage's field; the line names send and Tab exit.""" page, errors = open_page(browser, serve(TARGETS_PAGE)) line = page.locator(".lf-keyline") help_el = page.locator(".lf-help") @@ -3953,8 +4001,9 @@ def test_the_key_line_names_the_immediate_comment_and_its_other_responses( expect(help_el).to_contain_text("Go to the threads") page.keyboard.press("Escape") - # A real selection enters its field during the gesture. Once there, letters and `?` - # belong to the comment rather than falling through to page shortcuts. + # A real selection keeps the browser selection until Comment explicitly enters its + # field. Once there, letters and `?` belong to the comment rather than falling through + # to page shortcuts. box = page.locator("#prose").bounding_box() select( page, @@ -3963,6 +4012,9 @@ def test_the_key_line_names_the_immediate_comment_and_its_other_responses( steps=12, ) field = page.locator(".lf-fab-input") + expect(page.locator(".lf-fab-bar")).to_be_visible() + expect(field).not_to_be_focused() + page.keyboard.press("c") expect(field).to_be_focused() expect(line).to_contain_text("comment") expect(line).to_contain_text("other responses") @@ -3982,8 +4034,8 @@ def test_the_key_line_names_the_immediate_comment_and_its_other_responses( page.close() -def test_typing_in_the_immediate_comment_wins_over_page_shortcuts(browser, serve): - """Once targeting focuses Comment, a shortcut letter is ordinary comment text.""" +def test_typing_in_a_selected_comment_wins_over_page_shortcuts(browser, serve): + """Once Comment focuses a selected passage's field, shortcut letters are text.""" page, errors = open_page(browser, serve(TARGETS_PAGE)) box = page.locator("#prose").bounding_box() @@ -3994,6 +4046,9 @@ def test_typing_in_the_immediate_comment_wins_over_page_shortcuts(browser, serve steps=12, ) fab = page.locator(".lf-fab-input") + expect(page.locator(".lf-fab-bar")).to_be_visible() + expect(fab).not_to_be_focused() + page.keyboard.press("c") expect(fab).to_be_focused() page.keyboard.press("c") expect(fab).to_have_value("c") @@ -4047,6 +4102,9 @@ def test_submit_shortcuts_activate_the_controls_that_promise_the_action(browser, ) composer = page.locator(".lf-composer") field = page.locator(".lf-fab-input") + expect(page.locator(".lf-fab-bar")).to_be_visible() + expect(field).not_to_be_focused() + page.keyboard.press("c") expect(field).to_be_focused() send = composer.locator(".lf-composer-row .primary") send.evaluate( diff --git a/tests/test_render_options.py b/tests/test_render_options.py index b53ec86f3..616761901 100644 --- a/tests/test_render_options.py +++ b/tests/test_render_options.py @@ -1119,7 +1119,8 @@ def test_working_the_evidence_in_an_option_is_not_a_pick(browser, serve): "failure reads off the list instead of costing" ) page.mouse.up() - expect(page.locator(".lf-fab-input")).to_be_focused() + expect(page.locator(".lf-fab-bar")).to_be_visible() + expect(page.locator(".lf-fab-input")).not_to_be_focused() assert not option.evaluate(picked), "selecting the option's evidence answered it" assert [e for e in sent_events(serve.page_dir) if e["kind"] == "action"] == [], ( diff --git a/tests/test_render_options_settled.py b/tests/test_render_options_settled.py index 1a0968552..326136cc9 100644 --- a/tests/test_render_options_settled.py +++ b/tests/test_render_options_settled.py @@ -115,7 +115,8 @@ def test_settled_options_collapse_without_going_out_of_reach(browser, serve): box = title.bounding_box() y = box["y"] + box["height"] / 2 select(page, (box["x"] + 2, y), (box["x"] + box["width"] - 2, y)) - expect(page.locator(".lf-fab-input")).to_be_focused() + expect(page.locator("#lf-composer-quote")).to_have_text("“Settled: Lax cookie”") + expect(page.locator(".lf-fab-input")).not_to_be_focused() assert composer_quote(page)["text"].strip("“”") == "Settled: Lax cookie" expect(page.locator("#opt-strict")).to_be_hidden() expect(page.locator(".lf-composer")).to_be_visible() diff --git a/tests/test_render_outbox.py b/tests/test_render_outbox.py index da11c3c31..60ac25c89 100644 --- a/tests/test_render_outbox.py +++ b/tests/test_render_outbox.py @@ -2175,7 +2175,8 @@ def test_opening_the_panel_stands_down_the_field_without_losing_its_draft( page.get_by_role("button", name="Close threads").click() page.locator("#p30").click(click_count=3) - expect(page.locator(".lf-fab-input")).to_be_focused() + expect(page.locator(".lf-fab-input")).to_be_visible() + expect(page.locator(".lf-fab-input")).not_to_be_focused() expect(page.locator(".lf-fab-input")).to_have_value( "held open across the panel opening" ) @@ -2221,10 +2222,11 @@ def test_a_draft_that_outlives_its_passage_returns_with_that_passage(browser, se page.goto(url) page.wait_for_selector("#p") page.locator("#p").click(click_count=3) - expect(page.locator(".lf-fab-input")).to_be_focused() + expect(page.locator("#lf-composer-quote")).to_have_text(f"“{passage}”") expect(page.locator(".lf-fab-input")).to_have_value( "half-written when the version turned over" ) + expect(page.locator(".lf-fab-input")).not_to_be_focused() quote = composer_quote(page) assert quote["text"] == f"“{passage}”", f"the quote says {quote['text']!r}" assert errors == [] diff --git a/tests/test_render_projection.py b/tests/test_render_projection.py index 6e51e2cea..0c957238f 100644 --- a/tests/test_render_projection.py +++ b/tests/test_render_projection.py @@ -215,7 +215,8 @@ def test_pr_review_package_keeps_the_authors_brief_distinct_and_stable(browser, "passed" ) assert card.evaluate("el => el.__reviewIdentity") is True - expect(page.locator(".lf-fab-input")).to_be_focused() + expect(page.locator("#lf-composer-quote")).to_contain_text(f"“{selected}”") + expect(page.locator(".lf-fab-input")).not_to_be_focused() resized(page, 390, 900) assert page.evaluate( @@ -341,7 +342,8 @@ class Limiter: "gateway/limits.py:40" ) assert lines.nth(2).evaluate("el => el.__callIdentity") is True - expect(page.locator(".lf-fab-input")).to_be_focused() + expect(page.locator("#lf-composer-quote")).to_contain_text(f"“{selected}”") + expect(page.locator(".lf-fab-input")).not_to_be_focused() page.keyboard.press("Escape") expect(page.locator("#patch [data-line-type]")).to_have_count(0) diff --git a/tests/test_render_reactions.py b/tests/test_render_reactions.py index 5114f171c..7ba437223 100644 --- a/tests/test_render_reactions.py +++ b/tests/test_render_reactions.py @@ -138,6 +138,8 @@ def test_a_token_press_marks_the_passage_and_a_second_press_takes_it_back( ) assert tokens == ["ok", "no", "lost", "cut", "more", "this"], tokens expect(bar.locator(".lf-fab-input")).to_be_visible() + expect(bar.locator(".lf-fab-input")).not_to_be_focused() + page.keyboard.press("c") expect(bar.locator(".lf-fab-input")).to_have_attribute( "placeholder", re.compile(r"^Comment… .*⏎$") ) @@ -255,6 +257,8 @@ def test_tab_changes_the_compact_bar_in_place_and_r_still_needs_a_target( select_paragraph(page, "#how-cap") bar = page.locator(".lf-fab-bar") expect(bar).to_be_visible() + expect(bar.locator(".lf-fab-input")).not_to_be_focused() + page.keyboard.press("c") field_reading = bar.locator(".lf-fab-input").evaluate( """el => { const box = el.getBoundingClientRect(); const bar = el.closest('.lf-fab-bar'); const style = getComputedStyle(el); diff --git a/tests/test_render_semantic_selection.py b/tests/test_render_semantic_selection.py index ee3b35ca0..dbf7764ab 100644 --- a/tests/test_render_semantic_selection.py +++ b/tests/test_render_semantic_selection.py @@ -170,6 +170,9 @@ def test_a_passage_still_offers_suggest_when_the_layer_has_no_reactions(browser, prose = page.locator("#prose") prose.select_text() field = page.locator(".lf-fab-input") + expect(page.locator(".lf-fab-bar")).to_be_visible() + expect(field).not_to_be_focused() + page.keyboard.press("c") expect(field).to_be_focused() expect(page.locator(".lf-fab-bar .lf-react")).to_have_count(0) page.keyboard.press("Tab") @@ -466,8 +469,9 @@ def test_slash_finds_page_text_without_a_target_kind(browser, serve): page.keyboard.press("Enter") expect(page.locator(".lf-target-search")).to_be_hidden() - expect(page.locator(".lf-fab-input")).to_be_focused() + expect(page.locator(".lf-fab-input")).not_to_be_focused() expect(page.locator(".lf-composer")).to_be_visible() + assert page.evaluate("() => getSelection().toString()") == "button the key" assert pending_text(page) == "button the key" assert errors == [] page.close() @@ -608,7 +612,8 @@ def test_selection_search_brings_an_offscreen_match_into_view(browser, serve): expect(page.get_by_role("searchbox", name="Search page text")).to_be_focused() page.keyboard.press("Enter") - expect(page.locator(".lf-fab-input")).to_be_focused() + expect(page.locator(".lf-fab-input")).not_to_be_focused() + assert page.evaluate("() => getSelection().toString()") == "distant phrase" assert pending_text(page) == "distant phrase" assert errors == [] page.close() @@ -642,7 +647,8 @@ def test_selection_search_scrolls_to_the_match_inside_a_tall_text_block(browser, assert mark["y"] > 42 and mark["y"] + mark["height"] < keyline_top page.keyboard.press("Enter") - expect(page.locator(".lf-fab-input")).to_be_focused() + expect(page.locator(".lf-fab-input")).not_to_be_focused() + assert page.evaluate("() => getSelection().toString()") == "copper needle" assert pending_text(page) == "copper needle" assert errors == [] page.close() @@ -809,7 +815,8 @@ def test_selection_search_opens_when_the_viewport_has_no_hint_targets(browser, s expect(page.locator(".lf-target-match")).not_to_have_count(0) page.keyboard.press("Enter") - expect(page.locator(".lf-fab-input")).to_be_focused() + expect(page.locator(".lf-fab-input")).not_to_be_focused() + assert page.evaluate("() => getSelection().toString()") == "phrase only appears" assert pending_text(page) == "phrase only appears" assert errors == [] page.close() diff --git a/tests/test_render_startup.py b/tests/test_render_startup.py index 6a15347c5..944fe79c8 100644 --- a/tests/test_render_startup.py +++ b/tests/test_render_startup.py @@ -2662,7 +2662,8 @@ def test_a_captured_source_stays_pointable_and_frozen_in_an_export( ) assert bounds is not None select(page, (bounds["left"] + 1, bounds["y"]), (bounds["right"] - 1, bounds["y"])) - expect(page.locator(".lf-fab-input")).to_be_focused() + expect(page.locator("#lf-composer-quote")).to_have_text("“Original instructions.”") + expect(page.locator(".lf-fab-input")).not_to_be_focused() assert composer_quote(page)["text"].strip("“”") == "Original instructions." page.locator(".lf-composer textarea").fill("Keep this exact source.") page.keyboard.press("Enter") diff --git a/tests/test_render_widgets.py b/tests/test_render_widgets.py index 6db1bfc9a..dd4ddcae7 100644 --- a/tests/test_render_widgets.py +++ b/tests/test_render_widgets.py @@ -47,7 +47,6 @@ _until, actions, compare_with, - composer_quote, leaf_page, live_url, open_page, @@ -2347,8 +2346,8 @@ def drag(locator): box = intro.bounding_box() y = box["y"] + box["height"] / 2 select(page, (box["x"] + 2, y), (box["x"] + box["width"] - 2, y)) - expect(page.locator(".lf-fab-input")).to_be_focused() - assert "signed-cookie" in composer_quote(page)["text"] + expect(page.locator("#lf-composer-quote")).to_contain_text("signed-cookie") + expect(page.locator(".lf-fab-input")).not_to_be_focused() # Put it down again, so what follows is a rise and not a leftover. page.locator("#h").click() expect(page.locator(".lf-fab-input")).to_be_hidden()