From ea4eb0b634940dbbd7b13fbb7986f053f45a817c Mon Sep 17 00:00:00 2001 From: leaf-agent <318509791+leaf-agent@users.noreply.github.com> Date: Sun, 30 Aug 2026 23:28:04 +0000 Subject: [PATCH] Update the option-group tests for the removed seat, and ring the add field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Make reader-added answers real options` (292de9c) replaced the options group's catch-all conversation with an add-option form, dropped `x-conversation`/`x-work` from `lf-options`, took the status rule out of the option gutter, reworded the pick mark, and moved the keyboard ring from the decision onto the exact option row. Nineteen browser tests still read the old page. The durable-draft corpus and one navigation test used `#jobs`'s conversation seat as their subject; they now stand on a `lf-task talk` seat, which is the seat the shipped layer still has. Two of them asked whether the composer disappears after a send — a seat that can hold keeps it standing — so they ask of the words instead. The digit test drops the status rule from its gutter relation, the retired-slot test moves to a settled group's summary, which is the generated label that is still one of the page's words, and the ring test reads the band off the row the keyboard is on. Two product findings came out of the same walk. The add field's own outline is off, so its row has to answer the keyboard for it — and the rule was scoped to `:not([settled])`, leaving a reader who opened a settled group and tabbed into its field with nothing on screen saying where they were. And the row ring is inset on butted cells, so `RINGS_DRAWN` sampling half a pixel inside its outer edge hit-tested the seam and reported the next row as painting over it; it now samples the middle of the ring's own band. Two tests are not fixed here. `test_a_seat_conversation_leaves_the_pick_it_is _about_live` and `test_the_ring_holds_on_a_seat_the_agent_has_still_to_answer` both need a widget that is on the reader's ask list *and* seats a conversation, which no shipped widget now is. --- skills/leaf/packages/default/theme.css | 15 +++++--- tests/render_cases_interaction.py | 30 +++++++++++++--- tests/render_cases_layout.py | 17 ++++++--- tests/render_cases_navigation.py | 12 +++---- tests/render_support.py | 2 ++ tests/test_render_drafts.py | 42 +++++++++++++--------- tests/test_render_navigation.py | 40 +++++++++++---------- tests/test_render_projection.py | 48 ++++++++++++++++++-------- 8 files changed, 136 insertions(+), 70 deletions(-) diff --git a/skills/leaf/packages/default/theme.css b/skills/leaf/packages/default/theme.css index 5feb9bd94..beacb3ac1 100644 --- a/skills/leaf/packages/default/theme.css +++ b/skills/leaf/packages/default/theme.css @@ -671,10 +671,17 @@ lf-options > .lf-done[aria-pressed="true"] { color: var(--ok-ink); lf-options[choose]:not(:where([data-lf-exhibit]) *) :is(:focus, .lf-focus) ) { outline: none; } lf-options[choose]:not([settled]):not(:where([data-lf-exhibit]) *) - > :is( - lf-option:has(> .lf-pick:is(:focus-visible, .lf-focus-visible)), - .lf-another:has(input:is(:focus-visible, .lf-focus-visible)) - ) { + > lf-option:has(> .lf-pick:is(:focus-visible, .lf-focus-visible)) { + outline: var(--here-ring); --lf-here-ring: options-row; + outline-offset: calc(-1 * var(--here-ring-w)); + } + /* The add field's own outline is off (below), so the row it sits in is the only thing + * that can answer the keyboard for it — in every group that shows the field, not just + * the open ones. A settled group opened by its disclosure shows the same cell, and + * scoped to :not([settled]) this rule left a reader who had Tabbed into it with + * nothing on screen saying where they were. */ + lf-options[choose]:not(:where([data-lf-exhibit]) *) + > .lf-another:has(input:is(:focus-visible, .lf-focus-visible)) { outline: var(--here-ring); --lf-here-ring: options-row; outline-offset: calc(-1 * var(--here-ring-w)); } diff --git a/tests/render_cases_interaction.py b/tests/render_cases_interaction.py index e73bcc273..c3231c7d2 100644 --- a/tests/render_cases_interaction.py +++ b/tests/render_cases_interaction.py @@ -206,6 +206,25 @@ def panel_comment(d, text, anchor=None, author="user"): ) +# The other place a question lives: a widget that seats its own conversation +# (`x-conversation`), where the answer is words rather than a pick. The durable-draft +# tests stand on one because a seat is where a single draft has two views at once — the +# cell in the page and the row in the panel — which is the whole of what those tests +# compare. `jobs` names the seat, so the `say:jobs` draft key and a comment anchored to +# `{section: "jobs"}` are the one coordinate the panel and the page share. +SEATED_QUESTION_PAGE = leaf_page( + "seated question", + """ +

