From a36fbb696f79ae018b970d2f24494f0414fc7ee6 Mon Sep 17 00:00:00 2001 From: Zijian Zhang <35801754+futrime@users.noreply.github.com> Date: Mon, 12 Oct 2026 23:01:27 +0800 Subject: [PATCH] test(web): wait for the graph to come to rest, and take ResizeObserver's notice for one The graph test measured a node at once after a drag, a wheel and Recenter, and read a session's box that a redraw may just have replaced: on a busy machine the frame that moves it comes late, and the box is None. It now waits until the node comes within the two pixels it asks, and for a box to be drawn; and it settles only once the canvas, as well as the node, has held still for a second. WebKit raises "ResizeObserver loop completed with undelivered notifications" as a page error on a busy machine. It is a notice, and no longer fails a test. Co-Authored-By: Claude Opus 5.5 --- tests/system/web/doubles_browser.py | 11 +++++- tests/system/web/test_web_page.py | 61 ++++++++++++++++++++--------- 2 files changed, 52 insertions(+), 20 deletions(-) diff --git a/tests/system/web/doubles_browser.py b/tests/system/web/doubles_browser.py index 466d40a2..c3c3b623 100644 --- a/tests/system/web/doubles_browser.py +++ b/tests/system/web/doubles_browser.py @@ -88,7 +88,16 @@ def opened( context.add_init_script(init) page = context.new_page() failed: list[str] = [] - page.on("pageerror", lambda error: failed.append(str(error))) + # Bar the browser's word that a resize took more than one frame to settle: a + # notice, not a failure, which WebKit raises as an error on a busy machine. + page.on( + "pageerror", + lambda error: ( + failed.append(str(error)) + if not str(error).startswith("ResizeObserver loop") + else None + ), + ) # A refusal is the server answering, which the page says where it happened; what # fails the test is the page throwing, or the server failing. page.on( diff --git a/tests/system/web/test_web_page.py b/tests/system/web/test_web_page.py index 3f3cd16e..fe5ea1e8 100644 --- a/tests/system/web/test_web_page.py +++ b/tests/system/web/test_web_page.py @@ -11,6 +11,7 @@ import json import re import shutil +import time import urllib.parse from pathlib import Path from typing import TYPE_CHECKING @@ -25,7 +26,7 @@ from tests.system.web.doubles_browser import ENGINES, go, opened if TYPE_CHECKING: - from playwright.sync_api import Locator, Page + from playwright.sync_api import FloatRect, Locator, Page #: Every settings page, by the word its address ends in. _PAGES = ( @@ -108,6 +109,33 @@ def _centre(node: Locator) -> dict[str, float]: } +def _comes_to(node: Locator, x: float, y: float) -> dict[str, float]: + """Where a node's middle comes to rest, once it is within a pixel or two of where asked. + + A drag, a wheel or a button moves the graph on the page's next frame, and a page under + load draws that frame late: what is measured at once can be where it was. + """ + deadline = time.monotonic() + 10 + while not _near(at := _centre(node), x, y) and time.monotonic() < deadline: + node.page.wait_for_timeout(50) + assert _near(at, x, y), f"{at} is not ({x}, {y})" + return at + + +def _near(at: dict[str, float], x: float, y: float) -> bool: + """Whether a point is within a pixel or two of another.""" + return abs(at["x"] - x) < 2 and abs(at["y"] - y) < 2 + + +def _box(node: Locator) -> FloatRect: + """Where a node is on the page, once it is drawn: a graph redrawn replaces its nodes.""" + deadline = time.monotonic() + 10 + while (box := node.bounding_box()) is None and time.monotonic() < deadline: + node.page.wait_for_timeout(50) + assert box + return box + + def test_the_graph_is_dragged_anywhere_zoomed_by_the_wheel_and_recentred( project: Path, engine: str ) -> None: @@ -123,30 +151,29 @@ def test_the_graph_is_dragged_anywhere_zoomed_by_the_wheel_and_recentred( expect(page.locator("[data-hmz=bar] .badge")).to_have_text( re.compile("stopped") ) - # Measured once the graph has stopped moving: what settles around it resizes the canvas. + # Measured once the graph and what is drawn around it have stopped moving: what + # settles around it -- what the run spent, how long it took -- resizes the canvas. page.wait_for_function( """() => new Promise((settled) => { - const at = () => JSON.stringify(document.querySelector( - '[data-hmz=graph-agent][data-role=assistant]')?.getBoundingClientRect()) + const at = () => JSON.stringify(['[data-hmz=graph-agent][data-role=assistant]', + '[data-hmz=graph]'].map((one) => document.querySelector(one) + ?.getBoundingClientRect())) const before = at() - setTimeout(() => settled(before !== undefined && at() === before), 300) - })""" + setTimeout(() => settled(!before.includes('null') && at() === before), 1000) + })""", + polling=100, ) - canvas = page.locator("[data-hmz=graph]").bounding_box() + canvas = _box(page.locator("[data-hmz=graph]")) drawn = _centre(agent) - assert canvas address = page.url # Dragged from a session on the node, past where the graph ends: nothing opens. - session = agent.locator("[data-hmz=graph-session]").bounding_box() - assert session + session = _box(agent.locator("[data-hmz=graph-session]")) page.mouse.move(session["x"] + 10, session["y"] + 10) page.mouse.down() page.mouse.move(session["x"] - 290, session["y"] - 190, steps=8) page.mouse.up() - moved = _centre(agent) - assert abs(moved["x"] - (drawn["x"] - 300)) < 2 - assert abs(moved["y"] - (drawn["y"] - 200)) < 2 + moved = _comes_to(agent, drawn["x"] - 300, drawn["y"] - 200) assert page.url == address # Two fingers slid over a trackpad move it; a pinch and a mouse's wheel zoom it. @@ -154,9 +181,7 @@ def test_the_graph_is_dragged_anywhere_zoomed_by_the_wheel_and_recentred( before = zoom.inner_text() page.mouse.move(canvas["x"] + canvas["width"] / 2, canvas["y"] + 40) page.mouse.wheel(30, 40) - slid = _centre(agent) - assert abs(slid["x"] - (moved["x"] - 30)) < 2 - assert abs(slid["y"] - (moved["y"] - 40)) < 2 + _comes_to(agent, moved["x"] - 30, moved["y"] - 40) expect(zoom).to_have_text(before) for wheel in ({"deltaY": -60, "ctrlKey": True}, {"deltaY": -3, "deltaMode": 1}): said = zoom.inner_text() @@ -169,9 +194,7 @@ def test_the_graph_is_dragged_anywhere_zoomed_by_the_wheel_and_recentred( page.get_by_role("button", name="Recenter").click() expect(zoom).to_have_text(before) - back = _centre(agent) - assert abs(back["x"] - drawn["x"]) < 2 - assert abs(back["y"] - drawn["y"]) < 2 + _comes_to(agent, drawn["x"], drawn["y"]) def test_a_model_no_cli_would_list_is_typed(