mirror of
https://github.com/lexmount/moli.git
synced 2026-09-24 00:01:27 +00:00
test: cover input navigation through public protocols
This commit is contained in:
@@ -127,6 +127,7 @@ Covered well:
|
||||
Playwright `page.wait_for_selector()` / `locator.wait_for()` attached,
|
||||
visible, hidden, detached, and enabled-click auto-wait behavior.
|
||||
- Chromium inspector-protocol derived samples for `Page.domContentEventFired` before `Page.loadEventFired`, `Page.frameStartedLoading` / `Page.frameStoppedLoading`, non-empty `Page.frameAttached.parentFrameId`, `Page.getFrameTree`, dynamic and nested child-frame event fan-out across auxiliary sessions with session-local lifecycle/disable state, `Page.navigate` fragment navigation, `Page.getAppManifest` default/loading/parsing/error/redirect/dynamic-link contracts, successful-result caching and link invalidation, plus its `Manifest` Network request and terminal lifecycle, `Page.getLayoutMetrics`, `Runtime.executionContextCreated`, `Runtime.evaluate(returnByValue)` and exception details, session-local `Input.setIgnoreInputEvents` aggregation/navigation/detach behavior, `Input.insertText` bypass, idle `Input.cancelDragging`, `Audits.issueAdded` Quirks/CSP shape, replay ordering, navigation storage reset and session-local enable/disable, `Log.entryAdded` network metadata, buffered replay ordering, session-local delivery cursors, target-shared `Log.clear`, violations-report state and validation, `IO.resolveBlob` session-local object resolution and reopenable `blob:<uuid>` streams, session-local `Performance.enable/disable`, strict time-domain transitions, disabled `Performance.getMetrics`, `Emulation.setCPUThrottlingRate`, `Profiler.start` / `Profiler.stop` CPU profiles and CPU-throttling profile workflow, error contracts, auxiliary CDP-session profiler isolation across navigation and detach/reattach, `console.profile` / `console.profileEnd`, precise coverage / best-effort coverage including not-started error, counter reset, and detailed block coverage, `DOM.getAttributes`, `DOM.querySelector(All)` including default-depth node-path publication through ordered `DOM.setChildNodes` events, deep ancestry expansion, repeat suppression, and the chromedp `NodeReady` contract, live `DOMDebugger.getEventListeners`, session-owned event-listener breakpoint pause/re-pause/navigation/detach behavior, session-owned XHR/fetch breakpoint URL matching, synchronous pause data, navigation/child-frame/worker scope, multi-owner sequencing and detach behavior, live-node DOM mutation breakpoints for single-node and multi-child `DocumentFragment` insertion batches, connected-node removal/insertion phases with one pre-pause `DOM.childNodeRemoved`, same-value attributes and node removal with owner/peer data, parser mutation no-pause behavior, unbound-node path ordering, disable/navigation cleanup, and the `DOM.getNodeForLocation` hit-test capability boundary.
|
||||
- Raw `Input.dispatchKeyEvent` and `Input.dispatchMouseEvent` commands whose DOM handlers initiate a top-level Page replacement; both command responses must remain successful and the replacement Page must immediately accept follow-up CDP work. Focused Rust coverage separately holds the renderer ACK so the cleanup branch itself is deterministic.
|
||||
- Chromium-calibrated `Tracing` browser-global ownership, duplicate start and peer end errors, data-source start acknowledgement before the `Tracing.start` response, exactly-once synchronous start responses, stop-before-ack response/error ordering, response-before-data ordering, cross-session clock markers, bounded `ReportEvents`, JSON `ReturnAsStream` through `IO.read`, and owner-detach cleanup. The CPU-profiler configuration additionally requires real V8 `Profile` / `ProfileChunk` events, non-empty samples with aligned `timeDeltas`, and named hot functions across navigation replacement, dedicated/shared worker teardown, and closed page targets. The same content contract is applied to the real agent-browser profiler artifact. Proto, gzip, Perfetto, system tracing, and periodic buffer reporting remain explicit unsupported boundaries rather than mock output.
|
||||
- Chromium-calibrated live DOM mutation mirroring and editing: shallow/deep `DOM.getDocument` projections, `DOM.characterDataModified`, `DOM.childNodeCountUpdated`, `DOM.childNodeInserted`, `DOM.childNodeRemoved`, and event-before-response contracts for `DOM.moveTo`, `DOM.setAttributesAsText`, `DOM.setNodeName`, `DOM.setNodeValue`, and `DOM.setOuterHTML`, including same-value character-data writes, processing-instruction `xml` renaming, and returned frontend node identity.
|
||||
- Chromium-calibrated Inspector depth-boundary projection: `DOM.getDocument` and `DOM.requestChildNodes` still publish a container's only text child at depth zero, including the common `<title>Example Domain</title>` shape, while containers with multiple children remain collapsed.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
import urllib.parse
|
||||
@@ -192,6 +193,7 @@ async def run_dom_input_group(state: SmokeState) -> None:
|
||||
await run_locator_composition_workflows(state)
|
||||
await run_keyboard_editing_workflows(state)
|
||||
await run_cdp_control_key_name_workflow(state)
|
||||
await run_cdp_input_navigation_replacement_workflows(state)
|
||||
await run_mouse_event_workflows(state)
|
||||
await run_fill_input_type_workflows(state)
|
||||
await run_check_input_workflows(state)
|
||||
@@ -1030,6 +1032,98 @@ async def run_cdp_control_key_name_workflow(state: SmokeState) -> None:
|
||||
await page.close()
|
||||
|
||||
|
||||
async def run_cdp_input_navigation_replacement_workflows(state: SmokeState) -> None:
|
||||
page = state.page
|
||||
cdp = state.cdp
|
||||
|
||||
key_destination = f"{state.fixture}/plain?input-navigation=key"
|
||||
await page.set_content(
|
||||
f"""
|
||||
<input id="navigation-field" autofocus>
|
||||
<script>
|
||||
const navigationField = document.getElementById("navigation-field");
|
||||
navigationField.addEventListener("keydown", event => {{
|
||||
if (event.key === "Enter") location.href = {json.dumps(key_destination)};
|
||||
}});
|
||||
</script>
|
||||
""",
|
||||
wait_until="domcontentloaded",
|
||||
)
|
||||
await page.focus("#navigation-field")
|
||||
async with page.expect_navigation(
|
||||
url=key_destination,
|
||||
wait_until="domcontentloaded",
|
||||
timeout=10_000,
|
||||
):
|
||||
key_result = await asyncio.wait_for(
|
||||
cdp.send(
|
||||
"Input.dispatchKeyEvent",
|
||||
{
|
||||
"type": "keyDown",
|
||||
"key": "Enter",
|
||||
"code": "Enter",
|
||||
"text": "",
|
||||
"unmodifiedText": "",
|
||||
"windowsVirtualKeyCode": 13,
|
||||
"nativeVirtualKeyCode": 13,
|
||||
},
|
||||
),
|
||||
timeout=5,
|
||||
)
|
||||
assert_equal(key_result, {}, "CDP key input response across Page replacement")
|
||||
assert_equal(
|
||||
await page.text_content("main"),
|
||||
"plain ok",
|
||||
"CDP key input replacement Page remains usable",
|
||||
)
|
||||
|
||||
mouse_destination = f"{state.fixture}/plain?input-navigation=mouse"
|
||||
await page.set_content(
|
||||
f"""
|
||||
<style>
|
||||
body {{ margin: 0; }}
|
||||
#navigation-button {{ position: fixed; left: 0; top: 0; width: 200px; height: 100px; }}
|
||||
</style>
|
||||
<button id="navigation-button">navigate</button>
|
||||
<script>
|
||||
document.getElementById("navigation-button").addEventListener("mousedown", () => {{
|
||||
location.href = {json.dumps(mouse_destination)};
|
||||
}});
|
||||
</script>
|
||||
""",
|
||||
wait_until="domcontentloaded",
|
||||
)
|
||||
async with page.expect_navigation(
|
||||
url=mouse_destination,
|
||||
wait_until="domcontentloaded",
|
||||
timeout=10_000,
|
||||
):
|
||||
mouse_result = await asyncio.wait_for(
|
||||
cdp.send(
|
||||
"Input.dispatchMouseEvent",
|
||||
{
|
||||
"type": "mousePressed",
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
"button": "left",
|
||||
"buttons": 1,
|
||||
"clickCount": 1,
|
||||
},
|
||||
),
|
||||
timeout=5,
|
||||
)
|
||||
assert_equal(mouse_result, {}, "CDP mouse input response across Page replacement")
|
||||
assert_equal(
|
||||
await page.text_content("main"),
|
||||
"plain ok",
|
||||
"CDP mouse input replacement Page remains usable",
|
||||
)
|
||||
state.record(
|
||||
"cdp_input_navigation_replacement_liveness",
|
||||
{"methods": ["Input.dispatchKeyEvent", "Input.dispatchMouseEvent"]},
|
||||
)
|
||||
|
||||
|
||||
async def run_mouse_event_workflows(state: SmokeState) -> None:
|
||||
page = state.page
|
||||
|
||||
|
||||
@@ -35,12 +35,14 @@ liveness. The baseline was executed on 2026-08-09 with Debian Chromium
|
||||
- Classic navigation, current URL, title, page source, CSS/XPath element lookup and identity, element
|
||||
text/tag/displayed/enabled/rect/attribute/property/computed label/computed
|
||||
role, send keys, clear form controls with input/change events, file upload, click, execute script,
|
||||
key actions whose handler performs a top-level Page replacement while the action request is completing,
|
||||
explicit screenshot unsupported errors, cookies, alerts, unhandled prompt behavior, window-scoped prompt
|
||||
switching, shadow roots, page-side SharedWorker probe without polluting window
|
||||
handles, `document.open()` replacement stale-element behavior, and headless window state surface.
|
||||
- WebDriver BiDi `session.status`, `session.new`, `browsingContext.create`,
|
||||
`session.subscribe`, `browsingContext.navigate`, DOMContentLoaded lifecycle
|
||||
events, `input.performActions`, `input.releaseActions`, element-origin input
|
||||
events, `input.performActions`, `input.releaseActions`, key actions that trigger a top-level Page
|
||||
replacement and remain successful through the replacement lifecycle, element-origin input
|
||||
action routing without asserting layout hit-test effects, `input.setFiles`,
|
||||
`network.getData`, `browser.setDownloadBehavior`,
|
||||
`browsingContext.downloadWillBegin`/`downloadEnd`, `network.setCacheBehavior`,
|
||||
|
||||
@@ -80,6 +80,12 @@ class FixtureServer:
|
||||
if route == "/webdriver/actions":
|
||||
self._send_html(self._actions_page())
|
||||
return
|
||||
if route == "/webdriver/input-navigation":
|
||||
self._send_html(self._input_navigation_page())
|
||||
return
|
||||
if route == "/webdriver/input-navigation-complete":
|
||||
self._send_html(self._input_navigation_complete_page())
|
||||
return
|
||||
if route == "/webdriver/form":
|
||||
self._send_html(self._form_page())
|
||||
return
|
||||
@@ -274,6 +280,29 @@ class FixtureServer:
|
||||
root.innerHTML = '<span id="shadow-text" class="shadow-item">shadow ready</span><button id="shadow-button" class="shadow-item">Shadow</button>';
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
@staticmethod
|
||||
def _input_navigation_page() -> str:
|
||||
return """<!doctype html>
|
||||
<html>
|
||||
<head><title>WebDriver Input Navigation</title></head>
|
||||
<body>
|
||||
<input id="navigation-field" autofocus>
|
||||
<script>
|
||||
document.getElementById("navigation-field").addEventListener("keydown", event => {
|
||||
if (event.key === "Enter") location.href = "/webdriver/input-navigation-complete";
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
@staticmethod
|
||||
def _input_navigation_complete_page() -> str:
|
||||
return """<!doctype html>
|
||||
<html>
|
||||
<head><title>WebDriver Input Navigation Complete</title></head>
|
||||
<body><main id="input-navigation-complete">input navigation complete</main></body>
|
||||
</html>"""
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -490,6 +490,83 @@ async def _run_bidi_input_smoke(
|
||||
)
|
||||
record(results, "bidi_input_set_files", {"context": context})
|
||||
|
||||
input_navigation_url = f"{fixture}/webdriver/input-navigation"
|
||||
input_navigation_destination = f"{fixture}/webdriver/input-navigation-complete"
|
||||
await _navigate_complete(
|
||||
websocket,
|
||||
20,
|
||||
context,
|
||||
input_navigation_url,
|
||||
"BiDi input-navigation setup",
|
||||
)
|
||||
focus = await _evaluate_remote_value(
|
||||
websocket,
|
||||
21,
|
||||
context,
|
||||
"document.getElementById('navigation-field').focus(); document.activeElement.id",
|
||||
"BiDi input-navigation focus",
|
||||
)
|
||||
assert_equal(focus["value"], "navigation-field", "BiDi input-navigation active element")
|
||||
|
||||
await _send(
|
||||
websocket,
|
||||
22,
|
||||
"input.performActions",
|
||||
{
|
||||
"context": context,
|
||||
"actions": [
|
||||
{
|
||||
"type": "key",
|
||||
"id": "navigation-keyboard",
|
||||
"actions": [{"type": "keyDown", "value": "\ue007"}],
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
action_messages = await asyncio.wait_for(_recv_until_id(websocket, 22), timeout=5)
|
||||
action = action_messages[-1]
|
||||
assert_equal(
|
||||
action["type"],
|
||||
"success",
|
||||
f"BiDi input action responds across Page replacement: {action!r}",
|
||||
)
|
||||
assert_equal(action["result"], {}, "BiDi input-navigation action result")
|
||||
|
||||
lifecycle = _find_bidi_event(
|
||||
action_messages,
|
||||
"browsingContext.domContentLoaded",
|
||||
lambda message: message.get("params", {}).get("url") == input_navigation_destination,
|
||||
)
|
||||
if lifecycle is None:
|
||||
lifecycle = await _recv_until_bidi_event(
|
||||
websocket,
|
||||
"browsingContext.domContentLoaded",
|
||||
"BiDi input-navigation DOMContentLoaded",
|
||||
lambda message: message.get("params", {}).get("url") == input_navigation_destination,
|
||||
)
|
||||
assert_equal(
|
||||
lifecycle["params"]["context"],
|
||||
context,
|
||||
"BiDi input-navigation lifecycle context",
|
||||
)
|
||||
|
||||
await _send(websocket, 23, "input.releaseActions", {"context": context})
|
||||
release = await _recv_success(websocket, 23, "BiDi input-navigation releaseActions")
|
||||
assert_equal(release["result"], {}, "BiDi input-navigation release result")
|
||||
marker = await _evaluate_remote_value(
|
||||
websocket,
|
||||
24,
|
||||
context,
|
||||
"document.getElementById('input-navigation-complete')?.textContent",
|
||||
"BiDi input-navigation replacement marker",
|
||||
)
|
||||
assert_equal(
|
||||
marker["value"],
|
||||
"input navigation complete",
|
||||
"BiDi input-navigation replacement Page remains usable",
|
||||
)
|
||||
record(results, "bidi_input_navigation_replacement", {"context": context})
|
||||
|
||||
|
||||
async def _run_bidi_network_get_data_smoke(
|
||||
websocket: Any, context: str, fixture: str, results: list[dict[str, Any]]
|
||||
|
||||
@@ -51,6 +51,29 @@ async def _wait_for_alert_text(
|
||||
return
|
||||
|
||||
|
||||
async def _wait_for_current_url(
|
||||
client: ClassicClient,
|
||||
session_id: str,
|
||||
expected: str,
|
||||
*,
|
||||
timeout: float = 5.0,
|
||||
) -> None:
|
||||
loop = asyncio.get_running_loop()
|
||||
deadline = loop.time() + timeout
|
||||
observed: list[str] = []
|
||||
while True:
|
||||
actual = classic_value(client.get(f"/session/{session_id}/url"))
|
||||
if actual == expected:
|
||||
return
|
||||
if not observed or observed[-1] != actual:
|
||||
observed.append(actual)
|
||||
if loop.time() >= deadline:
|
||||
raise AssertionError(
|
||||
f"timed out waiting for Classic URL {expected!r}; observed {observed!r}"
|
||||
)
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
|
||||
async def run_classic_group(
|
||||
endpoint: str,
|
||||
fixture: str,
|
||||
@@ -86,6 +109,7 @@ def _classic_scenarios() -> tuple[tuple[str, ClassicScenario], ...]:
|
||||
return (
|
||||
("classic_navigation_element_script", _run_navigation_element_script_smoke),
|
||||
("classic_document_open_replacement_stale_element", _run_document_open_replacement_stale_element_smoke),
|
||||
("classic_input_navigation_replacement", _run_input_navigation_replacement_smoke),
|
||||
("classic_clear_form", _run_clear_form_smoke),
|
||||
("classic_file_upload", _run_file_upload_smoke),
|
||||
("classic_alert_prompt", _run_alert_smoke),
|
||||
@@ -446,6 +470,61 @@ async def _run_document_open_replacement_stale_element_smoke(
|
||||
)
|
||||
|
||||
|
||||
async def _run_input_navigation_replacement_smoke(
|
||||
client: ClassicClient,
|
||||
fixture: str,
|
||||
session_id: str,
|
||||
results: list[dict[str, Any]],
|
||||
) -> None:
|
||||
page_url = f"{fixture}/webdriver/input-navigation"
|
||||
destination = f"{fixture}/webdriver/input-navigation-complete"
|
||||
assert_equal(
|
||||
client.post(f"/session/{session_id}/url", {"url": page_url}),
|
||||
{"value": None},
|
||||
"Classic input-navigation setup",
|
||||
)
|
||||
|
||||
field = client.post(
|
||||
f"/session/{session_id}/element",
|
||||
{"using": "css selector", "value": "#navigation-field"},
|
||||
)
|
||||
field_id = classic_element_id(field)
|
||||
assert_equal(
|
||||
client.post(f"/session/{session_id}/element/{field_id}/click"),
|
||||
{"value": None},
|
||||
"Classic input-navigation focus",
|
||||
)
|
||||
|
||||
actions = client.post(
|
||||
f"/session/{session_id}/actions",
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"type": "key",
|
||||
"id": "navigation-keyboard",
|
||||
"actions": [{"type": "keyDown", "value": "\ue007"}],
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
assert_equal(
|
||||
actions,
|
||||
{"value": None},
|
||||
"Classic input action responds across Page replacement",
|
||||
)
|
||||
await _wait_for_current_url(client, session_id, destination)
|
||||
assert_true(
|
||||
"input navigation complete" in classic_value(client.get(f"/session/{session_id}/source")),
|
||||
"Classic input action replacement Page remains usable",
|
||||
)
|
||||
assert_equal(
|
||||
client.delete(f"/session/{session_id}/actions"),
|
||||
{"value": None},
|
||||
"Classic input action release after Page replacement",
|
||||
)
|
||||
record(results, "classic_input_navigation_replacement", {"url": destination})
|
||||
|
||||
|
||||
def _assert_classic_stale_element(action: Callable[[], Any], label: str) -> None:
|
||||
try:
|
||||
action()
|
||||
|
||||
Reference in New Issue
Block a user