From 0b1eaf92ab8741548793dadf4dda7280a422630d Mon Sep 17 00:00:00 2001
From: Bjoern Esswein <4-Bjoern@users.noreply.git.esswe.in>
Date: Sun, 10 Mar 2024 18:35:03 +0100
Subject: [PATCH] WIP: implement DomNodeWrapper

---
 Runtime/ChromeDevtools/Browser.cs             |  38 +-
 Runtime/ChromeDevtools/BrowserTab.cs          |  48 ++-
 .../ChromeDevtools/DevtoolsProtocolHandler.cs |  50 ++-
 Runtime/ChromeDevtools/DomNodeWrapper.cs      | 376 ++++++++++++++++++
 Runtime/ChromeDevtools/DomNodeWrapper.cs.meta |  11 +
 Runtime/ChromeDevtools/Protocol/DOM/DOM.cs    | 210 +++++++---
 .../Protocol/DOM/DOM_api.json.meta            |   7 +
 .../ChromeDevtools/Protocol/Input/Input.cs    |   1 +
 .../Protocol/JsonInterleavedArrayConverter.cs |  39 ++
 .../JsonInterleavedArrayConverter.cs.meta     |  11 +
 Runtime/WebViewComponent.cs                   |  33 +-
 WebViewComponent.prefab                       |   9 +-
 12 files changed, 736 insertions(+), 97 deletions(-)
 create mode 100644 Runtime/ChromeDevtools/DomNodeWrapper.cs
 create mode 100644 Runtime/ChromeDevtools/DomNodeWrapper.cs.meta
 create mode 100644 Runtime/ChromeDevtools/Protocol/DOM/DOM_api.json.meta
 create mode 100644 Runtime/ChromeDevtools/Protocol/JsonInterleavedArrayConverter.cs
 create mode 100644 Runtime/ChromeDevtools/Protocol/JsonInterleavedArrayConverter.cs.meta

