ladybird/Tests/LibWebView/test-webdriver-session-history.py
sideshowbarker 5af7dadd40 Tests: Wait for beforeunload before the script-back cancel assertion
Problem: TestWebDriverSessionHistory flakes on slow (Sanitizer) CI
runners. The subtest checking that cancellation of a script-initiated
cross-site “back” by a beforeunload handler intermittently failed the
assertion “Expected beforeunload to cancel script-initiated cross-site
history.back(), got [url_b, 0]”. scriptBeforeUnloadCount was 0, not 1.

Cause: history.back() appends its traversal to the traversable’s session
history queue and returns immediately. So the beforeunload prompt to
unload runs async. The test read scriptBeforeUnloadCount in a separate
execute_script right after — with no wait. So on a slow runner, the read
beats the dispatch, and sees 0. Every other script-initiated navigation
in the test waits for its target document to load — and the “refresh”
beforeunload check already polls. But the canceled “back” loads no
target document — and this one waited for nothing.

Fix: Poll with wait_for_script_result until the page is still on /b and
window.scriptBeforeUnloadCount has incremented, before asserting the
final state — mirroring the “refresh” beforeunload check.
2026-06-20 23:50:57 +02:00

4778 lines
187 KiB
Python
Executable file

#!/usr/bin/env python3
#
# Copyright (c) 2026-present, the Ladybird developers.
#
# SPDX-License-Identifier: BSD-2-Clause
import argparse
import http.client
import http.server
import json
import os
import socket
import subprocess
import sys
import tempfile
import threading
import time
from typing import cast
BLOCKED_RESPONSE_TIMEOUT_SECONDS = 30
EVENT_TIMEOUT_SECONDS = 30
WEBDRIVER_REQUEST_TIMEOUT_SECONDS = 60
class TestPageServer(http.server.ThreadingHTTPServer):
def __init__(self, server_address, request_handler_class):
super().__init__(server_address, request_handler_class)
self.blocked_frame_b_requested = threading.Event()
self.frame_b_blocked_document_ran = threading.Event()
self.release_blocked_frame_b = threading.Event()
self.blocked_reload_requested = threading.Event()
self.reload_recovery_requested = threading.Event()
self.release_blocked_reload = threading.Event()
self.reload_blocked_request_lock = threading.Lock()
self.reload_blocked_request_count = 0
self.blocked_process_swap_back_requested = threading.Event()
self.process_swap_back_recovery_requested = threading.Event()
self.process_swap_back_document_ran = threading.Event()
self.release_blocked_process_swap_back = threading.Event()
self.process_swap_back_blocked_request_lock = threading.Lock()
self.process_swap_back_blocked_request_count = 0
self.block_forward_load = False
self.blocked_forward_requested = threading.Event()
self.release_blocked_forward = threading.Event()
self.blocked_cross_site_post_requested = threading.Event()
self.release_blocked_cross_site_post = threading.Event()
self.blocked_same_site_post_requested = threading.Event()
self.release_blocked_same_site_post = threading.Event()
self.blocked_same_url_post_requested = threading.Event()
self.release_blocked_same_url_post = threading.Event()
self.state_replace_load_document_ran = threading.Event()
self.a_document_ran = threading.Event()
self.b_document_ran = threading.Event()
self.c_document_ran = threading.Event()
self.d_document_ran = threading.Event()
self.post_result_document_ran = threading.Event()
self.cross_site_post_result_document_ran = threading.Event()
self.same_site_post_result_document_ran = threading.Event()
self.same_url_post_result_document_ran = threading.Event()
self.reload_blocked_document_ran = threading.Event()
def handle_error(self, request, client_address):
if isinstance(sys.exc_info()[1], BrokenPipeError):
return
super().handle_error(request, client_address)
class TestPageHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
server = cast(TestPageServer, self.server)
server_port = server.server_port
if self.path == "/a":
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(
f"""<!doctype html>
<title>A</title>
<script>fetch('/document-ran?a');</script>
<a id="go" href="http://127.0.0.1:{server_port}/b">B</a>
<a id="redirect" href="http://localhost:{server_port}/redirect-to-b">Redirect to B</a>
<p>A</p>""".encode()
)
return
if self.path == "/document-ran?a":
server.a_document_ran.set()
self.send_response(204)
self.end_headers()
return
if self.path == "/redirect-to-b":
self.send_response(302)
self.send_header("Location", f"http://127.0.0.1:{server_port}/b")
self.end_headers()
return
if self.path.startswith("/nested"):
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(
f"""<!doctype html>
<title>Nested</title>
<iframe id="frame" src="http://localhost:{server_port}/frame-a"></iframe>
<p>Nested</p>""".encode()
)
return
if self.path.startswith("/state"):
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
document_ready_script = ""
if self.path == "/state?replace-load":
document_ready_script = "<script>fetch('/document-ran?state-replace-load');</script>"
self.wfile.write(
f"""<!doctype html>
<title>State</title>
{document_ready_script}
<p>State</p>""".encode()
)
return
if self.path.startswith("/scroll"):
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(
"""<!doctype html>
<title>Scroll</title>
<style>
body {
margin: 0;
min-height: 4000px;
}
</style>
<p>Scroll</p>""".encode()
)
return
if self.path == "/reload-blocked":
should_block = False
with server.reload_blocked_request_lock:
server.reload_blocked_request_count += 1
if server.reload_blocked_request_count > 2:
server.reload_recovery_requested.set()
should_block = server.reload_blocked_request_count > 1
if should_block:
server.blocked_reload_requested.set()
if not server.release_blocked_reload.wait(timeout=BLOCKED_RESPONSE_TIMEOUT_SECONDS):
self.send_response(500)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(b"timed out waiting to unblock reload")
return
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(
"""<!doctype html>
<title>Reload Blocked</title>
<script>fetch('/document-ran?reload-blocked');</script>
<p>Reload Blocked</p>""".encode()
)
return
if self.path == "/process-swap-back-blocked":
should_block = False
with server.process_swap_back_blocked_request_lock:
server.process_swap_back_blocked_request_count += 1
if server.process_swap_back_blocked_request_count > 2:
server.process_swap_back_recovery_requested.set()
should_block = server.process_swap_back_blocked_request_count > 1
if should_block:
server.blocked_process_swap_back_requested.set()
if not server.release_blocked_process_swap_back.wait(timeout=BLOCKED_RESPONSE_TIMEOUT_SECONDS):
self.send_response(500)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(b"timed out waiting to unblock process-swap back")
return
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(
f"""<!doctype html>
<title>Process Swap Back Blocked</title>
<script>fetch('/document-ran?process-swap-back-blocked');</script>
<a id="go" href="http://127.0.0.1:{server_port}/b">B</a>
<p>Process Swap Back Blocked</p>""".encode()
)
return
if self.path == "/forward-blocked":
if server.block_forward_load:
server.blocked_forward_requested.set()
if not server.release_blocked_forward.wait(timeout=BLOCKED_RESPONSE_TIMEOUT_SECONDS):
self.send_response(500)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(b"timed out waiting to unblock forward")
return
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(
"""<!doctype html>
<title>Forward Blocked</title>
<p>Forward Blocked</p>""".encode()
)
return
if self.path == "/frame-a":
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(
"""<!doctype html>
<title>Frame A</title>
<p>Frame A</p>""".encode()
)
return
if self.path == "/frame-b":
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(
"""<!doctype html>
<title>Frame B</title>
<p>Frame B</p>""".encode()
)
return
if self.path == "/frame-b-blocked":
server.blocked_frame_b_requested.set()
if not server.release_blocked_frame_b.wait(timeout=BLOCKED_RESPONSE_TIMEOUT_SECONDS):
self.send_response(500)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(b"timed out waiting to unblock frame")
return
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(
"""<!doctype html>
<title>Frame B Blocked</title>
<script>addEventListener('load', () => fetch('/document-ran?frame-b-blocked'));</script>
<p>Frame B Blocked</p>""".encode()
)
return
if self.path == "/b":
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(
f"""<!doctype html>
<title>B</title>
<script>fetch('/document-ran?b');</script>
<a id="go" href="http://127.0.0.1:{server_port}/c">C</a>
<a id="branch" href="http://localhost:{server_port}/d">D</a>
<p>B</p>""".encode()
)
return
if self.path == "/document-ran?b":
server.b_document_ran.set()
self.send_response(204)
self.end_headers()
return
if self.path == "/c":
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(
f"""<!doctype html>
<title>C</title>
<script>fetch('/document-ran?c');</script>
<a id="go" href="http://localhost:{server_port}/a">A</a>
<p>C</p>""".encode()
)
return
if self.path == "/document-ran?state-replace-load":
server.state_replace_load_document_ran.set()
self.send_response(204)
self.end_headers()
return
if self.path == "/document-ran?c":
server.c_document_ran.set()
self.send_response(204)
self.end_headers()
return
if self.path == "/document-ran?process-swap-back-blocked":
server.process_swap_back_document_ran.set()
self.send_response(204)
self.end_headers()
return
if self.path == "/document-ran?frame-b-blocked":
server.frame_b_blocked_document_ran.set()
self.send_response(204)
self.end_headers()
return
if self.path == "/document-ran?post-result":
server.post_result_document_ran.set()
self.send_response(204)
self.end_headers()
return
if self.path == "/document-ran?cross-site-post-result":
server.cross_site_post_result_document_ran.set()
self.send_response(204)
self.end_headers()
return
if self.path == "/document-ran?same-site-post-result":
server.same_site_post_result_document_ran.set()
self.send_response(204)
self.end_headers()
return
if self.path == "/document-ran?same-url-post-result":
server.same_url_post_result_document_ran.set()
self.send_response(204)
self.end_headers()
return
if self.path == "/document-ran?reload-blocked":
server.reload_blocked_document_ran.set()
self.send_response(204)
self.end_headers()
return
if self.path == "/d":
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(
"""<!doctype html>
<title>D</title>
<script>fetch('/document-ran?d');</script>
<script>
window.initialHistoryLength = history.length;
window.initialNavigationEntryCount = navigation.entries().length;
window.initialNavigationCurrentIndex = navigation.currentEntry.index;
</script>
<p>D</p>""".encode()
)
return
if self.path == "/document-ran?d":
server.d_document_ran.set()
self.send_response(204)
self.end_headers()
return
if self.path == "/post-form":
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(
"""<!doctype html>
<title>Post Form</title>
<form id="post-form" method="post" action="/post-result">
<input name="name" value="ladybird">
<button id="submit">Submit</button>
</form>
<p>Post Form</p>""".encode()
)
return
if self.path == "/post-blocked-form":
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(
"""<!doctype html>
<title>Post Blocked Form</title>
<form id="post-form" method="post" action="/post-result-blocked-same-site">
<input name="name" value="ladybird">
<button id="submit">Submit</button>
</form>
<p>Post Blocked Form</p>""".encode()
)
return
if self.path == "/post-same-url-blocked":
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(
"""<!doctype html>
<title>Post Same URL Blocked Form</title>
<form id="post-form" method="post" action="/post-same-url-blocked">
<input name="name" value="ladybird">
<button id="submit">Submit</button>
</form>
<p>Post Same URL Blocked Form</p>""".encode()
)
return
if self.path == "/cross-site-post-form":
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(
f"""<!doctype html>
<title>Cross-site Post Form</title>
<form id="post-form" method="post" action="http://127.0.0.1:{server_port}/post-result">
<input name="name" value="ladybird">
<button id="submit">Submit</button>
</form>
<p>Cross-site Post Form</p>""".encode()
)
return
if self.path == "/cross-site-post-blocked-form":
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(
f"""<!doctype html>
<title>Cross-site Post Blocked Form</title>
<form id="post-form" method="post" action="http://127.0.0.1:{server_port}/post-result-blocked">
<input name="name" value="ladybird">
<button id="submit">Submit</button>
</form>
<p>Cross-site Post Blocked Form</p>""".encode()
)
return
if self.path == "/post-result":
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(
"""<!doctype html>
<title>Post Result GET</title>
<p id="result">GET</p>""".encode()
)
return
self.send_response(404)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(b"not found")
def do_POST(self):
server = cast(TestPageServer, self.server)
if self.path == "/post-result-blocked":
content_length = int(self.headers.get("Content-Length", "0"))
body = self.rfile.read(content_length).decode()
server.blocked_cross_site_post_requested.set()
if not server.release_blocked_cross_site_post.wait(timeout=BLOCKED_RESPONSE_TIMEOUT_SECONDS):
self.send_response(500)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(b"timed out waiting to unblock cross-site POST")
return
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(
f"""<!doctype html>
<title>Post Result Blocked POST</title>
<script>fetch('/document-ran?cross-site-post-result');</script>
<p id="result">POST:{body}</p>""".encode()
)
return
if self.path == "/post-result-blocked-same-site":
content_length = int(self.headers.get("Content-Length", "0"))
body = self.rfile.read(content_length).decode()
server.blocked_same_site_post_requested.set()
if not server.release_blocked_same_site_post.wait(timeout=BLOCKED_RESPONSE_TIMEOUT_SECONDS):
self.send_response(500)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(b"timed out waiting to unblock same-site POST")
return
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(
f"""<!doctype html>
<title>Post Result Blocked Same-Site POST</title>
<script>fetch('/document-ran?same-site-post-result');</script>
<p id="result">POST:{body}</p>""".encode()
)
return
if self.path == "/post-same-url-blocked":
content_length = int(self.headers.get("Content-Length", "0"))
body = self.rfile.read(content_length).decode()
server.blocked_same_url_post_requested.set()
if not server.release_blocked_same_url_post.wait(timeout=BLOCKED_RESPONSE_TIMEOUT_SECONDS):
self.send_response(500)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(b"timed out waiting to unblock same-URL POST")
return
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(
f"""<!doctype html>
<title>Post Same URL Blocked POST</title>
<script>fetch('/document-ran?same-url-post-result');</script>
<p id="result">POST:{body}</p>""".encode()
)
return
if self.path == "/post-result":
content_length = int(self.headers.get("Content-Length", "0"))
body = self.rfile.read(content_length).decode()
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(
f"""<!doctype html>
<title>Post Result POST</title>
<script>fetch('/document-ran?post-result');</script>
<p id="result">POST:{body}</p>""".encode()
)
return
self.send_response(404)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(b"not found")
def log_message(self, format, *args):
pass
def unused_port():
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.bind(("127.0.0.1", 0))
return sock.getsockname()[1]
def wait_for_port(port, timeout=EVENT_TIMEOUT_SECONDS):
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
try:
with socket.create_connection(("127.0.0.1", port), timeout=0.1):
return
except OSError:
time.sleep(0.05)
raise RuntimeError(f"Timed out waiting for port {port}")
def wait_for_event(event, label, timeout=EVENT_TIMEOUT_SECONDS):
if event.wait(timeout=timeout):
return
raise RuntimeError(f"Timed out waiting for {label}")
def request_raw(webdriver_port, method, path, body=None):
encoded_body = None
headers = {}
if body is not None:
encoded_body = json.dumps(body).encode()
headers["Content-Type"] = "application/json"
connection = http.client.HTTPConnection("127.0.0.1", webdriver_port, timeout=WEBDRIVER_REQUEST_TIMEOUT_SECONDS)
try:
connection.request(method, path, encoded_body, headers)
response = connection.getresponse()
response_body = response.read().decode()
finally:
connection.close()
try:
payload = json.loads(response_body) if response_body else {}
except json.JSONDecodeError as error:
raise RuntimeError(f"{method} {path} returned invalid JSON: {response_body}") from error
return response.status, payload, response_body
def request(webdriver_port, method, path, body=None):
status, payload, response_body = request_raw(webdriver_port, method, path, body)
if status >= 400 or isinstance(payload.get("value"), dict) and payload["value"].get("error"):
raise RuntimeError(f"{method} {path} failed with HTTP {status}: {response_body}")
return payload
def current_url(webdriver_port, session_id):
return request(webdriver_port, "GET", f"/session/{session_id}/url")["value"]
def execute_script(webdriver_port, session_id, script):
return request(
webdriver_port,
"POST",
f"/session/{session_id}/execute/sync",
{
"script": script,
"args": [],
},
)["value"]
def wait_for_script_result(webdriver_port, session_id, label, script, predicate, log, timeout=EVENT_TIMEOUT_SECONDS):
deadline = time.monotonic() + timeout
result = None
while time.monotonic() < deadline:
result = execute_script(webdriver_port, session_id, script)
if predicate(result):
log.append(f"{label}: {result}")
return result
time.sleep(0.05)
raise AssertionError(f"Timed out waiting for {label}, got {result}\n" + "\n".join(log))
def execute_async_script(webdriver_port, session_id, script):
return request(
webdriver_port,
"POST",
f"/session/{session_id}/execute/async",
{
"script": script,
"args": [],
},
)["value"]
def navigate_from_renderer_using_link(
webdriver_port,
session_id,
link_selector,
expected_url,
log,
document_ran_event,
document_ran_label,
expected_scheduled_url=None,
):
actual_url = execute_script(
webdriver_port,
session_id,
f"""
const link = document.querySelector({json.dumps(link_selector)});
if (!link)
return null;
return link.href;
""",
)
log.append(f"clicked renderer link to {actual_url}")
expected_scheduled_url = expected_scheduled_url or expected_url
if actual_url != expected_scheduled_url:
raise AssertionError(
f"Expected renderer navigation target to be {expected_scheduled_url}, got {actual_url}\n" + "\n".join(log)
)
document_ran_event.clear()
execute_script(
webdriver_port,
session_id,
f"""
const link = document.querySelector({json.dumps(link_selector)});
link.click();
return null;
""",
)
wait_for_event(document_ran_event, document_ran_label)
def crash_current_page(webdriver_port, session_id):
request(webdriver_port, "POST", f"/session/{session_id}/ladybird/crash-current-page", {})
def crash_current_page_allowing_navigation_timeout(webdriver_port, session_id):
try:
status, payload, response_body = request_raw(
webdriver_port, "POST", f"/session/{session_id}/ladybird/crash-current-page", {}
)
except TimeoutError:
return
if status < 400:
return
value = payload.get("value")
if isinstance(value, dict) and value.get("error") == "timeout":
return
raise RuntimeError(
f"POST /session/{session_id}/ladybird/crash-current-page failed with HTTP {status}: {response_body}"
)
def load_url_from_ui(webdriver_port, session_id, url):
request(webdriver_port, "POST", f"/session/{session_id}/ladybird/load-url-from-ui", {"url": url})
def traverse_history_from_ui(webdriver_port, session_id, delta, wait_for_navigation_completion=True):
request(
webdriver_port,
"POST",
f"/session/{session_id}/ladybird/traverse-history-from-ui",
{
"delta": delta,
"waitForNavigationCompletion": wait_for_navigation_completion,
},
)
def mark_web_content_session_history_stale(webdriver_port, session_id):
request(webdriver_port, "POST", f"/session/{session_id}/ladybird/mark-web-content-session-history-stale", {})
def refresh(webdriver_port, session_id):
request(webdriver_port, "POST", f"/session/{session_id}/refresh", {})
def create_session(webdriver_port, enable_test_hooks=True):
always_match = {
"ladybird:headless": True,
"pageLoadStrategy": "normal",
"unhandledPromptBehavior": {"beforeUnload": "dismiss"},
}
if enable_test_hooks:
always_match["ladybird:enableTestHooks"] = True
created = request(
webdriver_port,
"POST",
"/session",
{"capabilities": {"alwaysMatch": always_match}},
)
session_id = created.get("value", {}).get("sessionId") or created.get("sessionId")
if not session_id:
raise RuntimeError(f"Could not find session id in response: {created}")
request(webdriver_port, "POST", f"/session/{session_id}/timeouts", {"pageLoad": 10000})
return session_id
def expect_ladybird_test_hooks_require_capability(webdriver_port):
session_id = create_session(webdriver_port, enable_test_hooks=False)
try:
status, payload, response_body = request_raw(
webdriver_port, "GET", f"/session/{session_id}/ladybird/session-history"
)
value = payload.get("value")
if status != 404 or not isinstance(value, dict) or value.get("error") != "unknown command":
raise AssertionError(
f"Expected Ladybird test hook to fail with unknown command, got HTTP {status}: {response_body}"
)
finally:
request(webdriver_port, "DELETE", f"/session/{session_id}")
def browser_history_shortcut_modifier():
if sys.platform == "darwin":
return "\ue03d", "Meta"
return "\ue00a", "Alt"
def perform_browser_history_shortcut(webdriver_port, session_id, direction, log):
if direction == "left":
arrow = "\ue012"
elif direction == "right":
arrow = "\ue014"
else:
raise ValueError(f"Unknown arrow direction {direction}")
modifier, modifier_name = browser_history_shortcut_modifier()
request(
webdriver_port,
"POST",
f"/session/{session_id}/actions",
{
"actions": [
{
"type": "key",
"id": "browser-shortcut-keyboard",
"actions": [
{"type": "keyDown", "value": modifier},
{"type": "keyDown", "value": arrow},
{"type": "keyUp", "value": arrow},
{"type": "keyUp", "value": modifier},
],
}
]
},
)
log.append(f"performed {modifier_name}+{direction}")
def perform_pointer_click(webdriver_port, session_id, x, y, log):
request(
webdriver_port,
"POST",
f"/session/{session_id}/actions",
{
"actions": [
{
"type": "pointer",
"id": "activation-pointer",
"parameters": {"pointerType": "mouse"},
"actions": [
{"type": "pointerMove", "origin": "viewport", "x": x, "y": y},
{"type": "pointerDown", "button": 0},
{"type": "pointerUp", "button": 0},
],
}
]
},
)
log.append(f"clicked viewport at {x},{y}")
def session_history(webdriver_port, session_id):
return request(webdriver_port, "GET", f"/session/{session_id}/ladybird/session-history")["value"]
def wait_for_session_history(webdriver_port, session_id, label, predicate, log, timeout=EVENT_TIMEOUT_SECONDS):
deadline = time.monotonic() + timeout
snapshot = None
while time.monotonic() < deadline:
snapshot = session_history(webdriver_port, session_id)
if predicate(snapshot):
log.append(f"{label} history: {summarize_history_snapshot(snapshot)}")
return snapshot
time.sleep(0.05)
raise AssertionError(
f"Timed out waiting for {label} history, got {summarize_history_snapshot(snapshot)}\n" + "\n".join(log)
)
def history_entry_urls(history):
return [entry["url"] for entry in history["entries"]]
def history_used_steps(history):
return history_step_values(history["usedSteps"])
def history_step_values(steps):
return [used_step["step"] for used_step in steps]
def history_current_step(steps):
current_steps = [used_step["step"] for used_step in steps if used_step.get("current", False)]
if len(current_steps) > 1:
raise AssertionError(f"Expected at most one current step, got {current_steps}")
if not current_steps:
return None
return current_steps[0]
def history_current_entry(history):
current_entries = [entry for entry in history["entries"] if entry["current"]]
if len(current_entries) != 1:
raise AssertionError(f"Expected exactly one current entry, got {current_entries}")
return current_entries[0]
def comparable_history(history):
def comparable_entry(entry):
return {
"step": entry["step"],
"url": entry["url"],
"current": entry.get("current", False),
"nestedHistories": [
{
"id": nested_history["id"],
"entries": [comparable_entry(nested_entry) for nested_entry in nested_history["entries"]],
}
for nested_history in entry["nestedHistories"]
],
}
return {
"currentUsedStepIndex": history["currentUsedStepIndex"],
"entries": [comparable_entry(entry) for entry in history["entries"]],
"usedSteps": history["usedSteps"],
}
def summarize_history_snapshot(snapshot):
ui = snapshot["ui"]
web_content = snapshot["webContent"]
return {
"ui": {
"entries": history_entry_urls(ui),
"usedSteps": history_used_steps(ui),
"currentUsedStepIndex": ui["currentUsedStepIndex"],
"back": ui["backButtonEnabled"],
"forward": ui["forwardButtonEnabled"],
"matches": ui["webContentHistoryMatchesUI"],
"waitingToSeedWebContent": ui["waitingToSeedWebContent"],
"waitingForWebContentSeedAck": ui["waitingForWebContentSeedAck"],
"ignoringWebContentUpdatesUntilSeed": ui["ignoringWebContentUpdatesUntilSeed"],
"reseedAfterCurrentHistoryLoad": ui["reseedAfterCurrentHistoryLoad"],
"webContentUsesUIStepCoordinates": ui["webContentUsesUIStepCoordinates"],
"webContentKnownUsedSteps": history_step_values(ui["webContentKnownUsedSteps"]),
"webContentCurrentStep": ui["webContentCurrentStep"],
"pendingWebContentHistoryStepAfterFallbackLoad": ui["pendingWebContentHistoryStepAfterFallbackLoad"],
"pendingSessionHistoryNavigation": ui["pendingSessionHistoryNavigation"],
"pendingSessionHistoryTraversal": ui["pendingSessionHistoryTraversal"],
"currentResource": history_current_entry(ui).get("resource"),
},
"webContent": {
"entries": history_entry_urls(web_content),
"usedSteps": history_used_steps(web_content),
"currentUsedStepIndex": web_content["currentUsedStepIndex"],
"currentResource": history_current_entry(web_content).get("resource"),
},
}
def expect_ui_session_history(
webdriver_port,
session_id,
label,
expected_entry_urls,
expected_used_steps,
expected_current_used_step_index,
expected_back_enabled,
expected_forward_enabled,
log,
expect_web_content_matches_ui=None,
expected_web_content_known_used_steps=None,
expected_web_content_current_step=None,
expected_waiting_to_seed_web_content=None,
expected_waiting_for_web_content_seed_ack=None,
expected_ignoring_web_content_updates_until_seed=None,
expected_reseed_after_current_history_load=None,
):
def ui_history_matches(ui):
return (
history_entry_urls(ui) == expected_entry_urls
and history_used_steps(ui) == expected_used_steps
and ui["currentUsedStepIndex"] == expected_current_used_step_index
and ui["backButtonEnabled"] is expected_back_enabled
and ui["forwardButtonEnabled"] is expected_forward_enabled
and (
expect_web_content_matches_ui is None
or ui["webContentHistoryMatchesUI"] is expect_web_content_matches_ui
)
and (
expected_web_content_known_used_steps is None
or history_step_values(ui["webContentKnownUsedSteps"]) == expected_web_content_known_used_steps
)
and (
expected_web_content_current_step is None
or ui["webContentCurrentStep"] == expected_web_content_current_step
)
and (
expected_web_content_current_step is None
or history_current_step(ui["webContentKnownUsedSteps"]) == expected_web_content_current_step
)
and (
expected_waiting_to_seed_web_content is None
or ui["waitingToSeedWebContent"] is expected_waiting_to_seed_web_content
)
and (
expected_waiting_for_web_content_seed_ack is None
or ui["waitingForWebContentSeedAck"] is expected_waiting_for_web_content_seed_ack
)
and (
expected_ignoring_web_content_updates_until_seed is None
or ui["ignoringWebContentUpdatesUntilSeed"] is expected_ignoring_web_content_updates_until_seed
)
and (
expected_reseed_after_current_history_load is None
or ui["reseedAfterCurrentHistoryLoad"] is expected_reseed_after_current_history_load
)
)
def web_content_matches_ui(snapshot):
ui = snapshot["ui"]
web_content = snapshot["webContent"]
return not (
ui["waitingToSeedWebContent"]
or ui["waitingForWebContentSeedAck"]
or ui["ignoringWebContentUpdatesUntilSeed"]
or ui["reseedAfterCurrentHistoryLoad"]
or ui["pendingWebContentHistoryStepAfterFallbackLoad"] is not None
or ui["pendingSessionHistoryNavigation"] is not None
or ui["pendingSessionHistoryTraversal"] is not None
or comparable_history(ui) != comparable_history(web_content)
)
def raise_ui_mismatch(snapshot):
raise AssertionError(
f"Expected {label} UI history to be entries={expected_entry_urls}, usedSteps={expected_used_steps}, "
f"currentUsedStepIndex={expected_current_used_step_index}, back={expected_back_enabled}, "
f"forward={expected_forward_enabled}, webContentMatchesUI={expect_web_content_matches_ui}, "
f"webContentKnownUsedSteps={expected_web_content_known_used_steps}, "
f"webContentCurrentStep={expected_web_content_current_step}, "
f"waitingToSeedWebContent={expected_waiting_to_seed_web_content}, "
f"waitingForWebContentSeedAck={expected_waiting_for_web_content_seed_ack}, "
f"ignoringWebContentUpdatesUntilSeed={expected_ignoring_web_content_updates_until_seed}; "
f"reseedAfterCurrentHistoryLoad={expected_reseed_after_current_history_load}; "
f"got {summarize_history_snapshot(snapshot)}\n" + "\n".join(log)
)
if expect_web_content_matches_ui:
deadline = time.monotonic() + EVENT_TIMEOUT_SECONDS
while True:
snapshot = session_history(webdriver_port, session_id)
if ui_history_matches(snapshot["ui"]) and web_content_matches_ui(snapshot):
break
if time.monotonic() >= deadline:
if not ui_history_matches(snapshot["ui"]):
raise_ui_mismatch(snapshot)
raise AssertionError(
f"Expected {label} WebContent history to match UI, got {summarize_history_snapshot(snapshot)}\n"
+ "\n".join(log)
)
time.sleep(0.05)
log.append(f"{label} history: {summarize_history_snapshot(snapshot)}")
return snapshot
snapshot = session_history(webdriver_port, session_id)
if not ui_history_matches(snapshot["ui"]):
raise_ui_mismatch(snapshot)
log.append(f"{label} history: {summarize_history_snapshot(snapshot)}")
return snapshot
def wait_for_ui_session_history(
webdriver_port,
session_id,
label,
expected_entry_urls,
expected_used_steps,
expected_current_used_step_index,
expected_back_enabled,
expected_forward_enabled,
log,
expected_web_content_known_used_steps,
expected_web_content_current_step,
):
def matches_expected_history(snapshot):
ui = snapshot["ui"]
web_content = snapshot["webContent"]
return (
history_entry_urls(ui) == expected_entry_urls
and history_used_steps(ui) == expected_used_steps
and ui["currentUsedStepIndex"] == expected_current_used_step_index
and ui["backButtonEnabled"] is expected_back_enabled
and ui["forwardButtonEnabled"] is expected_forward_enabled
and ui["webContentHistoryMatchesUI"]
and history_step_values(ui["webContentKnownUsedSteps"]) == expected_web_content_known_used_steps
and ui["webContentCurrentStep"] == expected_web_content_current_step
and history_current_step(ui["webContentKnownUsedSteps"]) == expected_web_content_current_step
and not ui["waitingToSeedWebContent"]
and not ui["waitingForWebContentSeedAck"]
and not ui["ignoringWebContentUpdatesUntilSeed"]
and not ui["reseedAfterCurrentHistoryLoad"]
and ui["pendingWebContentHistoryStepAfterFallbackLoad"] is None
and ui["pendingSessionHistoryNavigation"] is None
and ui["pendingSessionHistoryTraversal"] is None
and comparable_history(ui) == comparable_history(web_content)
)
return wait_for_session_history(webdriver_port, session_id, label, matches_expected_history, log)
def expect_beforeunload_cancels_webdriver_navigation(
webdriver_port,
session_id,
url,
label,
expected_url,
previous_beforeunload_count,
expected_history_snapshot,
log,
):
request(webdriver_port, "POST", f"/session/{session_id}/timeouts", {"pageLoad": 1000})
request(webdriver_port, "POST", f"/session/{session_id}/url", {"url": url})
request(webdriver_port, "POST", f"/session/{session_id}/timeouts", {"pageLoad": 10000})
state = execute_script(
webdriver_port,
session_id,
"return [location.href, window.beforeUnloadCount];",
)
if state[0] != expected_url or state[1] <= previous_beforeunload_count:
raise AssertionError(f"Expected beforeunload to cancel {label}, got {state}\n" + "\n".join(log))
ui_history = expected_history_snapshot["ui"]
expect_ui_session_history(
webdriver_port,
session_id,
f"after blocked {label}",
history_entry_urls(ui_history),
history_used_steps(ui_history),
ui_history["currentUsedStepIndex"],
ui_history["backButtonEnabled"],
ui_history["forwardButtonEnabled"],
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=history_step_values(ui_history["webContentKnownUsedSteps"]),
expected_web_content_current_step=ui_history["webContentCurrentStep"],
)
return state
def expect_javascript_noop_ui_load_does_not_change_history(
webdriver_port,
session_id,
expected_url,
expected_history_snapshot,
log,
):
load_url_from_ui(webdriver_port, session_id, "javascript:window.uiJavascriptNoopRan=true;void(0)")
state = execute_script(
webdriver_port,
session_id,
"return [location.href, window.uiJavascriptNoopRan === true];",
)
if state != [expected_url, True]:
raise AssertionError(
f"Expected javascript:void(0) UI load to stay on {expected_url}, got {state}\n" + "\n".join(log)
)
ui_history = expected_history_snapshot["ui"]
expect_ui_session_history(
webdriver_port,
session_id,
"after javascript:void(0) UI load",
history_entry_urls(ui_history),
history_used_steps(ui_history),
ui_history["currentUsedStepIndex"],
ui_history["backButtonEnabled"],
ui_history["forwardButtonEnabled"],
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=history_step_values(ui_history["webContentKnownUsedSteps"]),
expected_web_content_current_step=ui_history["webContentCurrentStep"],
)
def expect_javascript_noop_webdriver_navigation_does_not_change_history(
webdriver_port,
session_id,
expected_url,
expected_history_snapshot,
log,
):
request(webdriver_port, "POST", f"/session/{session_id}/timeouts", {"pageLoad": 1000})
request(webdriver_port, "POST", f"/session/{session_id}/url", {"url": "javascript:void(0)"})
request(webdriver_port, "POST", f"/session/{session_id}/timeouts", {"pageLoad": 10000})
state = execute_script(webdriver_port, session_id, "return location.href;")
if state != expected_url:
raise AssertionError(
f"Expected javascript:void(0) WebDriver navigation to stay on {expected_url}, got {state}\n"
+ "\n".join(log)
)
ui_history = expected_history_snapshot["ui"]
expect_ui_session_history(
webdriver_port,
session_id,
"after javascript:void(0) WebDriver navigation",
history_entry_urls(ui_history),
history_used_steps(ui_history),
ui_history["currentUsedStepIndex"],
ui_history["backButtonEnabled"],
ui_history["forwardButtonEnabled"],
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=history_step_values(ui_history["webContentKnownUsedSteps"]),
expected_web_content_current_step=ui_history["webContentCurrentStep"],
)
def expect_beforeunload_cancels_refresh(
webdriver_port,
session_id,
expected_url,
previous_beforeunload_count,
log,
):
expect_current_ui_entry_reload_pending(webdriver_port, session_id, "before blocked refresh from /b", False, log)
request(webdriver_port, "POST", f"/session/{session_id}/timeouts", {"pageLoad": 1000})
try:
refresh(webdriver_port, session_id)
finally:
request(webdriver_port, "POST", f"/session/{session_id}/timeouts", {"pageLoad": 10000})
def refresh_was_canceled_by_beforeunload(result):
return (
isinstance(result, list)
and len(result) == 2
and result[0] == expected_url
and isinstance(result[1], int)
and result[1] > previous_beforeunload_count
)
state = wait_for_script_result(
webdriver_port,
session_id,
"beforeunload-canceled refresh from /b",
"return [location.href, window.beforeUnloadCount];",
refresh_was_canceled_by_beforeunload,
log,
)
expect_current_ui_entry_reload_pending(webdriver_port, session_id, "after blocked refresh from /b", False, log)
return state
def expect_current_top_level_history_url(webdriver_port, session_id, label, expected_url, log):
snapshot = session_history(webdriver_port, session_id)
current_entry = history_current_entry(snapshot["ui"])
if current_entry["url"] == expected_url:
log.append(f"{label} current history URL: {expected_url}")
return snapshot
raise AssertionError(
f"Expected {label} current history URL to be {expected_url}, "
f"got {summarize_history_snapshot(snapshot)}\n" + "\n".join(log)
)
def expect_navigation_buttons(webdriver_port, session_id, label, expected_back_enabled, expected_forward_enabled, log):
snapshot = session_history(webdriver_port, session_id)
ui = snapshot["ui"]
if ui["backButtonEnabled"] is expected_back_enabled and ui["forwardButtonEnabled"] is expected_forward_enabled:
log.append(
f"{label} buttons: back={expected_back_enabled}, forward={expected_forward_enabled}, "
f"history={summarize_history_snapshot(snapshot)}"
)
return snapshot
raise AssertionError(
f"Expected {label} navigation buttons to be back={expected_back_enabled}, "
f"forward={expected_forward_enabled}, "
f"got {summarize_history_snapshot(snapshot)}\n" + "\n".join(log)
)
def expect_web_content_session_history_matches_ui(webdriver_port, session_id, label, log):
snapshot = session_history(webdriver_port, session_id)
ui = snapshot["ui"]
if (
ui["webContentHistoryMatchesUI"]
and not ui["waitingToSeedWebContent"]
and not ui["waitingForWebContentSeedAck"]
and not ui["ignoringWebContentUpdatesUntilSeed"]
and not ui["reseedAfterCurrentHistoryLoad"]
and ui["pendingWebContentHistoryStepAfterFallbackLoad"] is None
and ui["pendingSessionHistoryNavigation"] is None
and ui["pendingSessionHistoryTraversal"] is None
and comparable_history(ui) == comparable_history(snapshot["webContent"])
):
log.append(f"{label} matched history: {summarize_history_snapshot(snapshot)}")
return snapshot
raise AssertionError(
f"Expected {label} WebContent history to match UI, "
f"got {summarize_history_snapshot(snapshot)}\n" + "\n".join(log)
)
def expect_pending_session_history_traversal(
webdriver_port,
session_id,
label,
expected_entry_urls,
expected_used_steps,
expected_current_used_step_index,
expected_back_enabled,
expected_forward_enabled,
expected_stage,
expected_will_replace_web_content_process,
log,
):
snapshot = session_history(webdriver_port, session_id)
ui = snapshot["ui"]
pending_navigation = ui["pendingSessionHistoryNavigation"]
pending_traversal = ui["pendingSessionHistoryTraversal"]
log.append(f"{label} pending traversal: {summarize_history_snapshot(snapshot)}")
if history_entry_urls(ui) != expected_entry_urls:
raise AssertionError(f"Expected {label} entries to be {expected_entry_urls}\n" + "\n".join(log))
if history_used_steps(ui) != expected_used_steps:
raise AssertionError(f"Expected {label} used steps to be {expected_used_steps}\n" + "\n".join(log))
if ui["currentUsedStepIndex"] != expected_current_used_step_index:
raise AssertionError(
f"Expected {label} current used step index to be {expected_current_used_step_index}\n" + "\n".join(log)
)
if (
ui["backButtonEnabled"] is not expected_back_enabled
or ui["forwardButtonEnabled"] is not expected_forward_enabled
):
raise AssertionError(
f"Expected {label} buttons to be back={expected_back_enabled}, forward={expected_forward_enabled}\n"
+ "\n".join(log)
)
if pending_traversal is None:
raise AssertionError(f"Expected {label} to have a pending session history traversal\n" + "\n".join(log))
if pending_navigation is None:
raise AssertionError(f"Expected {label} to have a pending session history navigation\n" + "\n".join(log))
if pending_navigation["url"] != expected_entry_urls[expected_current_used_step_index]:
raise AssertionError(
f"Expected {label} pending navigation URL to be "
f"{expected_entry_urls[expected_current_used_step_index]}, got {pending_navigation['url']}\n"
+ "\n".join(log)
)
if pending_navigation["previousCurrentURL"] == pending_navigation["url"]:
raise AssertionError(
f"Expected {label} pending navigation to preserve the previous current URL\n" + "\n".join(log)
)
expected_step = expected_used_steps[expected_current_used_step_index]
if pending_traversal["targetStep"] != expected_step:
raise AssertionError(
f"Expected {label} pending traversal target step to be {expected_step}, "
f"got {pending_traversal['targetStep']}\n" + "\n".join(log)
)
if pending_traversal["targetStepIndex"] != expected_current_used_step_index:
raise AssertionError(
f"Expected {label} pending traversal target step index to be {expected_current_used_step_index}, "
f"got {pending_traversal['targetStepIndex']}\n" + "\n".join(log)
)
if pending_traversal["stage"] != expected_stage:
raise AssertionError(
f"Expected {label} pending traversal stage to be {expected_stage}, "
f"got {pending_traversal['stage']}\n" + "\n".join(log)
)
if pending_traversal["willReplaceWebContentProcess"] is not expected_will_replace_web_content_process:
raise AssertionError(
f"Expected {label} pending traversal willReplaceWebContentProcess to be "
f"{expected_will_replace_web_content_process}, got "
f"{pending_traversal['willReplaceWebContentProcess']}\n" + "\n".join(log)
)
expected_restore_mode = (
"restore-from-ui-process" if expected_will_replace_web_content_process else "preserve-current-process-state"
)
if pending_navigation["webContentRestoreMode"] != expected_restore_mode:
raise AssertionError(
f"Expected {label} pending navigation restore mode to be {expected_restore_mode}, "
f"got {pending_navigation['webContentRestoreMode']}\n" + "\n".join(log)
)
if ui["webContentHistoryMatchesUI"]:
raise AssertionError(f"Expected {label} WebContent history to still be catching up\n" + "\n".join(log))
return snapshot
def expect_current_entry_nested_history(webdriver_port, session_id, label, expected_url, expected_nested_urls, log):
snapshot = session_history(webdriver_port, session_id)
ui_current_entry = history_current_entry(snapshot["ui"])
web_content_current_entry = history_current_entry(snapshot["webContent"])
ui_nested_urls = []
if ui_current_entry["nestedHistories"]:
ui_nested_urls = [entry["url"] for entry in ui_current_entry["nestedHistories"][0]["entries"]]
web_content_nested_urls = []
if web_content_current_entry["nestedHistories"]:
web_content_nested_urls = [entry["url"] for entry in web_content_current_entry["nestedHistories"][0]["entries"]]
if (
ui_current_entry["url"] == expected_url
and web_content_current_entry["url"] == expected_url
and ui_nested_urls == expected_nested_urls
and web_content_nested_urls == expected_nested_urls
):
log.append(f"{label} nested history: {expected_nested_urls}")
return snapshot
raise AssertionError(
f"Expected {label} nested history to be {expected_nested_urls} at {expected_url}, "
f"got {summarize_history_snapshot(snapshot)}\n" + "\n".join(log)
)
def expect_current_ui_entry_reload_pending(webdriver_port, session_id, label, expected_reload_pending, log):
snapshot = session_history(webdriver_port, session_id)
ui_current_entry = history_current_entry(snapshot["ui"])
if ui_current_entry["reloadPending"] is expected_reload_pending:
log.append(f"{label} UI reload pending: {expected_reload_pending}")
return snapshot
raise AssertionError(
f"Expected {label} current UI entry reloadPending to be {expected_reload_pending}, "
f"got {summarize_history_snapshot(snapshot)}\n" + "\n".join(log)
)
def nested_history_urls_for_entry(history, url):
matching_entries = [entry for entry in history["entries"] if entry["url"] == url]
if len(matching_entries) > 1:
raise AssertionError(f"Expected at most one entry for {url}, got {matching_entries}")
if not matching_entries:
return None
entry = matching_entries[0]
if not entry["nestedHistories"]:
return []
return [nested_entry["url"] for nested_entry in entry["nestedHistories"][0]["entries"]]
def expect_entry_nested_history(webdriver_port, session_id, label, entry_url, expected_nested_urls, log):
snapshot = session_history(webdriver_port, session_id)
ui_nested_urls = nested_history_urls_for_entry(snapshot["ui"], entry_url)
web_content_nested_urls = nested_history_urls_for_entry(snapshot["webContent"], entry_url)
if ui_nested_urls == expected_nested_urls and web_content_nested_urls == expected_nested_urls:
log.append(f"{label} nested history: {expected_nested_urls}")
return snapshot
raise AssertionError(
f"Expected {label} nested history to be {expected_nested_urls} at {entry_url}, "
f"got {summarize_history_snapshot(snapshot)}\n" + "\n".join(log)
)
def expect_pending_web_content_history_step_after_fallback_load(webdriver_port, session_id, label, log):
snapshot = session_history(webdriver_port, session_id)
ui = snapshot["ui"]
web_content = snapshot["webContent"]
pending_step = ui["pendingWebContentHistoryStepAfterFallbackLoad"]
pending_traversal = ui["pendingSessionHistoryTraversal"]
ui_current_step = history_used_steps(ui)[ui["currentUsedStepIndex"]]
web_content_current_step = history_used_steps(web_content)[web_content["currentUsedStepIndex"]]
log.append(
f"{label} pending fallback step: {pending_step}, "
f"pendingTraversal={pending_traversal}, "
f"webContentCurrentStep={ui['webContentCurrentStep']}, "
f"matches={ui['webContentHistoryMatchesUI']}"
)
if pending_step is None:
raise AssertionError(f"Expected {label} to have a pending fallback step\n" + "\n".join(log))
if ui["webContentHistoryMatchesUI"]:
raise AssertionError(
f"Expected {label} WebContent history to be stale while step is pending\n" + "\n".join(log)
)
if ui["waitingToSeedWebContent"] or ui["waitingForWebContentSeedAck"] or ui["reseedAfterCurrentHistoryLoad"]:
raise AssertionError(f"Expected {label} WebContent seed to be applied\n" + "\n".join(log))
if pending_traversal is None:
raise AssertionError(f"Expected {label} to have a pending session history traversal\n" + "\n".join(log))
if pending_traversal["targetStep"] != pending_step:
raise AssertionError(
f"Expected {label} pending traversal target {pending_traversal['targetStep']} "
f"to match pending WebContent step {pending_step}\n" + "\n".join(log)
)
if pending_traversal["stage"] != "restoring-nested-step-after-seed":
raise AssertionError(
f"Expected {label} pending traversal to be restoring nested step after seed, "
f"got {pending_traversal['stage']}\n" + "\n".join(log)
)
if pending_step != ui_current_step:
raise AssertionError(
f"Expected {label} pending step {pending_step} to match UI current step {ui_current_step}\n"
+ "\n".join(log)
)
if ui["webContentCurrentStep"] != web_content_current_step:
raise AssertionError(
f"Expected {label} UI to report WebContent current step {web_content_current_step}, "
f"got {ui['webContentCurrentStep']}\n" + "\n".join(log)
)
if history_current_step(ui["webContentKnownUsedSteps"]) != web_content_current_step:
raise AssertionError(
f"Expected {label} known WebContent steps to mark current step {web_content_current_step}\n"
+ "\n".join(log)
)
if ui["webContentCurrentStep"] == pending_step:
raise AssertionError(
f"Expected {label} WebContent to still be before pending step {pending_step}\n" + "\n".join(log)
)
def expect_no_pending_web_content_history_step_after_fallback_load(webdriver_port, session_id, label, log):
snapshot = session_history(webdriver_port, session_id)
ui = snapshot["ui"]
web_content = snapshot["webContent"]
if (
ui["pendingWebContentHistoryStepAfterFallbackLoad"] is None
and ui["webContentHistoryMatchesUI"]
and not ui["waitingToSeedWebContent"]
and not ui["waitingForWebContentSeedAck"]
and not ui["ignoringWebContentUpdatesUntilSeed"]
and not ui["reseedAfterCurrentHistoryLoad"]
and ui["pendingSessionHistoryNavigation"] is None
and ui["pendingSessionHistoryTraversal"] is None
and ui["webContentCurrentStep"] == history_used_steps(ui)[ui["currentUsedStepIndex"]]
and history_current_step(ui["webContentKnownUsedSteps"]) == ui["webContentCurrentStep"]
and comparable_history(ui) == comparable_history(web_content)
):
log.append(f"{label} restored fallback step: {summarize_history_snapshot(snapshot)}")
return
raise AssertionError(
f"Expected {label} to clear pending fallback step and match WebContent, "
f"got {summarize_history_snapshot(snapshot)}\n" + "\n".join(log)
)
def expect_url(webdriver_port, session_id, label, expected_url, log):
actual_url = current_url(webdriver_port, session_id)
if actual_url == expected_url:
log.append(f"{label}: {actual_url}")
return
raise AssertionError(f"Expected {label} to be {expected_url}, got {actual_url}\n" + "\n".join(log))
def wait_for_url(webdriver_port, session_id, label, expected_url, log, timeout=EVENT_TIMEOUT_SECONDS):
deadline = time.monotonic() + timeout
actual_url = None
while time.monotonic() < deadline:
actual_url = current_url(webdriver_port, session_id)
if actual_url == expected_url:
log.append(f"{label}: {actual_url}")
return
time.sleep(0.05)
raise AssertionError(f"Timed out waiting for {label} to be {expected_url}, got {actual_url}\n" + "\n".join(log))
def expect_body_text(webdriver_port, session_id, label, expected_text, log):
actual = execute_script(webdriver_port, session_id, "return [document.body.innerText.trim(), document.readyState];")
if actual == [expected_text, "complete"]:
log.append(f"{label}: {actual[0]}")
return
raise AssertionError(f"Expected {label} body text to be {expected_text}, got {actual}\n" + "\n".join(log))
def expect_current_entry_resource(webdriver_port, session_id, label, expected_resource, log):
snapshot = session_history(webdriver_port, session_id)
ui_current_entry = history_current_entry(snapshot["ui"])
web_content_current_entry = history_current_entry(snapshot["webContent"])
if ui_current_entry["resource"] == expected_resource and web_content_current_entry["resource"] == expected_resource:
log.append(f"{label} current resource: {expected_resource}")
return snapshot
raise AssertionError(
f"Expected {label} current resource to be {expected_resource}, "
f"got {summarize_history_snapshot(snapshot)}\n" + "\n".join(log)
)
def expect_current_ui_entry_resource(webdriver_port, session_id, label, expected_resource, log):
snapshot = session_history(webdriver_port, session_id)
ui_current_entry = history_current_entry(snapshot["ui"])
if ui_current_entry["resource"] == expected_resource:
log.append(f"{label} current UI resource: {expected_resource}")
return snapshot
raise AssertionError(
f"Expected {label} current UI resource to be {expected_resource}, "
f"got {summarize_history_snapshot(snapshot)}\n" + "\n".join(log)
)
def expect_frame_url(webdriver_port, session_id, label, expected_url, log, expected_title=None):
result = execute_script(
webdriver_port,
session_id,
"""
const frame = document.getElementById('frame');
return [frame.contentWindow.location.href, frame.contentDocument.readyState, frame.contentDocument.title];
""",
)
actual_url, ready_state, actual_title = result
if (
actual_url == expected_url
and ready_state == "complete"
and (expected_title is None or actual_title == expected_title)
):
log.append(f"{label}: {actual_url}")
return
actual = f"{actual_url} ({actual_title})"
raise AssertionError(f"Expected {label} to be {expected_url}, got {actual}\n" + "\n".join(log))
def expect_history_entry_state(
webdriver_port,
session_id,
label,
expected_url,
expected_classic_state,
expected_navigation_state,
expected_scroll_restoration,
expected_navigation_key,
expected_navigation_id,
log,
):
result = execute_script(
webdriver_port,
session_id,
"""
return [
location.href,
JSON.stringify(history.state),
JSON.stringify(navigation.currentEntry.getState()),
history.scrollRestoration,
navigation.currentEntry.key,
navigation.currentEntry.id,
];
""",
)
log.append(f"{label}: {result}")
expected = [
expected_url,
expected_classic_state,
expected_navigation_state,
expected_scroll_restoration,
expected_navigation_key,
expected_navigation_id,
]
if result != expected:
raise AssertionError(f"Expected {label} to be {expected}, got {result}\n" + "\n".join(log))
def expect_window_name(webdriver_port, session_id, label, expected_name, log):
actual_name = execute_script(webdriver_port, session_id, "return window.name;")
log.append(f"{label}: {actual_name}")
if actual_name != expected_name:
raise AssertionError(f"Expected {label} to be {expected_name}, got {actual_name}\n" + "\n".join(log))
def expect_scroll_position(webdriver_port, session_id, label, expected_x, expected_y, log):
actual = execute_script(
webdriver_port,
session_id,
"return [Math.round(scrollX), Math.round(scrollY), document.readyState];",
)
if actual == [expected_x, expected_y, "complete"]:
log.append(f"{label}: {actual}")
return
raise AssertionError(
f"Expected {label} to be {[expected_x, expected_y, 'complete']}, got {actual}\n" + "\n".join(log)
)
def run_blocked_process_swap_ui_forward_crash_recovery_test(
webdriver_port,
page_server,
url_b,
url_process_swap_back_blocked,
):
session_id = create_session(webdriver_port)
log = [f"blocked process-swap UI forward crash recovery initial: {current_url(webdriver_port, session_id)}"]
with page_server.process_swap_back_blocked_request_lock:
page_server.process_swap_back_blocked_request_count = 0
page_server.blocked_process_swap_back_requested.clear()
page_server.process_swap_back_recovery_requested.clear()
page_server.release_blocked_process_swap_back.clear()
load_url_from_ui(webdriver_port, session_id, url_b)
expect_url(webdriver_port, session_id, "after process-swap forward crash recovery setup /b", url_b, log)
load_url_from_ui(webdriver_port, session_id, url_process_swap_back_blocked)
expect_url(
webdriver_port,
session_id,
"after process-swap forward crash recovery setup /process-swap-back-blocked",
url_process_swap_back_blocked,
log,
)
expect_ui_session_history(
webdriver_port,
session_id,
"after process-swap forward crash recovery setup",
[url_b, url_process_swap_back_blocked],
[0, 1],
1,
True,
False,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=1,
)
traverse_history_from_ui(webdriver_port, session_id, -1)
expect_url(webdriver_port, session_id, "after process-swap forward crash recovery setup back to /b", url_b, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after process-swap forward crash recovery setup back to /b",
[url_b, url_process_swap_back_blocked],
[0, 1],
0,
False,
True,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=0,
)
page_server.blocked_process_swap_back_requested.clear()
page_server.process_swap_back_recovery_requested.clear()
page_server.release_blocked_process_swap_back.clear()
page_server.process_swap_back_document_ran.clear()
traverse_history_from_ui(webdriver_port, session_id, 1, wait_for_navigation_completion=False)
wait_for_event(
page_server.blocked_process_swap_back_requested,
"blocked process-swap UI forward before crash",
)
expect_pending_session_history_traversal(
webdriver_port,
session_id,
"while process-swap UI forward loads target before crash",
[url_b, url_process_swap_back_blocked],
[0, 1],
1,
True,
False,
"replacing-webcontent-process",
True,
log,
)
crash_current_page_allowing_navigation_timeout(webdriver_port, session_id)
wait_for_event(page_server.process_swap_back_recovery_requested, "process-swap forward recovery request")
expect_ui_session_history(
webdriver_port,
session_id,
"while process-swap UI forward recovers target after crash",
[url_b, url_process_swap_back_blocked],
[0, 1],
1,
True,
False,
log,
expect_web_content_matches_ui=False,
expected_waiting_to_seed_web_content=True,
expected_waiting_for_web_content_seed_ack=False,
expected_ignoring_web_content_updates_until_seed=True,
expected_reseed_after_current_history_load=True,
)
page_server.release_blocked_process_swap_back.set()
wait_for_event(page_server.process_swap_back_document_ran, "process-swap UI forward recovery document")
expect_url(
webdriver_port,
session_id,
"after process-swap UI forward crash recovery completes",
url_process_swap_back_blocked,
log,
)
expect_ui_session_history(
webdriver_port,
session_id,
"after process-swap UI forward crash recovery completes",
[url_b, url_process_swap_back_blocked],
[0, 1],
1,
True,
False,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=1,
)
request(webdriver_port, "DELETE", f"/session/{session_id}")
def expect_second_ui_forward_during_pending_forward_does_not_hang(
webdriver_port,
session_id,
page_server,
url_a,
url_forward_blocked,
url_c,
):
log = [f"second UI forward during pending forward initial: {current_url(webdriver_port, session_id)}"]
page_server.block_forward_load = False
page_server.blocked_forward_requested.clear()
page_server.release_blocked_forward.clear()
load_url_from_ui(webdriver_port, session_id, url_a)
expect_url(webdriver_port, session_id, "after pending-forward setup /a", url_a, log)
load_url_from_ui(webdriver_port, session_id, url_forward_blocked)
expect_url(webdriver_port, session_id, "after pending-forward setup /forward-blocked", url_forward_blocked, log)
load_url_from_ui(webdriver_port, session_id, url_c)
expect_url(webdriver_port, session_id, "after pending-forward setup /c", url_c, log)
expect_web_content_session_history_matches_ui(webdriver_port, session_id, "after pending-forward setup", log)
traverse_history_from_ui(webdriver_port, session_id, -1)
expect_url(
webdriver_port, session_id, "after pending-forward setup back to /forward-blocked", url_forward_blocked, log
)
traverse_history_from_ui(webdriver_port, session_id, -1)
expect_url(webdriver_port, session_id, "after pending-forward setup back to /a", url_a, log)
page_server.block_forward_load = True
page_server.blocked_forward_requested.clear()
page_server.release_blocked_forward.clear()
traverse_history_from_ui(webdriver_port, session_id, 1, wait_for_navigation_completion=False)
wait_for_event(page_server.blocked_forward_requested, "blocked first UI forward")
second_forward_error = []
def request_second_forward():
try:
traverse_history_from_ui(webdriver_port, session_id, 1)
except Exception as error:
second_forward_error.append(error)
second_forward_thread = threading.Thread(target=request_second_forward)
second_forward_thread.start()
page_server.release_blocked_forward.set()
second_forward_thread.join(timeout=EVENT_TIMEOUT_SECONDS)
if second_forward_thread.is_alive():
raise AssertionError("Timed out waiting for second UI forward during pending forward\n" + "\n".join(log))
if second_forward_error:
raise AssertionError(
f"Second UI forward during pending forward failed: {second_forward_error[0]}\n" + "\n".join(log)
)
expect_url(webdriver_port, session_id, "after second UI forward during pending forward", url_c, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after second UI forward during pending forward",
[url_a, url_forward_blocked, url_c],
[0, 1, 2],
2,
True,
False,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2],
expected_web_content_current_step=2,
)
page_server.block_forward_load = False
def expect_sandboxed_history_back_to_be_blocked(webdriver_port, session_id, url_a, url_b, log):
sandboxed_history_back_state = execute_async_script(
webdriver_port,
session_id,
"""
const done = arguments[0];
window.sandboxedHistoryBackResult = null;
window.addEventListener("message", event => {
if (event.data.startsWith("sandboxed-history-back:")) {
window.sandboxedHistoryBackResult = event.data;
done([location.href, window.sandboxedHistoryBackResult]);
}
}, { once: true });
const iframe = document.createElement("iframe");
iframe.sandbox = "allow-same-origin allow-scripts";
iframe.srcdoc = `
<script>
try {
history.back();
parent.postMessage("sandboxed-history-back:called", "*");
} catch (e) {
parent.postMessage("sandboxed-history-back:threw:" + e.name, "*");
}
</scr` + `ipt>`;
document.body.append(iframe);
""",
)
if sandboxed_history_back_state != [url_b, "sandboxed-history-back:called"]:
raise AssertionError(
f"Expected sandboxed iframe history.back() to be blocked, got {sandboxed_history_back_state}\n"
+ "\n".join(log)
)
expect_ui_session_history(
webdriver_port,
session_id,
"after sandboxed iframe history.back() from /b",
[url_a, url_b],
[0, 1],
1,
True,
False,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=1,
)
execute_script(webdriver_port, session_id, "document.querySelector('iframe')?.remove(); return null;")
def expect_beforeunload_cancels_stale_ui_load(
webdriver_port,
session_id,
url_b,
url_c,
before_blocked_browser_ui_back,
blocked_link_navigation_state,
log,
):
mark_web_content_session_history_stale(webdriver_port, session_id)
expect_ui_session_history(
webdriver_port,
session_id,
"after marking WebContent session history stale",
history_entry_urls(before_blocked_browser_ui_back["ui"]),
history_used_steps(before_blocked_browser_ui_back["ui"]),
before_blocked_browser_ui_back["ui"]["currentUsedStepIndex"],
before_blocked_browser_ui_back["ui"]["backButtonEnabled"],
before_blocked_browser_ui_back["ui"]["forwardButtonEnabled"],
log,
expect_web_content_matches_ui=False,
expected_web_content_known_used_steps=history_step_values(
before_blocked_browser_ui_back["ui"]["webContentKnownUsedSteps"]
),
expected_web_content_current_step=before_blocked_browser_ui_back["ui"]["webContentCurrentStep"],
)
load_url_from_ui(webdriver_port, session_id, url_c)
blocked_stale_ui_load_state = execute_script(
webdriver_port,
session_id,
"return [location.href, window.beforeUnloadCount];",
)
if blocked_stale_ui_load_state[0] != url_b or blocked_stale_ui_load_state[1] <= blocked_link_navigation_state[1]:
raise AssertionError(
f"Expected beforeunload to cancel stale-history UI load from /b, got {blocked_stale_ui_load_state}\n"
+ "\n".join(log)
)
expect_ui_session_history(
webdriver_port,
session_id,
"after blocked stale-history UI load from /b",
history_entry_urls(before_blocked_browser_ui_back["ui"]),
history_used_steps(before_blocked_browser_ui_back["ui"]),
before_blocked_browser_ui_back["ui"]["currentUsedStepIndex"],
before_blocked_browser_ui_back["ui"]["backButtonEnabled"],
before_blocked_browser_ui_back["ui"]["forwardButtonEnabled"],
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=history_step_values(
before_blocked_browser_ui_back["ui"]["webContentKnownUsedSteps"]
),
expected_web_content_current_step=before_blocked_browser_ui_back["ui"]["webContentCurrentStep"],
)
return blocked_stale_ui_load_state
def expect_same_url_ui_load_replaces_current_entry(webdriver_port, session_id, url, log):
load_url_from_ui(webdriver_port, session_id, url)
expect_url(webdriver_port, session_id, "after same-URL UI load setup /a", url, log)
load_url_from_ui(webdriver_port, session_id, url)
expect_url(webdriver_port, session_id, "after same-URL UI load /a", url, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after same-URL UI load /a",
[url],
[0],
0,
False,
False,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0],
expected_web_content_current_step=0,
)
def expect_webdriver_fragment_navigation_completes(webdriver_port, session_id, url, log):
fragment_url = f"{url}#fragment"
request(webdriver_port, "POST", f"/session/{session_id}/timeouts", {"pageLoad": 1000})
request(webdriver_port, "POST", f"/session/{session_id}/url", {"url": fragment_url})
request(webdriver_port, "POST", f"/session/{session_id}/timeouts", {"pageLoad": 10000})
expect_url(webdriver_port, session_id, "after WebDriver fragment navigation", fragment_url, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after WebDriver fragment navigation",
[url, fragment_url],
[0, 1],
1,
True,
False,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=1,
)
def run_webdriver_fragment_navigation_test(webdriver_port, url):
session_id = create_session(webdriver_port)
log = [f"WebDriver fragment navigation initial: {current_url(webdriver_port, session_id)}"]
load_url_from_ui(webdriver_port, session_id, url)
expect_url(webdriver_port, session_id, "after WebDriver fragment navigation setup /a", url, log)
expect_webdriver_fragment_navigation_completes(webdriver_port, session_id, url, log)
request(webdriver_port, "DELETE", f"/session/{session_id}")
def run_test(webdriver_binary):
page_server = TestPageServer(("0.0.0.0", 0), TestPageHandler)
page_server_thread = threading.Thread(target=page_server.serve_forever, daemon=True)
page_server_thread.start()
webdriver_port = unused_port()
env = os.environ.copy()
env["LADYBIRD_WEBDRIVER_ENABLE_SITE_ISOLATION"] = "1"
env["LADYBIRD_SESSION_HISTORY_DEBUG"] = "1"
webdriver_stdout = tempfile.TemporaryFile(mode="w+", encoding="utf-8")
webdriver_stderr = tempfile.TemporaryFile(mode="w+", encoding="utf-8")
expect_reload_pending_history_log = False
expect_no_entry_history_log = False
webdriver = subprocess.Popen(
[webdriver_binary, "--headless", "-l", "127.0.0.1", "-p", str(webdriver_port)],
stdout=webdriver_stdout,
stderr=webdriver_stderr,
text=True,
env=env,
)
session_id = None
failed = False
try:
wait_for_port(webdriver_port)
page_port = page_server.server_port
url_a = f"http://localhost:{page_port}/a"
url_b = f"http://127.0.0.1:{page_port}/b"
url_c = f"http://127.0.0.1:{page_port}/c"
url_d = f"http://localhost:{page_port}/d"
url_redirect_to_b = f"http://localhost:{page_port}/redirect-to-b"
url_nested = f"http://localhost:{page_port}/nested"
url_state = f"http://localhost:{page_port}/state"
url_state_replace_load = f"http://localhost:{page_port}/state?replace-load"
url_state_replace = f"http://localhost:{page_port}/state?replace"
url_state_push = f"http://localhost:{page_port}/state?push"
url_scroll = f"http://localhost:{page_port}/scroll"
url_scroll_saved = f"http://localhost:{page_port}/scroll?saved"
url_reload_blocked = f"http://localhost:{page_port}/reload-blocked"
url_process_swap_back_blocked = f"http://localhost:{page_port}/process-swap-back-blocked"
url_forward_blocked = f"http://127.0.0.1:{page_port}/forward-blocked"
url_frame_a = f"http://localhost:{page_port}/frame-a"
url_frame_b_blocked = f"http://localhost:{page_port}/frame-b-blocked"
url_nested_same_document = f"http://localhost:{page_port}/nested?same-document"
url_post_form = f"http://localhost:{page_port}/post-form"
url_post_result = f"http://localhost:{page_port}/post-result"
url_post_blocked_form = f"http://localhost:{page_port}/post-blocked-form"
url_post_blocked_result = f"http://localhost:{page_port}/post-result-blocked-same-site"
url_post_same_url_blocked = f"http://localhost:{page_port}/post-same-url-blocked"
url_cross_site_post_form = f"http://localhost:{page_port}/cross-site-post-form"
url_cross_site_post_blocked_form = f"http://localhost:{page_port}/cross-site-post-blocked-form"
url_cross_site_post_blocked_result = f"http://127.0.0.1:{page_port}/post-result-blocked"
url_cross_site_post_result = f"http://127.0.0.1:{page_port}/post-result"
expect_ladybird_test_hooks_require_capability(webdriver_port)
session_id = create_session(webdriver_port)
expect_second_ui_forward_during_pending_forward_does_not_hang(
webdriver_port,
session_id,
page_server,
url_a,
url_forward_blocked,
url_c,
)
request(webdriver_port, "DELETE", f"/session/{session_id}")
session_id = None
session_id = create_session(webdriver_port)
log = [f"first-entry replace initial: {current_url(webdriver_port, session_id)}"]
load_url_from_ui(webdriver_port, session_id, url_a)
expect_url(webdriver_port, session_id, "after first-entry replace test /a", url_a, log)
page_server.state_replace_load_document_ran.clear()
execute_script(
webdriver_port, session_id, f"location.replace({json.dumps(url_state_replace_load)}); return null;"
)
wait_for_event(page_server.state_replace_load_document_ran, "state replace-load document script")
expect_url(
webdriver_port,
session_id,
"after first-entry location.replace() to /state?replace-load",
url_state_replace_load,
log,
)
expect_ui_session_history(
webdriver_port,
session_id,
"after first-entry location.replace() to /state?replace-load",
[url_state_replace_load],
[0],
0,
False,
False,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0],
expected_web_content_current_step=0,
)
request(webdriver_port, "DELETE", f"/session/{session_id}")
session_id = None
session_id = create_session(webdriver_port)
log = [f"same-URL UI load initial: {current_url(webdriver_port, session_id)}"]
expect_same_url_ui_load_replaces_current_entry(webdriver_port, session_id, url_a, log)
request(webdriver_port, "DELETE", f"/session/{session_id}")
session_id = None
run_webdriver_fragment_navigation_test(webdriver_port, url_a)
session_id = create_session(webdriver_port)
log = [f"duplicate URL crash recovery initial: {current_url(webdriver_port, session_id)}"]
load_url_from_ui(webdriver_port, session_id, url_a)
expect_url(webdriver_port, session_id, "after duplicate URL setup /a", url_a, log)
load_url_from_ui(webdriver_port, session_id, url_b)
expect_url(webdriver_port, session_id, "after first duplicate URL setup /b", url_b, log)
load_url_from_ui(webdriver_port, session_id, url_c)
expect_url(webdriver_port, session_id, "after duplicate URL setup /c", url_c, log)
load_url_from_ui(webdriver_port, session_id, url_b)
expect_url(webdriver_port, session_id, "after second duplicate URL setup /b", url_b, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after duplicate URL setup",
[url_a, url_b, url_c, url_b],
[0, 1, 2, 3],
3,
True,
False,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2, 3],
expected_web_content_current_step=3,
)
crash_current_page(webdriver_port, session_id)
expect_url(webdriver_port, session_id, "after duplicate URL crash recovery", url_b, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after duplicate URL crash recovery",
[url_a, url_b, url_c, url_b],
[0, 1, 2, 3],
3,
True,
False,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2, 3],
expected_web_content_current_step=3,
)
traverse_history_from_ui(webdriver_port, session_id, -1)
expect_url(webdriver_port, session_id, "after duplicate URL crash recovery back", url_c, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after duplicate URL crash recovery back",
[url_a, url_b, url_c, url_b],
[0, 1, 2, 3],
2,
True,
True,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2, 3],
expected_web_content_current_step=2,
)
traverse_history_from_ui(webdriver_port, session_id, -1)
expect_url(webdriver_port, session_id, "after duplicate URL crash recovery second back", url_b, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after duplicate URL crash recovery second back",
[url_a, url_b, url_c, url_b],
[0, 1, 2, 3],
1,
True,
True,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2, 3],
expected_web_content_current_step=1,
)
traverse_history_from_ui(webdriver_port, session_id, -1)
expect_url(webdriver_port, session_id, "after duplicate URL crash recovery third back", url_a, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after duplicate URL crash recovery third back",
[url_a, url_b, url_c, url_b],
[0, 1, 2, 3],
0,
False,
True,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2, 3],
expected_web_content_current_step=0,
)
traverse_history_from_ui(webdriver_port, session_id, -1)
expect_no_entry_history_log = True
expect_url(webdriver_port, session_id, "after duplicate URL crash recovery back past start", url_a, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after duplicate URL crash recovery back past start",
[url_a, url_b, url_c, url_b],
[0, 1, 2, 3],
0,
False,
True,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2, 3],
expected_web_content_current_step=0,
)
request(webdriver_port, "DELETE", f"/session/{session_id}")
session_id = None
session_id = create_session(webdriver_port)
log = [f"blocked cross-site POST process swap initial: {current_url(webdriver_port, session_id)}"]
load_url_from_ui(webdriver_port, session_id, url_cross_site_post_blocked_form)
expect_url(
webdriver_port,
session_id,
"after blocked cross-site POST form load",
url_cross_site_post_blocked_form,
log,
)
post_form_submit_point = execute_script(
webdriver_port,
session_id,
"""
const button = document.getElementById('submit');
const rect = button.getBoundingClientRect();
return [Math.round(rect.left + rect.width / 2), Math.round(rect.top + rect.height / 2)];
""",
)
page_server.blocked_cross_site_post_requested.clear()
page_server.release_blocked_cross_site_post.clear()
page_server.cross_site_post_result_document_ran.clear()
def request_blocked_cross_site_post():
perform_pointer_click(webdriver_port, session_id, post_form_submit_point[0], post_form_submit_point[1], log)
blocked_cross_site_post_thread = threading.Thread(target=request_blocked_cross_site_post)
blocked_cross_site_post_thread.start()
wait_for_event(page_server.blocked_cross_site_post_requested, "blocked cross-site POST")
expect_ui_session_history(
webdriver_port,
session_id,
"while cross-site POST process swap is blocked",
[url_cross_site_post_blocked_form, url_cross_site_post_blocked_result],
[0, 1],
1,
True,
False,
log,
expect_web_content_matches_ui=False,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=1,
)
expect_current_ui_entry_resource(
webdriver_port,
session_id,
"while cross-site POST process swap is blocked",
"post",
log,
)
page_server.release_blocked_cross_site_post.set()
blocked_cross_site_post_thread.join(timeout=EVENT_TIMEOUT_SECONDS)
if blocked_cross_site_post_thread.is_alive():
raise AssertionError("Timed out waiting for blocked cross-site POST request to finish\n" + "\n".join(log))
wait_for_event(page_server.cross_site_post_result_document_ran, "blocked cross-site POST result document")
expect_url(
webdriver_port,
session_id,
"after blocked cross-site POST form submit",
url_cross_site_post_blocked_result,
log,
)
expect_body_text(
webdriver_port, session_id, "after blocked cross-site POST form submit", "POST:name=ladybird", log
)
expect_current_entry_resource(
webdriver_port, session_id, "after blocked cross-site POST form submit", "post", log
)
request(webdriver_port, "DELETE", f"/session/{session_id}")
session_id = None
session_id = create_session(webdriver_port)
log = [f"cross-site POST process swap initial: {current_url(webdriver_port, session_id)}"]
load_url_from_ui(webdriver_port, session_id, url_cross_site_post_form)
expect_url(webdriver_port, session_id, "after cross-site POST form load", url_cross_site_post_form, log)
post_form_submit_point = execute_script(
webdriver_port,
session_id,
"""
const button = document.getElementById('submit');
const rect = button.getBoundingClientRect();
return [Math.round(rect.left + rect.width / 2), Math.round(rect.top + rect.height / 2)];
""",
)
page_server.post_result_document_ran.clear()
perform_pointer_click(webdriver_port, session_id, post_form_submit_point[0], post_form_submit_point[1], log)
wait_for_event(page_server.post_result_document_ran, "cross-site POST result document")
expect_url(webdriver_port, session_id, "after cross-site POST form submit", url_cross_site_post_result, log)
expect_body_text(webdriver_port, session_id, "after cross-site POST form submit", "POST:name=ladybird", log)
expect_current_entry_resource(webdriver_port, session_id, "after cross-site POST form submit", "post", log)
expect_ui_session_history(
webdriver_port,
session_id,
"after cross-site POST form submit",
[url_cross_site_post_form, url_cross_site_post_result],
[0, 1],
1,
True,
False,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=1,
)
request(webdriver_port, "DELETE", f"/session/{session_id}")
session_id = None
session_id = create_session(webdriver_port)
log = [f"blocked process-swap UI back initial: {current_url(webdriver_port, session_id)}"]
with page_server.process_swap_back_blocked_request_lock:
page_server.process_swap_back_blocked_request_count = 0
page_server.blocked_process_swap_back_requested.clear()
page_server.release_blocked_process_swap_back.clear()
load_url_from_ui(webdriver_port, session_id, url_process_swap_back_blocked)
expect_url(
webdriver_port,
session_id,
"after blocked process-swap setup /process-swap-back-blocked",
url_process_swap_back_blocked,
log,
)
load_url_from_ui(webdriver_port, session_id, url_b)
expect_url(webdriver_port, session_id, "after blocked process-swap setup /b", url_b, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after blocked process-swap setup",
[url_process_swap_back_blocked, url_b],
[0, 1],
1,
True,
False,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=1,
)
page_server.process_swap_back_document_ran.clear()
traverse_history_from_ui(webdriver_port, session_id, -1, wait_for_navigation_completion=False)
wait_for_event(page_server.blocked_process_swap_back_requested, "blocked process-swap UI back")
expect_pending_session_history_traversal(
webdriver_port,
session_id,
"while process-swap UI back loads target",
[url_process_swap_back_blocked, url_b],
[0, 1],
0,
False,
True,
"replacing-webcontent-process",
True,
log,
)
page_server.release_blocked_process_swap_back.set()
wait_for_event(page_server.process_swap_back_document_ran, "process-swap UI back document")
wait_for_ui_session_history(
webdriver_port,
session_id,
"after blocked process-swap UI back converges",
[url_process_swap_back_blocked, url_b],
[0, 1],
0,
False,
True,
log,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=0,
)
expect_url(
webdriver_port,
session_id,
"after blocked process-swap UI back completes",
url_process_swap_back_blocked,
log,
)
expect_ui_session_history(
webdriver_port,
session_id,
"after blocked process-swap UI back completes",
[url_process_swap_back_blocked, url_b],
[0, 1],
0,
False,
True,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=0,
)
request(webdriver_port, "DELETE", f"/session/{session_id}")
session_id = None
session_id = create_session(webdriver_port)
log = [f"blocked process-swap WebDriver back initial: {current_url(webdriver_port, session_id)}"]
with page_server.process_swap_back_blocked_request_lock:
page_server.process_swap_back_blocked_request_count = 0
page_server.blocked_process_swap_back_requested.clear()
page_server.release_blocked_process_swap_back.clear()
load_url_from_ui(webdriver_port, session_id, url_process_swap_back_blocked)
expect_url(
webdriver_port,
session_id,
"after blocked process-swap WebDriver setup /process-swap-back-blocked",
url_process_swap_back_blocked,
log,
)
load_url_from_ui(webdriver_port, session_id, url_b)
expect_url(webdriver_port, session_id, "after blocked process-swap WebDriver setup /b", url_b, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after blocked process-swap WebDriver setup",
[url_process_swap_back_blocked, url_b],
[0, 1],
1,
True,
False,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=1,
)
webdriver_back_error = []
def request_webdriver_back():
try:
request(webdriver_port, "POST", f"/session/{session_id}/back", {})
except Exception as error:
webdriver_back_error.append(error)
webdriver_back_thread = threading.Thread(target=request_webdriver_back)
page_server.process_swap_back_document_ran.clear()
webdriver_back_thread.start()
wait_for_event(page_server.blocked_process_swap_back_requested, "blocked process-swap WebDriver back")
page_server.release_blocked_process_swap_back.set()
wait_for_event(page_server.process_swap_back_document_ran, "process-swap WebDriver back document")
webdriver_back_thread.join(timeout=EVENT_TIMEOUT_SECONDS)
if webdriver_back_thread.is_alive():
raise AssertionError("Timed out waiting for WebDriver back request to finish\n" + "\n".join(log))
if webdriver_back_error:
raise webdriver_back_error[0]
expect_url(
webdriver_port,
session_id,
"after blocked process-swap WebDriver back completes",
url_process_swap_back_blocked,
log,
)
expect_ui_session_history(
webdriver_port,
session_id,
"after blocked process-swap WebDriver back completes",
[url_process_swap_back_blocked, url_b],
[0, 1],
0,
False,
True,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=0,
)
request(webdriver_port, "DELETE", f"/session/{session_id}")
session_id = None
session_id = create_session(webdriver_port)
log = [f"blocked process-swap WebDriver back beforeunload initial: {current_url(webdriver_port, session_id)}"]
with page_server.process_swap_back_blocked_request_lock:
page_server.process_swap_back_blocked_request_count = 0
page_server.blocked_process_swap_back_requested.clear()
page_server.release_blocked_process_swap_back.clear()
load_url_from_ui(webdriver_port, session_id, url_process_swap_back_blocked)
expect_url(
webdriver_port,
session_id,
"after blocked process-swap WebDriver beforeunload setup /process-swap-back-blocked",
url_process_swap_back_blocked,
log,
)
load_url_from_ui(webdriver_port, session_id, url_b)
expect_url(webdriver_port, session_id, "after blocked process-swap WebDriver beforeunload setup /b", url_b, log)
before_blocked_process_swap_webdriver_back = expect_web_content_session_history_matches_ui(
webdriver_port, session_id, "before blocked process-swap WebDriver back from /b", log
)
beforeunload_setup = execute_script(
webdriver_port,
session_id,
"""
window.beforeUnloadCount = 0;
window.onbeforeunload = event => {
++window.beforeUnloadCount;
event.preventDefault();
event.returnValue = "blocked";
return "blocked";
};
return [location.href, window.beforeUnloadCount];
""",
)
if beforeunload_setup != [url_b, 0]:
raise AssertionError(
f"Expected process-swap WebDriver beforeunload setup on /b to be {[url_b, 0]}, "
f"got {beforeunload_setup}\n" + "\n".join(log)
)
inert_click_point = execute_script(
webdriver_port,
session_id,
"""
const rect = document.querySelector("p").getBoundingClientRect();
return [Math.floor(rect.left + rect.width / 2), Math.floor(rect.top + rect.height / 2)];
""",
)
perform_pointer_click(webdriver_port, session_id, inert_click_point[0], inert_click_point[1], log)
webdriver_back_error = []
def request_blocked_webdriver_back():
try:
request(webdriver_port, "POST", f"/session/{session_id}/back", {})
except Exception as error:
webdriver_back_error.append(error)
webdriver_back_thread = threading.Thread(target=request_blocked_webdriver_back)
webdriver_back_thread.start()
webdriver_back_thread.join(timeout=EVENT_TIMEOUT_SECONDS)
if page_server.blocked_process_swap_back_requested.is_set():
page_server.release_blocked_process_swap_back.set()
webdriver_back_thread.join(timeout=EVENT_TIMEOUT_SECONDS)
raise AssertionError(
"Expected beforeunload to cancel process-swap WebDriver back before loading target\n" + "\n".join(log)
)
if webdriver_back_thread.is_alive():
raise AssertionError(
"Timed out waiting for blocked process-swap WebDriver back request to finish\n" + "\n".join(log)
)
if webdriver_back_error:
raise webdriver_back_error[0]
blocked_process_swap_webdriver_back_state = execute_script(
webdriver_port,
session_id,
"return [location.href, window.beforeUnloadCount];",
)
if blocked_process_swap_webdriver_back_state != [url_b, 1]:
raise AssertionError(
f"Expected beforeunload to cancel process-swap WebDriver back from /b, "
f"got {blocked_process_swap_webdriver_back_state}\n" + "\n".join(log)
)
expect_ui_session_history(
webdriver_port,
session_id,
"after blocked process-swap WebDriver back from /b",
history_entry_urls(before_blocked_process_swap_webdriver_back["ui"]),
history_used_steps(before_blocked_process_swap_webdriver_back["ui"]),
before_blocked_process_swap_webdriver_back["ui"]["currentUsedStepIndex"],
before_blocked_process_swap_webdriver_back["ui"]["backButtonEnabled"],
before_blocked_process_swap_webdriver_back["ui"]["forwardButtonEnabled"],
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=history_step_values(
before_blocked_process_swap_webdriver_back["ui"]["webContentKnownUsedSteps"]
),
expected_web_content_current_step=before_blocked_process_swap_webdriver_back["ui"]["webContentCurrentStep"],
)
request(webdriver_port, "DELETE", f"/session/{session_id}")
session_id = None
session_id = create_session(webdriver_port)
log = [f"blocked process-swap UI back crash recovery initial: {current_url(webdriver_port, session_id)}"]
with page_server.process_swap_back_blocked_request_lock:
page_server.process_swap_back_blocked_request_count = 0
page_server.blocked_process_swap_back_requested.clear()
page_server.process_swap_back_recovery_requested.clear()
page_server.release_blocked_process_swap_back.clear()
load_url_from_ui(webdriver_port, session_id, url_process_swap_back_blocked)
expect_url(
webdriver_port,
session_id,
"after process-swap crash recovery setup /process-swap-back-blocked",
url_process_swap_back_blocked,
log,
)
load_url_from_ui(webdriver_port, session_id, url_b)
expect_url(webdriver_port, session_id, "after process-swap crash recovery setup /b", url_b, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after process-swap crash recovery setup",
[url_process_swap_back_blocked, url_b],
[0, 1],
1,
True,
False,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=1,
)
page_server.process_swap_back_document_ran.clear()
traverse_history_from_ui(webdriver_port, session_id, -1, wait_for_navigation_completion=False)
wait_for_event(
page_server.blocked_process_swap_back_requested,
"blocked process-swap UI back before crash",
)
expect_pending_session_history_traversal(
webdriver_port,
session_id,
"while process-swap UI back loads target before crash",
[url_process_swap_back_blocked, url_b],
[0, 1],
0,
False,
True,
"replacing-webcontent-process",
True,
log,
)
crash_current_page_allowing_navigation_timeout(webdriver_port, session_id)
wait_for_event(page_server.process_swap_back_recovery_requested, "process-swap back recovery request")
expect_ui_session_history(
webdriver_port,
session_id,
"while process-swap UI back recovers target after crash",
[url_process_swap_back_blocked, url_b],
[0, 1],
0,
False,
True,
log,
expect_web_content_matches_ui=False,
expected_waiting_to_seed_web_content=True,
expected_waiting_for_web_content_seed_ack=False,
expected_ignoring_web_content_updates_until_seed=True,
expected_reseed_after_current_history_load=True,
)
page_server.release_blocked_process_swap_back.set()
wait_for_event(page_server.process_swap_back_document_ran, "process-swap UI back recovery document")
expect_url(
webdriver_port,
session_id,
"after process-swap UI back crash recovery completes",
url_process_swap_back_blocked,
log,
)
expect_ui_session_history(
webdriver_port,
session_id,
"after process-swap UI back crash recovery completes",
[url_process_swap_back_blocked, url_b],
[0, 1],
0,
False,
True,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=0,
)
request(webdriver_port, "DELETE", f"/session/{session_id}")
session_id = None
run_blocked_process_swap_ui_forward_crash_recovery_test(
webdriver_port,
page_server,
url_b,
url_process_swap_back_blocked,
)
session_id = create_session(webdriver_port)
log = [f"nested restore crash recovery initial: {current_url(webdriver_port, session_id)}"]
page_server.release_blocked_frame_b.set()
page_server.blocked_frame_b_requested.clear()
load_url_from_ui(webdriver_port, session_id, url_nested)
expect_url(webdriver_port, session_id, "after nested restore crash setup /nested", url_nested, log)
expect_frame_url(
webdriver_port,
session_id,
"after nested restore crash setup initial frame",
url_frame_a,
log,
"Frame A",
)
page_server.frame_b_blocked_document_ran.clear()
execute_script(
webdriver_port,
session_id,
f"document.getElementById('frame').contentWindow.location.href = '{url_frame_b_blocked}'; return null;",
)
wait_for_event(page_server.frame_b_blocked_document_ran, "nested frame setup document")
expect_frame_url(
webdriver_port,
session_id,
"after nested restore crash setup frame navigation",
url_frame_b_blocked,
log,
"Frame B Blocked",
)
expect_current_entry_nested_history(
webdriver_port,
session_id,
"after nested restore crash setup",
url_nested,
[url_frame_a, url_frame_b_blocked],
log,
)
load_url_from_ui(webdriver_port, session_id, url_b)
expect_url(webdriver_port, session_id, "after nested restore crash setup /b", url_b, log)
expect_web_content_session_history_matches_ui(
webdriver_port, session_id, "before nested restore crash recovery", log
)
page_server.release_blocked_frame_b.clear()
page_server.blocked_frame_b_requested.clear()
traverse_history_from_ui(webdriver_port, session_id, -1)
expect_url(webdriver_port, session_id, "while nested restore crash recovery loads /nested", url_nested, log)
wait_for_event(page_server.blocked_frame_b_requested, "blocked nested frame restore before crash")
expect_pending_web_content_history_step_after_fallback_load(
webdriver_port, session_id, "while nested restore crash recovery waits for frame", log
)
crash_current_page_allowing_navigation_timeout(webdriver_port, session_id)
expect_ui_session_history(
webdriver_port,
session_id,
"while nested restore crash recovery reloads current entry",
[url_nested, url_b],
[0, 1, 2],
1,
True,
True,
log,
expect_web_content_matches_ui=False,
)
page_server.frame_b_blocked_document_ran.clear()
page_server.release_blocked_frame_b.set()
wait_for_event(page_server.frame_b_blocked_document_ran, "nested frame restore document")
expect_url(webdriver_port, session_id, "after nested restore crash recovery", url_nested, log)
expect_frame_url(
webdriver_port,
session_id,
"after nested restore crash recovery restores iframe",
url_frame_b_blocked,
log,
"Frame B Blocked",
)
expect_current_entry_nested_history(
webdriver_port,
session_id,
"after nested restore crash recovery",
url_nested,
[url_frame_a, url_frame_b_blocked],
log,
)
expect_no_pending_web_content_history_step_after_fallback_load(
webdriver_port, session_id, "after nested restore crash recovery", log
)
expect_web_content_session_history_matches_ui(
webdriver_port, session_id, "after nested restore crash recovery", log
)
request(webdriver_port, "DELETE", f"/session/{session_id}")
session_id = None
session_id = create_session(webdriver_port)
log = [f"blocked same-site POST initial: {current_url(webdriver_port, session_id)}"]
load_url_from_ui(webdriver_port, session_id, url_post_blocked_form)
expect_url(webdriver_port, session_id, "after blocked same-site POST form load", url_post_blocked_form, log)
post_form_submit_point = execute_script(
webdriver_port,
session_id,
"""
const button = document.getElementById('submit');
const rect = button.getBoundingClientRect();
return [Math.round(rect.left + rect.width / 2), Math.round(rect.top + rect.height / 2)];
""",
)
page_server.blocked_same_site_post_requested.clear()
page_server.release_blocked_same_site_post.clear()
page_server.same_site_post_result_document_ran.clear()
def request_blocked_same_site_post():
perform_pointer_click(webdriver_port, session_id, post_form_submit_point[0], post_form_submit_point[1], log)
blocked_same_site_post_thread = threading.Thread(target=request_blocked_same_site_post)
blocked_same_site_post_thread.start()
wait_for_event(page_server.blocked_same_site_post_requested, "blocked same-site POST")
expect_ui_session_history(
webdriver_port,
session_id,
"while same-site POST is blocked",
[url_post_blocked_form, url_post_blocked_result],
[0, 1],
1,
True,
False,
log,
expect_web_content_matches_ui=False,
)
expect_current_ui_entry_resource(
webdriver_port,
session_id,
"while same-site POST is blocked",
"post",
log,
)
page_server.release_blocked_same_site_post.set()
blocked_same_site_post_thread.join(timeout=EVENT_TIMEOUT_SECONDS)
if blocked_same_site_post_thread.is_alive():
raise AssertionError("Timed out waiting for blocked same-site POST request to finish\n" + "\n".join(log))
wait_for_event(page_server.same_site_post_result_document_ran, "blocked same-site POST result document")
expect_url(webdriver_port, session_id, "after blocked same-site POST submit", url_post_blocked_result, log)
expect_body_text(webdriver_port, session_id, "after blocked same-site POST submit", "POST:name=ladybird", log)
expect_current_entry_resource(webdriver_port, session_id, "after blocked same-site POST submit", "post", log)
request(webdriver_port, "DELETE", f"/session/{session_id}")
session_id = None
session_id = create_session(webdriver_port)
log = [f"blocked same-URL POST initial: {current_url(webdriver_port, session_id)}"]
load_url_from_ui(webdriver_port, session_id, url_post_same_url_blocked)
expect_url(webdriver_port, session_id, "after blocked same-URL POST form load", url_post_same_url_blocked, log)
post_form_submit_point = execute_script(
webdriver_port,
session_id,
"""
const button = document.getElementById('submit');
const rect = button.getBoundingClientRect();
return [Math.round(rect.left + rect.width / 2), Math.round(rect.top + rect.height / 2)];
""",
)
page_server.blocked_same_url_post_requested.clear()
page_server.release_blocked_same_url_post.clear()
page_server.same_url_post_result_document_ran.clear()
def request_blocked_same_url_post():
perform_pointer_click(webdriver_port, session_id, post_form_submit_point[0], post_form_submit_point[1], log)
blocked_same_url_post_thread = threading.Thread(target=request_blocked_same_url_post)
blocked_same_url_post_thread.start()
wait_for_event(page_server.blocked_same_url_post_requested, "blocked same-URL POST")
expect_ui_session_history(
webdriver_port,
session_id,
"while same-URL POST is blocked",
[url_post_same_url_blocked],
[0],
0,
False,
False,
log,
expect_web_content_matches_ui=False,
)
expect_current_ui_entry_resource(
webdriver_port,
session_id,
"while same-URL POST is blocked",
"post",
log,
)
page_server.release_blocked_same_url_post.set()
blocked_same_url_post_thread.join(timeout=EVENT_TIMEOUT_SECONDS)
if blocked_same_url_post_thread.is_alive():
raise AssertionError("Timed out waiting for blocked same-URL POST request to finish\n" + "\n".join(log))
wait_for_event(page_server.same_url_post_result_document_ran, "blocked same-URL POST result document")
expect_url(webdriver_port, session_id, "after blocked same-URL POST submit", url_post_same_url_blocked, log)
expect_body_text(webdriver_port, session_id, "after blocked same-URL POST submit", "POST:name=ladybird", log)
expect_current_entry_resource(webdriver_port, session_id, "after blocked same-URL POST submit", "post", log)
request(webdriver_port, "DELETE", f"/session/{session_id}")
session_id = None
session_id = create_session(webdriver_port)
log = [f"POST crash recovery initial: {current_url(webdriver_port, session_id)}"]
load_url_from_ui(webdriver_port, session_id, url_post_form)
expect_url(webdriver_port, session_id, "after POST form load", url_post_form, log)
post_form_submit_point = execute_script(
webdriver_port,
session_id,
"""
const button = document.getElementById('submit');
const rect = button.getBoundingClientRect();
return [Math.round(rect.left + rect.width / 2), Math.round(rect.top + rect.height / 2)];
""",
)
page_server.post_result_document_ran.clear()
perform_pointer_click(webdriver_port, session_id, post_form_submit_point[0], post_form_submit_point[1], log)
wait_for_event(page_server.post_result_document_ran, "POST result document")
expect_url(webdriver_port, session_id, "after POST form submit", url_post_result, log)
expect_body_text(webdriver_port, session_id, "after POST form submit", "POST:name=ladybird", log)
expect_current_entry_resource(webdriver_port, session_id, "after POST form submit", "post", log)
expect_ui_session_history(
webdriver_port,
session_id,
"after POST form submit",
[url_post_form, url_post_result],
[0, 1],
1,
True,
False,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=1,
)
refresh(webdriver_port, session_id)
expect_url(webdriver_port, session_id, "after POST reload", url_post_result, log)
expect_body_text(webdriver_port, session_id, "after POST reload", "POST:name=ladybird", log)
expect_current_entry_resource(webdriver_port, session_id, "after POST reload", "post", log)
expect_ui_session_history(
webdriver_port,
session_id,
"after POST reload",
[url_post_form, url_post_result],
[0, 1],
1,
True,
False,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=1,
)
crash_current_page(webdriver_port, session_id)
expect_url(webdriver_port, session_id, "after POST crash recovery", url_post_result, log)
expect_body_text(webdriver_port, session_id, "after POST crash recovery", "POST:name=ladybird", log)
expect_current_entry_resource(webdriver_port, session_id, "after POST crash recovery", "post", log)
expect_ui_session_history(
webdriver_port,
session_id,
"after POST crash recovery",
[url_post_form, url_post_result],
[0, 1],
1,
True,
False,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=1,
)
request(webdriver_port, "DELETE", f"/session/{session_id}")
session_id = None
session_id = create_session(webdriver_port)
log = [f"initial: {current_url(webdriver_port, session_id)}"]
load_url_from_ui(webdriver_port, session_id, url_a)
expect_url(webdriver_port, session_id, "after /a", url_a, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after /a",
[url_a],
[0],
0,
False,
False,
log,
expected_web_content_known_used_steps=[0],
expected_web_content_current_step=0,
)
load_url_from_ui(webdriver_port, session_id, url_redirect_to_b)
expect_url(webdriver_port, session_id, "after UI redirect navigation to /b", url_b, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after UI redirect navigation to /b",
[url_a, url_b],
[0, 1],
1,
True,
False,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=1,
)
request(webdriver_port, "POST", f"/session/{session_id}/back", {})
expect_url(webdriver_port, session_id, "after back from UI redirect navigation", url_a, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after back from UI redirect navigation",
[url_a, url_b],
[0, 1],
0,
False,
True,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=0,
)
navigate_from_renderer_using_link(
webdriver_port,
session_id,
"#redirect",
url_b,
log,
page_server.b_document_ran,
"B document after renderer redirect link",
expected_scheduled_url=url_redirect_to_b,
)
expect_url(webdriver_port, session_id, "after renderer redirect navigation to /b", url_b, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after renderer redirect navigation to /b",
[url_a, url_b],
[0, 1],
1,
True,
False,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=1,
)
request(webdriver_port, "POST", f"/session/{session_id}/back", {})
expect_url(webdriver_port, session_id, "after back from renderer redirect navigation", url_a, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after back from renderer redirect navigation",
[url_a, url_b],
[0, 1],
0,
False,
True,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=0,
)
request(webdriver_port, "POST", f"/session/{session_id}/forward", {})
expect_url(webdriver_port, session_id, "after forward from renderer redirect navigation", url_b, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after forward from renderer redirect navigation",
[url_a, url_b],
[0, 1],
1,
True,
False,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=1,
)
request(webdriver_port, "POST", f"/session/{session_id}/back", {})
expect_url(webdriver_port, session_id, "after returning to /a from renderer redirect navigation", url_a, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after returning to /a from renderer redirect navigation",
[url_a, url_b],
[0, 1],
0,
False,
True,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=0,
)
navigate_from_renderer_using_link(
webdriver_port, session_id, "#go", url_b, log, page_server.b_document_ran, "B document after link"
)
expect_url(webdriver_port, session_id, "after renderer navigation to /b", url_b, log)
after_renderer_navigation_to_b = expect_ui_session_history(
webdriver_port,
session_id,
"after renderer navigation to /b",
[url_a, url_b],
[0, 1],
1,
True,
False,
log,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=1,
)
page_server.a_document_ran.clear()
execute_script(webdriver_port, session_id, "history.back(); return location.href;")
wait_for_event(page_server.a_document_ran, "A document after page-initiated history.back()")
expect_url(webdriver_port, session_id, "after page-initiated history.back() to /a", url_a, log)
after_page_initiated_history_back_to_a = expect_ui_session_history(
webdriver_port,
session_id,
"after page-initiated history.back() to /a",
[url_a, url_b],
[0, 1],
0,
False,
True,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=0,
)
if (
after_page_initiated_history_back_to_a["ui"]["webContentProcessID"]
== after_renderer_navigation_to_b["ui"]["webContentProcessID"]
):
raise AssertionError(
"Expected page-initiated cross-site history.back() to swap processes\n" + "\n".join(log)
)
page_server.b_document_ran.clear()
execute_script(webdriver_port, session_id, "history.forward(); return location.href;")
wait_for_event(page_server.b_document_ran, "B document after page-initiated history.forward()")
expect_url(webdriver_port, session_id, "after page-initiated history.forward() to /b", url_b, log)
after_page_initiated_history_forward_to_b = expect_ui_session_history(
webdriver_port,
session_id,
"after page-initiated history.forward() to /b",
[url_a, url_b],
[0, 1],
1,
True,
False,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=1,
)
if (
after_page_initiated_history_forward_to_b["ui"]["webContentProcessID"]
== after_page_initiated_history_back_to_a["ui"]["webContentProcessID"]
):
raise AssertionError(
"Expected page-initiated cross-site history.forward() to swap processes\n" + "\n".join(log)
)
expect_sandboxed_history_back_to_be_blocked(webdriver_port, session_id, url_a, url_b, log)
before_script_initiated_blocked_back = after_page_initiated_history_forward_to_b
perform_pointer_click(webdriver_port, session_id, 5, 5, log)
script_beforeunload_setup = execute_script(
webdriver_port,
session_id,
"""
window.scriptBeforeUnloadCount = 0;
window.onbeforeunload = event => {
++window.scriptBeforeUnloadCount;
event.preventDefault();
event.returnValue = "blocked";
return "blocked";
};
return [location.href, window.scriptBeforeUnloadCount, navigator.userActivation.hasBeenActive];
""",
)
if script_beforeunload_setup != [url_b, 0, True]:
raise AssertionError(
f"Expected script beforeunload setup on /b to be {[url_b, 0, True]}, "
f"got {script_beforeunload_setup}\n" + "\n".join(log)
)
execute_script(webdriver_port, session_id, "history.back(); return location.href;")
def script_back_canceled_by_beforeunload(result):
return (
isinstance(result, list)
and len(result) == 2
and result[0] == url_b
and isinstance(result[1], int)
and result[1] >= 1
)
script_blocked_back_state = wait_for_script_result(
webdriver_port,
session_id,
"blocked script-initiated cross-site history.back() from /b",
"return [location.href, window.scriptBeforeUnloadCount];",
script_back_canceled_by_beforeunload,
log,
)
if script_blocked_back_state != [url_b, 1]:
raise AssertionError(
f"Expected beforeunload to cancel script-initiated cross-site history.back(), "
f"got {script_blocked_back_state}\n" + "\n".join(log)
)
expect_ui_session_history(
webdriver_port,
session_id,
"after blocked script-initiated cross-site history.back()",
history_entry_urls(before_script_initiated_blocked_back["ui"]),
history_used_steps(before_script_initiated_blocked_back["ui"]),
before_script_initiated_blocked_back["ui"]["currentUsedStepIndex"],
before_script_initiated_blocked_back["ui"]["backButtonEnabled"],
before_script_initiated_blocked_back["ui"]["forwardButtonEnabled"],
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=history_step_values(
before_script_initiated_blocked_back["ui"]["webContentKnownUsedSteps"]
),
expected_web_content_current_step=before_script_initiated_blocked_back["ui"]["webContentCurrentStep"],
)
execute_script(webdriver_port, session_id, "window.onbeforeunload = null; return null;")
page_server.a_document_ran.clear()
perform_browser_history_shortcut(webdriver_port, session_id, "left", log)
wait_for_event(page_server.a_document_ran, "A document after browser shortcut back")
wait_for_ui_session_history(
webdriver_port,
session_id,
"after cross-site browser shortcut back to /a converges",
[url_a, url_b],
[0, 1],
0,
False,
True,
log,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=0,
)
expect_url(webdriver_port, session_id, "after cross-site browser shortcut back to /a", url_a, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after cross-site browser shortcut back to /a",
[url_a, url_b],
[0, 1],
0,
False,
True,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=0,
)
page_server.b_document_ran.clear()
perform_browser_history_shortcut(webdriver_port, session_id, "right", log)
wait_for_event(page_server.b_document_ran, "B document after browser shortcut forward")
wait_for_ui_session_history(
webdriver_port,
session_id,
"after cross-site browser shortcut forward to /b converges",
[url_a, url_b],
[0, 1],
1,
True,
False,
log,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=1,
)
expect_url(webdriver_port, session_id, "after cross-site browser shortcut forward to /b", url_b, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after cross-site browser shortcut forward to /b",
[url_a, url_b],
[0, 1],
1,
True,
False,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1],
expected_web_content_current_step=1,
)
navigate_from_renderer_using_link(
webdriver_port, session_id, "#go", url_c, log, page_server.c_document_ran, "C document after link"
)
expect_url(webdriver_port, session_id, "after renderer navigation to /c", url_c, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after renderer navigation to /c",
[url_a, url_b, url_c],
[0, 1, 2],
2,
True,
False,
log,
expected_web_content_known_used_steps=[0, 1, 2],
expected_web_content_current_step=2,
)
page_server.b_document_ran.clear()
perform_browser_history_shortcut(webdriver_port, session_id, "left", log)
wait_for_event(page_server.b_document_ran, "B document after browser shortcut back")
wait_for_ui_session_history(
webdriver_port,
session_id,
"after browser shortcut back to /b converges",
[url_a, url_b, url_c],
[0, 1, 2],
1,
True,
True,
log,
expected_web_content_known_used_steps=[0, 1, 2],
expected_web_content_current_step=1,
)
expect_url(webdriver_port, session_id, "after browser shortcut back to /b", url_b, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after browser shortcut back to /b",
[url_a, url_b, url_c],
[0, 1, 2],
1,
True,
True,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2],
expected_web_content_current_step=1,
)
page_server.c_document_ran.clear()
perform_browser_history_shortcut(webdriver_port, session_id, "right", log)
wait_for_event(page_server.c_document_ran, "C document after browser shortcut forward")
wait_for_ui_session_history(
webdriver_port,
session_id,
"after browser shortcut forward to /c converges",
[url_a, url_b, url_c],
[0, 1, 2],
2,
True,
False,
log,
expected_web_content_known_used_steps=[0, 1, 2],
expected_web_content_current_step=2,
)
expect_url(webdriver_port, session_id, "after browser shortcut forward to /c", url_c, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after browser shortcut forward to /c",
[url_a, url_b, url_c],
[0, 1, 2],
2,
True,
False,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2],
expected_web_content_current_step=2,
)
request(webdriver_port, "POST", f"/session/{session_id}/back", {})
expect_url(webdriver_port, session_id, "after back to /b", url_b, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after back to /b",
[url_a, url_b, url_c],
[0, 1, 2],
1,
True,
True,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2],
expected_web_content_current_step=1,
)
request(webdriver_port, "POST", f"/session/{session_id}/back", {})
expect_url(webdriver_port, session_id, "after back to /a", url_a, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after back to /a",
[url_a, url_b, url_c],
[0, 1, 2],
0,
False,
True,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2],
expected_web_content_current_step=0,
)
request(webdriver_port, "POST", f"/session/{session_id}/back", {})
expect_url(webdriver_port, session_id, "after back no-op at start", url_a, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after back no-op at start",
[url_a, url_b, url_c],
[0, 1, 2],
0,
False,
True,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2],
expected_web_content_current_step=0,
)
request(webdriver_port, "POST", f"/session/{session_id}/back", {})
expect_url(webdriver_port, session_id, "after second back no-op at start", url_a, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after second back no-op at start",
[url_a, url_b, url_c],
[0, 1, 2],
0,
False,
True,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2],
expected_web_content_current_step=0,
)
request(webdriver_port, "POST", f"/session/{session_id}/forward", {})
expect_url(webdriver_port, session_id, "after forward to /b", url_b, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after forward to /b",
[url_a, url_b, url_c],
[0, 1, 2],
1,
True,
True,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2],
expected_web_content_current_step=1,
)
request(webdriver_port, "POST", f"/session/{session_id}/forward", {})
expect_url(webdriver_port, session_id, "after forward to /c", url_c, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after forward to /c",
[url_a, url_b, url_c],
[0, 1, 2],
2,
True,
False,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2],
expected_web_content_current_step=2,
)
request(webdriver_port, "POST", f"/session/{session_id}/forward", {})
expect_url(webdriver_port, session_id, "after forward no-op at end", url_c, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after forward no-op at end",
[url_a, url_b, url_c],
[0, 1, 2],
2,
True,
False,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2],
expected_web_content_current_step=2,
)
crash_current_page(webdriver_port, session_id)
expect_url(webdriver_port, session_id, "after crash recovery at /c", url_c, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after crash recovery at /c",
[url_a, url_b, url_c],
[0, 1, 2],
2,
True,
False,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2],
expected_web_content_current_step=2,
)
page_server.b_document_ran.clear()
execute_script(webdriver_port, session_id, "history.back(); return location.href;")
wait_for_event(page_server.b_document_ran, "B document after crash-recovered page history.back()")
expect_url(webdriver_port, session_id, "after crash recovery page-initiated back to /b", url_b, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after crash recovery page-initiated back to /b",
[url_a, url_b, url_c],
[0, 1, 2],
1,
True,
True,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2],
expected_web_content_current_step=1,
)
page_server.c_document_ran.clear()
execute_script(webdriver_port, session_id, "history.forward(); return location.href;")
wait_for_event(page_server.c_document_ran, "C document after crash-recovered page history.forward()")
expect_url(webdriver_port, session_id, "after crash recovery page-initiated forward to /c", url_c, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after crash recovery page-initiated forward to /c",
[url_a, url_b, url_c],
[0, 1, 2],
2,
True,
False,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2],
expected_web_content_current_step=2,
)
traverse_history_from_ui(webdriver_port, session_id, -1)
expect_url(webdriver_port, session_id, "after crash recovery browser UI back to /b", url_b, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after crash recovery browser UI back to /b",
[url_a, url_b, url_c],
[0, 1, 2],
1,
True,
True,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2],
expected_web_content_current_step=1,
)
request(webdriver_port, "POST", f"/session/{session_id}/back", {})
expect_url(webdriver_port, session_id, "after crash recovery back to /a", url_a, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after crash recovery back to /a",
[url_a, url_b, url_c],
[0, 1, 2],
0,
False,
True,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2],
expected_web_content_current_step=0,
)
request(webdriver_port, "POST", f"/session/{session_id}/forward", {})
expect_url(webdriver_port, session_id, "after crash recovery forward to /b", url_b, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after crash recovery forward to /b",
[url_a, url_b, url_c],
[0, 1, 2],
1,
True,
True,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2],
expected_web_content_current_step=1,
)
request(webdriver_port, "POST", f"/session/{session_id}/forward", {})
expect_url(webdriver_port, session_id, "after crash recovery forward to /c", url_c, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after crash recovery forward to /c",
[url_a, url_b, url_c],
[0, 1, 2],
2,
True,
False,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2],
expected_web_content_current_step=2,
)
request(webdriver_port, "POST", f"/session/{session_id}/back", {})
expect_url(webdriver_port, session_id, "after crash recovery branch back to /b", url_b, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after crash recovery branch back to /b",
[url_a, url_b, url_c],
[0, 1, 2],
1,
True,
True,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2],
expected_web_content_current_step=1,
)
navigate_from_renderer_using_link(
webdriver_port, session_id, "#branch", url_d, log, page_server.d_document_ran, "D document after link"
)
expect_url(webdriver_port, session_id, "after renderer branch navigation to /d", url_d, log)
initial_history_state_after_branch = execute_script(
webdriver_port,
session_id,
"""
return [
window.initialHistoryLength,
window.initialNavigationEntryCount,
window.initialNavigationCurrentIndex,
];
""",
)
# history.length counts all entries, but navigation.entries() only
# exposes the same-origin contiguous entries around the current entry.
expected_initial_history_state_after_branch = [3, 1, 0]
if initial_history_state_after_branch != expected_initial_history_state_after_branch:
raise AssertionError(
f"Expected /d initial history state to be {expected_initial_history_state_after_branch}, "
f"got {initial_history_state_after_branch}\n" + "\n".join(log)
)
expect_ui_session_history(
webdriver_port,
session_id,
"after renderer branch navigation to /d",
[url_a, url_b, url_d],
[0, 1, 2],
2,
True,
False,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2],
expected_web_content_current_step=2,
)
crash_current_page(webdriver_port, session_id)
expect_url(webdriver_port, session_id, "after /d crash recovery", url_d, log)
initial_history_state_after_branch_crash_recovery = execute_script(
webdriver_port,
session_id,
"""
return [
window.initialHistoryLength,
window.initialNavigationEntryCount,
window.initialNavigationCurrentIndex,
];
""",
)
if initial_history_state_after_branch_crash_recovery != expected_initial_history_state_after_branch:
raise AssertionError(
f"Expected /d crash recovery initial history state to be {expected_initial_history_state_after_branch}, "
f"got {initial_history_state_after_branch_crash_recovery}\n" + "\n".join(log)
)
expect_ui_session_history(
webdriver_port,
session_id,
"after /d crash recovery",
[url_a, url_b, url_d],
[0, 1, 2],
2,
True,
False,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2],
expected_web_content_current_step=2,
)
traverse_history_from_ui(webdriver_port, session_id, -1)
expect_url(webdriver_port, session_id, "after UI back from /d to /b", url_b, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after UI back from /d to /b",
[url_a, url_b, url_d],
[0, 1, 2],
1,
True,
True,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2],
expected_web_content_current_step=1,
)
traverse_history_from_ui(webdriver_port, session_id, -1)
expect_url(webdriver_port, session_id, "after UI back from /b to /a", url_a, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after UI back from /b to /a",
[url_a, url_b, url_d],
[0, 1, 2],
0,
False,
True,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2],
expected_web_content_current_step=0,
)
traverse_history_from_ui(webdriver_port, session_id, -1)
expect_url(webdriver_port, session_id, "after UI back no-op at /a", url_a, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after UI back no-op at /a",
[url_a, url_b, url_d],
[0, 1, 2],
0,
False,
True,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2],
expected_web_content_current_step=0,
)
traverse_history_from_ui(webdriver_port, session_id, -1)
expect_url(webdriver_port, session_id, "after second UI back no-op at /a", url_a, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after second UI back no-op at /a",
[url_a, url_b, url_d],
[0, 1, 2],
0,
False,
True,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2],
expected_web_content_current_step=0,
)
traverse_history_from_ui(webdriver_port, session_id, 1)
expect_url(webdriver_port, session_id, "after UI forward from /a to /b", url_b, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after UI forward from /a to /b",
[url_a, url_b, url_d],
[0, 1, 2],
1,
True,
True,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2],
expected_web_content_current_step=1,
)
traverse_history_from_ui(webdriver_port, session_id, 1)
expect_url(webdriver_port, session_id, "after UI forward from /b to /d", url_d, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after UI forward from /b to /d",
[url_a, url_b, url_d],
[0, 1, 2],
2,
True,
False,
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=[0, 1, 2],
expected_web_content_current_step=2,
)
request(webdriver_port, "POST", f"/session/{session_id}/url", {"url": url_state})
expect_url(webdriver_port, session_id, "after /state", url_state, log)
state_setup = execute_script(
webdriver_port,
session_id,
"""
window.name = "ladybird-history-name";
history.scrollRestoration = "manual";
history.replaceState({ classic: "replace" }, "", "/state?replace");
navigation.updateCurrentEntry({ state: { navigation: "replace" } });
const replaceKey = navigation.currentEntry.key;
const replaceId = navigation.currentEntry.id;
history.pushState({ classic: "push" }, "", "/state?push");
navigation.updateCurrentEntry({ state: { navigation: "push" } });
return [
location.href,
JSON.stringify(history.state),
JSON.stringify(navigation.currentEntry.getState()),
history.scrollRestoration,
replaceKey,
replaceId,
navigation.currentEntry.key,
navigation.currentEntry.id,
window.name,
];
""",
)
expected_state_setup = [
url_state_push,
'{"classic":"push"}',
'{"navigation":"push"}',
"manual",
]
if state_setup[:4] != expected_state_setup:
raise AssertionError(
f"Expected state setup to start with {expected_state_setup}, got {state_setup}\n" + "\n".join(log)
)
replace_key, replace_id, push_key, push_id, window_name = state_setup[4:]
if window_name != "ladybird-history-name":
raise AssertionError(f"Expected window.name setup to survive setup, got {window_name}\n" + "\n".join(log))
def ui_history_is_at_state_push(snapshot):
ui_history = snapshot["ui"]
current_index = ui_history["currentUsedStepIndex"]
if current_index < 0 or current_index >= len(ui_history["entries"]):
return False
return (
history_entry_urls(ui_history)[current_index] == url_state_push
and ui_history["webContentHistoryMatchesUI"]
)
wait_for_session_history(
webdriver_port,
session_id,
"before state crash recovery",
ui_history_is_at_state_push,
log,
)
crash_current_page(webdriver_port, session_id)
expect_url(webdriver_port, session_id, "after state crash recovery", url_state_push, log)
expect_window_name(
webdriver_port,
session_id,
"after state crash restores window name",
"ladybird-history-name",
log,
)
expect_history_entry_state(
webdriver_port,
session_id,
"after state crash restores current entry state",
url_state_push,
'{"classic":"push"}',
'{"navigation":"push"}',
"manual",
push_key,
push_id,
log,
)
request(webdriver_port, "POST", f"/session/{session_id}/back", {})
expect_url(webdriver_port, session_id, "after state crash recovery back", url_state_replace, log)
expect_history_entry_state(
webdriver_port,
session_id,
"after state crash restores previous entry state",
url_state_replace,
'{"classic":"replace"}',
'{"navigation":"replace"}',
"manual",
replace_key,
replace_id,
log,
)
request(webdriver_port, "POST", f"/session/{session_id}/forward", {})
expect_url(webdriver_port, session_id, "after state crash recovery forward", url_state_push, log)
expect_history_entry_state(
webdriver_port,
session_id,
"after state crash restores forward entry state",
url_state_push,
'{"classic":"push"}',
'{"navigation":"push"}',
"manual",
push_key,
push_id,
log,
)
request(webdriver_port, "POST", f"/session/{session_id}/url", {"url": url_scroll})
expect_url(webdriver_port, session_id, "after /scroll", url_scroll, log)
scroll_setup = execute_script(
webdriver_port,
session_id,
"""
history.scrollRestoration = "auto";
scrollTo(0, 1200);
history.replaceState({ scroll: "saved" }, "", "/scroll?saved");
return [location.href, Math.round(scrollX), Math.round(scrollY)];
""",
)
expected_scroll_setup = [url_scroll_saved, 0, 1200]
if scroll_setup != expected_scroll_setup:
raise AssertionError(
f"Expected scroll setup to be {expected_scroll_setup}, got {scroll_setup}\n" + "\n".join(log)
)
expect_url(webdriver_port, session_id, "after scroll setup", url_scroll_saved, log)
crash_current_page(webdriver_port, session_id)
expect_url(webdriver_port, session_id, "after scroll crash recovery", url_scroll_saved, log)
expect_scroll_position(webdriver_port, session_id, "after scroll crash restores viewport", 0, 1200, log)
request(webdriver_port, "POST", f"/session/{session_id}/url", {"url": url_b})
expect_url(webdriver_port, session_id, "after leaving scrolled page", url_b, log)
request(webdriver_port, "POST", f"/session/{session_id}/back", {})
expect_url(webdriver_port, session_id, "after restoring scrolled page", url_scroll_saved, log)
expect_scroll_position(webdriver_port, session_id, "after back restores viewport", 0, 1200, log)
crash_current_page(webdriver_port, session_id)
expect_url(webdriver_port, session_id, "after restored scroll crash recovery", url_scroll_saved, log)
expect_scroll_position(
webdriver_port,
session_id,
"after restored scroll crash recovery keeps viewport",
0,
1200,
log,
)
request(webdriver_port, "DELETE", f"/session/{session_id}")
session_id = create_session(webdriver_port)
log.append(f"intercepted same-document scroll initial: {current_url(webdriver_port, session_id)}")
request(webdriver_port, "POST", f"/session/{session_id}/url", {"url": url_scroll})
expect_url(webdriver_port, session_id, "before intercepted scroll setup", url_scroll, log)
intercepted_scroll_setup = execute_script(
webdriver_port,
session_id,
"""
navigation.onnavigate = null;
history.scrollRestoration = "auto";
scrollTo(0, 900);
history.replaceState({ scroll: "intercept-source" }, "", "/scroll?intercept-source");
history.pushState({ scroll: "intercept-current" }, "", "/scroll?intercept-current");
scrollTo(0, 0);
navigation.onnavigate = event => {
if (event.navigationType === "traverse")
event.intercept({ scroll: "manual", handler: () => Promise.resolve() });
};
return [location.href, Math.round(scrollX), Math.round(scrollY)];
""",
)
url_scroll_intercept_source = f"http://localhost:{page_port}/scroll?intercept-source"
url_scroll_intercept_current = f"http://localhost:{page_port}/scroll?intercept-current"
expected_intercepted_scroll_setup = [url_scroll_intercept_current, 0, 0]
if intercepted_scroll_setup != expected_intercepted_scroll_setup:
raise AssertionError(
f"Expected intercepted scroll setup to be {expected_intercepted_scroll_setup}, got {intercepted_scroll_setup}\n"
+ "\n".join(log)
)
request(webdriver_port, "POST", f"/session/{session_id}/back", {})
wait_for_url(webdriver_port, session_id, "after intercepted scroll back", url_scroll_intercept_source, log)
expect_scroll_position(
webdriver_port, session_id, "after intercepted back suppresses scroll restore", 0, 0, log
)
browser_ui_traverse_setup = execute_script(
webdriver_port,
session_id,
"""
navigation.onnavigate = null;
history.pushState({ cancel: "current" }, "", "/scroll?cancel-current");
window.canceledTraverseCount = 0;
navigation.onnavigate = event => {
if (event.navigationType === "traverse") {
++window.canceledTraverseCount;
event.preventDefault();
}
};
return [location.href, window.canceledTraverseCount];
""",
)
url_scroll_cancel_current = f"http://localhost:{page_port}/scroll?cancel-current"
expected_browser_ui_traverse_setup = [url_scroll_cancel_current, 0]
if browser_ui_traverse_setup != expected_browser_ui_traverse_setup:
raise AssertionError(
f"Expected browser UI traverse setup to be {expected_browser_ui_traverse_setup}, got {browser_ui_traverse_setup}\n"
+ "\n".join(log)
)
def browser_ui_traverse_setup_is_mirrored(snapshot):
ui_history = snapshot["ui"]
if (
not ui_history["webContentHistoryMatchesUI"]
or ui_history["waitingToSeedWebContent"]
or ui_history["waitingForWebContentSeedAck"]
or ui_history["ignoringWebContentUpdatesUntilSeed"]
or ui_history["reseedAfterCurrentHistoryLoad"]
or ui_history["pendingWebContentHistoryStepAfterFallbackLoad"] is not None
or ui_history["pendingSessionHistoryNavigation"] is not None
or ui_history["pendingSessionHistoryTraversal"] is not None
or comparable_history(ui_history) != comparable_history(snapshot["webContent"])
):
return False
return history_current_entry(ui_history)["url"] == url_scroll_cancel_current
history_before_browser_ui_traverse = wait_for_session_history(
webdriver_port,
session_id,
"before browser UI traverse with navigate cancel handler",
browser_ui_traverse_setup_is_mirrored,
log,
)
if history_before_browser_ui_traverse["ui"]["currentUsedStepIndex"] == 0:
raise AssertionError(
"Expected browser UI traverse setup to have a previous history entry\n" + "\n".join(log)
)
expected_browser_ui_traverse_url = history_entry_urls(history_before_browser_ui_traverse["ui"])[
history_before_browser_ui_traverse["ui"]["currentUsedStepIndex"] - 1
]
traverse_history_from_ui(webdriver_port, session_id, -1)
browser_ui_traverse_state = execute_script(
webdriver_port,
session_id,
"return [location.href, window.canceledTraverseCount];",
)
if browser_ui_traverse_state != [expected_browser_ui_traverse_url, 1]:
raise AssertionError(
f"Expected browser UI traverse to ignore non-cancelable preventDefault() and move to "
f"{[expected_browser_ui_traverse_url, 1]}, got {browser_ui_traverse_state}\n" + "\n".join(log)
)
history_after_browser_ui_traverse = expect_web_content_session_history_matches_ui(
webdriver_port, session_id, "after browser UI traverse with navigate cancel handler", log
)
log.append(f"after browser UI traverse: {summarize_history_snapshot(history_after_browser_ui_traverse)}")
request(webdriver_port, "POST", f"/session/{session_id}/url", {"url": url_nested})
expect_url(webdriver_port, session_id, "after /nested", url_nested, log)
expect_frame_url(webdriver_port, session_id, "after initial frame load", url_frame_a, log, "Frame A")
expect_current_entry_nested_history(webdriver_port, session_id, "after /nested", url_nested, [url_frame_a], log)
page_server.release_blocked_frame_b.set()
page_server.frame_b_blocked_document_ran.clear()
execute_script(
webdriver_port,
session_id,
f"document.getElementById('frame').contentWindow.location.href = '{url_frame_b_blocked}'; return null;",
)
wait_for_event(page_server.frame_b_blocked_document_ran, "nested frame document")
expect_frame_url(
webdriver_port, session_id, "after nested frame navigation", url_frame_b_blocked, log, "Frame B Blocked"
)
expect_current_entry_nested_history(
webdriver_port,
session_id,
"after nested frame navigation",
url_nested,
[url_frame_a, url_frame_b_blocked],
log,
)
page_server.frame_b_blocked_document_ran.clear()
crash_current_page(webdriver_port, session_id)
wait_for_event(page_server.frame_b_blocked_document_ran, "nested frame document after crash recovery")
expect_url(webdriver_port, session_id, "after nested crash recovery", url_nested, log)
expect_frame_url(
webdriver_port,
session_id,
"after nested crash restores iframe",
url_frame_b_blocked,
log,
"Frame B Blocked",
)
expect_current_entry_nested_history(
webdriver_port,
session_id,
"after nested crash recovery",
url_nested,
[url_frame_a, url_frame_b_blocked],
log,
)
expect_no_pending_web_content_history_step_after_fallback_load(
webdriver_port, session_id, "after nested crash recovery", log
)
expect_web_content_session_history_matches_ui(webdriver_port, session_id, "after nested crash recovery", log)
request(webdriver_port, "POST", f"/session/{session_id}/back", {})
expect_url(webdriver_port, session_id, "after nested crash recovery frame back", url_nested, log)
expect_frame_url(
webdriver_port,
session_id,
"after nested crash recovery frame back",
url_frame_a,
log,
"Frame A",
)
expect_current_entry_nested_history(
webdriver_port,
session_id,
"after nested crash recovery frame back",
url_nested,
[url_frame_a, url_frame_b_blocked],
log,
)
expect_web_content_session_history_matches_ui(
webdriver_port, session_id, "after nested crash recovery frame back", log
)
request(webdriver_port, "POST", f"/session/{session_id}/forward", {})
expect_url(webdriver_port, session_id, "after nested crash recovery frame forward", url_nested, log)
expect_frame_url(
webdriver_port,
session_id,
"after nested crash recovery frame forward",
url_frame_b_blocked,
log,
"Frame B Blocked",
)
expect_current_entry_nested_history(
webdriver_port,
session_id,
"after nested crash recovery frame forward",
url_nested,
[url_frame_a, url_frame_b_blocked],
log,
)
expect_web_content_session_history_matches_ui(
webdriver_port, session_id, "after nested crash recovery frame forward", log
)
same_document_nested_setup = execute_script(
webdriver_port,
session_id,
"""
history.pushState({ nested: "same-document" }, "", "/nested?same-document");
return location.href;
""",
)
if same_document_nested_setup != url_nested_same_document:
raise AssertionError(
f"Expected same-document nested setup to be {url_nested_same_document}, "
f"got {same_document_nested_setup}\n" + "\n".join(log)
)
expect_current_entry_nested_history(
webdriver_port,
session_id,
"after same-document nested setup",
url_nested_same_document,
[url_frame_a, url_frame_b_blocked],
log,
)
page_server.frame_b_blocked_document_ran.clear()
crash_current_page(webdriver_port, session_id)
wait_for_event(
page_server.frame_b_blocked_document_ran,
"same-document nested frame document after crash recovery",
)
expect_url(
webdriver_port, session_id, "after same-document nested crash recovery", url_nested_same_document, log
)
expect_frame_url(
webdriver_port,
session_id,
"after same-document nested crash restores iframe",
url_frame_b_blocked,
log,
"Frame B Blocked",
)
expect_current_entry_nested_history(
webdriver_port,
session_id,
"after same-document nested crash recovery",
url_nested_same_document,
[url_frame_a, url_frame_b_blocked],
log,
)
expect_no_pending_web_content_history_step_after_fallback_load(
webdriver_port, session_id, "after same-document nested crash recovery", log
)
request(webdriver_port, "POST", f"/session/{session_id}/back", {})
expect_url(webdriver_port, session_id, "after same-document nested back", url_nested, log)
expect_frame_url(
webdriver_port,
session_id,
"after same-document nested back keeps iframe history",
url_frame_b_blocked,
log,
"Frame B Blocked",
)
expect_current_entry_nested_history(
webdriver_port,
session_id,
"after same-document nested back",
url_nested,
[url_frame_a, url_frame_b_blocked],
log,
)
request(webdriver_port, "POST", f"/session/{session_id}/url", {"url": url_b})
expect_url(webdriver_port, session_id, "after cross-site /b", url_b, log)
expect_current_top_level_history_url(webdriver_port, session_id, "after cross-site /b", url_b, log)
expect_navigation_buttons(webdriver_port, session_id, "after cross-site /b", True, False, log)
expect_web_content_session_history_matches_ui(
webdriver_port, session_id, "after cross-site /b process swap", log
)
before_blocked_browser_ui_back = expect_web_content_session_history_matches_ui(
webdriver_port, session_id, "before blocked browser UI back from /b", log
)
navigate_event_cancel_setup = execute_script(
webdriver_port,
session_id,
"""
window.canceledCrossSiteNavigateCount = 0;
navigation.onnavigate = event => {
if (event.navigationType === "push") {
++window.canceledCrossSiteNavigateCount;
event.preventDefault();
}
};
return [location.href, window.canceledCrossSiteNavigateCount];
""",
)
if navigate_event_cancel_setup != [url_b, 0]:
raise AssertionError(
f"Expected navigate event cancel setup on /b to be {[url_b, 0]}, got {navigate_event_cancel_setup}\n"
+ "\n".join(log)
)
cross_site_navigate_event_click_point = execute_script(
webdriver_port,
session_id,
"""
const rect = document.querySelector("#branch").getBoundingClientRect();
return [Math.floor(rect.left + rect.width / 2), Math.floor(rect.top + rect.height / 2)];
""",
)
perform_pointer_click(
webdriver_port,
session_id,
cross_site_navigate_event_click_point[0],
cross_site_navigate_event_click_point[1],
log,
)
canceled_cross_site_navigate_state = execute_script(
webdriver_port,
session_id,
"return [location.href, window.canceledCrossSiteNavigateCount];",
)
if canceled_cross_site_navigate_state != [url_b, 1]:
raise AssertionError(
f"Expected navigate event to cancel cross-site link navigation from /b, got {canceled_cross_site_navigate_state}\n"
+ "\n".join(log)
)
expect_ui_session_history(
webdriver_port,
session_id,
"after navigate event canceled cross-site link navigation from /b",
history_entry_urls(before_blocked_browser_ui_back["ui"]),
history_used_steps(before_blocked_browser_ui_back["ui"]),
before_blocked_browser_ui_back["ui"]["currentUsedStepIndex"],
before_blocked_browser_ui_back["ui"]["backButtonEnabled"],
before_blocked_browser_ui_back["ui"]["forwardButtonEnabled"],
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=history_step_values(
before_blocked_browser_ui_back["ui"]["webContentKnownUsedSteps"]
),
expected_web_content_current_step=before_blocked_browser_ui_back["ui"]["webContentCurrentStep"],
)
execute_script(webdriver_port, session_id, "navigation.onnavigate = null; return null;")
expect_javascript_noop_webdriver_navigation_does_not_change_history(
webdriver_port,
session_id,
url_b,
before_blocked_browser_ui_back,
log,
)
expect_javascript_noop_ui_load_does_not_change_history(
webdriver_port,
session_id,
url_b,
before_blocked_browser_ui_back,
log,
)
beforeunload_setup = execute_script(
webdriver_port,
session_id,
"""
window.beforeUnloadCount = 0;
window.onbeforeunload = event => {
++window.beforeUnloadCount;
event.preventDefault();
event.returnValue = "blocked";
return "blocked";
};
return [location.href, window.beforeUnloadCount];
""",
)
if beforeunload_setup != [url_b, 0]:
raise AssertionError(
f"Expected beforeunload setup on /b to be {[url_b, 0]}, got {beforeunload_setup}\n" + "\n".join(log)
)
link_click_point = execute_script(
webdriver_port,
session_id,
"""
const rect = document.querySelector("#go").getBoundingClientRect();
return [Math.floor(rect.left + rect.width / 2), Math.floor(rect.top + rect.height / 2)];
""",
)
perform_pointer_click(webdriver_port, session_id, link_click_point[0], link_click_point[1], log)
user_activation = execute_script(webdriver_port, session_id, "return navigator.userActivation.hasBeenActive;")
if user_activation is not True:
raise AssertionError("Expected pointer click to give /b sticky activation\n" + "\n".join(log))
blocked_link_navigation_state = execute_script(
webdriver_port,
session_id,
"return [location.href, window.beforeUnloadCount];",
)
if blocked_link_navigation_state[0] != url_b or blocked_link_navigation_state[1] < 1:
raise AssertionError(
f"Expected beforeunload to cancel link navigation from /b, got {blocked_link_navigation_state}\n"
+ "\n".join(log)
)
expect_ui_session_history(
webdriver_port,
session_id,
"after blocked link navigation from /b",
history_entry_urls(before_blocked_browser_ui_back["ui"]),
history_used_steps(before_blocked_browser_ui_back["ui"]),
before_blocked_browser_ui_back["ui"]["currentUsedStepIndex"],
before_blocked_browser_ui_back["ui"]["backButtonEnabled"],
before_blocked_browser_ui_back["ui"]["forwardButtonEnabled"],
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=history_step_values(
before_blocked_browser_ui_back["ui"]["webContentKnownUsedSteps"]
),
expected_web_content_current_step=before_blocked_browser_ui_back["ui"]["webContentCurrentStep"],
)
blocked_stale_ui_load_state = expect_beforeunload_cancels_stale_ui_load(
webdriver_port,
session_id,
url_b,
url_c,
before_blocked_browser_ui_back,
blocked_link_navigation_state,
log,
)
blocked_webdriver_navigate_state = expect_beforeunload_cancels_webdriver_navigation(
webdriver_port,
session_id,
url_c,
"WebDriver navigation from /b",
url_b,
blocked_stale_ui_load_state[1],
before_blocked_browser_ui_back,
log,
)
blocked_cross_site_webdriver_navigate_state = expect_beforeunload_cancels_webdriver_navigation(
webdriver_port,
session_id,
url_d,
"cross-site WebDriver navigation from /b",
url_b,
blocked_webdriver_navigate_state[1],
before_blocked_browser_ui_back,
log,
)
blocked_refresh_state = expect_beforeunload_cancels_refresh(
webdriver_port,
session_id,
url_b,
blocked_cross_site_webdriver_navigate_state[1],
log,
)
traverse_history_from_ui(webdriver_port, session_id, -1)
blocked_beforeunload_state = execute_script(
webdriver_port,
session_id,
"return [location.href, window.beforeUnloadCount];",
)
if blocked_beforeunload_state[0] != url_b or blocked_beforeunload_state[1] <= blocked_refresh_state[1]:
raise AssertionError(
f"Expected beforeunload to cancel browser UI back from /b, got {blocked_beforeunload_state}\n"
+ "\n".join(log)
)
expect_ui_session_history(
webdriver_port,
session_id,
"after blocked browser UI back from /b",
history_entry_urls(before_blocked_browser_ui_back["ui"]),
history_used_steps(before_blocked_browser_ui_back["ui"]),
before_blocked_browser_ui_back["ui"]["currentUsedStepIndex"],
before_blocked_browser_ui_back["ui"]["backButtonEnabled"],
before_blocked_browser_ui_back["ui"]["forwardButtonEnabled"],
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=history_step_values(
before_blocked_browser_ui_back["ui"]["webContentKnownUsedSteps"]
),
expected_web_content_current_step=before_blocked_browser_ui_back["ui"]["webContentCurrentStep"],
)
request(webdriver_port, "POST", f"/session/{session_id}/back", {})
blocked_webdriver_back_state = execute_script(
webdriver_port,
session_id,
"return [location.href, window.beforeUnloadCount];",
)
if blocked_webdriver_back_state[0] != url_b or blocked_webdriver_back_state[1] <= blocked_beforeunload_state[1]:
raise AssertionError(
f"Expected beforeunload to cancel WebDriver back from /b, got {blocked_webdriver_back_state}\n"
+ "\n".join(log)
)
expect_ui_session_history(
webdriver_port,
session_id,
"after blocked WebDriver back from /b",
history_entry_urls(before_blocked_browser_ui_back["ui"]),
history_used_steps(before_blocked_browser_ui_back["ui"]),
before_blocked_browser_ui_back["ui"]["currentUsedStepIndex"],
before_blocked_browser_ui_back["ui"]["backButtonEnabled"],
before_blocked_browser_ui_back["ui"]["forwardButtonEnabled"],
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=history_step_values(
before_blocked_browser_ui_back["ui"]["webContentKnownUsedSteps"]
),
expected_web_content_current_step=before_blocked_browser_ui_back["ui"]["webContentCurrentStep"],
)
perform_browser_history_shortcut(webdriver_port, session_id, "left", log)
blocked_shortcut_back_state = execute_script(
webdriver_port,
session_id,
"return [location.href, window.beforeUnloadCount];",
)
if blocked_shortcut_back_state[0] != url_b or blocked_shortcut_back_state[1] <= blocked_webdriver_back_state[1]:
raise AssertionError(
f"Expected beforeunload to cancel browser history shortcut from /b, got {blocked_shortcut_back_state}\n"
+ "\n".join(log)
)
expect_ui_session_history(
webdriver_port,
session_id,
"after blocked browser history shortcut from /b",
history_entry_urls(before_blocked_browser_ui_back["ui"]),
history_used_steps(before_blocked_browser_ui_back["ui"]),
before_blocked_browser_ui_back["ui"]["currentUsedStepIndex"],
before_blocked_browser_ui_back["ui"]["backButtonEnabled"],
before_blocked_browser_ui_back["ui"]["forwardButtonEnabled"],
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=history_step_values(
before_blocked_browser_ui_back["ui"]["webContentKnownUsedSteps"]
),
expected_web_content_current_step=before_blocked_browser_ui_back["ui"]["webContentCurrentStep"],
)
cross_site_link_click_point = execute_script(
webdriver_port,
session_id,
"""
const rect = document.querySelector("#branch").getBoundingClientRect();
return [Math.floor(rect.left + rect.width / 2), Math.floor(rect.top + rect.height / 2)];
""",
)
perform_pointer_click(
webdriver_port, session_id, cross_site_link_click_point[0], cross_site_link_click_point[1], log
)
blocked_cross_site_link_navigation_state = execute_script(
webdriver_port,
session_id,
"return [location.href, window.beforeUnloadCount];",
)
if (
blocked_cross_site_link_navigation_state[0] != url_b
or blocked_cross_site_link_navigation_state[1] <= blocked_shortcut_back_state[1]
):
raise AssertionError(
f"Expected beforeunload to cancel cross-site link navigation from /b, got {blocked_cross_site_link_navigation_state}\n"
+ "\n".join(log)
)
expect_ui_session_history(
webdriver_port,
session_id,
"after blocked cross-site link navigation from /b",
history_entry_urls(before_blocked_browser_ui_back["ui"]),
history_used_steps(before_blocked_browser_ui_back["ui"]),
before_blocked_browser_ui_back["ui"]["currentUsedStepIndex"],
before_blocked_browser_ui_back["ui"]["backButtonEnabled"],
before_blocked_browser_ui_back["ui"]["forwardButtonEnabled"],
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=history_step_values(
before_blocked_browser_ui_back["ui"]["webContentKnownUsedSteps"]
),
expected_web_content_current_step=before_blocked_browser_ui_back["ui"]["webContentCurrentStep"],
)
execute_script(webdriver_port, session_id, "window.onbeforeunload = null; return null;")
page_server.release_blocked_frame_b.clear()
page_server.blocked_frame_b_requested.clear()
traverse_history_from_ui(webdriver_port, session_id, -1)
expect_url(webdriver_port, session_id, "after browser UI back to nested", url_nested, log)
expect_navigation_buttons(webdriver_port, session_id, "after browser UI back to nested", True, True, log)
wait_for_event(page_server.blocked_frame_b_requested, "blocked nested frame restore")
expect_pending_web_content_history_step_after_fallback_load(
webdriver_port, session_id, "while browser UI back restores nested frame navigation", log
)
page_server.frame_b_blocked_document_ran.clear()
page_server.release_blocked_frame_b.set()
wait_for_event(
page_server.frame_b_blocked_document_ran,
"nested frame document after browser UI back",
)
expect_frame_url(
webdriver_port,
session_id,
"after browser UI back restores nested frame navigation",
url_frame_b_blocked,
log,
"Frame B Blocked",
)
expect_current_entry_nested_history(
webdriver_port,
session_id,
"after browser UI back to nested",
url_nested,
[url_frame_a, url_frame_b_blocked],
log,
)
expect_no_pending_web_content_history_step_after_fallback_load(
webdriver_port, session_id, "after browser UI back restores nested frame navigation", log
)
request(webdriver_port, "POST", f"/session/{session_id}/back", {})
expect_url(webdriver_port, session_id, "after nested frame back", url_nested, log)
expect_navigation_buttons(webdriver_port, session_id, "after nested frame back", True, True, log)
expect_frame_url(
webdriver_port, session_id, "after nested frame back restores previous entry", url_frame_a, log, "Frame A"
)
expect_current_entry_nested_history(
webdriver_port,
session_id,
"after nested frame back",
url_nested,
[url_frame_a, url_frame_b_blocked],
log,
)
request(webdriver_port, "POST", f"/session/{session_id}/url", {"url": url_b})
expect_url(webdriver_port, session_id, "after branching from nested frame history", url_b, log)
expect_entry_nested_history(
webdriver_port,
session_id,
"after branching from nested frame history",
url_nested,
[url_frame_a],
log,
)
before_location_replace = expect_web_content_session_history_matches_ui(
webdriver_port, session_id, "before location.replace()", log
)
replace_current_index = before_location_replace["ui"]["currentUsedStepIndex"]
expected_urls_after_replace = history_entry_urls(before_location_replace["ui"])
expected_urls_after_replace[replace_current_index] = url_c
page_server.c_document_ran.clear()
execute_script(webdriver_port, session_id, f"location.replace({json.dumps(url_c)}); return null;")
wait_for_event(page_server.c_document_ran, "C document script after location.replace()")
expect_url(webdriver_port, session_id, "after location.replace() to /c", url_c, log)
expect_ui_session_history(
webdriver_port,
session_id,
"after location.replace() to /c",
expected_urls_after_replace,
history_used_steps(before_location_replace["ui"]),
replace_current_index,
before_location_replace["ui"]["backButtonEnabled"],
before_location_replace["ui"]["forwardButtonEnabled"],
log,
expect_web_content_matches_ui=True,
expected_web_content_known_used_steps=history_step_values(
before_location_replace["ui"]["webContentKnownUsedSteps"]
),
expected_web_content_current_step=before_location_replace["ui"]["webContentCurrentStep"],
)
load_url_from_ui(webdriver_port, session_id, url_reload_blocked)
expect_url(webdriver_port, session_id, "after /reload-blocked", url_reload_blocked, log)
expect_current_ui_entry_reload_pending(webdriver_port, session_id, "before blocked reload", False, log)
page_server.blocked_reload_requested.clear()
page_server.reload_recovery_requested.clear()
page_server.release_blocked_reload.clear()
page_server.reload_blocked_document_ran.clear()
refresh(webdriver_port, session_id)
expect_reload_pending_history_log = True
wait_for_event(page_server.blocked_reload_requested, "blocked reload")
expect_current_ui_entry_reload_pending(webdriver_port, session_id, "during blocked reload", True, log)
page_server.release_blocked_reload.set()
wait_for_event(page_server.reload_blocked_document_ran, "blocked reload document")
expect_url(webdriver_port, session_id, "after blocked reload completes", url_reload_blocked, log)
expect_current_ui_entry_reload_pending(webdriver_port, session_id, "after blocked reload completes", False, log)
before_blocked_reload_crash_recovery = expect_web_content_session_history_matches_ui(
webdriver_port, session_id, "before blocked reload crash recovery", log
)
page_server.blocked_reload_requested.clear()
page_server.reload_recovery_requested.clear()
page_server.release_blocked_reload.clear()
page_server.reload_blocked_document_ran.clear()
refresh(webdriver_port, session_id)
wait_for_event(page_server.blocked_reload_requested, "blocked reload before crash")
expect_current_ui_entry_reload_pending(
webdriver_port, session_id, "during blocked reload before crash", True, log
)
crash_current_page_allowing_navigation_timeout(webdriver_port, session_id)
wait_for_event(page_server.reload_recovery_requested, "reload recovery request")
expect_ui_session_history(
webdriver_port,
session_id,
"while blocked crash recovery reloads current entry",
history_entry_urls(before_blocked_reload_crash_recovery["ui"]),
history_used_steps(before_blocked_reload_crash_recovery["ui"]),
before_blocked_reload_crash_recovery["ui"]["currentUsedStepIndex"],
before_blocked_reload_crash_recovery["ui"]["backButtonEnabled"],
before_blocked_reload_crash_recovery["ui"]["forwardButtonEnabled"],
log,
expect_web_content_matches_ui=False,
expected_waiting_to_seed_web_content=True,
expected_waiting_for_web_content_seed_ack=False,
expected_ignoring_web_content_updates_until_seed=True,
expected_reseed_after_current_history_load=True,
)
page_server.release_blocked_reload.set()
wait_for_event(page_server.reload_blocked_document_ran, "reload document after crash recovery")
expect_url(webdriver_port, session_id, "after blocked reload crash recovery", url_reload_blocked, log)
expect_current_ui_entry_reload_pending(
webdriver_port, session_id, "after blocked reload crash recovery", False, log
)
except Exception:
failed = True
raise
finally:
if session_id is not None:
try:
request(webdriver_port, "DELETE", f"/session/{session_id}")
except Exception:
pass
webdriver.terminate()
try:
webdriver.wait(timeout=5)
except subprocess.TimeoutExpired:
webdriver.kill()
webdriver.wait()
webdriver_stdout.seek(0)
webdriver_stderr.seek(0)
stdout = webdriver_stdout.read()
stderr = webdriver_stderr.read()
webdriver_stdout.close()
webdriver_stderr.close()
page_server.release_blocked_frame_b.set()
page_server.release_blocked_reload.set()
page_server.release_blocked_process_swap_back.set()
page_server.release_blocked_forward.set()
page_server.shutdown()
page_server.server_close()
if not failed and expect_reload_pending_history_log and "reload_pending=true" not in stderr:
print(stdout, file=sys.stdout)
print(stderr, file=sys.stderr)
raise AssertionError("Expected session history debug log to include reload_pending=true")
if not failed and expect_no_entry_history_log and "reason=traverse-no-entry" not in stderr:
print(stdout, file=sys.stdout)
print(stderr, file=sys.stderr)
raise AssertionError("Expected session history debug log to include reason=traverse-no-entry")
if failed or webdriver.returncode not in (0, -15):
print(stdout, file=sys.stdout)
print(stderr, file=sys.stderr)
if webdriver.returncode not in (0, -15):
raise RuntimeError(f"WebDriver exited with status {webdriver.returncode}")
def main():
parser = argparse.ArgumentParser()
parser.add_argument("webdriver_binary")
args = parser.parse_args()
run_test(args.webdriver_binary)
if __name__ == "__main__":
main()