[wdspec] Add proper support for tests to use WebFrame and WebWindow.

Differential Revision: https://phabricator.services.mozilla.com/D191434

bugzilla-url: https://bugzilla.mozilla.org/show_bug.cgi?id=1274251
gecko-commit: 92a611eda6b1ad91ec74242b49baad374910a4a6
gecko-reviewers: webdriver-reviewers, jdescottes
diff --git a/tools/webdriver/webdriver/__init__.py b/tools/webdriver/webdriver/__init__.py
index a817514..dfd264f 100644
--- a/tools/webdriver/webdriver/__init__.py
+++ b/tools/webdriver/webdriver/__init__.py
@@ -2,13 +2,14 @@
 
 from .client import (
     Cookies,
-    Element,
     Find,
-    Frame,
     Session,
     ShadowRoot,
     Timeouts,
-    Window)
+    WebElement,
+    WebFrame,
+    WebWindow,
+)
 from .error import (
     ElementNotSelectableException,
     ElementNotVisibleException,
diff --git a/tools/webdriver/webdriver/client.py b/tools/webdriver/webdriver/client.py
index f33fc34..e41df7f 100644
--- a/tools/webdriver/webdriver/client.py
+++ b/tools/webdriver/webdriver/client.py
@@ -295,9 +295,7 @@
         return ActionSequence(self.session, *args, **kwargs)
 
 
-class Window:
-    identifier = "window-fcc6-11e5-b4f8-330a88ab9d7f"
-
+class BrowserWindow:
     def __init__(self, session):
         self.session = session
 
@@ -372,59 +370,6 @@
     def fullscreen(self):
         return self.session.send_session_command("POST", "window/fullscreen")
 
-    @classmethod
-    def from_json(cls, json, session):
-        uuid = json[Window.identifier]
-        return cls(uuid, session)
-
-
-class Frame:
-    identifier = "frame-075b-4da1-b6ba-e579c2d3230a"
-
-    def __init__(self, session):
-        self.session = session
-
-    @classmethod
-    def from_json(cls, json, session):
-        uuid = json[Frame.identifier]
-        return cls(uuid, session)
-
-
-class ShadowRoot:
-    identifier = "shadow-6066-11e4-a52e-4f735466cecf"
-
-    def __init__(self, session, id):
-        """
-        Construct a new shadow root representation.
-
-        :param id: Shadow root UUID which must be unique across
-            all browsing contexts.
-        :param session: Current ``webdriver.Session``.
-        """
-        self.id = id
-        self.session = session
-
-    @classmethod
-    def from_json(cls, json, session):
-        uuid = json[ShadowRoot.identifier]
-        return cls(session, uuid)
-
-    def send_shadow_command(self, method, uri, body=None):
-        url = f"shadow/{self.id}/{uri}"
-        return self.session.send_session_command(method, url, body)
-
-    @command
-    def find_element(self, strategy, selector):
-        body = {"using": strategy,
-                "value": selector}
-        return self.send_shadow_command("POST", "element", body)
-
-    @command
-    def find_elements(self, strategy, selector):
-        body = {"using": strategy,
-                "value": selector}
-        return self.send_shadow_command("POST", "elements", body)
-
 
 class Find:
     def __init__(self, session):
@@ -512,7 +457,7 @@
         self.extension_cls = extension
 
         self.timeouts = Timeouts(self)
-        self.window = Window(self)
+        self.window = BrowserWindow(self)
         self.find = Find(self)
         self.alert = UserPrompt(self)
         self.actions = Actions(self)
@@ -795,7 +740,44 @@
     def screenshot(self):
         return self.send_session_command("GET", "screenshot")
 
-class Element:
+
+class ShadowRoot:
+    identifier = "shadow-6066-11e4-a52e-4f735466cecf"
+
+    def __init__(self, session, id):
+        """
+        Construct a new shadow root representation.
+
+        :param id: Shadow root UUID which must be unique across
+            all browsing contexts.
+        :param session: Current ``webdriver.Session``.
+        """
+        self.id = id
+        self.session = session
+
+    @classmethod
+    def from_json(cls, json, session):
+        uuid = json[ShadowRoot.identifier]
+        return cls(session, uuid)
+
+    def send_shadow_command(self, method, uri, body=None):
+        url = f"shadow/{self.id}/{uri}"
+        return self.session.send_session_command(method, url, body)
+
+    @command
+    def find_element(self, strategy, selector):
+        body = {"using": strategy,
+                "value": selector}
+        return self.send_shadow_command("POST", "element", body)
+
+    @command
+    def find_elements(self, strategy, selector):
+        body = {"using": strategy,
+                "value": selector}
+        return self.send_shadow_command("POST", "elements", body)
+
+
+class WebElement:
     """
     Representation of a web element.
 
@@ -818,12 +800,12 @@
         return "<%s %s>" % (self.__class__.__name__, self.id)
 
     def __eq__(self, other):
-        return (isinstance(other, Element) and self.id == other.id and
+        return (isinstance(other, WebElement) and self.id == other.id and
                 self.session == other.session)
 
     @classmethod
     def from_json(cls, json, session):
-        uuid = json[Element.identifier]
+        uuid = json[WebElement.identifier]
         return cls(session, uuid)
 
     def send_element_command(self, method, uri, body=None):
@@ -902,3 +884,42 @@
     @command
     def property(self, name):
         return self.send_element_command("GET", "property/%s" % name)
+
+class WebFrame:
+    identifier = "frame-075b-4da1-b6ba-e579c2d3230a"
+
+    def __init__(self, session, id):
+        self.id = id
+        self.session = session
+
+    def __repr__(self):
+        return "<%s %s>" % (self.__class__.__name__, self.id)
+
+    def __eq__(self, other):
+        return (isinstance(other, WebFrame) and self.id == other.id and
+                self.session == other.session)
+
+    @classmethod
+    def from_json(cls, json, session):
+        uuid = json[WebFrame.identifier]
+        return cls(session, uuid)
+
+
+class WebWindow:
+    identifier = "window-fcc6-11e5-b4f8-330a88ab9d7f"
+
+    def __init__(self, session, id):
+        self.id = id
+        self.session = session
+
+    def __repr__(self):
+        return "<%s %s>" % (self.__class__.__name__, self.id)
+
+    def __eq__(self, other):
+        return (isinstance(other, WebWindow) and self.id == other.id and
+                self.session == other.session)
+
+    @classmethod
+    def from_json(cls, json, session):
+        uuid = json[WebWindow.identifier]
+        return cls(session, uuid)
diff --git a/tools/webdriver/webdriver/protocol.py b/tools/webdriver/webdriver/protocol.py
index 1972c3f..d6c89af2 100644
--- a/tools/webdriver/webdriver/protocol.py
+++ b/tools/webdriver/webdriver/protocol.py
@@ -16,14 +16,14 @@
     def default(self, obj):
         if isinstance(obj, (list, tuple)):
             return [self.default(x) for x in obj]
-        elif isinstance(obj, webdriver.Element):
-            return {webdriver.Element.identifier: obj.id}
-        elif isinstance(obj, webdriver.Frame):
-            return {webdriver.Frame.identifier: obj.id}
-        elif isinstance(obj, webdriver.Window):
-            return {webdriver.Frame.identifier: obj.id}
+        elif isinstance(obj, webdriver.WebElement):
+            return {webdriver.WebElement.identifier: obj.id}
+        elif isinstance(obj, webdriver.WebFrame):
+            return {webdriver.WebFrame.identifier: obj.id}
         elif isinstance(obj, webdriver.ShadowRoot):
             return {webdriver.ShadowRoot.identifier: obj.id}
+        elif isinstance(obj, webdriver.WebWindow):
+            return {webdriver.WebWindow.identifier: obj.id}
         return super().default(obj)
 
 
@@ -36,14 +36,14 @@
     def object_hook(self, payload):
         if isinstance(payload, (list, tuple)):
             return [self.object_hook(x) for x in payload]
-        elif isinstance(payload, dict) and webdriver.Element.identifier in payload:
-            return webdriver.Element.from_json(payload, self.session)
-        elif isinstance(payload, dict) and webdriver.Frame.identifier in payload:
-            return webdriver.Frame.from_json(payload, self.session)
-        elif isinstance(payload, dict) and webdriver.Window.identifier in payload:
-            return webdriver.Window.from_json(payload, self.session)
+        elif isinstance(payload, dict) and webdriver.WebElement.identifier in payload:
+            return webdriver.WebElement.from_json(payload, self.session)
+        elif isinstance(payload, dict) and webdriver.WebFrame.identifier in payload:
+            return webdriver.WebFrame.from_json(payload, self.session)
         elif isinstance(payload, dict) and webdriver.ShadowRoot.identifier in payload:
             return webdriver.ShadowRoot.from_json(payload, self.session)
+        elif isinstance(payload, dict) and webdriver.WebWindow.identifier in payload:
+            return webdriver.WebWindow.from_json(payload, self.session)
         elif isinstance(payload, dict):
             return {k: self.object_hook(v) for k, v in payload.items()}
         return payload
diff --git a/tools/webdriver/webdriver/transport.py b/tools/webdriver/webdriver/transport.py
index e1e16bd..ca1ff74 100644
--- a/tools/webdriver/webdriver/transport.py
+++ b/tools/webdriver/webdriver/transport.py
@@ -102,9 +102,9 @@
     Transports messages (commands and responses) over the WebDriver
     wire protocol.
 
-    Complex objects, such as ``webdriver.Element``, ``webdriver.Frame``,
-    and ``webdriver.Window`` are by default not marshaled to enable
-    use of `session.transport.send` in WPT tests::
+    Complex objects, such as ``webdriver.ShadowRoot``, ``webdriver.WebElement``,
+    ``webdriver.WebFrame``, and ``webdriver.WebWindow`` are by default not
+    marshaled to enable use of `session.transport.send` in WPT tests::
 
         session = webdriver.Session("127.0.0.1", 4444)
         response = transport.send("GET", "element/active", None)
@@ -180,17 +180,17 @@
         """
         Send a command to the remote.
 
-        The request `body` must be JSON serialisable unless a
+        The request `body` must be JSON serializable unless a
         custom `encoder` has been provided.  This means complex
-        objects such as ``webdriver.Element``, ``webdriver.Frame``,
-        and `webdriver.Window`` are not automatically made
-        into JSON.  This behaviour is, however, provided by
+        objects such as ``webdriver.ShadowRoot``, ``webdriver.WebElement``,
+        ``webdriver.WebFrame``, and `webdriver.Window`` are not automatically
+        made into JSON.  This behavior is, however, provided by
         ``webdriver.protocol.Encoder``, should you want it.
 
         Similarly, the response body is returned au natural
         as plain JSON unless a `decoder` that converts web
         element references to ``webdriver.Element`` is provided.
-        Use ``webdriver.protocol.Decoder`` to achieve this behaviour.
+        Use ``webdriver.protocol.Decoder`` to achieve this behavior.
 
         The client will attempt to use persistent HTTP connections.
 
@@ -211,7 +211,7 @@
             describing the HTTP response received from the remote end.
 
         :raises ValueError: If `body` or the response body are not
-            JSON serialisable.
+            JSON serializable.
         """
         if body is None and method == "POST":
             body = {}
diff --git a/webdriver/tests/bidi/script/classic_interop/node_shared_id.py b/webdriver/tests/bidi/script/classic_interop/node_shared_id.py
index 82b39b4..aeb2bc4 100644
--- a/webdriver/tests/bidi/script/classic_interop/node_shared_id.py
+++ b/webdriver/tests/bidi/script/classic_interop/node_shared_id.py
@@ -1,6 +1,6 @@
 import pytest
 
-from webdriver import Element, ShadowRoot
+from webdriver import ShadowRoot, WebElement
 from webdriver.bidi.modules.script import ContextTarget
 
 pytestmark = pytest.mark.asyncio
@@ -51,7 +51,7 @@
     assert nodeType == ELEMENT_NODE
 
     # Use element reference from WebDriver BiDi in WebDriver classic
-    node = Element(current_session, result["sharedId"])
+    node = WebElement(current_session, result["sharedId"])
     nodeType = current_session.execute_script(
         """return arguments[0].nodeType""", args=(node,)
     )
diff --git a/webdriver/tests/classic/element_clear/clear.py b/webdriver/tests/classic/element_clear/clear.py
index 9b0d7f2..22c07b6 100644
--- a/webdriver/tests/classic/element_clear/clear.py
+++ b/webdriver/tests/classic/element_clear/clear.py
@@ -1,7 +1,7 @@
 # META: timeout=long
 
 import pytest
-from webdriver import Element
+from webdriver import WebElement
 
 from tests.support.asserts import (
     assert_element_has_focus,
@@ -45,7 +45,7 @@
 
 
 def test_no_top_browsing_context(session, closed_window):
-    element = Element(session, "foo")
+    element = WebElement(session, "foo")
     response = element_clear(session, element)
     assert_error(response, "no such window")
 
@@ -59,14 +59,14 @@
 
 
 def test_no_browsing_context(session, closed_frame):
-    element = Element(session, "foo")
+    element = WebElement(session, "foo")
 
     response = element_clear(session, element)
     assert_error(response, "no such window")
 
 
 def test_no_such_element_with_invalid_value(session):
-    element = Element(session, "foo")
+    element = WebElement(session, "foo")
 
     response = element_clear(session, element)
     assert_error(response, "no such element")
diff --git a/webdriver/tests/classic/element_click/click.py b/webdriver/tests/classic/element_click/click.py
index 3c3f7d7..61acc92 100644
--- a/webdriver/tests/classic/element_click/click.py
+++ b/webdriver/tests/classic/element_click/click.py
@@ -1,5 +1,5 @@
 import pytest
-from webdriver import Element
+from webdriver import WebElement
 
 from tests.support.asserts import assert_error, assert_success
 
@@ -21,7 +21,7 @@
 
 
 def test_no_top_browsing_context(session, closed_window):
-    element = Element(session, "foo")
+    element = WebElement(session, "foo")
     response = element_click(session, element)
     assert_error(response, "no such window")
 
@@ -35,14 +35,14 @@
 
 
 def test_no_browsing_context(session, closed_frame):
-    element = Element(session, "foo")
+    element = WebElement(session, "foo")
 
     response = element_click(session, element)
     assert_error(response, "no such window")
 
 
 def test_no_such_element_with_invalid_value(session):
-    element = Element(session, "foo")
+    element = WebElement(session, "foo")
 
     response = element_click(session, element)
     assert_error(response, "no such element")
diff --git a/webdriver/tests/classic/element_click/events.py b/webdriver/tests/classic/element_click/events.py
index 30f2dfa..5e80b52 100644
--- a/webdriver/tests/classic/element_click/events.py
+++ b/webdriver/tests/classic/element_click/events.py
@@ -1,4 +1,4 @@
-from webdriver import Element
+from webdriver import WebElement
 from tests.support.asserts import assert_success
 from tests.support.helpers import filter_dict
 
diff --git a/webdriver/tests/classic/element_send_keys/send_keys.py b/webdriver/tests/classic/element_send_keys/send_keys.py
index 281c7ad..92002f2 100644
--- a/webdriver/tests/classic/element_send_keys/send_keys.py
+++ b/webdriver/tests/classic/element_send_keys/send_keys.py
@@ -1,6 +1,6 @@
 import pytest
 
-from webdriver import Element
+from webdriver import WebElement
 from webdriver.transport import Response
 
 from tests.support.asserts import assert_error, assert_success
@@ -34,7 +34,7 @@
 
 
 def test_no_top_browsing_context(session, closed_window):
-    element = Element(session, "foo")
+    element = WebElement(session, "foo")
     response = element_send_keys(session, element, "foo")
     assert_error(response, "no such window")
 
@@ -48,14 +48,14 @@
 
 
 def test_no_browsing_context(session, closed_frame):
-    element = Element(session, "foo")
+    element = WebElement(session, "foo")
 
     response = element_send_keys(session, element, "foo")
     assert_error(response, "no such window")
 
 
 def test_no_such_element_with_invalid_value(session):
-    element = Element(session, "foo")
+    element = WebElement(session, "foo")
 
     response = element_send_keys(session, element, "foo")
     assert_error(response, "no such element")
diff --git a/webdriver/tests/classic/execute_async_script/arguments.py b/webdriver/tests/classic/execute_async_script/arguments.py
index ead6e0c..81b30de 100644
--- a/webdriver/tests/classic/execute_async_script/arguments.py
+++ b/webdriver/tests/classic/execute_async_script/arguments.py
@@ -1,6 +1,6 @@
 import pytest
 
-from webdriver.client import Element, Frame, ShadowRoot, Window
+from webdriver.client import ShadowRoot, WebElement, WebFrame, WebWindow
 
 from tests.support.asserts import assert_error, assert_success
 from . import execute_async_script
@@ -54,8 +54,8 @@
     assert actual[1] == value
 
 
-def test_no_such_element_with_invalid_value(session):
-    element = Element(session, "foo")
+def test_no_such_element_with_unknown_id(session):
+    element = WebElement(session, "foo")
 
     result = execute_async_script(session, """
         arguments[1](true);
@@ -101,7 +101,7 @@
     assert_error(result, "no such element")
 
 
-def test_no_such_shadow_root_with_unknown_shadow_root(session):
+def test_no_such_shadow_root_with_unknown_id(session):
     shadow_root = ShadowRoot(session, "foo")
 
     result = execute_async_script(session, """
@@ -159,18 +159,47 @@
     assert_error(result, "stale element reference")
 
 
-@pytest.mark.parametrize("expression, expected_type, expected_class", [
-    ("window.frames[0]", Frame, "Frame"),
-    ("document.querySelector('div')", Element, "HTMLDivElement"),
-    ("document.querySelector('custom-element').shadowRoot", ShadowRoot, "ShadowRoot"),
-    ("window", Window, "Window")
+@pytest.mark.parametrize("type", [WebFrame, WebWindow], ids=["frame", "window"])
+@pytest.mark.parametrize("value", [None, False, 42, [], {}])
+def test_invalid_argument_for_window_with_invalid_type(session, type, value):
+    reference = type(session, value)
+
+    result = execute_async_script(session, "arguments[1](true)", args=(reference,))
+    assert_error(result, "invalid argument")
+
+
+def test_no_such_window_for_window_with_invalid_value(session, get_test_page):
+    session.url = get_test_page()
+
+    result = execute_async_script(session, "arguments[0]([window, window.frames[0]]);")
+    [window, frame] = assert_success(result)
+
+    assert isinstance(window, WebWindow)
+    assert isinstance(frame, WebFrame)
+
+    window_reference = WebWindow(session, frame.id)
+    frame_reference = WebFrame(session, window.id)
+
+    for reference in [window_reference, frame_reference]:
+        result = execute_async_script(session, "arguments[1](true)", args=(reference,))
+        assert_error(result, "no such window")
+
+
+@pytest.mark.parametrize("expression, expected_type", [
+    ("window.frames[0]", WebFrame),
+    ("document.querySelector('div')", WebElement),
+    ("document.querySelector('custom-element').shadowRoot", ShadowRoot),
+    ("window", WebWindow)
 ], ids=["frame", "node", "shadow-root", "window"])
-def test_element_reference(session, get_test_page, expression, expected_type, expected_class):
+def test_element_reference(session, get_test_page, expression, expected_type):
     session.url = get_test_page(as_frame=False)
 
     result = execute_async_script(session, f"arguments[0]({expression})")
     reference = assert_success(result)
     assert isinstance(reference, expected_type)
 
-    result = execute_async_script(session, "arguments[1](arguments[0].constructor.name)", [reference])
-    assert_success(result, expected_class)
+    result = execute_async_script(session, f"""
+        let resolve = arguments[1];
+        resolve(arguments[0] == {expression})
+        """, [reference])
+    assert_success(result, True)
diff --git a/webdriver/tests/classic/execute_async_script/execute_async.py b/webdriver/tests/classic/execute_async_script/execute_async.py
index 42cf4aa..3c8cc62 100644
--- a/webdriver/tests/classic/execute_async_script/execute_async.py
+++ b/webdriver/tests/classic/execute_async_script/execute_async.py
@@ -1,6 +1,6 @@
 import pytest
 
-from webdriver import Element
+from webdriver import WebElement
 from webdriver.error import NoSuchAlertException
 from webdriver.transport import Response
 
@@ -16,12 +16,12 @@
 
 
 def test_no_top_browsing_context(session, closed_window):
-    response = execute_async_script(session, "argument[0](1);")
+    response = execute_async_script(session, "arguments[0](1);")
     assert_error(response, "no such window")
 
 
 def test_no_browsing_context(session, closed_frame):
-    response = execute_async_script(session, "argument[0](1);")
+    response = execute_async_script(session, "arguments[0](1);")
     assert_error(response, "no such window")
 
 
diff --git a/webdriver/tests/classic/execute_async_script/node.py b/webdriver/tests/classic/execute_async_script/node.py
index 53abda4..2f1bf75 100644
--- a/webdriver/tests/classic/execute_async_script/node.py
+++ b/webdriver/tests/classic/execute_async_script/node.py
@@ -1,6 +1,6 @@
 import pytest
 
-from webdriver.client import Element, Frame, ShadowRoot, Window
+from webdriver.client import ShadowRoot, WebElement
 
 from tests.support.asserts import assert_error, assert_success
 from . import execute_async_script
@@ -59,11 +59,9 @@
 
 
 @pytest.mark.parametrize("expression, expected_type", [
-    ("window.frames[0]", Frame),
-    ("document.querySelector('div')", Element),
+    ("document.querySelector('div')", WebElement),
     ("document.querySelector('custom-element').shadowRoot", ShadowRoot),
-    ("window", Window),
-], ids=["frame", "node", "shadow-root", "window"])
+], ids=["element", "shadow-root"])
 def test_element_reference(session, get_test_page, expression, expected_type):
     session.url = get_test_page()
 
@@ -81,7 +79,7 @@
     (""" document"""),
     (""" document.doctype"""),
 ], ids=["attribute", "text", "cdata", "processing_instruction", "comment", "document", "doctype"])
-def test_non_element_nodes(session, inline, expression):
+def test_not_supported_nodes(session, inline, expression):
     session.url = inline(PAGE_DATA)
 
     result = execute_async_script(session, f"arguments[0]({expression})")
diff --git a/webdriver/tests/classic/execute_async_script/window.py b/webdriver/tests/classic/execute_async_script/window.py
new file mode 100644
index 0000000..f79bfdf
--- /dev/null
+++ b/webdriver/tests/classic/execute_async_script/window.py
@@ -0,0 +1,33 @@
+import pytest
+
+from webdriver.client import WebFrame, WebWindow
+
+from tests.support.asserts import assert_success
+from . import execute_async_script
+
+
+@pytest.mark.parametrize("expression, expected_type", [
+    ("window.frames[0]", WebFrame),
+    ("window", WebWindow),
+], ids=["frame", "window"])
+def test_web_reference(session, get_test_page, expression, expected_type):
+    session.url = get_test_page()
+
+    result = execute_async_script(session, f"arguments[0]({expression})")
+    reference = assert_success(result)
+
+    assert isinstance(reference, expected_type)
+
+    if isinstance(reference, WebWindow):
+        assert reference.id in session.handles
+    else:
+        assert reference.id not in session.handles
+
+
+def test_window_open(session):
+    result = execute_async_script(
+        session, "window.foo = window.open(); arguments[0](window.foo);")
+    reference = assert_success(result)
+
+    assert isinstance(reference, WebWindow)
+    assert reference.id in session.handles
diff --git a/webdriver/tests/classic/execute_script/arguments.py b/webdriver/tests/classic/execute_script/arguments.py
index b8657ce..ab5c523 100644
--- a/webdriver/tests/classic/execute_script/arguments.py
+++ b/webdriver/tests/classic/execute_script/arguments.py
@@ -1,6 +1,6 @@
 import pytest
 
-from webdriver.client import Element, Frame, ShadowRoot, Window
+from webdriver.client import ShadowRoot, WebElement, WebFrame, WebWindow
 
 from tests.support.asserts import assert_error, assert_success
 from . import execute_script
@@ -46,8 +46,8 @@
     assert actual[1] == value
 
 
-def test_no_such_element_with_invalid_value(session):
-    element = Element(session, "foo")
+def test_no_such_element_with_unknown_id(session):
+    element = WebElement(session, "foo")
 
     result = execute_script(session, "return true;", args=[element])
     assert_error(result, "no such element")
@@ -87,7 +87,7 @@
     assert_error(result, "no such element")
 
 
-def test_no_such_shadow_root_with_unknown_shadow_root(session):
+def test_no_such_shadow_root_with_unknown_id(session):
     shadow_root = ShadowRoot(session, "foo")
 
     result = execute_script(session, "return true;", args=[shadow_root])
@@ -147,18 +147,44 @@
     assert_error(result, "stale element reference")
 
 
-@pytest.mark.parametrize("expression, expected_type, expected_class", [
-    ("window.frames[0]", Frame, "Frame"),
-    ("document.querySelector('div')", Element, "HTMLDivElement"),
-    ("document.querySelector('custom-element').shadowRoot", ShadowRoot, "ShadowRoot"),
-    ("window", Window, "Window")
+@pytest.mark.parametrize("type", [WebFrame, WebWindow], ids=["frame", "window"])
+@pytest.mark.parametrize("value", [None, False, 42, [], {}])
+def test_invalid_argument_for_window_with_invalid_type(session, type, value):
+    reference = type(session, value)
+
+    result = execute_script(session, "return true", args=(reference,))
+    assert_error(result, "invalid argument")
+
+
+def test_no_such_window_for_window_with_invalid_value(session, get_test_page):
+    session.url = get_test_page()
+
+    result = execute_script(session, "return [window, window.frames[0]];")
+    [window, frame] = assert_success(result)
+
+    assert isinstance(window, WebWindow)
+    assert isinstance(frame, WebFrame)
+
+    window_reference = WebWindow(session, frame.id)
+    frame_reference = WebFrame(session, window.id)
+
+    for reference in [window_reference, frame_reference]:
+        result = execute_script(session, "return true", args=(reference,))
+        assert_error(result, "no such window")
+
+
+@pytest.mark.parametrize("expression, expected_type", [
+    ("window.frames[0]", WebFrame),
+    ("document.querySelector('div')", WebElement),
+    ("document.querySelector('custom-element').shadowRoot", ShadowRoot),
+    ("window", WebWindow)
 ], ids=["frame", "node", "shadow-root", "window"])
-def test_element_reference(session, get_test_page, expression, expected_type, expected_class):
+def test_element_reference(session, get_test_page, expression, expected_type):
     session.url = get_test_page(as_frame=False)
 
     result = execute_script(session, f"return {expression}")
     reference = assert_success(result)
     assert isinstance(reference, expected_type)
 
-    result = execute_script(session, "return arguments[0].constructor.name", [reference])
-    assert_success(result, expected_class)
+    result = execute_script(session, f"return arguments[0] == {expression}", [reference])
+    assert_success(result, True)
diff --git a/webdriver/tests/classic/execute_script/execute.py b/webdriver/tests/classic/execute_script/execute.py
index fbccc98..15ac1d0 100644
--- a/webdriver/tests/classic/execute_script/execute.py
+++ b/webdriver/tests/classic/execute_script/execute.py
@@ -1,6 +1,6 @@
 import pytest
 
-from webdriver import Element
+from webdriver import WebElement
 from webdriver.error import NoSuchAlertException
 from webdriver.transport import Response
 
diff --git a/webdriver/tests/classic/execute_script/json_serialize_windowproxy.py b/webdriver/tests/classic/execute_script/json_serialize_windowproxy.py
deleted file mode 100644
index 8e76fed..0000000
--- a/webdriver/tests/classic/execute_script/json_serialize_windowproxy.py
+++ /dev/null
@@ -1,51 +0,0 @@
-import json
-
-from tests.support.asserts import assert_success
-from . import execute_script
-
-_window_id = "window-fcc6-11e5-b4f8-330a88ab9d7f"
-_frame_id = "frame-075b-4da1-b6ba-e579c2d3230a"
-
-
-def test_initial_window(session):
-    # non-auxiliary top-level browsing context
-    response = execute_script(session, "return window;")
-    raw_json = assert_success(response)
-
-    obj = json.loads(raw_json)
-    assert len(obj) == 1
-    assert _window_id in obj
-    handle = obj[_window_id]
-    assert handle in session.window_handles
-
-
-def test_window_open(session):
-    # auxiliary browsing context
-    session.execute_script("window.foo = window.open()")
-
-    response = execute_script(session, "return window.foo;")
-    raw_json = assert_success(response)
-
-    obj = json.loads(raw_json)
-    assert len(obj) == 1
-    assert _window_id in obj
-    handle = obj[_window_id]
-    assert handle in session.window_handles
-
-
-def test_frame(session):
-    # nested browsing context
-    append = """
-        window.frame = document.createElement('iframe');
-        document.body.appendChild(frame);
-    """
-    session.execute_script(append)
-
-    response = execute_script(session, "return frame.contentWindow;")
-    raw_json = assert_success(response)
-
-    obj = json.loads(raw_json)
-    assert len(obj) == 1
-    assert _frame_id in obj
-    handle = obj[_frame_id]
-    assert handle not in session.window_handles
diff --git a/webdriver/tests/classic/execute_script/node.py b/webdriver/tests/classic/execute_script/node.py
index caf8598..61cf346 100644
--- a/webdriver/tests/classic/execute_script/node.py
+++ b/webdriver/tests/classic/execute_script/node.py
@@ -1,6 +1,6 @@
 import pytest
 
-from webdriver.client import Element, Frame, ShadowRoot, Window
+from webdriver.client import WebElement, ShadowRoot
 from tests.support.asserts import assert_error, assert_success
 from . import execute_script
 
@@ -58,12 +58,10 @@
 
 
 @pytest.mark.parametrize("expression, expected_type", [
-    ("window.frames[0]", Frame),
-    ("document.querySelector('div')", Element),
+    ("document.querySelector('div')", WebElement),
     ("document.querySelector('custom-element').shadowRoot", ShadowRoot),
-    ("window", Window),
-], ids=["frame", "node", "shadow-root", "window"])
-def test_element_reference(session, get_test_page, expression, expected_type):
+], ids=["element", "shadow-root"])
+def test_web_reference(session, get_test_page, expression, expected_type):
     session.url = get_test_page()
 
     result = execute_script(session, f"return {expression}")
@@ -80,7 +78,7 @@
     (""" document"""),
     (""" document.doctype"""),
 ], ids=["attribute", "text", "cdata", "processing_instruction", "comment", "document", "doctype"])
-def test_non_element_nodes(session, inline, expression):
+def test_not_supported_nodes(session, inline, expression):
     session.url = inline(PAGE_DATA)
 
     result = execute_script(session, f"return {expression}")
diff --git a/webdriver/tests/classic/execute_script/window.py b/webdriver/tests/classic/execute_script/window.py
new file mode 100644
index 0000000..9ab45d7
--- /dev/null
+++ b/webdriver/tests/classic/execute_script/window.py
@@ -0,0 +1,87 @@
+import pytest
+
+from webdriver.client import WebFrame, WebWindow
+
+from tests.support.asserts import assert_success
+from . import execute_script
+
+
+@pytest.mark.parametrize("expression, expected_type", [
+    ("window.frames[0]", WebFrame),
+    ("window", WebWindow),
+], ids=["frame", "window"])
+def test_web_reference(session, get_test_page, expression, expected_type):
+    session.url = get_test_page()
+
+    result = execute_script(session, f"return {expression}")
+    reference = assert_success(result)
+
+    assert isinstance(reference, expected_type)
+
+    if isinstance(reference, WebWindow):
+        assert reference.id in session.handles
+    else:
+        assert reference.id not in session.handles
+
+
+@pytest.mark.parametrize("expression, expected_type", [
+    ("window.frames[0]", WebFrame),
+    ("window", WebWindow),
+], ids=["frame", "window"])
+def test_web_reference_in_array(session, get_test_page, expression, expected_type):
+    session.url = get_test_page()
+
+    result = execute_script(session, f"return [{expression}]")
+    value = assert_success(result)
+
+    assert isinstance(value[0], expected_type)
+
+    if isinstance(value[0], WebWindow):
+        assert value[0].id in session.handles
+    else:
+        assert value[0].id not in session.handles
+
+
+@pytest.mark.parametrize("expression, expected_type", [
+    ("window.frames[0]", WebFrame),
+    ("window", WebWindow),
+], ids=["frame", "window"])
+def test_web_reference_in_object(session, get_test_page, expression, expected_type):
+    session.url = get_test_page()
+
+    result = execute_script(session, f"""return {{"ref": {expression}}}""")
+    reference = assert_success(result)
+
+    assert isinstance(reference["ref"], expected_type)
+
+    if isinstance(reference["ref"], WebWindow):
+        assert reference["ref"].id in session.handles
+    else:
+        assert reference["ref"].id not in session.handles
+
+
+def test_window_open(session):
+    result = execute_script(session, "window.foo = window.open(); return window.foo;")
+    reference = assert_success(result)
+
+    assert isinstance(reference, WebWindow)
+    assert reference.id in session.handles
+
+
+def test_same_id_after_cross_origin_navigation(session, get_test_page):
+    params = {"pipe": "header(Cross-Origin-Opener-Policy,same-origin)"}
+
+    first_page = get_test_page(parameters=params, protocol="https")
+    second_page = get_test_page(parameters=params, protocol="https", domain="alt")
+
+    session.url = first_page
+
+    result = execute_script(session, "return window")
+    window_before = assert_success(result)
+
+    session.url = second_page
+
+    result = execute_script(session, "return window")
+    window_after = assert_success(result)
+
+    assert window_before == window_after
diff --git a/webdriver/tests/classic/find_element_from_shadow_root/find.py b/webdriver/tests/classic/find_element_from_shadow_root/find.py
index 3f1b64a..c658152 100644
--- a/webdriver/tests/classic/find_element_from_shadow_root/find.py
+++ b/webdriver/tests/classic/find_element_from_shadow_root/find.py
@@ -1,5 +1,5 @@
 import pytest
-from webdriver.client import Element, ShadowRoot
+from webdriver.client import WebElement, ShadowRoot
 from webdriver.transport import Response
 
 from tests.support.asserts import assert_error, assert_same_element, assert_success
@@ -174,7 +174,7 @@
     result = find_element(session, shadow_root.id, using, value)
     value = assert_success(result)
 
-    element = Element.from_json(value, session)
+    element = WebElement.from_json(value, session)
     assert element.text == expected_text
 
 
@@ -243,5 +243,5 @@
     result = find_element(session, nested_shadow_root.id, "css selector", "#linkText")
     value = assert_success(result)
 
-    element = Element.from_json(value, session)
+    element = WebElement.from_json(value, session)
     assert element.text == expected_text
diff --git a/webdriver/tests/classic/find_elements_from_shadow_root/find.py b/webdriver/tests/classic/find_elements_from_shadow_root/find.py
index ffdaa7e..188fff2 100644
--- a/webdriver/tests/classic/find_elements_from_shadow_root/find.py
+++ b/webdriver/tests/classic/find_elements_from_shadow_root/find.py
@@ -1,5 +1,5 @@
 import pytest
-from webdriver.client import Element, ShadowRoot
+from webdriver.client import WebElement, ShadowRoot
 from webdriver.transport import Response
 
 from tests.support.asserts import assert_error, assert_same_element, assert_success
@@ -177,7 +177,7 @@
 
     assert len(value) == 1
 
-    element = Element.from_json(value[0], session)
+    element = WebElement.from_json(value[0], session)
     assert element.text == expected_text
 
 
@@ -256,5 +256,5 @@
 
     assert len(value) == 1
 
-    element = Element.from_json(value[0], session)
+    element = WebElement.from_json(value[0], session)
     assert element.text == expected_text
diff --git a/webdriver/tests/classic/get_computed_label/get.py b/webdriver/tests/classic/get_computed_label/get.py
index 0dc00a4..e023b79 100644
--- a/webdriver/tests/classic/get_computed_label/get.py
+++ b/webdriver/tests/classic/get_computed_label/get.py
@@ -1,6 +1,6 @@
 import pytest
 
-from webdriver import Element
+from webdriver import WebElement
 from webdriver.error import NoSuchAlertException
 
 from tests.support.asserts import assert_error, assert_success
@@ -19,7 +19,7 @@
 
 
 def test_no_such_element_with_invalid_value(session):
-    element = Element(session, "foo")
+    element = WebElement(session, "foo")
 
     result = get_computed_label(session, element.id)
     assert_error(result, "no such element")
diff --git a/webdriver/tests/classic/get_computed_role/get.py b/webdriver/tests/classic/get_computed_role/get.py
index 51b6a8b..1b84896 100644
--- a/webdriver/tests/classic/get_computed_role/get.py
+++ b/webdriver/tests/classic/get_computed_role/get.py
@@ -1,6 +1,6 @@
 import pytest
 
-from webdriver import Element
+from webdriver import WebElement
 from webdriver.error import NoSuchAlertException
 
 from tests.support.asserts import assert_error, assert_success
@@ -19,7 +19,7 @@
 
 
 def test_no_such_element_with_invalid_value(session):
-    element = Element(session, "foo")
+    element = WebElement(session, "foo")
 
     result = get_computed_role(session, element.id)
     assert_error(result, "no such element")
diff --git a/webdriver/tests/classic/get_element_attribute/get.py b/webdriver/tests/classic/get_element_attribute/get.py
index 375f250..0fcfd00 100644
--- a/webdriver/tests/classic/get_element_attribute/get.py
+++ b/webdriver/tests/classic/get_element_attribute/get.py
@@ -1,6 +1,6 @@
 import pytest
 
-from webdriver import Element
+from webdriver import WebElement
 
 from tests.support.asserts import assert_error, assert_success
 
@@ -30,7 +30,7 @@
 
 
 def test_no_such_element_with_invalid_value(session):
-    element = Element(session, "foo")
+    element = WebElement(session, "foo")
 
     response = get_element_attribute(session, element.id, "id")
     assert_error(response, "no such element")
diff --git a/webdriver/tests/classic/get_element_css_value/get.py b/webdriver/tests/classic/get_element_css_value/get.py
index 6f0a8a5..1f6f571 100644
--- a/webdriver/tests/classic/get_element_css_value/get.py
+++ b/webdriver/tests/classic/get_element_css_value/get.py
@@ -1,6 +1,6 @@
 import pytest
 
-from webdriver import Element
+from webdriver import WebElement
 
 from tests.support.asserts import assert_error, assert_success
 
@@ -34,7 +34,7 @@
 
 
 def test_no_such_element_with_invalid_value(session):
-    element = Element(session, "foo")
+    element = WebElement(session, "foo")
 
     response = get_element_css_value(session, element.id, "display")
     assert_error(response, "no such element")
diff --git a/webdriver/tests/classic/get_element_property/get.py b/webdriver/tests/classic/get_element_property/get.py
index bb63481..12d48a3 100644
--- a/webdriver/tests/classic/get_element_property/get.py
+++ b/webdriver/tests/classic/get_element_property/get.py
@@ -1,6 +1,6 @@
 import pytest
 
-from webdriver import Element, Frame, ShadowRoot, Window
+from webdriver import WebElement, WebFrame, ShadowRoot, WebWindow
 
 from tests.support.asserts import assert_error, assert_same_element, assert_success
 
@@ -31,7 +31,7 @@
 
 
 def test_no_such_element_with_invalid_value(session):
-    element = Element(session, "foo")
+    element = WebElement(session, "foo")
 
     response = get_element_property(session, element.id, "id")
     assert_error(response, "no such element")
@@ -165,10 +165,10 @@
 
 
 @pytest.mark.parametrize("js_web_reference,py_web_reference", [
-    ("element", Element),
-    ("frame", Frame),
+    ("element", WebElement),
+    ("frame", WebFrame),
     ("shadowRoot", ShadowRoot),
-    ("window", Window),
+    ("window", WebWindow),
 ])
 def test_web_reference(session, get_test_page, js_web_reference, py_web_reference):
     session.url = get_test_page()
diff --git a/webdriver/tests/classic/get_element_rect/get.py b/webdriver/tests/classic/get_element_rect/get.py
index 942f119..959ccc4 100644
--- a/webdriver/tests/classic/get_element_rect/get.py
+++ b/webdriver/tests/classic/get_element_rect/get.py
@@ -1,6 +1,6 @@
 import pytest
 
-from webdriver import Element
+from webdriver import WebElement
 
 from tests.support.asserts import assert_error, assert_success
 from tests.support.helpers import element_rect
@@ -34,7 +34,7 @@
 
 
 def test_no_such_element_with_invalid_value(session):
-    element = Element(session, "foo")
+    element = WebElement(session, "foo")
 
     response = get_element_rect(session, element.id)
     assert_error(response, "no such element")
diff --git a/webdriver/tests/classic/get_element_shadow_root/get.py b/webdriver/tests/classic/get_element_shadow_root/get.py
index d9adde0..25e68c1 100644
--- a/webdriver/tests/classic/get_element_shadow_root/get.py
+++ b/webdriver/tests/classic/get_element_shadow_root/get.py
@@ -1,6 +1,6 @@
 import pytest
 
-from webdriver import Element
+from webdriver import WebElement
 
 from tests.support.asserts import assert_error, assert_same_element, assert_success
 
@@ -30,7 +30,7 @@
 
 
 def test_no_such_element_with_invalid_value(session):
-    element = Element(session, "foo")
+    element = WebElement(session, "foo")
 
     response = get_shadow_root(session, element.id)
     assert_error(response, "no such element")
diff --git a/webdriver/tests/classic/get_element_tag_name/get.py b/webdriver/tests/classic/get_element_tag_name/get.py
index 3bb03d7..d8bb3ac 100644
--- a/webdriver/tests/classic/get_element_tag_name/get.py
+++ b/webdriver/tests/classic/get_element_tag_name/get.py
@@ -1,6 +1,6 @@
 import pytest
 
-from webdriver import Element
+from webdriver import WebElement
 
 from tests.support.asserts import assert_error, assert_success
 
@@ -30,7 +30,7 @@
 
 
 def test_no_such_element_with_invalid_value(session):
-    element = Element(session, "foo")
+    element = WebElement(session, "foo")
 
     response = get_element_tag_name(session, element.id)
     assert_error(response, "no such element")
diff --git a/webdriver/tests/classic/get_element_text/get.py b/webdriver/tests/classic/get_element_text/get.py
index e8d559c..2a2363c 100644
--- a/webdriver/tests/classic/get_element_text/get.py
+++ b/webdriver/tests/classic/get_element_text/get.py
@@ -1,6 +1,6 @@
 import pytest
 
-from webdriver import Element
+from webdriver import WebElement
 
 from tests.support.asserts import assert_error, assert_success
 
@@ -30,7 +30,7 @@
 
 
 def test_no_such_element_with_invalid_value(session):
-    element = Element(session, "foo")
+    element = WebElement(session, "foo")
 
     response = get_element_text(session, element.id)
     assert_error(response, "no such element")
diff --git a/webdriver/tests/classic/is_element_enabled/enabled.py b/webdriver/tests/classic/is_element_enabled/enabled.py
index fccff38..24fc85f 100644
--- a/webdriver/tests/classic/is_element_enabled/enabled.py
+++ b/webdriver/tests/classic/is_element_enabled/enabled.py
@@ -1,6 +1,6 @@
 import pytest
 
-from webdriver import Element
+from webdriver import WebElement
 
 from tests.support.asserts import assert_error, assert_success
 
@@ -33,7 +33,7 @@
 
 
 def test_no_such_element_with_invalid_value(session):
-    element = Element(session, "foo")
+    element = WebElement(session, "foo")
 
     response = is_element_enabled(session, element.id)
     assert_error(response, "no such element")
diff --git a/webdriver/tests/classic/is_element_selected/selected.py b/webdriver/tests/classic/is_element_selected/selected.py
index 1fb5b9c..bf650de 100644
--- a/webdriver/tests/classic/is_element_selected/selected.py
+++ b/webdriver/tests/classic/is_element_selected/selected.py
@@ -1,6 +1,6 @@
 import pytest
 
-from webdriver import Element
+from webdriver import WebElement
 
 from tests.support.asserts import assert_error, assert_success
 
@@ -49,7 +49,7 @@
 
 
 def test_no_such_element_with_invalid_value(session):
-    element = Element(session, "foo")
+    element = WebElement(session, "foo")
 
     response = is_element_selected(session, element.id)
     assert_error(response, "no such element")
diff --git a/webdriver/tests/classic/take_element_screenshot/screenshot.py b/webdriver/tests/classic/take_element_screenshot/screenshot.py
index ea4cc29..fdc0d65 100644
--- a/webdriver/tests/classic/take_element_screenshot/screenshot.py
+++ b/webdriver/tests/classic/take_element_screenshot/screenshot.py
@@ -1,6 +1,6 @@
 import pytest
 
-from webdriver import Element
+from webdriver import WebElement
 
 from tests.support.asserts import assert_error, assert_success
 from tests.support.image import png_dimensions
@@ -33,7 +33,7 @@
 
 
 def test_no_such_element_with_invalid_value(session):
-    element = Element(session, "foo")
+    element = WebElement(session, "foo")
 
     response = take_element_screenshot(session, element.id)
     assert_error(response, "no such element")
diff --git a/webdriver/tests/support/asserts.py b/webdriver/tests/support/asserts.py
index 04bd199..9d31ff7 100644
--- a/webdriver/tests/support/asserts.py
+++ b/webdriver/tests/support/asserts.py
@@ -1,7 +1,7 @@
 import imghdr
 from base64 import decodebytes
 
-from webdriver import Element, NoSuchAlertException, WebDriverException
+from webdriver import NoSuchAlertException, WebDriverException, WebElement
 
 
 # WebDriver specification ID: dfn-error-response-data
@@ -148,17 +148,17 @@
 def assert_same_element(session, a, b):
     """Verify that two element references describe the same element."""
     if isinstance(a, dict):
-        assert Element.identifier in a, "Actual value does not describe an element"
-        a_id = a[Element.identifier]
-    elif isinstance(a, Element):
+        assert WebElement.identifier in a, "Actual value does not describe an element"
+        a_id = a[WebElement.identifier]
+    elif isinstance(a, WebElement):
         a_id = a.id
     else:
         raise AssertionError("Actual value is not a dictionary or web element")
 
     if isinstance(b, dict):
-        assert Element.identifier in b, "Expected value does not describe an element"
-        b_id = b[Element.identifier]
-    elif isinstance(b, Element):
+        assert WebElement.identifier in b, "Expected value does not describe an element"
+        b_id = b[WebElement.identifier]
+    elif isinstance(b, WebElement):
         b_id = b.id
     else:
         raise AssertionError("Expected value is not a dictionary or web element")