Skip to content

Commit b296cfc

Browse files
author
Mathew Seymour
committed
[RD-37208] Implemented review feedback
- Moved methods related finding nodes and retrieving HTML of nodes to a new _DOMManager class. - Cleaning up the node map when the devtools client is reset. - Simplified _get_iframe_html - it doesn't need to be recursive because we only do it twice. - A further simplification removed IFrameNotFoundError and let the ResourceNotFoundError bubble up with appropriate error messages. - A context manager _get_node_ids is used to return up to a specified maximum number of node ids that match a given xpath, in a manner that we won't leak search sessions.
1 parent b1e6b23 commit b296cfc

File tree

7 files changed

+184
-82
lines changed

7 files changed

+184
-82
lines changed

browserdebuggertools/chrome/interface.py

Lines changed: 53 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44

55
from lxml.etree import XPath, XPathSyntaxError
66

7-
from browserdebuggertools.exceptions import (
8-
InvalidXPathError, ResourceNotFoundError, IFrameNotFoundError
9-
)
7+
from browserdebuggertools.exceptions import InvalidXPathError, ResourceNotFoundError
108
from browserdebuggertools.sockethandler import SocketHandler
119

1210

@@ -34,7 +32,7 @@ def __init__(self, port, timeout=30, domains=None):
3432
is a dictionary of the arguments passed with the domain upon enabling.
3533
"""
3634
self._socket_handler = SocketHandler(port, timeout, domains=domains) # type: SocketHandler
37-
self._node_map = {}
35+
self._dom_manager = _DOMManager(self._socket_handler)
3836

3937
def quit(self):
4038
self._socket_handler.close()
@@ -43,6 +41,7 @@ def reset(self):
4341
""" Clears all stored messages
4442
"""
4543
self._socket_handler.reset()
44+
self._dom_manager.reset()
4645

4746
def get_events(self, domain, clear=False):
4847
""" Retrieves all events for a given domain
@@ -208,7 +207,7 @@ def get_iframe_source_content(self, xpath):
208207
except XPathSyntaxError:
209208
raise InvalidXPathError("{0} is not a valid xpath".format(xpath))
210209

211-
return self._get_iframe_html(xpath)
210+
return self._dom_manager.get_iframe_html(xpath)
212211