diff --git a/Runtime/ChromeDevtools/Browser.cs b/Runtime/ChromeDevtools/Browser.cs
index ca1bbaf..ec6755b 100644
--- a/Runtime/ChromeDevtools/Browser.cs
+++ b/Runtime/ChromeDevtools/Browser.cs
@@ -14,7 +14,7 @@ namespace bessw.Unity.WebView.ChromeDevTools
     {
         /* singleton */
         private static Browser instance;
-        private static Process browserProcess;
+        private Process browserProcess;
 
         /* browser settings */
         public static string browserExecutablePath = "chrome";
@@ -37,7 +37,7 @@ namespace bessw.Unity.WebView.ChromeDevTools
          */
         public static Browser getInstance()
         {
-            if (instance == null)
+            if (instance == null || instance.browserProcess.HasExited)
             {
                 instance = new Browser();
                 instance.launchBrowser();
@@ -64,31 +64,45 @@ namespace bessw.Unity.WebView.ChromeDevTools
         private  void launchBrowser()
         {
             // allow only one instance of chrome
-            if (Browser.browserProcess == null || Browser.browserProcess.HasExited)
+            if (browserProcess == null || browserProcess.HasExited)
             {
-                Browser.browserProcess = new Process();
-                Browser.browserProcess.StartInfo.FileName = browserExecutablePath;
-                Browser.browserProcess.StartInfo.Arguments = $"--user-data-dir={Path.Join(Application.temporaryCachePath, "BrowserView")} --remote-debugging-port={debugPort} --remote-allow-origins=http://localhost:{debugPort} --hide-crash-restore-bubble";
+                browserProcess = new Process();
+                browserProcess.StartInfo.FileName = browserExecutablePath;
+                browserProcess.StartInfo.Arguments = String.Join(" ", new []{
+                    $"--user-data-dir={Path.Join(Application.temporaryCachePath, "BrowserView")}",
+                    $"--remote-debugging-port={debugPort}",
+                    $"--remote-allow-origins=http://localhost:{debugPort}",
+                    "--hide-crash-restore-bubble",
+                    "--disable-first-run-ui",
+                    "--no-first-run"
+                });
 
                 // set headlessBrowser to false to see the browser window
                 if (headless)
                 {
-                    Browser.browserProcess.StartInfo.Arguments = string.Concat(Browser.browserProcess.StartInfo.Arguments, " --headless=new");
+                    browserProcess.StartInfo.Arguments = string.Concat(browserProcess.StartInfo.Arguments, " --headless=new");
                 }
                 else
                 {
-                    Browser.browserProcess.StartInfo.WindowStyle = ProcessWindowStyle.Minimized;
+                    browserProcess.StartInfo.WindowStyle = ProcessWindowStyle.Minimized;
                 }
 
-                Browser.browserProcess.Start();
-                UnityEngine.Debug.Log($"launched '{Browser.browserProcess.StartInfo.FileName} {Browser.browserProcess.StartInfo.Arguments}'");
+                // register an error handler
+                browserProcess.ErrorDataReceived += (sender, e) => UnityEngine.Debug.LogError($"Browser Error: {e.Data} ExitCode: {browserProcess.ExitCode}");
+                browserProcess.Exited += (sender, e) => UnityEngine.Debug.LogError($"Browser Exited, ExitCode: {browserProcess.ExitCode}");
+
+                browserProcess.Start();
+                if (browserProcess.HasExited) {
+                    UnityEngine.Debug.LogError("Failed to start browser");
+                }
+                UnityEngine.Debug.Log($"launched '{browserProcess.StartInfo.FileName} {browserProcess.StartInfo.Arguments}'");
             }
         }
 
         /**
          * send web request to the devTools API
          */
-        private IEnumerator DevToolsApiRequest(bool isPUT, string apiAddress, System.Action<string> callback)
+        private IEnumerator DevToolsApiRequest(bool isPUT, string apiAddress, Action<string> callback)
         {
             UnityEngine.Debug.Log($"DevTools api Request: {apiAddress}");
             UnityWebRequest webRequest;
@@ -114,7 +128,7 @@ namespace bessw.Unity.WebView.ChromeDevTools
             }
         }
 
-        public IEnumerator OpenNewTab(string targetUrl, System.Action<BrowserTab> callback)
+        public IEnumerator OpenNewTab(string targetUrl, Action<BrowserTab> callback)
         {
             yield return DevToolsApiRequest(true, $"/json/new?{targetUrl}", (response) =>
             {
diff --git a/Runtime/ChromeDevtools/BrowserTab.cs b/Runtime/ChromeDevtools/BrowserTab.cs
index 74b8ce0..0f4e23f 100644
--- a/Runtime/ChromeDevtools/BrowserTab.cs
+++ b/Runtime/ChromeDevtools/BrowserTab.cs
@@ -3,6 +3,7 @@ using bessw.Unity.WebView.ChromeDevTools.Protocol.Page;
 using bessw.Unity.WebView.ChromeDevTools.Protocol.Target;
 using System;
 using System.Collections;
+using System.Collections.Generic;
 using UnityEngine;
 using UnityEngine.EventSystems;
 
@@ -12,19 +13,31 @@ namespace bessw.Unity.WebView.ChromeDevTools
     public class BrowserTab
     {
         private PageTargetInfo pageTarget;
-        private DevtoolsProtocolHandler devtools;
+        public DevtoolsProtocolHandler devtools;
 
         /// <summary>
         /// width and height of the brower device
         /// </summary>
         public Vector2Int size { get; private set; }
 
+        public Dictionary<int, DomNodeWrapper> domNodes = new Dictionary<int, DomNodeWrapper>();
+
         public BrowserTab(PageTargetInfo pageTarget)
         {
             this.pageTarget = pageTarget;
             Debug.Log($"tab WebSocket: '{pageTarget.WebSocketDebuggerUrl}'");
 
             this.devtools = new DevtoolsProtocolHandler(new DevtoolsWebsocket(pageTarget.WebSocketDebuggerUrl));
+
+            // register DOM event handlers
+            this.devtools.DOM_AttributeModifiedEventHandler += (attributeModifiedEvent) => DomNodeWrapper.onAttributeModified(this, attributeModifiedEvent);
+            this.devtools.DOM_AttributeRemovedEventHandler += (attributeRemovedEvent) => DomNodeWrapper.onAttributeRemoved(this, attributeRemovedEvent);
+            this.devtools.DOM_CharacterDataModifiedEventHandler += (characterDataModifiedEvent) => DomNodeWrapper.onCharacterDataModified(this, characterDataModifiedEvent);
+            this.devtools.DOM_ChildNodeCountUpdatedEventHandler += (childNodeCountUpdatedEvent) => DomNodeWrapper.onChildNodeCountUpdated(this, childNodeCountUpdatedEvent);
+            this.devtools.DOM_ChildNodeInsertedEventHandler += (childNodeInsertedEvent) => DomNodeWrapper.onChildNodeInserted(this, childNodeInsertedEvent);
+            this.devtools.DOM_ChildNodeRemovedEventHandler += (childNodeRemovedEvent) => DomNodeWrapper.onChildNodeRemoved(this, childNodeRemovedEvent);
+            this.devtools.DOM_DocumentUpdatedEventHandler += (documentUpdatedEvent) => DomNodeWrapper.onDocumentUpdated(this, documentUpdatedEvent);
+            this.devtools.DOM_SetChildNodesEventHandler += (setChildNodesEvent) => DomNodeWrapper.onSetChildNodes(this, setChildNodesEvent);
         }
 
         public IEnumerator Update()
@@ -34,10 +47,14 @@ namespace bessw.Unity.WebView.ChromeDevTools
 
         public IEnumerator CreateScreenshot(double width, double height, Action<Texture2D> callback)
         {
-            var screenshotCommand = new captureScreenshot();
-            screenshotCommand.Clip = new Protocol.Types.Viewport();
-            screenshotCommand.Clip.Width = width;
-            screenshotCommand.Clip.Height = height;
+            var screenshotCommand = new captureScreenshot
+            {
+                Clip = new Protocol.Types.Viewport
+                {
+                    Width = width,
+                    Height = height
+                }
+            };
 
             return devtools.SendCommand(screenshotCommand,
                 (response) => {
@@ -59,8 +76,10 @@ namespace bessw.Unity.WebView.ChromeDevTools
             devtools.screencastFrameEventHandler += (screencastFrameEvent frameEvent) =>
             {
                 // send an ack for this frame
-                var frameAck = new screencastFrameAck();
-                frameAck.sessionId = frameEvent.sessionId;
+                var frameAck = new screencastFrameAck
+                {
+                    sessionId = frameEvent.sessionId
+                };
                 _ = devtools.SendCommandAsync(frameAck);
 
                 size = new Vector2Int(frameEvent.metadata.deviceWidth, frameEvent.metadata.deviceHeight);
@@ -76,10 +95,12 @@ namespace bessw.Unity.WebView.ChromeDevTools
                 callback(frameTexture);
             };
 
-            var startScreencast = new startScreencast();
-            startScreencast.MaxWidth = maxWidth;
-            startScreencast.maxHeight = maxHeight;
-            startScreencast.everyNthFrame = 1;
+            var startScreencast = new startScreencast
+            {
+                MaxWidth = maxWidth,
+                maxHeight = maxHeight,
+                everyNthFrame = 1
+            };
 
             return devtools.SendCommand(startScreencast);
         }
@@ -139,6 +160,11 @@ namespace bessw.Unity.WebView.ChromeDevTools
             _ = devtools.SendCommandAsync(new cancelDragging());
         }
 
+        public IEnumerator GetDocument(Action<DomNodeWrapper> callback)
+        {
+            return DomNodeWrapper.getDocument(this, callback);
+        }
+
         /**
          * close this tab
          */
diff --git a/Runtime/ChromeDevtools/DevtoolsProtocolHandler.cs b/Runtime/ChromeDevtools/DevtoolsProtocolHandler.cs
index aed3916..fe7e710 100644
--- a/Runtime/ChromeDevtools/DevtoolsProtocolHandler.cs
+++ b/Runtime/ChromeDevtools/DevtoolsProtocolHandler.cs
@@ -1,4 +1,5 @@
 using bessw.Unity.WebView.ChromeDevTools.Protocol;
+using bessw.Unity.WebView.ChromeDevTools.Protocol.DOM;
 using bessw.Unity.WebView.ChromeDevTools.Protocol.Page;
 using Newtonsoft.Json;
 using Newtonsoft.Json.Linq;
@@ -25,6 +26,19 @@ namespace bessw.Unity.WebView.ChromeDevTools
         public event Action<screencastFrameEvent> screencastFrameEventHandler;
         public event Action<screencastVisibilityChangedEvent> screencastVisibilityChangedEventHandler;
 
+        #region DOM events
+        public event Action<attributeModifiedEvent> DOM_AttributeModifiedEventHandler;
+        public event Action<attributeRemovedEvent> DOM_AttributeRemovedEventHandler;
+        public event Action<characterDataModifiedEvent> DOM_CharacterDataModifiedEventHandler;
+        public event Action<childNodeCountUpdatedEvent> DOM_ChildNodeCountUpdatedEventHandler;
+        public event Action<childNodeInsertedEvent> DOM_ChildNodeInsertedEventHandler;
+        public event Action<childNodeRemovedEvent> DOM_ChildNodeRemovedEventHandler;
+        public event Action<documentUpdatedEvent> DOM_DocumentUpdatedEventHandler;
+        public event Action<setChildNodesEvent> DOM_SetChildNodesEventHandler;
+        #endregion DOM events
+
+        public event Action<DevtoolsEventWrapper> unknownEventHandler;
+
         public DevtoolsProtocolHandler(IDevtoolsConnection devtools)
         {
             this.devtools = devtools;
@@ -113,8 +127,42 @@ namespace bessw.Unity.WebView.ChromeDevTools
                 case "Page.screencastVisibilityChanged":
                     screencastVisibilityChangedEventHandler?.Invoke( ev.Params.ToObject<screencastVisibilityChangedEvent>(Browser.devtoolsSerializer) );
                     break;
+                
+                // switch cases that invoke the event handlers for the DOM events
+                #region DOM events
+                case "DOM.attributeModified":
+                    DOM_AttributeModifiedEventHandler?.Invoke( ev.Params.ToObject<attributeModifiedEvent>(Browser.devtoolsSerializer) );
+                    break;
+                case "DOM.attributeRemoved":
+                    DOM_AttributeRemovedEventHandler?.Invoke( ev.Params.ToObject<attributeRemovedEvent>(Browser.devtoolsSerializer) );
+                    break;
+                case "DOM.characterDataModified":
+                    DOM_CharacterDataModifiedEventHandler?.Invoke( ev.Params.ToObject<characterDataModifiedEvent>(Browser.devtoolsSerializer) );
+                    break;
+                case "DOM.childNodeCountUpdated":
+                    DOM_ChildNodeCountUpdatedEventHandler?.Invoke( ev.Params.ToObject<childNodeCountUpdatedEvent>(Browser.devtoolsSerializer) );
+                    break;
+                case "DOM.childNodeInserted":
+                    DOM_ChildNodeInsertedEventHandler?.Invoke( ev.Params.ToObject<childNodeInsertedEvent>(Browser.devtoolsSerializer) );
+                    break;
+                case "DOM.childNodeRemoved":
+                    DOM_ChildNodeRemovedEventHandler?.Invoke( ev.Params.ToObject<childNodeRemovedEvent>(Browser.devtoolsSerializer) );
+                    break;
+                case "DOM.documentUpdated":
+                    DOM_DocumentUpdatedEventHandler?.Invoke( ev.Params.ToObject<documentUpdatedEvent>(Browser.devtoolsSerializer) );
+                    break;
+                case "DOM.setChildNodes":
+                    DOM_SetChildNodesEventHandler?.Invoke( ev.Params.ToObject<setChildNodesEvent>(Browser.devtoolsSerializer) );
+                    break;
+                #endregion DOM events
+
                 default:
-                    throw new UnexpectedMessageException($"Event of type '{ev}' is not implemented");
+                    if (unknownEventHandler != null) {
+                        unknownEventHandler.Invoke(ev);
+                        break;
+                    } else {
+                        throw new UnexpectedMessageException($"Event of type '{ev}' is not implemented");
+                    }
             }
         }
 
diff --git a/Runtime/ChromeDevtools/DomNodeWrapper.cs b/Runtime/ChromeDevtools/DomNodeWrapper.cs
new file mode 100644
index 0000000..5f97af3
--- /dev/null
+++ b/Runtime/ChromeDevtools/DomNodeWrapper.cs
@@ -0,0 +1,376 @@
+using bessw.Unity.WebView.ChromeDevTools.Protocol.DOM;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+#nullable enable
+namespace bessw.Unity.WebView.ChromeDevTools
+{
+    public class DomNodeWrapper
+    {
+        /// <summary>
+        /// Node identifier that is passed into the rest of the DOM messages as the `nodeId`. Backend
+        /// will only push node with given `id` once. It is aware of all requested nodes and will only
+        /// fire DOM events for nodes known to the client.
+        /// </summary>
+        public int NodeId
+        {
+            get => Node.nodeId;
+            protected set => Node.nodeId = value;
+        }
+        /// <summary>
+        /// The id of the parent node if any.
+        /// </summary>
+        public int? ParentId
+        {
+            get => Node.parentId;
+            protected set => Node.parentId = value;
+        }
+
+        /// <summary>
+        /// The `Node`object returend by the browser
+        /// </summary>
+        /// <remarks>May be null if it has not jet been requested from the browser</remarks>
+        public Node Node { get; protected set; }
+
+
+        /// <summary>
+        /// Reference to the browser tab that this dom node belongs to
+        /// </summary>
+        private BrowserTab tab;
+
+        /// <summary>
+        /// private constructor, create instances with the static method <see cref="getDocument"/>,
+        /// or by calling instance methods on a <see cref="DomNodeWrapper"/> to get its child nodes.
+        /// </summary>
+        /// <param name="tab"></param>
+        private DomNodeWrapper(BrowserTab tab, Node node)
+        {
+            this.tab = tab;
+            this.Node = node;
+        }
+
+        private static DomNodeWrapper createOrUpdateNode(BrowserTab tab, int nodeId, int? parentId = null, int? backendNodeId = null, Node? node = null)
+        {
+            if (tab.domNodes.ContainsKey(nodeId))
+            {
+                var domNode = tab.domNodes[nodeId];
+                if (node != null) domNode.Node = node;
+                if (parentId != null) domNode.ParentId = parentId;
+                if (backendNodeId != null) domNode.Node.backendNodeId = (int)backendNodeId;
+                return domNode;
+            }
+            else
+            {
+                var domNode = new DomNodeWrapper(tab, node ?? new Node()
+                {
+                    nodeId = nodeId,
+                    parentId = parentId
+                });
+                tab.domNodes[nodeId] = domNode;
+                return domNode;
+            }
+        }
+
+        #region Event Handlers
+
+        public static void onAttributeModified(BrowserTab tab, attributeModifiedEvent eventData)
+        {
+            var domNode = createOrUpdateNode(tab, eventData.nodeId);
+            if (domNode.Node != null)
+            {
+                domNode.Node.attributes[eventData.name] = eventData.value;
+            } else {
+                throw new InvalidOperationException("AttributeModified event was fired, the node info has not yet been recieved");
+            }
+        }
+
+        public static void onAttributeRemoved(BrowserTab tab, attributeRemovedEvent eventData)
+        {
+            var domNode = createOrUpdateNode(tab, eventData.nodeId);
+            if (domNode.Node != null)
+            {
+                domNode.Node.attributes.Remove(eventData.name);
+            } else {
+                throw new InvalidOperationException("AttributeRemoved event was fired, the node info has not yet been recieved");
+            }
+        }
+
+        public static void onCharacterDataModified(BrowserTab tab, characterDataModifiedEvent eventData)
+        {
+            var domNode = createOrUpdateNode(tab, eventData.nodeId);
+            if (domNode.Node != null)
+            {
+                domNode.Node.nodeValue = eventData.characterData;
+            } else {
+                throw new InvalidOperationException("CharacterDataModified event was fired, the node info has not yet been recieved");
+            }
+        }
+
+        public static void onChildNodeCountUpdated(BrowserTab tab, childNodeCountUpdatedEvent eventData)
+        {
+            var domNode = createOrUpdateNode(tab, eventData.nodeId);
+            if (domNode.Node != null)
+            {
+                domNode.Node.childNodeCount = eventData.childNodeCount;
+            } else {
+                throw new InvalidOperationException("ChildNodeCountUpdated event was fired, the node info has not yet been recieved");
+            }
+        }
+
+        public static void onChildNodeInserted(BrowserTab tab, childNodeInsertedEvent eventData)
+        {
+            var parentNode = createOrUpdateNode(tab, eventData.parentNodeId);
+            var childNode = createOrUpdateNode(tab, eventData.node.nodeId, eventData.parentNodeId, eventData.node.backendNodeId, eventData.node);
+            if (parentNode.Node != null && childNode.Node != null)
+            {
+                parentNode.Node.children.Insert(eventData.previousNodeId, childNode.Node);
+            } else {
+                throw new InvalidOperationException("ChildNodeInserted event was fired, the node info has not yet been recieved");
+            }
+        }
+
+        public static void onChildNodeRemoved(BrowserTab tab, childNodeRemovedEvent eventData)
+        {
+            if (
+                tab.domNodes.TryGetValue(eventData.parentNodeId, out var parentNode) &&
+                tab.domNodes.TryGetValue(eventData.nodeId, out var childNode) &&
+                parentNode.Node != null &&
+                childNode.Node != null)
+            {
+                parentNode.Node.children.Remove(childNode.Node);
+            } else {
+                //throw new InvalidOperationException("ChildNodeRemoved event was fired, the node info has not yet been recieved");
+            }
+        }
+
+        public static void onDocumentUpdated(BrowserTab tab, documentUpdatedEvent eventData)
+        {
+            // clear the domNodes dictionary
+            tab.domNodes.Clear();
+        }
+
+        public static void onSetChildNodes(BrowserTab tab, setChildNodesEvent eventData)
+        {
+            var parentNode = createOrUpdateNode(tab, eventData.parentId);
+            if (parentNode.Node != null)
+            {
+                foreach (var node in eventData.nodes)
+                {
+                    createOrUpdateNode(tab, node.nodeId, eventData.parentId, node.backendNodeId, node);
+                }
+                parentNode.Node.children = new(eventData.nodes);
+            } else {
+                throw new InvalidOperationException("SetChildNodes event was fired, the node info has not yet been recieved");
+            }
+        }
+
+        #endregion Event Handlers
+
+        #region Commands
+
+        /// <summary>
+        /// Enables DOM agent for the given page.
+        /// </summary>
+        public static IEnumerator enable(BrowserTab tab)
+        {
+            var enableCommand = new enable();
+            return tab.devtools.SendCommand(enableCommand);
+        }
+
+        /// <summary>
+        /// Disables DOM agent for the given page.
+        /// </summary>
+        public static IEnumerator disable(BrowserTab tab)
+        {
+            return tab.devtools.SendCommand( new disable() );
+        }
+
+        /// <summary>
+        /// Get the root node of the document, the root node is returned as an argument to the callback.
+        /// </summary>
+        /// <param name="tab"></param>
+        /// <param name="callback">The callback recieves the root node as argument</param>
+        /// <returns><see cref="DomNodeWrapper"/></returns>
+        public static IEnumerator getDocument(BrowserTab tab, Action<DomNodeWrapper> callback)
+        {
+            var domCommand = new getDocument();
+            return tab.devtools.SendCommand(domCommand, (response) =>
+            {
+                Node documentRoot = ((getDocumentCommandResponse)response).root;
+
+                var domNode = createOrUpdateNode(tab,
+                    documentRoot.nodeId,
+                    documentRoot.parentId,
+                    documentRoot.backendNodeId,
+                    documentRoot
+                );
+
+                callback(domNode);
+            });
+        }
+
+        /// <summary>
+        /// Describes the node, does not require domain to be enabled. Does not start tracking any
+        /// objects, can be used for automation.
+        /// </summary>
+        /// <param name="callback">The callback recieves the node as argument</param>
+        public IEnumerator describeNode(Action<Node> callback, int? depth = null, bool? pierce = null)
+        {
+            var describeNodeCommand = new describeNode
+            {
+                nodeId = NodeId,
+                depth = depth,
+                pierce = pierce
+            };
+            return tab.devtools.SendCommand(describeNodeCommand, (response) =>
+            {
+                callback(((describeNodeCommandResponse)response).node);
+            });
+        }
+
+        /// <summary>
+        /// Focus this node.
+        /// </summary>
+        public IEnumerator focus()
+        {
+            var focusCommand = new focus
+            {
+                nodeId = NodeId
+            };
+            return tab.devtools.SendCommand(focusCommand);
+        }
+
+        /// <summary>
+        /// Gets the nodes Attributes
+        /// </summary>
+        public IEnumerator getAttributes(Action<Dictionary<string, string>> callback)
+        {
+            var attributesCommand = new getAttributes
+            {
+                nodeId = NodeId
+            };
+            return tab.devtools.SendCommand(attributesCommand, (response) =>
+            {
+                var attibutes = ((getAttributesCommandResponse)response).attributes;
+                tab.domNodes[NodeId].Node.attributes = attibutes;
+
+                callback(attibutes);
+            });
+        }
+
+        public IEnumerator getBoxModel(Action<BoxModel> callback)
+        {
+            var boxModelCommand = new getBoxModel
+            {
+                nodeId = NodeId
+            };
+            return tab.devtools.SendCommand(boxModelCommand, (response) =>
+            {
+                callback(((getBoxModelCommandResponse) response).model);
+            });
+        }
+
+        public IEnumerator getNodeForLocation(int x, int y, Action<DomNodeWrapper> callback, Action<string>? errorCallback = null)
+        {
+            var nodeForLocationCommand = new getNodeForLocation
+            {
+                x = x,
+                y = y
+            };
+            return tab.devtools.SendCommand(nodeForLocationCommand, (response) =>
+            {
+                var responseNodeId = ((getNodeForLocationCommandResponse) response).nodeId;
+                if (responseNodeId != null) {
+                    var domNode = createOrUpdateNode(tab, (int)responseNodeId);
+                    callback(domNode);
+                } else {
+                    errorCallback?.Invoke("nodeId was not provided, is the dom domain enabled?");
+                }
+            });
+        }
+
+        /// <summary>
+        /// Returns node's HTML markup.
+        /// </summary>
+        public IEnumerator getOuterHtml(Action<string> callback)
+        {
+            var outerHtmlCommand = new getOuterHTML
+            {
+                nodeId = NodeId
+            };
+            return tab.devtools.SendCommand(outerHtmlCommand, (response) =>
+            {
+                callback(((getOuterHTMLCommandResponse) response).outerHTML);
+            });
+        }
+
+        /// <summary>
+        /// Moves node into an other container node
+        /// </summary>
+        public IEnumerator moveTo(DomNodeWrapper targetNode, DomNodeWrapper? insertBeforeNode = null)
+        {
+            var moveToCommand = new moveTo
+            {
+                nodeId = NodeId,
+                targetNodeId = targetNode.NodeId,
+                insertBeforeNodeId = insertBeforeNode?.NodeId
+            };
+            return tab.devtools.SendCommand(moveToCommand, (response) =>
+            {
+                // update the new node id of the moved node
+                tab.domNodes.Remove(NodeId);
+                NodeId = ((moveToCommandResponse) response).nodeId;
+                tab.domNodes[NodeId] = this;
+            });
+        }
+
+        /// <summary>
+        /// search a child node by querySelector
+        /// </summary>
+        public IEnumerator querySelector(string selector, Action<DomNodeWrapper> callback)
+        {
+            var querySelectorCommand = new querySelector
+            {
+                nodeId = NodeId,
+                selector = selector
+            };
+            return tab.devtools.SendCommand(querySelectorCommand, (response) =>
+            {
+                var domNode = createOrUpdateNode(tab, ((querySelectorCommandResponse) response).nodeId);
+
+                callback(domNode);
+            });
+        }
+
+        /// <summary>
+        /// search all child nodes matching the querySelector
+        /// </summary>
+        public IEnumerator querySelectorAll(string selector, Action<DomNodeWrapper[]> callback)
+        {
+            var querySelectorAllCommand = new querySelectorAll
+            {
+                nodeId = NodeId,
+                selector = selector
+            };
+            return tab.devtools.SendCommand(querySelectorAllCommand, (response) =>
+            {
+                var nodeIds = ((querySelectorAllCommandResponse) response).nodeIds;
+                var domNodes = new DomNodeWrapper[nodeIds.Length];
+                for (int i = 0; i < nodeIds.Length; i++)
+                {
+                    domNodes[i] = createOrUpdateNode(tab, nodeIds[i]);
+                }
+                callback(domNodes);
+            });
+        }
+
+        //TODO: implement the other commands of the DOM domain
+
+        #endregion Commands
+
+
+    }
+}
diff --git a/Runtime/ChromeDevtools/DomNodeWrapper.cs.meta b/Runtime/ChromeDevtools/DomNodeWrapper.cs.meta
new file mode 100644
index 0000000..8b48774
--- /dev/null
+++ b/Runtime/ChromeDevtools/DomNodeWrapper.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 9a49fdc400bfe5d46b76c0c1e154940e
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Runtime/ChromeDevtools/Protocol/DOM/DOM.cs b/Runtime/ChromeDevtools/Protocol/DOM/DOM.cs
index ac13b57..08c6e70 100644
--- a/Runtime/ChromeDevtools/Protocol/DOM/DOM.cs
+++ b/Runtime/ChromeDevtools/Protocol/DOM/DOM.cs
@@ -1,3 +1,9 @@
+using System.Collections.Generic;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+using Newtonsoft.Json.Serialization;
+
+#nullable enable annotations
 namespace bessw.Unity.WebView.ChromeDevTools.Protocol.DOM
 {
     #region commands
@@ -5,30 +11,35 @@ namespace bessw.Unity.WebView.ChromeDevTools.Protocol.DOM
     /// Describes node given its id, does not require domain to be enabled. Does not start tracking any
     /// objects, can be used for automation.
     /// </summary>
-    [CommandResponseAttribute(typeof(describeNodeCommandResponse))]
-    public class describeNode : IDevtoolsCommand
+    [CommandResponse(typeof(describeNodeCommandResponse))]
+    public class describeNode : IDevtoolsCommandWithResponse
     {
         /// <summary>
         /// Identifier of the node.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public int? nodeId { get; set; }
         /// <summary>
         /// Identifier of the backend node.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public int? backendNodeId { get; set; }
         /// <summary>
         /// JavaScript object id of the node wrapper.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public string? objectId { get; set; }
         /// <summary>
         /// The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the
         /// entire subtree or provide an integer larger than 0.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public int? depth { get; set; }
         /// <summary>
         /// Whether or not iframes and shadow roots should be traversed when returning the subtree
         /// (default is false).
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public bool? pierce { get; set; }
     }
 
@@ -57,6 +68,7 @@ namespace bessw.Unity.WebView.ChromeDevTools.Protocol.DOM
         /// Whether to include whitespaces in the children array of returned Nodes.
         /// </summary>
         /// <remarks>experimental</remarks>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public string? includeWhitespace { get; set; }
     }
 
@@ -68,22 +80,25 @@ namespace bessw.Unity.WebView.ChromeDevTools.Protocol.DOM
         /// <summary>
         /// Identifier of the node.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public int? nodeId { get; set; }
         /// <summary>
         /// Identifier of the backend node.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public int? backendNodeId { get; set; }
         /// <summary>
         /// JavaScript object id of the node wrapper.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public string? objectId { get; set; }
     }
 
     /// <summary>
     /// Returns attributes for the specified node.
     /// </summary>
-    [CommandResponseAttribute(typeof(getAttributesCommandResponse))]
-    public class getAttributes : IDevtoolsCommand
+    [CommandResponse(typeof(getAttributesCommandResponse))]
+    public class getAttributes : IDevtoolsCommandWithResponse
     {
         /// <summary>
         /// Id of the node to retrieve attibutes for.
@@ -99,26 +114,30 @@ namespace bessw.Unity.WebView.ChromeDevTools.Protocol.DOM
         /// <summary>
         /// An interleaved array of node attribute names and values.
         /// </summary>
-        public string[] attributes { get; set; }
+        [JsonConverter(typeof(JsonInterleavedArrayConverter<string, string>))]
+        public Dictionary<string, string> attributes { get; set; }
     }
 
     /// <summary>
     /// Returns boxes for the currently selected nodes.
     /// </summary>
-    [CommandResponseAttribute(typeof(getBoxModelCommandResponse))]
-    public class getBoxModel : IDevtoolsCommand
+    [CommandResponse(typeof(getBoxModelCommandResponse))]
+    public class getBoxModel : IDevtoolsCommandWithResponse
     {
         /// <summary>
         /// Identifier of the node.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public int? nodeId { get; set; }
         /// <summary>
         /// Identifier of the backend node.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public int? backendNodeId { get; set; }
         /// <summary>
         /// JavaScript object id of the node wrapper.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public string? objectId { get; set; }
     }
 
@@ -138,18 +157,20 @@ namespace bessw.Unity.WebView.ChromeDevTools.Protocol.DOM
     /// Returns the root DOM node (and optionally the subtree) to the caller.
     /// Implicitly enables the DOM domain events for the current target.
     /// </summary>
-    [CommandResponseAttribute(typeof(getDocumentCommandResponse))]
-    public class getDocument : IDevtoolsCommand
+    [CommandResponse(typeof(getDocumentCommandResponse))]
+    public class getDocument : IDevtoolsCommandWithResponse
     {
         /// <summary>
         /// The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the
         /// entire subtree or provide an integer larger than 0.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public int? depth { get; set; }
         /// <summary>
         /// Whether or not iframes and shadow roots should be traversed when returning the subtree
         /// (default is false).
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public bool? pierce { get; set; }
     }
 
@@ -169,8 +190,8 @@ namespace bessw.Unity.WebView.ChromeDevTools.Protocol.DOM
     /// Returns node id at given location. Depending on whether DOM domain is enabled, nodeId is
     /// either returned or not.
     /// </summary>
-    [CommandResponseAttribute(typeof(getNodeForLocationCommandResponse))]
-    public class getNodeForLocation : IDevtoolsCommand
+    [CommandResponse(typeof(getNodeForLocationCommandResponse))]
+    public class getNodeForLocation : IDevtoolsCommandWithResponse
     {
         /// <summary>
         /// X coordinate.
@@ -183,10 +204,12 @@ namespace bessw.Unity.WebView.ChromeDevTools.Protocol.DOM
         /// <summary>
         /// False to skip to the nearest non-UA shadow root ancestor (default: false).
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public bool? includeUserAgentShadowDOM { get; set; }
         /// <summary>
         /// Whether to ignore pointer-events: none on elements and hit test them.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public bool? ignorePointerEventsNone { get; set; }
     }
 
@@ -198,14 +221,15 @@ namespace bessw.Unity.WebView.ChromeDevTools.Protocol.DOM
         /// <summary>
         /// Resulting node.
         /// </summary>
-        public int? backendNodeId { get; set; }
+        public int backendNodeId { get; set; }
         /// <summary>
         /// Frame this node belongs to.
         /// </summary>
         public string frameId { get; set; }
         /// <summary>
-        /// Id of the node at given coordinates.
+        /// Id of the node at given coordinates, only when enabled and requested document.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public int? nodeId { get; set; }
     }
 
@@ -213,20 +237,23 @@ namespace bessw.Unity.WebView.ChromeDevTools.Protocol.DOM
     /// <summary>
     /// Returns node's HTML markup.
     /// </summary>
-    [CommandResponseAttribute(typeof(getOuterHTMLCommandResponse))]
-    public class getOuterHTML : IDevtoolsCommand
+    [CommandResponse(typeof(getOuterHTMLCommandResponse))]
+    public class getOuterHTML : IDevtoolsCommandWithResponse
     {
         /// <summary>
         /// Identifier of the node.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public int? nodeId { get; set; }
         /// <summary>
         /// Identifier of the backend node.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public int? backendNodeId { get; set; }
         /// <summary>
         /// JavaScript object id of the node wrapper.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public string? objectId { get; set; }
     }
 
@@ -245,6 +272,7 @@ namespace bessw.Unity.WebView.ChromeDevTools.Protocol.DOM
     /// <summary>
     /// Hides any highlight.
     /// </summary>
+    /// <remarks>see Overlay.hideHighlight</remarks>
     public class hideHighlight : IDevtoolsCommand
     {
     }
@@ -252,6 +280,7 @@ namespace bessw.Unity.WebView.ChromeDevTools.Protocol.DOM
     /// <summary>
     /// Highlights DOM node.
     /// </summary>
+    /// <remarks>see Overlay.highlightNode</remarks>
     public class highlightNode : IDevtoolsCommand
     {
     }
@@ -259,6 +288,7 @@ namespace bessw.Unity.WebView.ChromeDevTools.Protocol.DOM
     /// <summary>
     /// highlights given rectangle.
     /// </summary>
+    /// <remarks>see Overlay.highlightRect</remarks>
     public class highlightRect : IDevtoolsCommand
     {
     }
@@ -266,8 +296,8 @@ namespace bessw.Unity.WebView.ChromeDevTools.Protocol.DOM
     /// <summary>
     /// Moves node into the new container, places it before the given anchor.
     /// </summary>
-    [CommandResponseAttribute(typeof(moveToCommandResponse))]
-    public class moveTo : IDevtoolsCommand
+    [CommandResponse(typeof(moveToCommandResponse))]
+    public class moveTo : IDevtoolsCommandWithResponse
     {
         /// <summary>
         /// Id of the node to move.
@@ -281,6 +311,7 @@ namespace bessw.Unity.WebView.ChromeDevTools.Protocol.DOM
         /// Drop node before this one (if absent, the moved node becomes the last child of
         /// `targetNodeId`).
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public int? insertBeforeNodeId { get; set; }
     }
 
@@ -299,8 +330,8 @@ namespace bessw.Unity.WebView.ChromeDevTools.Protocol.DOM
     /// <summary>
     /// Executes `querySelector` on a given node.
     /// </summary>
-    [CommandResponseAttribute(typeof(querySelectorCommandResponse))]
-    public class querySelector : IDevtoolsCommand
+    [CommandResponse(typeof(querySelectorCommandResponse))]
+    public class querySelector : IDevtoolsCommandWithResponse
     {
         /// <summary>
         /// Id of the node to query upon.
@@ -327,8 +358,8 @@ namespace bessw.Unity.WebView.ChromeDevTools.Protocol.DOM
     /// <summary>
     /// Executes `querySelectorAll` on a given node.
     /// </summary>
-    [CommandResponseAttribute(typeof(querySelectorAllCommandResponse))]
-    public class querySelectorAll : IDevtoolsCommand
+    [CommandResponse(typeof(querySelectorAllCommandResponse))]
+    public class querySelectorAll : IDevtoolsCommandWithResponse
     {
         /// <summary>
         /// Id of the node to query upon.
@@ -393,11 +424,13 @@ namespace bessw.Unity.WebView.ChromeDevTools.Protocol.DOM
         /// The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the
         /// entire subtree or provide an integer larger than 0.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public int? depth { get; set; }
         /// <summary>
         /// Whether or not iframes and shadow roots should be traversed when returning the sub-tree
         /// (default is false).
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public bool? pierce { get; set; }
     }
 
@@ -406,8 +439,8 @@ namespace bessw.Unity.WebView.ChromeDevTools.Protocol.DOM
     /// nodes that form the path from the node to the root are also sent to the client as a series of
     /// `setChildNodes` notifications.
     /// </summary>
-    [CommandResponseAttribute(typeof(requestNodeCommandResponse))]
-    public class requestNode : IDevtoolsCommand
+    [CommandResponse(typeof(requestNodeCommandResponse))]
+    public class requestNode : IDevtoolsCommandWithResponse
     {
         /// <summary>
         /// JavaScript object id to convert into node.
@@ -426,43 +459,43 @@ namespace bessw.Unity.WebView.ChromeDevTools.Protocol.DOM
         public int nodeId { get; set; }
     }
 
-/* // Runtime.RemoteObject is not implemented
-
-    /// <summary>
-    /// Resolves the JavaScript node object for a given NodeId or BackendNodeId.
-    /// </summary>
-    [CommandResponseAttribute(typeof(resolveNodeCommandResponse))]
-    public class resolveNode : IDevtoolsCommand
-    {
-        /// <summary>
-        /// Id of the node to resolve.
-        /// </summary>
-        public int? nodeId { get; set; }
-        /// <summary>
-        /// Backend identifier of the node to resolve.
-        /// </summary>
-        public int? backendNodeId { get; set; }
-        /// <summary>
-        /// Symbolic group name that can be used to release multiple objects.
-        /// </summary>
-        public string? objectGroup { get; set; }
-        /// <summary>
-        /// Execution context in which to resolve the node.
-        /// </summary>
-        public int? executionContextId { get; set; }
-    }
-
-    /// <summary>
-    /// Response to <see cref="resolveNode"/> command
-    /// </summary>
-    public class resolveNodeCommandResponse : IDevtoolsResponse
-    {
-        /// <summary>
-        /// JavaScript object for given node.
-        /// </summary>
-        public Runtime.RemoteObject object_ { get; set; }
-    }
-*/
+    /* // Runtime.RemoteObject is not implemented
+
+        /// <summary>
+        /// Resolves the JavaScript node object for a given NodeId or BackendNodeId.
+        /// </summary>
+        [CommandResponse(typeof(resolveNodeCommandResponse))]
+        public class resolveNode : IDevtoolsCommandWithResponse
+        {
+            /// <summary>
+            /// Id of the node to resolve.
+            /// </summary>
+            public int? nodeId { get; set; }
+            /// <summary>
+            /// Backend identifier of the node to resolve.
+            /// </summary>
+            public int? backendNodeId { get; set; }
+            /// <summary>
+            /// Symbolic group name that can be used to release multiple objects.
+            /// </summary>
+            public string? objectGroup { get; set; }
+            /// <summary>
+            /// Execution context in which to resolve the node.
+            /// </summary>
+            public int? executionContextId { get; set; }
+        }
+
+        /// <summary>
+        /// Response to <see cref="resolveNode"/> command
+        /// </summary>
+        public class resolveNodeCommandResponse : IDevtoolsResponse
+        {
+            /// <summary>
+            /// JavaScript object for given node.
+            /// </summary>
+            public Runtime.RemoteObject object_ { get; set; }
+        }
+    */
 
     /// <summary>
     /// Scrolls the specified rect of the given node into view if not already visible.
@@ -474,19 +507,23 @@ namespace bessw.Unity.WebView.ChromeDevTools.Protocol.DOM
         /// <summary>
         /// Identifier of the node.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public int? nodeId { get; set; }
         /// <summary>
         /// Identifier of the backend node.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public int? backendNodeId { get; set; }
         /// <summary>
         /// JavaScript object id of the node wrapper.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public string? objectId { get; set; }
         /// <summary>
         /// The rect to be scrolled into view, relative to the node's border box, in CSS pixels.
         /// When omitted, center of the node will be used, similar to Element.scrollIntoView.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public Rect? rect { get; set; }
     }
 
@@ -508,6 +545,7 @@ namespace bessw.Unity.WebView.ChromeDevTools.Protocol.DOM
         /// Attribute name to replace with new attributes derived from text in case text parsed
         /// successfully.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public string? name { get; set; }
     }
 
@@ -542,22 +580,25 @@ namespace bessw.Unity.WebView.ChromeDevTools.Protocol.DOM
         /// <summary>
         /// Identifier of the node.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public int? nodeId { get; set; }
         /// <summary>
         /// Identifier of the backend node.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public int? backendNodeId { get; set; }
         /// <summary>
         /// JavaScript object id of the node wrapper.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public string? objectId { get; set; }
     }
 
     /// <summary>
     /// Set node name for a node with given id.
     /// </summary>
-    [CommandResponseAttribute(typeof(setNodeNameCommandResponse))]
-    public class setNodeName : IDevtoolsCommand
+    [CommandResponse(typeof(setNodeNameCommandResponse))]
+    public class setNodeName : IDevtoolsCommandWithResponse
     {
         /// <summary>
         /// Id of the node to set name for.
@@ -599,8 +640,8 @@ namespace bessw.Unity.WebView.ChromeDevTools.Protocol.DOM
     /// <summary>
     /// Sets node HTML markup, returns new node id.
     /// </summary>
-    [CommandResponseAttribute(typeof(setOuterHTMLCommandResponse))]
-    public class setOuterHTML : IDevtoolsCommand
+    [CommandResponse(typeof(setOuterHTMLCommandResponse))]
+    public class setOuterHTML : IDevtoolsCommandWithResponse
     {
         /// <summary>
         /// Id of the node to set markup for.
@@ -802,6 +843,7 @@ namespace bessw.Unity.WebView.ChromeDevTools.Protocol.DOM
         /// <summary>
         /// Shape outside coordinates
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public ShapeOutsideInfo? shapeOutside { get; set; }
     }
 
@@ -850,6 +892,7 @@ namespace bessw.Unity.WebView.ChromeDevTools.Protocol.DOM
         /// <summary>
         /// The id of the parent node if any.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public int? parentId { get; set; }
         /// <summary>
         /// The BackendNodeId for this node.
@@ -874,79 +917,97 @@ namespace bessw.Unity.WebView.ChromeDevTools.Protocol.DOM
         /// <summary>
         /// Child count for `Container` nodes.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public int? childNodeCount { get; set; }
         /// <summary>
         /// Child nodes of this node when requested with children.
         /// </summary>
-        public Node[]? children { get; set; }
+        public List<Node> children { get; set; }
         /// <summary>
         /// Attributes of the `Element` node in the form of flat array `[name1, value1, name2, value2]`.
         /// </summary>
-        public string[]? attributes { get; set; }
+        [JsonConverter(typeof(JsonInterleavedArrayConverter<string, string>))]
+        public Dictionary<string,string> attributes { get; set; }
         /// <summary>
         /// Document URL that `Document` or `FrameOwner` node points to.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public string? documentURL { get; set; }
         /// <summary>
         /// Base URL that `Document` or `FrameOwner` node uses for URL completion.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public string? baseURL { get; set; }
         /// <summary>
         /// `DocumentType`'s publicId.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public string? publicId { get; set; }
         /// <summary>
         /// `DocumentType`'s systemId.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public string? systemId { get; set; }
         /// <summary>
         /// `DocumentType`'s internalSubset.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public string? internalSubset { get; set; }
         /// <summary>
         /// `Document`'s XML version in case of XML documents.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public string? xmlVersion { get; set; }
         /// <summary>
         /// `Attr`'s name.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public string? name { get; set; }
         /// <summary>
         /// `Attr`'s value.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public string? value { get; set; }
         /// <summary>
         /// Pseudo element type for this node.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public PseudoType? pseudoType { get; set; }
         /// <summary>
         /// Pseudo element identifier for this node. Only present if there is a
         /// valid pseudoType.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public string? pseudoIdentifier { get; set; }
         /// <summary>
         /// Shadow root type.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public ShadowRootType? shadowRootType { get; set; }
         /// <summary>
         /// Frame ID for frame owner elements.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public string? frameId { get; set; }
         /// <summary>
         /// Content document for frame owner elements.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public Node? contentDocument { get; set; }
         /// <summary>
         /// Shadow root list for given element host.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public Node[]? shadowRoots { get; set; }
         /// <summary>
         /// Content document fragment for template elements.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public Node? templateContent { get; set; }
         /// <summary>
         /// Pseudo elements associated with this node.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public Node[]? pseudoElements { get; set; }
         /// <summary>
         /// Deprecated, as the HTML Imports API has been removed (crbug.com/937746).
@@ -954,16 +1015,21 @@ namespace bessw.Unity.WebView.ChromeDevTools.Protocol.DOM
         /// The property is always undefined now.
         /// </summary>
         /// <remarks>deprecated</remarks>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public Node? importedDocument { get; set; }
         /// <summary>
         /// Distributed nodes for given insertion point.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public BackendNode[]? distributedNodes { get; set; }
         /// <summary>
         /// Whether the node is SVG.
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public bool? isSVG { get; set; }
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public CompatibilityMode? compatibilityMode { get; set; }
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public BackendNode? assignedSlot { get; set; }
     }
 
@@ -985,6 +1051,17 @@ namespace bessw.Unity.WebView.ChromeDevTools.Protocol.DOM
         firstLine, firstLetter, before, after, marker, backdrop, selection, targetText, spellingError, grammarError, highlight, firstLineInherited, scrollbar, scrollbarThumb, scrollbarButton, scrollbarTrack, scrollbarTrackPiece, scrollbarCorner, resizer, inputListButton, viewTransition, viewTransitionGroup, viewTransitionImagePair, viewTransitionOld, viewTransitionNew
     }
 
+    /// <summary>
+    /// An array of quad vertices, x immediately followed by y for each point, points clock-wise.
+    /// </summary>
+    public class Quad
+    {
+        /// <summary>
+        /// Quad vertices
+        /// </summary>
+        public double[] quad { get; set; }
+    }
+
     /// <summary>
     /// Rectangle.
     /// </summary>
@@ -1028,6 +1105,7 @@ namespace bessw.Unity.WebView.ChromeDevTools.Protocol.DOM
         /// <summary>
         /// The alpha component, in the [0-1] range (default: 1).
         /// </summary>
+        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
         public double? a { get; set; }
     }
 
