diff --git a/webdriver/tests/bidi/browsing_context/traverse_history/__init__.py b/webdriver/tests/bidi/browsing_context/traverse_history/__init__.py
new file mode 100644
index 00000000000000..12278315491820
--- /dev/null
+++ b/webdriver/tests/bidi/browsing_context/traverse_history/__init__.py
@@ -0,0 +1,4 @@
+async def get_url_for_context(bidi_session, context):
+ contexts = await bidi_session.browsing_context.get_tree(root=context)
+
+ return contexts[0]["url"]
diff --git a/webdriver/tests/bidi/browsing_context/traverse_history/context.py b/webdriver/tests/bidi/browsing_context/traverse_history/context.py
new file mode 100644
index 00000000000000..7635c0e9ddd8a4
--- /dev/null
+++ b/webdriver/tests/bidi/browsing_context/traverse_history/context.py
@@ -0,0 +1,66 @@
+import pytest
+
+import webdriver.bidi.error as error
+
+from . import get_url_for_context
+
+
+pytestmark = pytest.mark.asyncio
+
+
+async def test_params_context_invalid_value(bidi_session):
+ with pytest.raises(error.NoSuchFrameException):
+ await bidi_session.browsing_context.traverse_history(context="foo", delta=1)
+
+
+async def test_top_level_contexts(bidi_session, top_context, new_tab, inline):
+ pages = [
+ inline("
page 1
"),
+ inline("page 2
"),
+ ]
+ for page in pages:
+ for context in [top_context["context"], new_tab["context"]]:
+ await bidi_session.browsing_context.navigate(
+ context=context, url=page, wait="complete"
+ )
+ assert await get_url_for_context(bidi_session, context) == page
+
+ await bidi_session.browsing_context.traverse_history(
+ context=new_tab["context"], delta=-1
+ )
+
+ assert await get_url_for_context(bidi_session, top_context["context"]) == pages[1]
+ assert await get_url_for_context(bidi_session, new_tab["context"]) == pages[0]
+
+
+@pytest.mark.parametrize("domain", ["", "alt"], ids=["same_origin", "cross_origin"])
+async def test_iframe(bidi_session, new_tab, inline, domain):
+ iframe_url_1 = inline("page 1")
+ page_url = inline(f"", domain=domain)
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=page_url, wait="complete"
+ )
+ assert await get_url_for_context(bidi_session, new_tab["context"]) == page_url
+
+ contexts = await bidi_session.browsing_context.get_tree(root=new_tab["context"])
+ iframe_context = contexts[0]["children"][0]
+
+ iframe_url_2 = inline("page 2")
+ await bidi_session.browsing_context.navigate(
+ context=iframe_context["context"], url=iframe_url_2, wait="complete"
+ )
+ assert (
+ await get_url_for_context(bidi_session, iframe_context["context"])
+ == iframe_url_2
+ )
+
+ await bidi_session.browsing_context.traverse_history(
+ context=iframe_context["context"], delta=-1
+ )
+
+ assert await get_url_for_context(bidi_session, new_tab["context"]) == page_url
+ assert (
+ await get_url_for_context(bidi_session, iframe_context["context"])
+ == iframe_url_1
+ )
diff --git a/webdriver/tests/bidi/browsing_context/traverse_history/delta.py b/webdriver/tests/bidi/browsing_context/traverse_history/delta.py
new file mode 100644
index 00000000000000..05f4d99544fc85
--- /dev/null
+++ b/webdriver/tests/bidi/browsing_context/traverse_history/delta.py
@@ -0,0 +1,162 @@
+from pathlib import Path
+
+import pytest
+
+import webdriver.bidi.error as error
+from webdriver.bidi.modules.script import ContextTarget
+
+from . import get_url_for_context
+
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("value", [-2, 1])
+async def test_delta_invalid_value(bidi_session, new_tab, inline, value):
+ page = inline("page 1
")
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=page, wait="complete"
+ )
+ assert await get_url_for_context(bidi_session, new_tab["context"]) == page
+
+ with pytest.raises(error.NoSuchHistoryEntryException):
+ await bidi_session.browsing_context.traverse_history(
+ context=new_tab["context"], delta=value
+ )
+
+
+async def test_delta_0(bidi_session, new_tab, inline):
+ page = inline("page 1
")
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=page, wait="complete"
+ )
+ assert await get_url_for_context(bidi_session, new_tab["context"]) == page
+
+ await bidi_session.browsing_context.traverse_history(
+ context=new_tab["context"], delta=0
+ )
+
+ # Check that url didn't change
+ assert await get_url_for_context(bidi_session, new_tab["context"]) == page
+
+
+async def test_delta_forward_and_back(bidi_session, new_tab, inline):
+ pages = [
+ inline("page 1
"),
+ inline("page 2
"),
+ inline("page 3
"),
+ ]
+ for page in pages:
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=page, wait="complete"
+ )
+ assert await get_url_for_context(bidi_session, new_tab["context"]) == page
+
+ await bidi_session.browsing_context.traverse_history(
+ context=new_tab["context"], delta=-2
+ )
+
+ assert await get_url_for_context(bidi_session, new_tab["context"]) == pages[0]
+
+ await bidi_session.browsing_context.traverse_history(
+ context=new_tab["context"], delta=2
+ )
+
+ assert await get_url_for_context(bidi_session, new_tab["context"]) == pages[2]
+
+
+async def test_navigate_in_the_same_document(bidi_session, new_tab, url):
+ page_url = "/webdriver/tests/bidi/browsing_context/support/empty.html"
+ pages = [
+ url(page_url),
+ url(page_url + "#foo"),
+ url(page_url + "#bar"),
+ ]
+ for page in pages:
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=page, wait="complete"
+ )
+ assert await get_url_for_context(bidi_session, new_tab["context"]) == page
+
+ await bidi_session.browsing_context.traverse_history(
+ context=new_tab["context"], delta=-1
+ )
+
+ assert await get_url_for_context(bidi_session, new_tab["context"]) == pages[1]
+
+ await bidi_session.browsing_context.traverse_history(
+ context=new_tab["context"], delta=1
+ )
+
+ assert await get_url_for_context(bidi_session, new_tab["context"]) == pages[2]
+
+
+async def test_history_push_state(bidi_session, new_tab, url):
+ page_url = url("/webdriver/tests/bidi/browsing_context/support/empty.html")
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=page_url, wait="complete"
+ )
+ assert await get_url_for_context(bidi_session, new_tab["context"]) == page_url
+
+ pages = [
+ f"{page_url}#foo",
+ f"{page_url}#bar",
+ ]
+ for page in pages:
+ await bidi_session.script.call_function(
+ function_declaration="""(url) => {
+ history.pushState(null, null, url);
+ }""",
+ arguments=[
+ {"type": "string", "value": page},
+ ],
+ await_promise=False,
+ target=ContextTarget(new_tab["context"]),
+ )
+ assert await get_url_for_context(bidi_session, new_tab["context"]) == page
+
+ await bidi_session.browsing_context.traverse_history(
+ context=new_tab["context"], delta=-1
+ )
+
+ assert await get_url_for_context(bidi_session, new_tab["context"]) == pages[0]
+
+ await bidi_session.browsing_context.traverse_history(
+ context=new_tab["context"], delta=1
+ )
+
+ assert await get_url_for_context(bidi_session, new_tab["context"]) == pages[1]
+
+
+@pytest.mark.parametrize(
+ "pages",
+ [
+ ["data:text/html,foo
", "data:text/html,bar
"],
+ [
+ f"{Path(__file__).parents[1].as_uri()}/support/empty.html",
+ f"{Path(__file__).parents[1].as_uri()}/support/other.html",
+ ],
+ ],
+ ids=[
+ "data url",
+ "file url",
+ ],
+)
+async def test_navigate_special_protocols(bidi_session, new_tab, pages):
+ for page in pages:
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=page, wait="complete"
+ )
+ assert await get_url_for_context(bidi_session, new_tab["context"]) == page
+
+ await bidi_session.browsing_context.traverse_history(
+ context=new_tab["context"], delta=-1
+ )
+
+ assert await get_url_for_context(bidi_session, new_tab["context"]) == pages[0]
+
+ await bidi_session.browsing_context.traverse_history(
+ context=new_tab["context"], delta=1
+ )
+
+ assert await get_url_for_context(bidi_session, new_tab["context"]) == pages[1]
diff --git a/webdriver/tests/bidi/browsing_context/traverse_history/invalid.py b/webdriver/tests/bidi/browsing_context/traverse_history/invalid.py
new file mode 100644
index 00000000000000..abb0d69c937d0a
--- /dev/null
+++ b/webdriver/tests/bidi/browsing_context/traverse_history/invalid.py
@@ -0,0 +1,29 @@
+import pytest
+
+import webdriver.bidi.error as error
+
+
+pytestmark = pytest.mark.asyncio
+
+
+MAX_INT = 9007199254740991
+MIN_INT = -MAX_INT
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_context_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.traverse_history(
+ context=value,
+ delta=1
+ )
+
+
+@pytest.mark.parametrize(
+ "value", [None, False, "foo", 1.5, MIN_INT - 1, MAX_INT + 1, {}, []]
+)
+async def test_params_delta_invalid_type(bidi_session, top_context, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.traverse_history(
+ context=top_context["context"], delta=value
+ )