From 957893f7e9652e24ef23093a593ebd5ab3584739 Mon Sep 17 00:00:00 2001 From: Tasos Papaioannou Date: Tue, 19 Nov 2019 13:16:13 -0500 Subject: [PATCH] Add regex capability to match_messages --- src/widgetastic_patternfly/__init__.py | 78 +++++++++++++++++++------- testing/test_flashmessages.py | 44 +++++++++++++++ testing/testing_page.html | 59 +++++++++++++++++-- 3 files changed, 156 insertions(+), 25 deletions(-) create mode 100644 testing/test_flashmessages.py diff --git a/src/widgetastic_patternfly/__init__.py b/src/widgetastic_patternfly/__init__.py index d879df7..c57cf3d 100644 --- a/src/widgetastic_patternfly/__init__.py +++ b/src/widgetastic_patternfly/__init__.py @@ -274,25 +274,55 @@ def dismiss(self): message.dismiss() def match_messages(self, text=None, t=None, partial=False, inverse=False): + """ + Return a list of flash messages matching the specified alert text and type(s) + + Args: + text: text to compare against each flash message's text attribute. This parameter + can be either a string, in which case normal string comparison will be performed, + or a compiled regular expression, in which case it will use re.match() + (default: None). + + t: an alert type string or tuple/list/set of types, to compare against each flash + message's type attribute (default: None). + + partial: If partial is False, and if `text` is a string, then a message will be + considered a match if the message's text attribute and `text` are equal. If partial + is True, then `text` needs only be contained in the message's text to be considered + a match. This argument has no effect if `text` is a compiled regular expression + (default: False). + + inverse: If inverse is False, then the match is performed as described above. If + inverse is True, then the matching logic is inverted, and only messages that fail + to match will be returned. + + Returns: + List of matching FlashMessage objects. + """ msg_type = t if isinstance(t, (tuple, list, set, type(None))) else (t, ) - # below is a little bit tricky statement - # if inverse is True, further comparison statements will be treated as is - # and inverted by not_ otherwise + # If inverse is True, further comparison statements will be treated as is. + # Otherwise, they will be inverted by not_(). op = bool if inverse else not_ - log_part = 'partial' if partial else 'exact' - if t is not None: - self.logger.info('%s match of the flash message %r of type %r', log_part, text, t) + log_inverse = "inverse " if inverse else "" + log_type = f", type: {t!r}" if msg_type else "" + if isinstance(text, Pattern): + log_part = "pattern" + log_text = f", pattern: {text.pattern!r}" else: - self.logger.info('%s match of the flash message %r', log_part, text) + log_part = "partial" if partial else "exact" + log_text = f", text: {text!r}" if text else "" + log_msg = f"Performing {log_inverse}{log_part} match of flash messages{log_text}{log_type}" + self.logger.info(log_msg) + matched_messages = [] for message in self.messages: if msg_type and op(message.type in msg_type): continue - - if text and op((partial and text in message.text) or - (not partial and text == message.text)): + if isinstance(text, Pattern) and op(text.match(message.text)): + continue + if isinstance(text, str) and op((partial and text in message.text) or + (not partial and text == message.text)): continue - matched_messages.append(message) return matched_messages @@ -309,7 +339,13 @@ def assert_no_error(self, wait=0): return True def assert_message(self, text, t=None, partial=False, wait=0): - log_part = 'partial' if partial else 'exact' + # If text is a compiled regex instead of a string, log it as a pattern match. + if isinstance(text, Pattern): + log_part = 'pattern' + log_text = text.pattern + else: + log_part = 'partial' if partial else 'exact' + log_text = text try: msgs = wait_for(self.match_messages, func_kwargs=dict(text=text, t=t, partial=partial), timeout=wait)[0] @@ -319,11 +355,13 @@ def assert_message(self, text, t=None, partial=False, wait=0): raise TimedOutError except TimedOutError: if t is not None: - e_text = '{}: {}'.format(t, text) + e_text = f"{t}: {log_text}" else: - e_text = text - raise AssertionError('assert {} match of message: {}. \n Available messages: {}' - .format(log_part, e_text, [msg.text for msg in self.messages])) + e_text = log_text + msg_text = [msg.text for msg in self.messages] + log_msg = (f'assert {log_part} match of message: {e_text}.\n ' + f'Available messages: {msg_text}') + raise AssertionError(log_msg) def assert_success_message(self, text, t=None, partial=False, wait=0): self.assert_no_error(wait=wait) @@ -1284,7 +1322,7 @@ def get_item_by_nodeid(self, nodeid): except NoSuchElementException: raise CandidateNotFound({ 'message': - 'Could not find the item with nodeid {} in Boostrap tree {}'.format( + 'Could not find the item with nodeid {} in Bootstrap tree {}'.format( nodeid, self.tree_id), 'path': '', @@ -1430,7 +1468,7 @@ def expand_path(self, *path, **kwargs): if not self.validate_node(node, step, image): raise CandidateNotFound({ 'message': - 'Could not find the item {} in Boostrap tree {}'.format( + 'Could not find the item {} in Bootstrap tree {}'.format( self.pretty_path(steps_tried), self.tree_id), 'path': path, @@ -1444,7 +1482,7 @@ def expand_path(self, *path, **kwargs): if node is not None and not self.expand_node(self.get_nodeid(node)): raise CandidateNotFound({ 'message': - 'Could not find the item {} in Boostrap tree {}'.format( + 'Could not find the item {} in Bootstrap tree {}'.format( self.pretty_path(steps_tried), self.tree_id), 'path': path, @@ -1462,7 +1500,7 @@ def expand_path(self, *path, **kwargs): else: raise CandidateNotFound({ 'message': - 'Could not find the item {} in Boostrap tree {}'.format( + 'Could not find the item {} in Bootstrap tree {}'.format( self.pretty_path(steps_tried), self.tree_id), 'path': path, diff --git a/testing/test_flashmessages.py b/testing/test_flashmessages.py new file mode 100644 index 0000000..0339224 --- /dev/null +++ b/testing/test_flashmessages.py @@ -0,0 +1,44 @@ +import re +from collections import namedtuple + +from widgetastic.widget import View +from widgetastic_patternfly import FlashMessages + +Message = namedtuple('Message', 'text type') + +MSGS = [Message('Retirement date set to 12/31/19 15:55 UTC', 'success'), + Message('Retirement date removed', 'success'), + Message('All changes have been reset', 'warning'), + Message('Set/remove retirement date was cancelled by the user', 'success'), + Message('Retirement initiated for 1 VM and Instance from the CFME Database', 'success')] + + +def test_flashmessage(browser): + class TestView(View): + flash = FlashMessages('.//div[@id="flash_msg_div"]') + + view = TestView(browser) + msgs = view.flash.read() + assert len(msgs) == len(MSGS) + for i in range(len(msgs)): + assert msgs[i] == MSGS[i].text + + view.flash.assert_no_error() + + # regex match + t = re.compile('^Retirement') + view.flash.assert_message(t) + view.flash.assert_success_message(t) + + # partial match + t = 'etirement' + view.flash.assert_message(t, partial=True) + view.flash.assert_success_message(t, partial=True) + + # inverse match + t = 'This message does not exist' + assert not view.flash.match_messages(t) + assert view.flash.match_messages(t, inverse=True) + + view.flash.dismiss() + assert not view.flash.read() diff --git a/testing/testing_page.html b/testing/testing_page.html index 186a1af..6440ebd 100644 --- a/testing/testing_page.html +++ b/testing/testing_page.html @@ -76,7 +76,7 @@

Widgetastic About Modal

- +