diff --git a/Runtime/ChromeDevtools/Protocol/DOM/DOM_api.json.meta b/Runtime/ChromeDevtools/Protocol/DOM/DOM_api.json.meta
new file mode 100644
index 0000000..364952e
--- /dev/null
+++ b/Runtime/ChromeDevtools/Protocol/DOM/DOM_api.json.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: f4e9b65319713e3478ca1d350419f8c4
+TextScriptImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Runtime/ChromeDevtools/Protocol/Input/Input.cs b/Runtime/ChromeDevtools/Protocol/Input/Input.cs
index c15cfcd..cd1fedb 100644
--- a/Runtime/ChromeDevtools/Protocol/Input/Input.cs
+++ b/Runtime/ChromeDevtools/Protocol/Input/Input.cs
@@ -5,6 +5,7 @@ using Newtonsoft.Json.Serialization;
 using UnityEngine.EventSystems;
 using static UnityEngine.EventSystems.PointerEventData;
 
+#nullable enable annotations
 namespace bessw.Unity.WebView.ChromeDevTools.Protocol.Input
 {
     #region dispatchMouseEvent
diff --git a/Runtime/ChromeDevtools/Protocol/JsonInterleavedArrayConverter.cs b/Runtime/ChromeDevtools/Protocol/JsonInterleavedArrayConverter.cs
new file mode 100644
index 0000000..17842d5
--- /dev/null
+++ b/Runtime/ChromeDevtools/Protocol/JsonInterleavedArrayConverter.cs
@@ -0,0 +1,39 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using System;
+using System.Collections.Generic;
+
+namespace bessw.Unity.WebView.ChromeDevTools.Protocol
+{
+    /// <summary>
+    /// Converts a interleaved JSON array like `[key1, value1, key2, value2, ...]` to a C# dictionary.
+    /// </summary>
+    /// <typeparam name="keyT"></typeparam>
+    /// <typeparam name="valueT"></typeparam>
+    public class JsonInterleavedArrayConverter<keyT, valueT> : JsonConverter<Dictionary<keyT,valueT>>
+    {
+        public override void WriteJson(JsonWriter writer, Dictionary<keyT,valueT> value, JsonSerializer serializer)
+        {
+            JArray obj = new JArray();
+
+            foreach (var pair in value)
+            {
+                obj.Add(pair.Key);
+                obj.Add(pair.Value);
+            }
+            obj.WriteTo(writer);
+        }
+
+        public override Dictionary<keyT,valueT> ReadJson(JsonReader reader, Type objectType, Dictionary<keyT,valueT> existingValue, bool hasExistingValue, JsonSerializer serializer)
+        {
+            JArray obj = JArray.Load(reader);
+            Dictionary<keyT,valueT> result = new Dictionary<keyT,valueT>();
+
+            for (int i = 0; i < obj.Count; i+=2)
+            {
+                result.Add(obj[i].Value<keyT>(), obj[i+1].Value<valueT>());
+            }
+            return result;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Runtime/ChromeDevtools/Protocol/JsonInterleavedArrayConverter.cs.meta b/Runtime/ChromeDevtools/Protocol/JsonInterleavedArrayConverter.cs.meta
new file mode 100644
index 0000000..bafd844
--- /dev/null
+++ b/Runtime/ChromeDevtools/Protocol/JsonInterleavedArrayConverter.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 31c472a5e6adcf247b4e0d9835835e75
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Runtime/WebViewComponent.cs b/Runtime/WebViewComponent.cs
index fa94db7..76dad43 100644
--- a/Runtime/WebViewComponent.cs
+++ b/Runtime/WebViewComponent.cs
@@ -39,7 +39,7 @@ namespace bessw.Unity.WebView
         #endregion json serializer
 
         private Browser browser;
-        private BrowserTab tab;
+        protected BrowserTab tab;
 
         private RawImage rawImage;
         private RectTransform rectTransform;
@@ -87,6 +87,32 @@ namespace bessw.Unity.WebView
             });
         }
 
+        private IEnumerator getDropzoneState ()
+        {
+            Debug.LogWarning($"dropzone pre");
+
+            DomNodeWrapper doc = null;
+            yield return DomNodeWrapper.getDocument(tab, (document) => {
+                Debug.LogWarning($"dropzone 1: '{document}'");
+                doc = document;
+                
+                StartCoroutine(document.querySelectorAll("[dropzone='copy']", (dropzones) => {
+                    foreach (var dropzone in dropzones)
+                    {
+                        Debug.LogWarning($"dropzone 2: Node is Null?: '{dropzone.Node == null}'");
+                        StartCoroutine(dropzone.getAttributes((attributes) =>
+                        {
+                            Debug.LogWarning($"dropzone 3 getAttributes: '{string.Join(", ", attributes.Values)}'");
+                        }));
+                    }
+                }));
+            });
+            Debug.LogWarning($"dropzone post: '{doc}'");
+
+            // alternative way to get the dropzone state
+
+        }
+
         // Update is called once per frame
         private void Update()
         {
@@ -132,12 +158,13 @@ namespace bessw.Unity.WebView
             }
         }
 
