class Test_WSSessionManager_wait_for_result(TestCase): def test_no_messages_with_result_timeout(self): with patch.object(_WSMessageProducer, "_get_websocket", new=MagicMock(return_value=_DummyWebsocket())): self.session_manager = WSSessionManager(1234, 1) with self.assertRaises(DevToolsTimeoutException): self.session_manager._wait_for_result(99) def test_message_spamming_with_result_timeout(self): with patch.object(_WSMessageProducer, "_get_websocket", new=MagicMock(return_value=_DummyWebsocket())): self.session_manager = WSSessionManager(1234, 1) with self.assertRaises(DevToolsTimeoutException): self.session_manager._message_producer.ws.set_recv_message({ "method": "Network.Something", "params": {} }) self.session_manager._wait_for_result(99)
def test_no_messages_with_result_timeout(self): with patch.object(_WSMessageProducer, "_get_websocket", new=MagicMock(return_value=_DummyWebsocket())): self.session_manager = WSSessionManager(1234, 1) with self.assertRaises(DevToolsTimeoutException): self.session_manager._wait_for_result(99)
def test_thread_died_once(self): with patch.object(_WSMessageProducer, "_get_websocket", new=MagicMock(return_value=ExceptionThrowingWS())): self.session_manager = WSSessionManager(1234, 60) self.resetWS() start = time.time() self.session_manager.execute("Network", "enable") self.assertLess(time.time() - start, 10)
def test_thread_blocks_causes_timeout(self): with patch.object(_WSMessageProducer, "_get_websocket", new=MagicMock(return_value=TimeoutBlockingWS())): self.session_manager = WSSessionManager(1234, 3) self.resetWS() with self.assertRaises(DevToolsTimeoutException): start = time.time() self.session_manager.execute("Network", "enable") self.assertLess(time.time() - start, 5)
def test_thread_blocked_twice(self): with patch.object( _WSMessageProducer, "_get_websocket", new=MagicMock(return_value=BlockingWS(times_to_block=2))): self.session_manager = WSSessionManager(1234, 30) self.resetWS() start = time.time() self.session_manager.execute("Network", "enable") self.assertLess(time.time() - start, 15)
def test_thread_died_too_many_times(self): with patch.object(_WSMessageProducer, "_get_websocket", new=MagicMock(return_value=ExceptionThrowingWS( times_to_except=4))): self.session_manager = WSSessionManager(1234, 30) self.resetWS() start = time.time() with self.assertRaises(MaxRetriesException): self.session_manager.execute("Network", "enable") self.assertLess(time.time() - start, 10)
def test_max_thread_blocks_exceeded(self): with patch.object( _WSMessageProducer, "_get_websocket", new=MagicMock(return_value=BlockingWS(times_to_block=4))): self.session_manager = WSSessionManager(1234, 60) self.resetWS() start = time.time() with self.assertRaises(MaxRetriesException): self.session_manager.execute("Network", "enable") self.assertLess(time.time() - start, 25)
def __init__(self, port, timeout=30, domains=None): """ Initialises the interface starting the websocket connection and enabling a series of domains. :param port: remote-debugging-port to connect. :param timeout: Timeout between executing a command and receiving a result. :param domains: Dictionary of dictionaries where the Key is the domain string and the Value is a dictionary of the arguments passed with the domain upon enabling. """ # type: WSSessionManager self._session_manager = WSSessionManager(port, timeout, domains=domains) self._dom_manager = _DOMManager(self._session_manager)
def test_thread_blocked_once(self): with patch.object( _WSMessageProducer, "_get_websocket", new=MagicMock(return_value=BlockingWS(times_to_block=1))): self.session_manager = WSSessionManager(1234, 30) start = time.time() self.session_manager.execute("Network", "enable") # We should find the execution result after 5 seconds because # we give the thread 5 seconds to poll before we consider it blocked, self.assertLess(time.time() - start, 10)
def test_locked_get_events(self): self.ws = FullWebSocket() with patch.object(_WSMessageProducer, "_get_websocket", new=MagicMock(return_value=self.ws)): self.session_manager = WSSessionManager(1234, 1, {"Network": {}}) events = list( reversed(self.session_manager.get_events("Network", clear=True))) while self.session_manager._message_producer.ws.queue: # Wait until all messages have been processed pass # make sure we don't lose any last_event_collected = events[0] next_event = { "method": "Network.Something", "params": { "index": -1 } } first_event = { "method": "Network.Something", "params": { "index": -1 } } if self.session_manager._events["Network"]: first_event = self.session_manager._events["Network"][0] assert (last_event_collected["params"]["index"] == next_event["params"]["index"] - 1 or last_event_collected["params"]["index"] == first_event["params"]["index"] - 1)
def test_no_dupe_ids(self): with patch.object(_WSMessageProducer, "_get_websocket", new=MagicMock(return_value=_DummyWebsocket())): self.session_manager = WSSessionManager(1234, 1, {"Network": {}}) self.ids = [] def _send(message): self.ids.append(message["id"]) self.session_manager._send = _send self.pool.map(self.continually_send, range(10)) time.sleep(5) self.assertEqual(len(set(self.ids)), len(self.ids))
class Test_WSSessionManager_execute(TestCase): def resetWS(self): """ flush_messages runs async so we only want the socket to start blocking when we're ready """ ExceptionThrowingWS.exceptions = 0 BlockingWS.blocked = 0 def setUp(self): """ We don't want the socket to block before we call execute() """ ExceptionThrowingWS.exceptions = 2 BlockingWS.blocked = 2 def tearDown(self): self.session_manager._message_producer.ws.unblock() self.session_manager.close() def test_thread_blocked_once(self): with patch.object( _WSMessageProducer, "_get_websocket", new=MagicMock(return_value=BlockingWS(times_to_block=1))): self.session_manager = WSSessionManager(1234, 30) start = time.time() self.session_manager.execute("Network", "enable") # We should find the execution result after 5 seconds because # we give the thread 5 seconds to poll before we consider it blocked, self.assertLess(time.time() - start, 10) def test_thread_blocked_twice(self): with patch.object( _WSMessageProducer, "_get_websocket", new=MagicMock(return_value=BlockingWS(times_to_block=2))): self.session_manager = WSSessionManager(1234, 30) self.resetWS() start = time.time() self.session_manager.execute("Network", "enable") self.assertLess(time.time() - start, 15) def test_thread_blocks_causes_timeout(self): with patch.object(_WSMessageProducer, "_get_websocket", new=MagicMock(return_value=TimeoutBlockingWS())): self.session_manager = WSSessionManager(1234, 3) self.resetWS() with self.assertRaises(DevToolsTimeoutException): start = time.time() self.session_manager.execute("Network", "enable") self.assertLess(time.time() - start, 5) def test_max_thread_blocks_exceeded(self): with patch.object( _WSMessageProducer, "_get_websocket", new=MagicMock(return_value=BlockingWS(times_to_block=4))): self.session_manager = WSSessionManager(1234, 60) self.resetWS() start = time.time() with self.assertRaises(MaxRetriesException): self.session_manager.execute("Network", "enable") self.assertLess(time.time() - start, 25) def test_thread_died_once(self): with patch.object(_WSMessageProducer, "_get_websocket", new=MagicMock(return_value=ExceptionThrowingWS())): self.session_manager = WSSessionManager(1234, 60) self.resetWS() start = time.time() self.session_manager.execute("Network", "enable") self.assertLess(time.time() - start, 10) def test_thread_died_twice(self): with patch.object(_WSMessageProducer, "_get_websocket", new=MagicMock(return_value=ExceptionThrowingWS( times_to_except=2))): self.session_manager = WSSessionManager(1234, 30) self.resetWS() start = time.time() self.session_manager.execute("Network", "enable") self.assertLess(time.time() - start, 10) def test_thread_died_too_many_times(self): with patch.object(_WSMessageProducer, "_get_websocket", new=MagicMock(return_value=ExceptionThrowingWS( times_to_except=4))): self.session_manager = WSSessionManager(1234, 30) self.resetWS() start = time.time() with self.assertRaises(MaxRetriesException): self.session_manager.execute("Network", "enable") self.assertLess(time.time() - start, 10)
class ChromeInterface(object): """ The Chrome Interface communicates with the browser through the remote-debugging-port using the Chrome DevTools Protocol. For a thorough reference check: https://chromedevtools.github.io/devtools-protocol/ Usage example: interface = ChromeInterface(9123) interface.navigate(url="https://github.com/scivisum/browser-debugger-tools") """ def __init__(self, port, timeout=30, domains=None): """ Initialises the interface starting the websocket connection and enabling a series of domains. :param port: remote-debugging-port to connect. :param timeout: Timeout between executing a command and receiving a result. :param domains: Dictionary of dictionaries where the Key is the domain string and the Value is a dictionary of the arguments passed with the domain upon enabling. """ # type: WSSessionManager self._session_manager = WSSessionManager(port, timeout, domains=domains) self._dom_manager = _DOMManager(self._session_manager) def quit(self): self._session_manager.close() def reset(self): """ Clears all stored messages """ self._session_manager.reset() self._dom_manager.reset() def get_events(self, domain, clear=False): """ Retrieves all events for a given domain :param domain: The domain to get the events for. :param clear: Removes the stored events if set to true. :return: List of events. """ return self._session_manager.get_events(domain, clear) def execute(self, domain, method, params=None): """ Executes a command and returns the result. Usage example: self.execute("Network", "Cookies", args={"urls": ["http://www.urls.com/"]}) https://chromedevtools.github.io/devtools-protocol/tot/Network#method-getCookies :param domain: Chrome DevTools Protocol Domain :param method: Domain specific method. :param params: Parameters to be executed :return: The result of the command """ return self._session_manager.execute(domain, method, params=params) def enable_domain(self, domain, params=None): """ Enables notifications for the given domain. """ self._session_manager.enable_domain(domain, parameters=params) def disable_domain(self, domain): """ Disables further notifications from the given domain. Also clears any events cached for that domain, it is recommended that you get events for the domain before disabling it. """ self._session_manager.disable_domain(domain) @contextlib.contextmanager def set_timeout(self, value): """ Switches the timeout to the given value. """ _timeout = self._session_manager.timeout self._session_manager.timeout = value try: yield finally: self._session_manager.timeout = _timeout def navigate(self, url): """ Navigates to the given url asynchronously """ return self.execute("Page", "navigate", {"url": url}) def take_screenshot(self, filepath): """ Takes a screenshot of the current page """ response = self.execute("Page", "captureScreenshot") image_data = response["data"] with open(filepath, "wb") as f: f.write(b64decode(image_data)) def stop_page_load(self): return self.execute("Page", "stopLoading") def execute_javascript(self, script): result = self.execute("Runtime", "evaluate", { "expression": script, "returnByValue": True })["result"] return result.get("value") def get_url(self): # type: () -> str """ Consider enabling the Page domain to increase performance. :returns: The url of the current page. """ return self._session_manager.event_handlers[ "PageLoad"].get_current_url() def get_document_readystate(self): """ Gets the document.readyState of the page. """ return self.execute_javascript("document.readyState") def set_user_agent_override(self, user_agent): """ Overriding user agent with the given string. :param user_agent: :return: """ return self.execute("Network", "setUserAgentOverride", {"userAgent": user_agent}) def emulate_network_conditions(self, latency, download, upload, offline=False): """ :param latency: Minimum latency from request sent to response headers (ms). :param download: Maximal aggregated download throughput (bytes/sec). :param upload: Maximal aggregated upload throughput (bytes/sec). :param offline: Whether to emulate network disconnection """ # Note: Currently, there's a bug in the devtools protocol when disabling parameters, # i.e setting download to -1, therefore we enforce that all parameters must be passed with # a sensible value (bigger than 0) assert min(latency, download, upload) > 0 or offline network_conditions = { "offline": offline, "latency": latency, "downloadThroughput": download, "uploadThroughput": upload, } return self.execute("Network", "emulateNetworkConditions", network_conditions) def set_basic_auth(self, username, password): """ Creates a basic type Authorization header from the username and password strings and applies it to all requests """ auth = "Basic " + b64encode("%s:%s" % (username, password)) self.set_request_headers({"Authorization": auth}) def set_request_headers(self, headers): """ The specified headers are applied to all requests :param headers: A dictionary of the form {"headerKey": "headerValue"} """ self.execute("Network", "setExtraHTTPHeaders", {"headers": headers}) def get_opened_javascript_dialog(self): # type: () -> JavascriptDialog """ Gets the opened javascript dialog. :raises DomainNotEnabledError: If the Page domain isn't enabled :raises JavascriptDialogNotFoundError: If there is currently no dialog open """ return (self._session_manager.event_handlers["JavascriptDialog"]. get_opened_javascript_dialog()) def get_iframe_source_content(self, xpath): # type: (str) -> str """ Returns the HTML markup for an iframe document, where the iframe node can be located in the DOM with the given xpath. :param xpath: following the spec 3.1 https://www.w3.org/TR/xpath-31/ :return: HTML markup :raises IFrameNotFoundError: A matching iframe document could not be found :raises UnknownError: The socket handler received a message with an unknown error code """ return self._dom_manager.get_iframe_html(xpath) def get_page_source(self): # type: () -> str """ Returns the HTML markup of the current page. Iframe tags are included but the enclosed documents are not. Consider enabling the Page domain to increase performance. :return: HTML markup """ root_node_id = self._session_manager.event_handlers[ "PageLoad"].get_root_backend_node_id() return self._dom_manager.get_outer_html(root_node_id)