213212
def get_page_source(self):
214213
# type: () -> str
@@ -219,65 +218,90 @@ def get_page_source(self):
219218
:return: HTML markup
220219
"""
221220

222-
root_node_id = self._socket_handler.event_handlers["PageLoad"].get_root_node_id()
223-
return self._get_outer_html(root_node_id)
221+
root_node_id = self._socket_handler.event_handlers["PageLoad"].get_root_backend_node_id()
222+
return self._dom_manager.get_outer_html(root_node_id)
223+
224224

225-
def _get_iframe_html(self, xpath, _attempts=0):
226-
# type: (str, int) -> str
225+
class _DOMManager(object):
226+
227+
def __init__(self, socket_handler):
228+
self._socket_handler = socket_handler
229+
self._node_map = {}
230+
231+
def get_outer_html(self, backend_node_id):
232+
# type: (int) -> str
233+
return self._socket_handler.execute(
234+
"DOM", "getOuterHTML", {"backendNodeId": backend_node_id}
235+
)["outerHTML"]
236+
237+
def get_iframe_html(self, xpath):
238+
# type: (str) -> str
227239

228240
backend_node_id = self._get_iframe_backend_node_id(xpath)
229241
try:
230-
return self._get_outer_html(backend_node_id)
242+
return self.get_outer_html(backend_node_id)
231243
except ResourceNotFoundError:
232244
# The cached node doesn't exist any more, so we need to find a new one that matches
233245
# the xpath. Backend node IDs are unique, so there is not a risk of getting the
234246
# outer html of the wrong node.
235-
if _attempts < 1:
236-
if xpath in self._node_map:
237-
del self._node_map[xpath]
238-
return self._get_iframe_html(xpath, _attempts=_attempts + 1)
239-
240-
raise IFrameNotFoundError()
247+
if xpath in self._node_map:
248+
del self._node_map[xpath]
249+
backend_node_id = self._get_iframe_backend_node_id(xpath)
250+
return self.get_outer_html(backend_node_id)
241251

242252
def _get_iframe_backend_node_id(self, xpath):
243253
# type: (str) -> int
244254

245255
if xpath in self._node_map:
246256
return self._node_map[xpath]
247257

258+
node_info = self._get_info_for_first_matching_node(xpath)
248259
try:
249-
node_info = self._get_info_for_first_matching_node(xpath)
260+
250261
backend_node_id = node_info["node"]["contentDocument"]["backendNodeId"]
251-
except (KeyError, ResourceNotFoundError):
252-
# Either we couldn't find a node matching the xpath, or we did find a node but it's
253-
# not an iframe.
254-
raise IFrameNotFoundError()
262+
except KeyError:
263+
raise ResourceNotFoundError("The node found by xpath '%s' is not an iframe" % xpath)
255264

256265
self._node_map[xpath] = backend_node_id
257266
return backend_node_id
258267

259268
def _get_info_for_first_matching_node(self, xpath):
260269
# type: (str) -> dict
261270

271+
with self._get_node_ids(xpath) as node_ids:
272+
if node_ids:
273+
return self._describe_node(node_ids[0])
274+
raise ResourceNotFoundError("No matching nodes for xpath: %s" % xpath)
275+
276+
@contextlib.contextmanager
277+
def _get_node_ids(self, xpath, max_matches=1):
278+
# type: (str, int) -> list
279+
280+
assert max_matches > 0
262281
search_info = self._perform_search(xpath)
263-
if search_info.get("resultCount", 0) > 0:
264-
search_results = self._get_search_results(search_info["searchId"], 0, 1)
282+
try:
283+
results = []
284+
if search_info["resultCount"] > 0:
285+
results = self._get_search_results(
286+
search_info["searchId"], 0, min([max_matches, search_info["resultCount"]])
287+
)["nodeIds"]
288+
yield results
289+
290+
finally:
265291
self._discard_search(search_info["searchId"])
266-
return self._describe_node(search_results["nodeIds"].pop())
267-
raise ResourceNotFoundError("No matching nodes for xpath: %s" % xpath)
268292

269293
def _perform_search(self, xpath):
270294
# type: (str) -> dict
271295

272296
# DOM.getDocument must have been called on the current page first otherwise performSearch
273297
# returns an array of 0s.
274298
self._socket_handler.event_handlers["PageLoad"].check_page_load()
275-
return self.execute("DOM", "performSearch", {"query": xpath})
299+
return self._socket_handler.execute("DOM", "performSearch", {"query": xpath})
276300

277301
def _get_search_results(self, search_id, from_index, to_index):
278302
# type: (str, int, int) -> dict
279303

280-
return self.execute("DOM", "getSearchResults", {
304+
return self._socket_handler.execute("DOM", "getSearchResults", {
281305
"searchId": search_id, "fromIndex": from_index, "toIndex": to_index
282306
})
283307

@@ -295,9 +319,5 @@ def _describe_node(self, node_id):
295319

296320
return self._socket_handler.execute("DOM", "describeNode", {"nodeId": node_id})
297321

298-
def _get_outer_html(self, backend_node_id):
299-
# type: (int) -> str
300-
301-
return self.execute(
302-
"DOM", "getOuterHTML", {"backendNodeId": backend_node_id}
303-
)["outerHTML"]
322+
def reset(self):
323+
self._node_map = {}

browserdebuggertools/eventhandlers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def get_current_url(self):
6464
self.check_page_load()
6565
return self._url
6666

67-
def get_root_node_id(self):
67+
def get_root_backend_node_id(self):
6868
self.check_page_load()
6969
return self._root_node_id
7070

browserdebuggertools/exceptions.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,6 @@ class ResourceNotFoundError(NotFoundError):
3434
pass
3535

3636

37-
class IFrameNotFoundError(NotFoundError):
38-
pass
39-
40-
4137
class JavascriptDialogNotFoundError(NotFoundError):
4238
pass
4339

browserdebuggertools/sockethandler.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ def reset(self):
241241
self._events[domain] = []
242242

243243
self._results = {}
244+
self._next_result_id = 0
244245

245246
def _wait_for_result(self):
246247
""" Waits for a result to complete within the timeout duration then returns it.

tests/e2etests/chrome/test_interface.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
import shutil
44
import time
55
import tempfile
6-
from unittest import TestCase, main
6+
from unittest import TestCase
77

88
from requests import ConnectionError
99

1010
from browserdebuggertools.exceptions import (
1111
DevToolsException, DevToolsTimeoutException, JavascriptDialogNotFoundError,
12-
InvalidXPathError, IFrameNotFoundError
12+
InvalidXPathError, ResourceNotFoundError
1313
)
1414
from browserdebuggertools.models import JavascriptDialog
1515
from tests.e2etests.testsite.start import Server as TestSiteServer, env
@@ -570,14 +570,14 @@ def test_node_not_found(self):
570570

571571
assert isinstance(self, (ChromeInterfaceTest, TestCase))
572572

573-
with self.assertRaises(IFrameNotFoundError):
573+
with self.assertRaises(ResourceNotFoundError):
574574
self.devtools_client.get_iframe_source_content("//iframe[@id='unknown']")
575575

576576
def test_node_found_but_its_not_an_iframe(self):
577577

578578
assert isinstance(self, (ChromeInterfaceTest, TestCase))
579579

580-
with self.assertRaises(IFrameNotFoundError):
580+
with self.assertRaises(ResourceNotFoundError):
581581
self.devtools_client.get_iframe_source_content("//div")
582582

583583
def test_invalid_xpath(self):

tests/integrationtests/chrome/test_interface.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from mock import MagicMock, patch
77

8-
from browserdebuggertools.chrome.interface import ChromeInterface
8+
from browserdebuggertools.chrome.interface import ChromeInterface, _DOMManager
99
from browserdebuggertools.exceptions import DevToolsTimeoutException
1010
from browserdebuggertools.sockethandler import SocketHandler
1111

@@ -16,6 +16,7 @@ class MockChromeInterface(ChromeInterface):
1616

1717
def __init__(self, port, timeout=30, domains=None):
1818
self._socket_handler = MockSocketHandler(port, timeout, domains=domains)
19+
self._dom_manager = _DOMManager(self._socket_handler)
1920

2021

2122
class MockSocketHandler(SocketHandler):

0 commit comments

Comments
 (0)