Skip to content
84 changes: 61 additions & 23 deletions tests/test_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,42 @@ def refuse(route):
route.abort("aborted")


# A refusal is lifted by a switch the handler reads, never by taking the route away.
# `page.unroute` drops the handler while a request is in its hands, and that request is
# then neither continued nor refused: it never comes back, and nothing in the page times
# it out. The poll lost is not the cost. A send ends in a poll of its own, so a lift
# landing inside that poll leaves `post` awaiting forever — the widget the gesture was
# made on stays in `sending`, reconciliation skips it for the rest of the tab's life
# (reconcileState), and the page sits at `aria-busy` disagreeing with the log about a
# decision the log has taken. Nothing times that out either, so what runs out is the
# assertion: one CI run in ten on the cross-tab settlement below, and the same window
# open in every test that stops a page's polls and starts them again.
#
# So the route stays on the page for its whole life and answers every request it is
# handed. Lifting changes which answer, not whether there is one.
class CutOff:
"""A page's polls, stopped from outside it until the test says it can hear again."""

def __init__(self, lets_through=0):
self._lets_through = lets_through
self._live = False

def hold(self, page, pattern="**/api/state*"):
seen = itertools.count()
page.route(
pattern,
Comment thread
leaf-agent marked this conversation as resolved.
Outdated
lambda route: (
route.continue_()
if self._live or next(seen) < self._lets_through
else refuse(route)
),
)
return self

def restore(self):
self._live = True


# A tab a test holds stale is stale for one poll interval and no longer: every poll
# reconciles the draft store against shared storage before it settles anything
# (settleAcceptedDrafts), so a stale view the assertions take their time reaching is a
Expand All @@ -656,23 +692,18 @@ def refuse(route):
# reconciles when its response lands rather than when its request went out, so a
# refusal registered on a live page leaves whatever is outstanding free to arrive
# later, against storage the test has moved in the meantime. Registered through
# `primed`, the route is on the page before it navigates and no poll is ever unrouted.
# `primed`, the route is on the page before it navigates.
#
# The first is let through because `open_page` waits for the page's readiness facts,
# including `lf-applied`, which rides on it — and that same wait is what leaves nothing
# outstanding when the page is handed over. Where the tab has to hear the log again,
# the test lifts the refusal itself.
# the test lifts the refusal itself, through the returned `restore`.
def held_stale(context):
"""A context whose next page is refused every poll after the one that stamps it."""

def hold(page):
polls = itertools.count()
page.route(
"**/api/state*",
lambda route: route.continue_() if next(polls) == 0 else refuse(route),
)

return primed(context, hold)
cut = CutOff(lets_through=1)
stale = primed(context, cut.hold)
stale.restore = cut.restore
return stale


# The page's three readiness facts: `lf-upgraded` is the document's — widgets upgraded
Expand Down Expand Up @@ -1332,6 +1363,11 @@ def arrive():
assert held, "the first poll went through, so no arrival was stood in"
held.pop(0).continue_()
page.wait_for_function(BOTH_STAMPS)
# The one route here a lift may take away rather than switch off (CutOff).
# What that strands is a poll, and a stranded poll costs this page nothing:
# this arrival is over, nothing is waiting on a send, and the next `arrive`
# navigates away from it. `told` and `round_trip` read the counters from
# where they were called, so neither is held by a trip that never ends.
page.unroute("**/api/state*")
return moved()