Three jobs

+ + Which jobs are worth starting? + The mounts came down in January, the bird bath froze eleven mornings, and the camera + is still in its box. + +""", +) + + def sent_events(page_dir): return [ json.loads(line) @@ -1075,9 +1094,12 @@ def drifting_widget(tmp_path, monkeypatch, deep=False, bare=False): # A suggestion whose losing slot holds a widget. lf-old takes prose, and prose takes -# widgets, so the mark on a chosen option can sit inside the half a decision removes. -# `choose`, because that is the shape that bites: a group offering a pick renders the -# mark as a press, which wears the chrome class *and* declares its word the page's. +# widgets, so a generated label can sit inside the half a decision removes. +# `settled`, because that is the shape that bites: the summary a retired group collapses +# to is written by the module and declares its words the page's (`says: true`), which is +# what lets a quote land on them. A pick mark is not that shape — it names the option for +# a listening reader and says nothing the page speaks — so a group offering a live pick +# would leave this case unexercised. RETIRED_WIDGET_PAGE = leaf_page( "retired", """ @@ -1086,7 +1108,7 @@ def drifting_widget(tmp_path, monkeypatch, deep=False, bare=False):

How should sessions travel?

- + Lax cookie The way it stands.
diff --git a/tests/render_cases_layout.py b/tests/render_cases_layout.py index 8fe58dc25..613dee0ee 100644 --- a/tests/render_cases_layout.py +++ b/tests/render_cases_layout.py @@ -1700,11 +1700,20 @@ def in_threads_scrollport(page, selector): const runX = [Math.max(ring.left, 0), Math.min(ring.right, innerWidth)]; const runY = [Math.max(ring.top, 0), Math.min(ring.bottom, innerHeight)]; const shownRun = runX[0] <= runX[1] && runY[0] <= runY[1]; + // Each run sampled in the middle of the band it is, rather than half a pixel inside + // its outer edge. Both points are on the ring; the outer one is also the last + // fraction of a pixel of the control, and hit testing rounds a subpixel edge to the + // device pixel it shares with the next box. Butted cells are where that shows: an + // options group's rows meet on a fractional line, so the ring on the row the + // keyboard is on reported the row below as painting over its bottom edge — the + // seam's rounding, not anything drawn there. Floored at half a pixel so a hairline + // ring still samples inside itself. + const into = Math.max(w / 2, 0.5); for (const [side, x, y] of ordered && shownRun ? [ - ['top', mid(...runX), ring.top + 0.5], - ['bottom', mid(...runX), ring.bottom - 0.5], - ['left', ring.left + 0.5, mid(...runY)], - ['right', ring.right - 0.5, mid(...runY)], + ['top', mid(...runX), ring.top + into], + ['bottom', mid(...runX), ring.bottom - into], + ['left', ring.left + into, mid(...runY)], + ['right', ring.right - into, mid(...runY)], ] : []) {{ if (x < 0 || y < 0 || x > innerWidth || y > innerHeight) continue; for (const over of document.elementsFromPoint(x, y)) {{ diff --git a/tests/render_cases_navigation.py b/tests/render_cases_navigation.py index 4ea2ec68b..7d041ddcb 100644 --- a/tests/render_cases_navigation.py +++ b/tests/render_cases_navigation.py @@ -112,16 +112,15 @@ # group would sit one pixel apart from the rest while the page shows them level. # # The gutter the chip stands in comes back with it, because where the chip belongs is a -# relation to the two boxes either side of it rather than a number. The status rule is the -# option's own `::before` and the prose opens at the column the option pads to, so -# `afterStatus` and `opens` are read where the theme spends them. Written as the number -# they came to, the reading would have to be re-pinned every time either neighbour moved, -# and a re-pinned number proves only that somebody ran the test. +# relation to the boxes either side of it rather than a number. The cell's own start is +# one side and the prose opens at the column the option pads to, so `opens` is read where +# the theme spends it. Written as the number it came to, the reading would have to be +# re-pinned every time either neighbour moved, and a re-pinned number proves only that +# somebody ran the test. INSIDE_ITS_OPTION = """el => { const chip = el.getBoundingClientRect(); const opt = el.parentElement.getBoundingClientRect(); const s = getComputedStyle(el.parentElement); - const status = getComputedStyle(el.parentElement, '::before'); const top = opt.y + parseFloat(s.borderTopWidth); const left = opt.x + parseFloat(s.borderLeftWidth); const bottom = opt.bottom - parseFloat(s.borderBottomWidth); @@ -130,7 +129,6 @@ return {x: chip.x - left, ends: chip.right - left, y: chip.y - top, past: chip.bottom - bottom, level: (chip.y + chip.height / 2) - words, - afterStatus: parseFloat(status.left) + parseFloat(status.width), opens: parseFloat(s.paddingInlineStart)}; }""" diff --git a/tests/render_support.py b/tests/render_support.py index 1d5192432..2e0d0463c 100644 --- a/tests/render_support.py +++ b/tests/render_support.py @@ -51,6 +51,7 @@ ROOMS, ROSTER_PAGE, SCROLL_SETTLED, + SEATED_QUESTION_PAGE, SETTLED_DECISION_PAGE, SHORT_SUGGESTION, SPECIMEN_EXAMPLES, @@ -505,6 +506,7 @@ "SCROLL_SETTLED", "SCROLL_SETTLE_MS", "SCROLL_STILL", + "SEATED_QUESTION_PAGE", "SENTENCE", "SETTLED_DECISION_PAGE", "SETTLED_PAGE", diff --git a/tests/test_render_drafts.py b/tests/test_render_drafts.py index 361ef0f8c..72f84c628 100644 --- a/tests/test_render_drafts.py +++ b/tests/test_render_drafts.py @@ -21,6 +21,7 @@ KEYS_PAGE, LONG_PAGE, NOTED_PAGE, + SEATED_QUESTION_PAGE, SENTENCE, SMOOTH_LONG_PAGE, STORED_DRAFT_SETTLED, @@ -981,7 +982,7 @@ def test_a_stale_question_first_message_cannot_append_across_tabs( proves that readable absence is settlement rather than permission to trust the old in-memory value. """ - url = serve(DECISION_PAGE) + url = serve(SEATED_QUESTION_PAGE) first, first_errors = open_page(browser, url, context=one_reader) second, second_errors = open_page( browser, @@ -1034,7 +1035,7 @@ def test_a_stale_question_first_message_cannot_append_across_tabs( def test_a_question_reply_appends_one_event_across_tabs(browser, serve, one_reader): """Both inline views may POST the shared reply; its attempt appends it once.""" - url = serve(DECISION_PAGE) + url = serve(SEATED_QUESTION_PAGE) root = events_model.append_event( serve.page_dir, { @@ -1082,7 +1083,7 @@ def test_a_held_conversation_send_cannot_clear_a_newer_raw_draft( browser, serve, one_reader ): """Settlement compares raw words, so an older POST cannot erase a later edit.""" - url = serve(DECISION_PAGE) + url = serve(SEATED_QUESTION_PAGE) root = events_model.append_event( serve.page_dir, { @@ -1139,7 +1140,7 @@ def test_a_failed_concurrent_question_send_keeps_the_accepted_attempt( """One request may lose its answer while another tab gets the same attempt accepted. The first tab adopts that durable outcome instead of reporting failure or offering the words as a second message.""" - url = serve(DECISION_PAGE) + url = serve(SEATED_QUESTION_PAGE) first, first_errors = open_page(browser, url, context=one_reader) second, second_errors = open_page(browser, url, context=one_reader) first_say = first.locator("#jobs > .lf-conversation > .lf-say") @@ -1163,8 +1164,11 @@ def test_a_failed_concurrent_question_send_keeps_the_accepted_attempt( event for event in sent_events(serve.page_dir) if event["kind"] == "comment" ] assert [event["text"] for event in roots] == [raw.strip()] - expect(first_say).to_be_hidden() - expect(second_say).to_be_hidden() + # Asked of the words rather than of the box: a seat that can hold keeps its composer + # standing after every root (renderConversations), so an empty one is what says the + # tab adopted the durable outcome instead of holding the words for a second send. + expect(first_say.locator("textarea")).to_have_value("") + expect(second_say.locator("textarea")).to_have_value("") assert first_errors == [] assert second_errors == [] @@ -1173,7 +1177,7 @@ def test_a_question_can_send_when_draft_storage_refuses_writes(browser, serve): """Persistence failure costs recovery, not the live textarea's Send action.""" page, errors = open_page( browser, - serve(DECISION_PAGE), + serve(SEATED_QUESTION_PAGE), init_script="""Storage.prototype.setItem = function () { throw new DOMException('blocked', 'SecurityError'); };""", @@ -1213,7 +1217,7 @@ def test_a_closed_sender_cannot_append_its_accepted_attempt_twice( replacement must not learn the attempt is in the log before it sends, or it would correctly decline to. Both go through `held_stale` rather than a live `page.route`, which reaches no poll already in the wire.""" - url = serve(DECISION_PAGE) + url = serve(SEATED_QUESTION_PAGE) first, _ = open_page(browser, url, context=held_stale(one_reader)) second_held = held_stale(one_reader) second, second_errors = open_page(browser, url, context=second_held) @@ -1257,7 +1261,7 @@ def test_an_older_settlement_cannot_erase_a_newer_failed_write( browser, serve, one_reader ): """A nondurable local generation outranks storage news about its predecessor.""" - url = serve(DECISION_PAGE) + url = serve(SEATED_QUESTION_PAGE) other, other_errors = open_page(browser, url, context=one_reader) old = "The older persisted answer." other_say = other.locator("#jobs > .lf-conversation > .lf-say") @@ -1299,7 +1303,7 @@ def test_an_accepted_nondurable_branch_cannot_tombstone_a_newer_shared_generatio browser, serve, one_reader ): """A held older send reconciles its base before writing settlement.""" - url = serve(DECISION_PAGE) + url = serve(SEATED_QUESTION_PAGE) older, older_errors = open_page( browser, url, @@ -1352,7 +1356,7 @@ def test_a_nondurable_branch_yields_to_unrelated_live_storage_news( browser, serve, one_reader ): """Only news from a branch's base may be replaced by that local branch.""" - url = serve(DECISION_PAGE) + url = serve(SEATED_QUESTION_PAGE) local, local_errors = open_page( browser, url, @@ -1393,7 +1397,7 @@ def test_a_delayed_storage_event_cannot_send_a_stale_durable_generation( browser, serve, one_reader ): """Send refreshes shared storage instead of trusting a stale durable cache.""" - url = serve(DECISION_PAGE) + url = serve(SEATED_QUESTION_PAGE) stale, stale_errors = open_page( browser, url, @@ -1464,7 +1468,7 @@ def test_poll_settlement_cannot_tombstone_a_newer_durable_generation( browser, serve, one_reader ): """Log reconciliation settles only the generation still shared by storage.""" - url = serve(DECISION_PAGE) + url = serve(SEATED_QUESTION_PAGE) # The hold costs the most here, where the poll released at the end is the subject # rather than an interruption: an earlier poll reconciling this tab onto the newer # generation leaves settlement nothing older to be tempted by, so the assertions @@ -1521,7 +1525,7 @@ def test_a_read_failure_cannot_make_a_successfully_written_draft_unsendable( """The document cache owns its generation even when getItem later refuses it.""" page, errors = open_page( browser, - serve(DECISION_PAGE), + serve(SEATED_QUESTION_PAGE), init_script="""Storage.prototype.getItem = function () { throw new DOMException('blocked', 'SecurityError'); };""", @@ -1544,7 +1548,7 @@ def test_a_remove_failure_cannot_resurrect_an_accepted_draft( browser, serve, one_reader ): """Settlement is a record and a log fact; draft cleanup never calls removeItem.""" - url = serve(DECISION_PAGE) + url = serve(SEATED_QUESTION_PAGE) first, first_errors = open_page( browser, url, @@ -1561,7 +1565,11 @@ def test_a_remove_failure_cannot_resurrect_an_accepted_draft( first.wait_for_function(STORED_DRAFT_SETTLED, arg="say:jobs") again, again_errors = open_page(browser, url, context=one_reader) - expect(again.locator("#jobs > .lf-conversation > .lf-say")).to_have_count(0) + # The composer is still standing — a seat that can hold keeps it — so the claim is + # about what it opens with: a settled draft is words the next tab must not be handed. + expect(again.locator("#jobs > .lf-conversation > .lf-say textarea")).to_have_value( + "" + ) roots = [ event for event in sent_events(serve.page_dir) if event["kind"] == "comment" ] @@ -1572,7 +1580,7 @@ def test_a_remove_failure_cannot_resurrect_an_accepted_draft( def test_an_intentional_later_identical_reply_gets_a_fresh_attempt(browser, serve): """Identity follows the edit generation, never content or a time window.""" - url = serve(DECISION_PAGE) + url = serve(SEATED_QUESTION_PAGE) root = events_model.append_event( serve.page_dir, { diff --git a/tests/test_render_navigation.py b/tests/test_render_navigation.py index 4f0401e8d..6a0fe8a42 100644 --- a/tests/test_render_navigation.py +++ b/tests/test_render_navigation.py @@ -287,10 +287,11 @@ def test_a_questions_digits_are_drawn_whole(browser, serve): digit is level with its words, was checked by nothing. How far in it stands is the whole group's, and it is asked as the relation it is: the - gutter reads status rule, digit, then prose, so the digit is measured against those two + gutter reads cell edge, digit, then prose, so the digit is measured against those two neighbours and against the other form's seat. Pinned as the number the gutter came to, - the reading broke the day a status rule took the head of the column and the digit moved - along behind it — a move the page wanted, reported as a failure of the digit.""" + the reading broke twice over a neighbour it was never about — once when a status rule + took the head of the column and the digit moved along behind it, and again when that + rule left and it moved back.""" page, errors = open_page(browser, serve(ADDRESS_PAGE)) seats = {} for options, sitting in [ @@ -305,15 +306,14 @@ def test_a_questions_digits_are_drawn_whole(browser, serve): assert cut is None, f"{id_}'s digit is cut: {cut}" # Never on the hairline the outer corner would have shared with the cells # around it, and never in either neighbour's room: the option's gutter opens - # with the status rule, and its words open at the column the option pads to. - # Read the row form here as well as the cards above; both reserve status, + # at the cell's own start, and its words open at the column the option pads + # to. Read the row form here as well as the cards above; both reserve # address, then prose in the same leading gutter. sits = chip.evaluate(INSIDE_ITS_OPTION) - assert sits["afterStatus"] < sits["x"] < sits["ends"] < sits["opens"], ( - f"{id_}'s digit runs {sits['x']}…{sits['ends']} in a gutter whose status " - f"rule ends at {sits['afterStatus']} and whose words open at " - f"{sits['opens']}, so the gutter is holding one of the three in another's " - "room" + assert 0 < sits["x"] < sits["ends"] < sits["opens"], ( + f"{id_}'s digit runs {sits['x']}…{sits['ends']} in a gutter that starts " + f"at its cell's own edge and whose words open at {sits['opens']}, so the " + "gutter is holding one of the two in the other's room" ) seats.setdefault(round(sits["x"], 1), []).append(id_) if sitting == "in the corner": @@ -4555,16 +4555,13 @@ def test_c_in_a_seated_conversation_reaches_the_thread_it_is_in(browser, serve): Two threads, and the reader in the second: with one there is no wrong answer to give, so the pair is what makes the assertion mean anything. The first phase is the control - — standing on the widget rather than in a thread still opens the composer on the - widget, so a green here is the standing being read and not every press landing in a + — a decision beside the seat, where standing on the widget opens the composer on that + widget, so a green below is the standing being read and not every press landing in a conversation. The agent has answered both remarks, so each thread here is a whole exchange. Nothing - in this test turns on that: `standingIn` reads the unanswered decisions rather than the - reader's list, so the group is what the reader is standing in whichever way the seat's - conversations are facing, and this control says the same thing before a reply and - after one. test_the_ring_holds_on_a_seat_the_agent_has_still_to_answer is what holds - that, and it is why the replies are the exchange's shape and not a premise.""" + in this test turns on that: the press reads where the reader is standing rather than + the reader's list, so the seat answers the same way before a reply and after one.""" url = serve( leaf_page( "seated", @@ -4575,6 +4572,10 @@ def test_c_in_a_seated_conversation_reaches_the_thread_it_is_in(browser, serve): Steel Galvanised, drop-in. Cedar Cheap; needs sealing. + + Who fits the rail? + Either crew can take it, and neither has said which week. + """, ) ) @@ -4588,7 +4589,7 @@ def test_c_in_a_seated_conversation_reaches_the_thread_it_is_in(browser, serve): "author": "user", "revision": 1, "text": text, - "anchor": {"section": "shape"}, + "anchor": {"section": "fitting"}, }, ) said.append(events_model.read_events(d)[-1]["id"]) @@ -4608,7 +4609,8 @@ def test_c_in_a_seated_conversation_reaches_the_thread_it_is_in(browser, serve): threads = page.locator(".lf-conversation-thread") expect(threads).to_have_count(2) - # The control: standing on the widget, not in either thread. + # The control: standing on a widget that seats nothing, so the press has no thread + # to prefer and opens the composer on the widget itself. page.locator("#shape .lf-pick").first.focus() expect(line).to_contain_text("comment on the decision") page.keyboard.press("c") diff --git a/tests/test_render_projection.py b/tests/test_render_projection.py index f46b29323..60b205fdb 100644 --- a/tests/test_render_projection.py +++ b/tests/test_render_projection.py @@ -575,17 +575,26 @@ def test_the_ring_says_where_the_reader_is_standing(browser, serve): The chrome wears the same band, because a reader who has backed out of the panel is standing on a button and that is the same fact about them. It wore the browser's own ring there, in the browser's blue, a few inches from a decision ringed in the page's - accent, with nothing saying the two rectangles meant one thing.""" + accent, with nothing saying the two rectangles meant one thing. + + A joined options control is the one shape that draws the band somewhere else: it is + already a framed box, so a ring around the decision outside it would read as a second + border that comes and goes, and the exact row the keyboard is on carries it instead. + Which row, in the same band — one ring still meaning one thing.""" page, errors = open_page(browser, serve(DECISIONS_PAGE)) question = page.locator("#live-question-decision") page.keyboard.press("a") expect(question).to_have_attribute("data-lf-decision", "1") - decision_ring = question.evaluate(RING) - assert decision_ring == [ + assert question.evaluate(RING)[0] == "none", ( + "the decision drew its own ring around a control that is already a frame: " + f"{question.evaluate(RING)}" + ) + row_ring = page.locator("#lq-keep").evaluate(RING) + assert row_ring == [ "solid", "2px", token_colour(page, "--accent"), - ], f"the decision is not ringed in the page's own band: {decision_ring}" + ], f"the row the reader is on is not ringed in the page's own band: {row_ring}" # A suggestion hangs its ✓ Accept out in the page margin and the focus lands on # it, so this arrival paints two marks for one fact — the ring on the change, the @@ -596,6 +605,13 @@ def test_the_ring_says_where_the_reader_is_standing(browser, serve): page.keyboard.press("a") accept = page.locator(".lf-sug-accept") expect(accept).to_be_focused() + # A decision that is not a joined control wears the ring itself, and it is the band + # the row above wore: the two shapes say one thing about the reader. + decision_ring = page.locator("#sug-refill").evaluate(RING) + assert decision_ring == row_ring, ( + "a decision and an options row are drawn in two different bands for the one " + f"fact: {decision_ring} against {row_ring}" + ) assert accept.evaluate(RING) == decision_ring, ( "the control in the margin is drawn in some other band than the decision it decides: " f"{accept.evaluate(RING)} against {decision_ring}" @@ -607,13 +623,11 @@ def test_the_ring_says_where_the_reader_is_standing(browser, serve): # A pointer landing inside an open decision is standing in it, though no walk brought # them there: the ring renders the focus rather than remembering a press. - page.locator("#live-question textarea").click() + page.locator("#live-question .lf-another input").click() expect(question).to_have_attribute("data-lf-decision", "1") # Answering takes it off with the focus still inside: the ring is for the question - # the reader is working, and an answered one is no longer a question. Answering is - # what does this — leaving the reader's list does not, so a widget waiting on the - # agent in its own seat keeps the ring while the count drops. + # the reader is working, and an answered one is no longer a question. page.locator("#lq-token .lf-pick").click() expect(page.locator(".lf-decisions")).to_have_text("Asks (3)") expect(page.locator("[data-lf-decision]")).to_have_count(0) @@ -2480,11 +2494,12 @@ def test_the_render_gate_holds_a_settled_slot_to_the_logs_decision( def test_a_label_in_a_retired_slot_leaves_the_page_with_the_slot(browser, serve): """A decided suggestion's losing slot is off the page, and a label inside it goes - too. The label is the one thing that reads back over chrome — a pick mark says - "chosen" and declares those words the page's, which is what lets a user point at - it anywhere else — so the rule has to stop at the slot: a marker that outranks a look - must not outrank a decision, or a quote lands in the half the user removed.""" - url = serve(RETIRED_WIDGET_PAGE, anchored=[("sug-swap", "chosen")]) + too. The label is the one thing that reads back over chrome — a settled group's + summary says "Settled: …" and declares those words the page's, which is what lets a + user point at it anywhere else — so the rule has to stop at the slot: a marker that + outranks a look must not outrank a decision, or a quote lands in the half the user + removed.""" + url = serve(RETIRED_WIDGET_PAGE, anchored=[("sug-swap", "Settled: Lax cookie")]) events_model.append_event( serve.page_dir, { @@ -2499,8 +2514,11 @@ def test_a_label_in_a_retired_slot_leaves_the_page_with_the_slot(browser, serve) page, errors = open_page(browser, url) expect(page.locator("#sug-swap lf-old")).to_be_hidden() assert ( - page.locator("#old-lax .lf-pick").evaluate("el => el.textContent") == "chosen" - ), "fixture is not exercising the case — the mark the slot hides never rendered" + page.locator("#old-group .lf-settled [data-lf-said]").evaluate( + "el => el.textContent" + ) + == "Settled: Lax cookie" + ), "fixture is not exercising the case — the label the slot hides never rendered" expect(page.locator(".lf-thread .lf-quote").first).to_have_class( re.compile(r"\bdetached\b") )