-        // TODO: OnDragMove -> PointerMove
-
         public void OnDrop(PointerEventData eventData)
         {
             Debug.LogWarning($"OnDrop: {eventData.position}");
             createDragEvent(DragEventType.drop, eventData);
+
+            // TODO: remove debug code
+            StartCoroutine(getDropzoneState());
         }
 
         public void OnPointerExit(PointerEventData eventData)
diff --git a/WebViewComponent.prefab b/WebViewComponent.prefab
index 4b9baec..9bf29f6 100644
--- a/WebViewComponent.prefab
+++ b/WebViewComponent.prefab
@@ -34,11 +34,11 @@ RectTransform:
   m_Father: {fileID: 0}
   m_RootOrder: 0
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
-  m_AnchorMin: {x: 0.5, y: 0.5}
-  m_AnchorMax: {x: 0.5, y: 0.5}
-  m_AnchoredPosition: {x: -444.39624, y: -175.33963}
+  m_AnchorMin: {x: 0, y: 0}
+  m_AnchorMax: {x: 0, y: 0}
+  m_AnchoredPosition: {x: 0, y: 0}
   m_SizeDelta: {x: 100, y: 100}
-  m_Pivot: {x: 0.5, y: 0.5}
+  m_Pivot: {x: 0, y: 0}
 --- !u!222 &5559415116192402672
 CanvasRenderer:
   m_ObjectHideFlags: 0
@@ -87,3 +87,4 @@ MonoBehaviour:
   m_Name: 
   m_EditorClassIdentifier: 
   headlessBrowser: 1
+  targetUrl: https://google.de
-- 
GitLab