Expand Down Expand Up @@ -2919,7 +2955,7 @@ def test_an_open_tab_reloads_before_posting_through_a_revendored_layer(browser,
url = serve(REPLAYED_PAGE)
page, errors = open_page(browser, url)
old_layer = page_registry(page)["$layer"]["generation"]
page.route("**/api/state*", refuse)
cut = CutOff().hold(page)
page.wait_for_event(
"request", predicate=lambda request: "/api/state" in request.url
)
Expand Down Expand Up @@ -2953,7 +2989,7 @@ def test_an_open_tab_reloads_before_posting_through_a_revendored_layer(browser,
if event["kind"] == "action"
] == []

page.unroute("**/api/state*")
cut.restore()
told(page)
page.wait_for_function(BOTH_STAMPS)
page.locator("#opt-stage").click()
Expand Down Expand Up @@ -9675,14 +9711,14 @@ def test_a_decision_travels_between_tabs_and_the_log_has_the_last_word(browser,
# decisions on one change, and the log's order — not either tab's belief —
# settles it for both once the cut-off one catches up.
third, third_errors = open_page(browser, url)
third.route("**/api/state", refuse)
cut = CutOff().hold(third)
first.locator("[data-lf-for='sug-thistle'] .lf-sug-accept").click()
# In the log before the reject is clicked, so which one is later is this test's
# to decide rather than the network's.
told(second)
expect(second.get_by_role("button", name="Accept all (1)")).to_be_visible()
third.locator("[data-lf-for='sug-thistle'] .lf-sug-reject").click()
third.unroute("**/api/state")
cut.restore()
# The reject went out over a live channel, so every tab has to read it back —
# the cut-off one included, which is where it stops being its own local click.
for tab in (first, second, third):
Expand Down Expand Up @@ -15195,7 +15231,7 @@ def test_a_withdrawal_restores_what_still_stands_not_what_stood_then(browser, se

# Hold the first tab's reading of the log where it is, so its `z` names the move
# it knows about while the second tab moves the same card on past it.
stale.route("**/api/state", refuse)
cut = CutOff().hold(stale)
# Back to the list it came from and up past its neighbour, which is neither where
# the first move put it nor where the version wrote it: a second move that landed
# on the authored placement would make the wrong restore a no-op by luck, and the
Expand All @@ -15216,7 +15252,7 @@ def test_a_withdrawal_restores_what_still_stands_not_what_stood_then(browser, se
# Not round_trip: this tab's polls are stopped, so what it sent can never come
# back to it. The server answering the post is the fact this wait consumes.
_until(stale, lambda t: t.acked >= 2, "heard the server take the undo")
stale.unroute("**/api/state")
cut.restore()
told(heard)
told(heard)

Expand Down Expand Up @@ -19455,7 +19491,7 @@ def test_a_stale_question_first_message_cannot_append_across_tabs(
expect(second_say.locator("textarea")).to_have_value(raw)
# The init-script capture listener beats the runtime's listener for settlement,
# leaving the old value on screen after the other tab stores its tombstone.
second.route("**/api/state*", refuse)
cut = CutOff().hold(second)

held = []
first.route("**/api/event", lambda route: held.append(route))
Expand All @@ -19472,7 +19508,7 @@ def test_a_stale_question_first_message_cannot_append_across_tabs(
expect(second_send).to_have_attribute("aria-disabled", "false")
second_send.click()
expect(second_say.locator("textarea")).to_have_value("")
second.unroute("**/api/state*")
cut.restore()

roots = [
event for event in sent_events(serve.page_dir) if event["kind"] == "comment"
Expand Down Expand Up @@ -19664,7 +19700,8 @@ def test_a_closed_sender_cannot_append_its_accepted_attempt_twice(
which reaches no poll already in the wire."""
url = serve(ASK_PAGE)
first, _ = open_page(browser, url, context=held_stale(one_reader))
second, second_errors = 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)
raw = "One answer survives its sender closing."
first_say = first.locator("#jobs > .lf-conversation > .lf-say")
second_say = second.locator("#jobs > .lf-conversation > .lf-say")
Expand All @@ -19690,7 +19727,7 @@ def test_a_closed_sender_cannot_append_its_accepted_attempt_twice(
_until(second, lambda t: t.acked == 1, "received the accepted attempt")

first.close()
second.unroute("**/api/state*")
second_held.restore()
told(second)
roots = [
event for event in sent_events(serve.page_dir) if event["kind"] == "comment"
Expand Down Expand Up @@ -19917,10 +19954,11 @@ def test_poll_settlement_cannot_tombstone_a_newer_durable_generation(
# 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
# below would pass while asking nothing rather than fail.
stale_held = held_stale(one_reader)
stale, stale_errors = open_page(
browser,
url,
context=held_stale(one_reader),
context=stale_held,
init_script="""addEventListener('storage', event => {
if (event.key === 'lf-draft:say:jobs') event.stopImmediatePropagation();
}, true);""",
Expand Down Expand Up @@ -19953,7 +19991,7 @@ def test_poll_settlement_cannot_tombstone_a_newer_durable_generation(
# and leaves it standing. `held_stale`'s refusal is lifted here rather than earlier,
# with the older attempt in the log and the older generation still cached, which is
# the only arrangement that asks anything.
stale.unroute("**/api/state*")
stale_held.restore()
told(stale)
assert current.evaluate(STORED_DRAFT_TEXT, "say:jobs") == newer
expect(stale_say.locator("textarea")).to_have_value(newer)
Expand Down
Loading