diff --git a/src/Selenium2Library/keywords/browsermanagement.py b/src/Selenium2Library/keywords/browsermanagement.py index edf969248..69d4235ea 100644 --- a/src/Selenium2Library/keywords/browsermanagement.py +++ b/src/Selenium2Library/keywords/browsermanagement.py @@ -1,4 +1,7 @@ import os.path +import os +import signal + import time import types @@ -6,15 +9,14 @@ from robot.utils import secs_to_timestr, timestr_to_secs from selenium import webdriver -from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver from selenium.common.exceptions import NoSuchWindowException from Selenium2Library.utils import BrowserCache from Selenium2Library.locators import WindowManager +from Selenium2Library.keywords.logging import LoggingKeywords from .keywordgroup import KeywordGroup - ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) FIREFOX_PROFILE_DIR = os.path.join(ROOT_DIR, 'resources', 'firefoxprofile') BROWSER_NAMES = {'ff': "_make_ff", @@ -24,19 +26,48 @@ 'googlechrome': "_make_chrome", 'gc': "_make_chrome", 'chrome': "_make_chrome", - 'opera' : "_make_opera", - 'phantomjs' : "_make_phantomjs", - 'htmlunit' : "_make_htmlunit", - 'htmlunitwithjs' : "_make_htmlunitwithjs", + 'opera': "_make_opera", + 'phantomjs': "_make_phantomjs", + 'htmlunit': "_make_htmlunit", + 'htmlunitwithjs': "_make_htmlunitwithjs", 'android': "_make_android", 'iphone': "_make_iphone", 'safari': "_make_safari", 'edge': "_make_edge" - } + } -class BrowserManagementKeywords(KeywordGroup): +class _ReusableDriver(webdriver.Remote, LoggingKeywords): + def __init__(self, command_executor, sid, keep_alive=False, file_detector=None): + + self.reuse_session_id = sid + super(_ReusableDriver, self).__init__(command_executor, desired_capabilities={}, + browser_profile=None, + proxy=None, keep_alive=keep_alive, + file_detector=file_detector) + + def stop_client(self): + if self.pid is not None and self.pid != '': + self._info("Stopping driver for session '%s' with PID: %s" % (self.session_id, self.pid)) + try: + os.kill(int(self.pid), signal.SIGTERM) + except EnvironmentError: + self._warn("Failed to stop the driver with PID '%s' - please kill the process manually " % (self.pid)) + else: + self._info("No driver process found for session '%s'" % (self.session_id)) + + def start_session(self, desired_capabilities, browser_profile=None): + self.session_id = self.reuse_session_id + self._info("Reconnecting to session '%s' " % self.session_id) + self.command_executor._commands["GetSessionData"] = ('GET', '/session/$sessionId') + response = self.execute("GetSessionData") + self._debug("Reconnect OK, session data: '%s' " % response) + self.capabilities = response['value'] + self.w3c = "specificationLevel" in self.capabilities + + +class BrowserManagementKeywords(KeywordGroup): def __init__(self): self._cache = BrowserCache() self._window_manager = WindowManager() @@ -65,8 +96,8 @@ def close_browser(self): % self._cache.current.session_id) self._cache.close() - def open_browser(self, url, browser='firefox', alias=None,remote_url=False, - desired_capabilities=None,ff_profile_dir=None): + def open_browser(self, url, browser='firefox', alias=None, remote_url=False, + desired_capabilities=None, ff_profile_dir=None): """Opens a new browser instance to given URL. Returns the index of this browser instance which can be used later to @@ -117,11 +148,11 @@ def open_browser(self, url, browser='firefox', alias=None,remote_url=False, """ if remote_url: self._info("Opening browser '%s' to base url '%s' through remote server at '%s'" - % (browser, url, remote_url)) + % (browser, url, remote_url)) else: self._info("Opening browser '%s' to base url '%s'" % (browser, url)) browser_name = browser - browser = self._make_browser(browser_name,desired_capabilities,ff_profile_dir,remote_url) + browser = self._make_browser(browser_name, desired_capabilities, ff_profile_dir, remote_url) try: browser.get(url) except: @@ -187,6 +218,141 @@ def create_webdriver(self, driver_name, alias=None, kwargs={}, **init_kwargs): self._debug("Created %s WebDriver instance with session id %s" % (driver_name, driver.session_id)) return self._cache.register(driver, alias) + def save_webdriver(self, file=''): + """Stores the current web-driver session. + + NOTE: This KW is not for production use. The `Save Webdriver` and `Restore Webdriver` KW's are there to + speed up the test case development i.e. instead of always opening new browser window you can reconnect to + an already opened Browser window. + + This KW makes the WebDriver sessions persistent. I.e. you can store the session(s) and leave the browser + window(s) open after a pybot (test suite) execution. On the next pybot execution you can reconnect to those + already opened browser windows and avoid that slow opening of new browser window(s) step and possibly other + steps (e.g. login) that you might do in your suite/test set-up phase. + NOTE: When re-connecting the session must exists on the server side, in some scenarios the session + might have timed out and the re-connect will fail. + + Saves the session information into file or returns it. + + The file name is taken from the _file_ argument. + This accepts plain file name (e.g. "mysession.txt") and path-to-file (e.g. "/tmp/mysession.txt") format. + The _file_ argument has following special values: + - empty string i.e. ``${EMPTY}`` (default): the session is stored in the current execution directory in a + file called "session_.tmp" (if the connection has an alias) or "session_.tmp" + (if the connection does not have an alias). + - None i.e. ``${None}``: no file is created. Returns a tuplet where the 1st value is the session ID and the 2nd + value is the Webdriver URL. + + Returns the session file-name, unless _file_ argument is None (see above). + + Examples: + | Create Webdriver | Firefox | | | | + | ${file} = | Save Webdriver | | | # saved to "session_1.tmp" | + | Create Webdriver | Firefox | alias=foo | | | + | ${file} = | Save Webdriver | | | # saved to "session_foo.tmp" | + | ${file} = | Save Webdriver | file=dummy.tmp | | # saved to "dummy.tmp" | + | ${sid} | ${url} = | Save Webdriver | file=${None} | # no file created | + + See also `Restore Webdriver` + """ + self._info("Saving WebDriver session data") + driver_service_attr_names = ['service', 'iedriver'] + cb = self._current_browser() + sid = cb.session_id + curl = cb.command_executor._url + # get pid if we are in a restored this session + pid = cb.__dict__.get("pid", None) + if pid is None: + # get driver executable PID + # this part is browser-dependant, currently tested only with: FF, GC & IE + self._debug("'%s'" % self._current_browser().__dict__) + for srvc_name in driver_service_attr_names: + srvc = cb.__dict__.get(srvc_name, None) + if srvc is not None: + pid = srvc.process.pid + break + if pid is None: + self._info( + "Could not find driver process - we will not attempt to " + + " kill the driver process after re-connecting to it with restore_session() ") + if file is None: + return (sid, curl, pid) + elif file == "": + inv_dict = {v: k for k, v in self._cache._aliases.items()} + session_name = inv_dict.get(self._cache.current_index, self._cache.current_index) + file = "session_%s.tmp" % (session_name) + self._debug("Storing session (%s-%s) in file '%s'" % (sid, curl, file)) + session_file = open(file, 'w+') + session_file.write("%s\n" % sid) + session_file.write("%s\n" % curl) + if pid is not None: + session_file.write("%s\n" % pid) + session_file.close() + return file + + def restore_webdriver(self, alias=None, file=None, session_id=None, session_url=None, session_pid=None, + delete_file=True): + """Connects to an already opened web-driver session. + + NOTE: This KW is not for production use. The `Save Webdriver` and `Restore Webdriver` KW's are there to + speed up the test case development i.e. instead of always opening new browser window you can reconnect to + an already opened Browser window. + + Restores a Webdriver Session that has been saved using the `Save Webdriver` KW. + + Optional _alias_ is an alias for the browser instance and it can be used + for switching between browsers (just as index can be used). See `Switch + Browser` for more details. + + The session can be restored by: + - using the default session file (e.g. with `alias=foo` we try to read the session info from a file called + "session_foo.tmp") + - using explicit file: file-name is passed in _file_ argument. See `Save Webdriver` KW for _file_ + argument documentation + - using the _session_id_ and _session_url_ arguments + + Optional _delete_file_ can be used to delete (default) the file after the session has been restored. + + Returns the index of this browser instance which can be used later to + switch back to it. Index starts from 1 and is reset back to it when + `Close All Browsers` keyword is used. See `Switch Browser` for + example. + + Examples: + | Restore Webdriver | alias=foo | | #read from "session_foo.tmp" and register with alias "foo" | + | Restore Webdriver | alias=bar | file=foo.tmp | #read from "foo.tmp" and register with alias "bar" | + | Restore Webdriver | | file=foo.tmp | #read from "foo.tmp" and register without alias | + | #do not use file and register without alias | | | | + | Restore Webdriver | session_id=x | session_url=http://127.0.0.1:9999/hub | | + """ + self._info("Restoring WebDriver session") + if session_id is not None or session_url is not None: + if session_id is None and session_url is None: + raise RuntimeError("both session_id and session_url cannot be None.") + saved_sid = session_id + saved_curl = session_url + else: + if file is None: + if alias is None: + raise RuntimeError("both alias and file cannot be None.") + else: + file = "session_%s.tmp" % (alias) + session_file = open(file, 'r+') + saved_sid = session_file.readline().replace("\n", "").replace("\r", "") + saved_curl = session_file.readline().replace("\n", "").replace("\r", "") + session_pid = session_file.readline().replace("\n", "").replace("\r", "") + session_file.close() + self._debug("Reconnecting to '%s' with session id '%s' - alias: '%s' " % (saved_curl, saved_sid, alias)) + try: + driver = _ReusableDriver(saved_curl, saved_sid) + driver.pid = session_pid + except: + raise + finally: + if delete_file and session_id is None and session_url is None: + os.remove(file) + return self._cache.register(driver, alias) + def switch_browser(self, index_or_alias): """Switches between active browsers using index or alias. @@ -215,7 +381,7 @@ def switch_browser(self, index_or_alias): try: self._cache.switch(index_or_alias) self._debug('Switched to browser with Selenium session id %s' - % self._cache.current.session_id) + % self._cache.current.session_id) except (RuntimeError, DataError): # RF 2.6 uses RE, earlier DE raise RuntimeError("No browser with index or alias '%s' found." % index_or_alias) @@ -372,7 +538,7 @@ def get_title(self): def location_should_be(self, url): """Verifies that current URL is exactly `url`.""" actual = self.get_location() - if actual != url: + if actual != url: raise AssertionError("Location should have been '%s' but was '%s'" % (url, actual)) self._info("Current location is '%s'." % url) @@ -412,7 +578,7 @@ def title_should_be(self, title): actual = self.get_title() if actual != title: raise AssertionError("Title should have been '%s' but was '%s'" - % (title, actual)) + % (title, actual)) self._info("Page title is '%s'." % title) # Public, navigation @@ -516,7 +682,6 @@ def set_selenium_implicit_wait(self, seconds): browser.implicitly_wait(self._implicit_wait_in_secs) return old_wait - def set_browser_implicit_wait(self, seconds): """Sets current browser's implicit wait in seconds. @@ -556,71 +721,71 @@ def _make_browser(self, browser_name, desired_capabilities=None, return browser - - def _make_ff(self , remote , desired_capabilites , profile_dir): + def _make_ff(self, remote, desired_capabilites, profile_dir): if not profile_dir: profile_dir = FIREFOX_PROFILE_DIR profile = webdriver.FirefoxProfile(profile_dir) if remote: - browser = self._create_remote_web_driver(webdriver.DesiredCapabilities.FIREFOX , - remote , desired_capabilites , profile) + browser = self._create_remote_web_driver(webdriver.DesiredCapabilities.FIREFOX, + remote, desired_capabilites, profile) else: browser = webdriver.Firefox(firefox_profile=profile) return browser - def _make_ie(self , remote , desired_capabilities , profile_dir): + def _make_ie(self, remote, desired_capabilities, profile_dir): return self._generic_make_browser(webdriver.Ie, - webdriver.DesiredCapabilities.INTERNETEXPLORER, remote, desired_capabilities) + webdriver.DesiredCapabilities.INTERNETEXPLORER, remote, desired_capabilities) - def _make_chrome(self , remote , desired_capabilities , profile_dir): + def _make_chrome(self, remote, desired_capabilities, profile_dir): return self._generic_make_browser(webdriver.Chrome, - webdriver.DesiredCapabilities.CHROME, remote, desired_capabilities) + webdriver.DesiredCapabilities.CHROME, remote, desired_capabilities) - def _make_opera(self , remote , desired_capabilities , profile_dir): + def _make_opera(self, remote, desired_capabilities, profile_dir): return self._generic_make_browser(webdriver.Opera, - webdriver.DesiredCapabilities.OPERA, remote, desired_capabilities) + webdriver.DesiredCapabilities.OPERA, remote, desired_capabilities) - def _make_phantomjs(self , remote , desired_capabilities , profile_dir): + def _make_phantomjs(self, remote, desired_capabilities, profile_dir): return self._generic_make_browser(webdriver.PhantomJS, - webdriver.DesiredCapabilities.PHANTOMJS, remote, desired_capabilities) + webdriver.DesiredCapabilities.PHANTOMJS, remote, desired_capabilities) - def _make_htmlunit(self , remote , desired_capabilities , profile_dir): + def _make_htmlunit(self, remote, desired_capabilities, profile_dir): return self._generic_make_browser(webdriver.Remote, - webdriver.DesiredCapabilities.HTMLUNIT, remote, desired_capabilities) + webdriver.DesiredCapabilities.HTMLUNIT, remote, desired_capabilities) - def _make_htmlunitwithjs(self , remote , desired_capabilities , profile_dir): + def _make_htmlunitwithjs(self, remote, desired_capabilities, profile_dir): return self._generic_make_browser(webdriver.Remote, - webdriver.DesiredCapabilities.HTMLUNITWITHJS, remote, desired_capabilities) + webdriver.DesiredCapabilities.HTMLUNITWITHJS, remote, desired_capabilities) - def _make_android(self , remote , desired_capabilities , profile_dir): + def _make_android(self, remote, desired_capabilities, profile_dir): return self._generic_make_browser(webdriver.Remote, - webdriver.DesiredCapabilities.ANDROID, remote, desired_capabilities) + webdriver.DesiredCapabilities.ANDROID, remote, desired_capabilities) - def _make_iphone(self , remote , desired_capabilities , profile_dir): + def _make_iphone(self, remote, desired_capabilities, profile_dir): return self._generic_make_browser(webdriver.Remote, - webdriver.DesiredCapabilities.IPHONE, remote, desired_capabilities) + webdriver.DesiredCapabilities.IPHONE, remote, desired_capabilities) - def _make_safari(self , remote , desired_capabilities , profile_dir): + def _make_safari(self, remote, desired_capabilities, profile_dir): return self._generic_make_browser(webdriver.Safari, - webdriver.DesiredCapabilities.SAFARI, remote, desired_capabilities) + webdriver.DesiredCapabilities.SAFARI, remote, desired_capabilities) - def _make_edge(self , remote , desired_capabilities , profile_dir): + def _make_edge(self, remote, desired_capabilities, profile_dir): if hasattr(webdriver, 'Edge'): return self._generic_make_browser(webdriver.Edge, - webdriver.DesiredCapabilities.EDGE, remote, desired_capabilities) + webdriver.DesiredCapabilities.EDGE, remote, desired_capabilities) else: - raise ValueError("Edge is not a supported browser with your version of Selenium python library. Please, upgrade to minimum required version 2.47.0.") + raise ValueError( + "Edge is not a supported browser with your version of Selenium python library. Please, upgrade to minimum required version 2.47.0.") - def _generic_make_browser(self, webdriver_type , desired_cap_type, remote_url, desired_caps): + def _generic_make_browser(self, webdriver_type, desired_cap_type, remote_url, desired_caps): '''most of the make browser functions just call this function which creates the appropriate web-driver''' if not remote_url: browser = webdriver_type() else: - browser = self._create_remote_web_driver(desired_cap_type,remote_url , desired_caps) + browser = self._create_remote_web_driver(desired_cap_type, remote_url, desired_caps) return browser - def _create_remote_web_driver(self , capabilities_type , remote_url , desired_capabilities=None , profile=None): + def _create_remote_web_driver(self, capabilities_type, remote_url, desired_capabilities=None, profile=None): '''parses the string based desired_capabilities if neccessary and creates the associated remote web driver''' @@ -632,7 +797,7 @@ def _create_remote_web_driver(self , capabilities_type , remote_url , desired_ca desired_capabilities_object.update(desired_capabilities or {}) return webdriver.Remote(desired_capabilities=desired_capabilities_object, - command_executor=str(remote_url), browser_profile=profile) + command_executor=str(remote_url), browser_profile=profile) def _parse_capabilities_string(self, capabilities_string): '''parses the string based desired_capabilities which should be in the form @@ -659,6 +824,7 @@ def execute(self, driver_command, params=None): if speed > 0: time.sleep(speed) return result + if not hasattr(browser, '_base_execute'): browser._base_execute = browser.execute browser.execute = types.MethodType(execute, browser) diff --git a/src/Selenium2Library/utils/browsercache.py b/src/Selenium2Library/utils/browsercache.py index 14667000b..3a1550b1f 100644 --- a/src/Selenium2Library/utils/browsercache.py +++ b/src/Selenium2Library/utils/browsercache.py @@ -1,4 +1,5 @@ from robot.utils import ConnectionCache +from robot.api.logger import debug class BrowserCache(ConnectionCache): @@ -17,7 +18,7 @@ def get_open_browsers(self): if browser not in self._closed: open_browsers.append(browser) return open_browsers - + def close(self): if self.current: browser = self.current @@ -31,3 +32,11 @@ def close_all(self): browser.quit() self.empty_cache() return self.current + + def register(self, connection, alias=None): + for browser in self._connections: + if browser.session_id == connection.session_id: # and connection.command_executor._url == browser.command_executor._url: + debug('Browser with session %s already registered. Marking previous one as obsolete' + % connection.session_id) + self._closed.add(browser) + return super(BrowserCache, self).register(connection, alias) diff --git a/test/acceptance/create_webdriver.robot b/test/acceptance/create_webdriver.robot index 6f902827d..adc790672 100644 --- a/test/acceptance/create_webdriver.robot +++ b/test/acceptance/create_webdriver.robot @@ -30,6 +30,64 @@ Create Webdriver With Bad Keyword Argument Dictionary Run Keyword And Expect Error kwargs must be a dictionary. ... Create Webdriver Firefox kwargs={'spam': 'eggs'} +Create Reusable Webdriver with alias + [Documentation] Reconnect to an existing browser session + [Setup] Set Driver Variables + Create Webdriver ${DRIVER_NAME} alias=dummy kwargs=${KWARGS} + ${file}= Save Webdriver + Restore Webdriver alias=dummy + ${file}= Save Webdriver + Restore Webdriver alias=dummy delete_file=${False} + Restore Webdriver alias=dummy + ${file}= Save Webdriver dummy.tmp + Restore Webdriver alias=dummy file=${file} + ${file}= Save Webdriver ${CURDIR}/dummy.tmp + Restore Webdriver alias=dummy file=${file} + ${file}= Save Webdriver + Restore Webdriver alias=dummy2 file=session_dummy.tmp + ${sid} ${url} ${pid} = Save Webdriver ${None} + Restore Webdriver alias=dummy session_id=${sid} session_url=${url} session_pid=${pid} + Go To ${FRONT PAGE} + Page Should Contain needle + [Teardown] Close Browser + +Create Reusable Webdriver without alias + [Documentation] Reconnect to an existing browser session + [Setup] Set Driver Variables + ${i1} = Create Webdriver ${DRIVER_NAME} kwargs=${KWARGS} + ${file}= Save Webdriver + ${i2} = Restore Webdriver file=${file} + log to console \n${i1}-${i2} + ${file}= Save Webdriver + Restore Webdriver file=${file} delete_file=${False} + Restore Webdriver file=${file} delete_file=${False} + Restore Webdriver alias=dummy file=${file} + ${file}= Save Webdriver dummy.tmp + Restore Webdriver file=${file} + ${file}= Save Webdriver ${CURDIR}/dummy.tmp + Restore Webdriver file=${file} + ${sid} ${url} ${pid} = Save Webdriver ${None} + Restore Webdriver session_id=${sid} session_url=${url} session_pid=${pid} + Go To ${FRONT PAGE} + Page Should Contain needle + [Teardown] Close Browser + +Create Reusable Webdriver with Invalid Arguments + [Documentation] Invalid arguments & run-time errors + run keyword and expect error *both alias and file* Restore Webdriver + run keyword and expect error *IOError*No such file* Restore Webdriver alias=foo + run keyword and expect error *IOError*No such file* Restore Webdriver file=foo.tmp + +Create Reusable Webdriver with runtime errors + [Documentation] run-time errors + [Setup] Set Driver Variables + run keyword and expect error *No browser is open* Save Webdriver + run keyword and expect error *URLError*refused* Restore Webdriver session_id=foo session_url=http://127.0.0.1:52470/foo + Create Webdriver ${DRIVER_NAME} kwargs=${KWARGS} + ${sid} ${url} ${pid} = Save Webdriver ${None} + run keyword and expect error ** Restore Webdriver session_id=bogus session_url=${url} + [Teardown] Close Browser + *** Keywords *** Set Driver Variables [Documentation] Selects proper driver diff --git a/test/resources/testserver/testserver.py b/test/resources/testserver/testserver.py index 19e82efc9..8c321c395 100644 --- a/test/resources/testserver/testserver.py +++ b/test/resources/testserver/testserver.py @@ -54,4 +54,4 @@ def start_server(path, port=7000): path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) start_server(path) else: - stop_server() + stop_server() \ No newline at end of file diff --git a/test/run_tests.py b/test/run_tests.py index 05f49029f..352877974 100755 --- a/test/run_tests.py +++ b/test/run_tests.py @@ -73,6 +73,10 @@ '--log', 'NONE', '--loglevel', 'DEBUG', '--pythonpath', os.pathsep.join([SRC_DIR, TEST_LIBS_DIR]), + #'--suite', 'Acceptance.Create_Webdriver', + #'-t', '*Functioning*', + #'-t', '*Reusable*', + '-b', 'debug.log', ] REBOT_OPTIONS = [ '--outputdir', RESULTS_DIR, @@ -81,7 +85,6 @@ '--noncritical', 'known issue {browser}', ] - def unit_tests(): print('Running unit tests') failures = run_unit_tests() diff --git a/test/unit/keywords/test_browsermanagement.py b/test/unit/keywords/test_browsermanagement.py index 2e52ec1ff..5ca3306aa 100644 --- a/test/unit/keywords/test_browsermanagement.py +++ b/test/unit/keywords/test_browsermanagement.py @@ -3,7 +3,7 @@ from mockito import when, mock, verify, verifyNoMoreInteractions from selenium import webdriver -from Selenium2Library.keywords.browsermanagement import BrowserManagementKeywords +from Selenium2Library.keywords.browsermanagement import BrowserManagementKeywords, _ReusableDriver class BrowserManagementTests(unittest.TestCase): @@ -120,6 +120,32 @@ def test_create_webdriver(self): finally: del webdriver.FakeWebDriver + def test_create_reusable_webdriver(self): + bm = _BrowserManagementWithLoggingStubs() + FakeCmdExecutor = mock() + FakeCmdExecutor._url = "http://127.0.0.1:9999/dummy" + FakeWebDriver = mock() + FakeWebDriver.command_executor = FakeCmdExecutor + FakeWebDriver.session_id = "dummy_id" + when(FakeWebDriver).__call__(some_arg=1).thenReturn(FakeWebDriver) + when(FakeWebDriver).__call__(some_arg=2).thenReturn(FakeWebDriver) + when(_ReusableDriver).execute("GetSessionData").thenReturn({'value': 'dummy'}) + webdriver.FakeWebDriver = FakeWebDriver + try: + bm.create_webdriver('FakeWebDriver', 'fake', some_arg=1) + self.assertEquals(FakeWebDriver.session_id, bm._current_browser().session_id) + self.assertEquals(FakeCmdExecutor._url, bm._current_browser().command_executor._url) + (sid, url, pid) = bm.save_webdriver(None) + self.assertEquals(FakeWebDriver.session_id, sid) + self.assertEquals(FakeCmdExecutor._url, url) + old_index = bm._cache.current_index + new_index = bm.restore_webdriver(session_id=sid, session_url=url) + self.assertNotEqual(old_index, new_index) + self.assertEquals(sid, FakeWebDriver.session_id) + self.assertEquals(url, FakeCmdExecutor._url) + finally: + del webdriver.FakeWebDriver + def verify_browser(self , webdriver_type , browser_name, **kw): #todo try lambda *x: was_called = true bm = BrowserManagementKeywords()