Jan Kovařík
Jan Kovařík

Reputation: 1582

Capture Screenshot on Test failure in complex Test Suite

I'd like to setup a nice way to capture screenshots when some of our Robot Framework front-end test fails. Of course I can add:

Run Keyword If Test Failed    Capture Page Screenshot

To test Teardown, but considering I have huge and complex test suites with hundreds of tests and a nested structure - I would need to add this to so many Teardowns that it seems ugly to me.

I've experimented a bit. I thought the way forward was to use listener. So I tried this:

class ExtendedSelenium(Selenium2Library):
    ROBOT_LISTENER_API_VERSION = 3

    def __init__(self, timeout=5.0, implicit_wait=0.0, run_on_failure='Capture Page Screenshot', screenshot_root_directory=None):
        super(ExtendedSelenium, self).__init__(timeout, implicit_wait, run_on_failure, screenshot_root_directory)
        self.ROBOT_LIBRARY_LISTENER = self

    def _end_test(self, data, result):
        if not result.passed:
            screenshot_name = data.id + data.name.replace(" ", "_") + ".png"
            try:
                self.capture_page_screenshot(screenshot_name)
            except:
                pass

It captures the screenshot but the picture is then not visible in the log. I'm able to display it in the test message, adding this step after capturing:

BuiltIn().set_test_message("*HTML* {} <br/> <img src={}>".format(result.message, screenshot_name))

But still, it isn't the best.

Then I tried a different approach with the Visitor interface (used with --prerunmodifier):

from robot.api import SuiteVisitor


class CaptureScreenshot(SuiteVisitor):

    def end_test(self, test):
        if test.status == 'FAIL':
            test.keywords.create('Capture Page Screenshot', type='teardown')

But it replaces any existing Test Teardown by the new one (with only one keyword 'Capture Page Screenshot'). I thought I would be able to modify existing Teardowns by adding the Capture keyword, but I wasn't.

Is there any nice, clean, pythonic way to do this? Did I miss something?

Upvotes: 0

Views: 1711

Answers (2)

geoappdeveloper
geoappdeveloper

Reputation: 1

I wrapped everything in a try/except block for my tests. That way at any point if the test fails the screen shot is in the except block and I get a screen shot the moment before/after/...whatever.

try:
    class UnitTestRunner():
        ...
except:
    driver.save_screenshot('filepath')

Upvotes: 0

Jan Kovař&#237;k
Jan Kovař&#237;k

Reputation: 1582

Finally it ended up with library listener as below. I have to agree with Bryan: the code isn't short and nice but it fulfils the desired goal - single point in suite where screenshot capturing is defined. As a big advantage I see the possibility to capture screenshot for failed setup - in some cases it helps us to identify possible infrastructure problems. Please note the part with ActionChains - it zooms out in browser. Our front-end app uses partial page scroll and with this zoom we are able to see more content inside of that scroll which is really helpful for us. Result of this ActionChains differs for each browser, so this is truly workaround.

#!/usr/bin/python
# -*- coding: utf-8 -*-
from Selenium2Library import Selenium2Library
from selenium.common.exceptions import StaleElementReferenceException, WebDriverException
import re


class ExtendedSelenium(Selenium2Library):
    """ Robot Framework library extending Robot Framework Selenium2Library library.
    """
    ROBOT_LISTENER_API_VERSION = 2
    DON_NOT_CAPTURE_KEYWORDS = ["Run Keyword And Ignore Error", "Run Keyword And Expect Error", "Run Keyword And Return Status", "Wait Until.*"]

    def __init__(self, timeout=5.0, implicit_wait=0.0, run_on_failure='', screenshot_root_directory=None):
        super(ExtendedSelenium, self).__init__(timeout, implicit_wait, run_on_failure, screenshot_root_directory)
        self.ROBOT_LIBRARY_LISTENER = self
        self._is_current_keyword_inside_teardown = False
        self._do_not_capture_parent_keywords_count = 0
        self._screenshot_was_captured = False

    def _start_test(self, name, attributes):
        """ Reset flags at the begin of each test.
        """
        self._do_not_capture_parent_keywords_count = 0
        self._is_current_keyword_inside_teardown = False
        self._screenshot_was_captured = False

    def _start_keyword(self, name, attributes):
        """ Set keyword flag at the beginning of teardown.
        If the keyword is one of the 'do not capture keywords' increase _do_not_capture_parent_keywords_count counter.
        """
        if attributes["type"] == "Teardown":
            self._is_current_keyword_inside_teardown = True
        if any(kw for kw in self.DON_NOT_CAPTURE_KEYWORDS if re.match(kw, attributes["kwname"])):
            self._do_not_capture_parent_keywords_count += 1

    def _end_keyword(self, name, attributes):
        """If the keyword is one of the 'do not capture keywords' decrease _do_not_capture_parent_keywords_count counter.
        Capture Screenshot if:
        - keyword failed AND
        - test is not in teardown phase AND
        - the parent keyword isn't one of the 'do not capture keywords'
        RuntimeError exception is thrown when no browser is open (backend test), no screenshot is captured in this case.
        """
        if any(kw for kw in self.DON_NOT_CAPTURE_KEYWORDS if re.match(kw, attributes["kwname"])):
            self._do_not_capture_parent_keywords_count -= 1
        if not attributes["status"] == "PASS" and not self._is_current_keyword_inside_teardown and self._do_not_capture_parent_keywords_count == 0 and not self._screenshot_was_captured:
            self._screenshot_was_captured = True
            try:
                self.capture_page_screenshot()
                # TODO refactor this so it is reusable and nice!
                from selenium.webdriver.common.action_chains import ActionChains
                from selenium.webdriver.common.keys import Keys
                ActionChains(super(ExtendedSelenium, self)._current_browser()).send_keys(Keys.CONTROL, Keys.SUBTRACT, Keys.NULL).perform()
                ActionChains(super(ExtendedSelenium, self)._current_browser()).send_keys(Keys.CONTROL, Keys.SUBTRACT, Keys.NULL).perform()
                self.capture_page_screenshot()
                ActionChains(super(ExtendedSelenium, self)._current_browser()).send_keys(Keys.CONTROL, '0', Keys.NULL).perform()
            except RuntimeError:
                pass

Any comments are welcome.

Upvotes: 1

Related Questions