diff --git a/features/cli/internals.feature b/features/cli/internals.feature index fb0a9a3e..0484c910 100644 --- a/features/cli/internals.feature +++ b/features/cli/internals.feature @@ -7,7 +7,7 @@ Feature: Internals Then I should see "{STDOUT}" matches the following: """ [\s\S]* - .*File ".*\/src\/cucu\/steps\/text_steps.py", line 32, in \ + .*File ".*\/src\/cucu\/steps\/text_steps.py", line 30, in \ [\s\S]* """ diff --git a/pyproject.toml b/pyproject.toml index 71bffb11..76cf6791 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "cucu" -version = "0.137.0" +version = "0.138.0" license = "MIT" description = "Easy BDD web testing" authors = ["Rodney Gomes ", "Cedric Young "] diff --git a/src/cucu/browser/frames.py b/src/cucu/browser/frames.py index ca55c0fc..951ae2f7 100644 --- a/src/cucu/browser/frames.py +++ b/src/cucu/browser/frames.py @@ -1,12 +1,4 @@ -import pkgutil - - -def load_jquery_lib(): - """ - load jquery library - """ - jquery_lib = pkgutil.get_data("cucu", "external/jquery/jquery-3.5.1.min.js") - return jquery_lib.decode("utf8") +from cucu.browser.core import Browser def search_in_all_frames(browser, search_function): @@ -77,35 +69,38 @@ def run_in_all_frames(browser, search_function): return result -def search_text_in_all_frames(browser, search_function, value): +def try_in_frames_until_success(browser: Browser, function_to_run) -> None: + """ + Run the function on all of the possible frames one by one. It terminates + if the function doesn't raise an exception on a frame. + + Warning: This leaves the browser in whatever frame the function is run successfully + so that users of the this method are in that frame. + + Args: + browser (Browser): the browser session + function_to_run : a function that raises an exception if it fails + + Raises: + RuntimeError: when the function fails in all frames + """ + browser.switch_to_default_frame() try: - search_function(value=value) - except RuntimeError: - # we might have not been in the default frame so check again - browser.switch_to_default_frame() - text = browser.execute( - 'return jQuery("body").children(":visible").text();' - ) - try: - search_function(value=text) - except RuntimeError: - frames = browser.execute( - 'return document.querySelectorAll("iframe");' - ) - for frame in frames: - # need to be in the default frame in order to switch to a child - # frame w/o getting a stale element exception - browser.switch_to_default_frame() - browser.switch_to_frame(frame) - browser.execute(load_jquery_lib()) - text = browser.execute( - 'return jQuery("body").children(":visible").text();' - ) - try: - search_function(value=text) - except RuntimeError as e: - if frames.index(frame) < len(frames) - 1: - continue - else: - raise RuntimeError(e) - return + function_to_run() + except Exception: + frames = browser.execute('return document.querySelectorAll("iframe");') + for frame in frames: + # need to be in the default frame in order to switch to a child + # frame w/o getting a stale element exception + browser.switch_to_default_frame() + browser.switch_to_frame(frame) + try: + function_to_run() + except Exception: + if frames.index(frame) < len(frames) - 1: + continue + else: + raise RuntimeError( + f"{function_to_run.__name__} failed in all frames" + ) + return diff --git a/src/cucu/browser/selenium.py b/src/cucu/browser/selenium.py index 64faa0d1..82966d83 100644 --- a/src/cucu/browser/selenium.py +++ b/src/cucu/browser/selenium.py @@ -261,11 +261,9 @@ def click(self, element): def switch_to_default_frame(self): self.driver.switch_to.default_content() - logger.debug("switched browser to the default frame") def switch_to_frame(self, frame): self.driver.switch_to.frame(frame) - logger.debug(f"switched browser to an iframe: {frame}") def screenshot(self, filepath): self.driver.get_screenshot_as_file(filepath) diff --git a/src/cucu/fuzzy/core.py b/src/cucu/fuzzy/core.py index b43a931f..94544034 100644 --- a/src/cucu/fuzzy/core.py +++ b/src/cucu/fuzzy/core.py @@ -28,7 +28,10 @@ def init(browser): browser - ... """ browser.execute(load_jquery_lib()) - script = "return window.jQuery && jQuery.fn.jquery;" + + # to prevent interference with the jQuery used on the web page + browser.execute("window.jqCucu = jQuery.noConflict(true);") + script = "return window.jqCucu && jqCucu.fn.jquery;" jquery_version = browser.execute(script) while jquery_version is None or not jquery_version.startswith("3.5.1"): diff --git a/src/cucu/fuzzy/fuzzy.js b/src/cucu/fuzzy/fuzzy.js index fda253cc..4a9256c9 100644 --- a/src/cucu/fuzzy/fuzzy.js +++ b/src/cucu/fuzzy/fuzzy.js @@ -13,14 +13,14 @@ * one visible parent. * */ - jQuery.extend( - jQuery.expr[ ":" ], + jqCucu.extend( + jqCucu.expr[ ":" ], { has_text: function(elem, index, match) { - return (elem.textContent || elem.innerText || jQuery(elem).text() || '') === match[3].trim(); + return (elem.textContent || elem.innerText || jqCucu(elem).text() || '') === match[3].trim(); }, vis: function (elem) { - return !(jQuery(elem).is(":hidden") || jQuery(elem).parents(":hidden").length); + return !(jqCucu(elem).is(":hidden") || jqCucu(elem).parents(":hidden").length); } } ); @@ -76,7 +76,7 @@ /* * name */ - results = jQuery(thing + ':vis:' + matcher + '("' + name + '")', document.body).toArray(); + results = jqCucu(thing + ':vis:' + matcher + '("' + name + '")', document.body).toArray(); if (cucu.debug) { console.log('name', results); } elements = elements.concat(results); @@ -84,10 +84,10 @@ for(var aIndex=0; aIndex < attributes.length; aIndex++) { var attribute_name = attributes[aIndex]; if (matcher == 'has_text') { - results = jQuery(thing + '[' + attribute_name + '="' + name + '"]:vis', document.body).toArray(); + results = jqCucu(thing + '[' + attribute_name + '="' + name + '"]:vis', document.body).toArray(); if (cucu.debug) { console.log('', results); } } else if (matcher == 'contains') { - results = jQuery(thing + '[' + attribute_name + '*="' + name + '"]:vis', document.body).toArray(); + results = jqCucu(thing + '[' + attribute_name + '*="' + name + '"]:vis', document.body).toArray(); if (cucu.debug) { console.log('', results); } } elements = elements.concat(results); @@ -107,12 +107,12 @@ // if (matcher == 'has_text') { - results = jQuery(thing + ':vis', document.body).filter(function(){ + results = jqCucu(thing + ':vis', document.body).filter(function(){ return this.value == name; }).toArray(); if (cucu.debug) { console.log('', results); } } else if (matcher == 'contains') { - results = jQuery(thing + ':vis', document.body).filter(function(){ + results = jqCucu(thing + ':vis', document.body).filter(function(){ return this.value !== undefined && String(this.value).indexOf(name) != -1; }).toArray(); if (cucu.debug) { console.log('', results); } @@ -123,7 +123,7 @@ /* * element labeled by another using the for/id attributes */ - var labels = jQuery('*[for]:vis:' + matcher + '("' + name + '")', document.body).toArray(); + var labels = jqCucu('*[for]:vis:' + matcher + '("' + name + '")', document.body).toArray(); for(var tIndex = 0; tIndex < things.length; tIndex++) { var thing = things[tIndex]; results = []; @@ -134,7 +134,7 @@ for(var lIndex=0; lIndex < labels.length; lIndex++) { var label = labels[lIndex]; var id = label.getAttribute('for'); - results = jQuery(thing + '[id="' + id + '"]:vis', document.body).toArray(); + results = jqCucu(thing + '[id="' + id + '"]:vis', document.body).toArray(); if (cucu.debug) { console.log('<* for=...>name...', results); } elements = elements.concat(results); } @@ -149,14 +149,14 @@ /* * <*>...name... */ - results = jQuery('*:vis:' + matcher + '("' + name + '")', document.body).parents(thing).toArray(); + results = jqCucu('*:vis:' + matcher + '("' + name + '")', document.body).parents(thing).toArray(); if (cucu.debug) { console.log('<*>...name...', results); } elements = elements.concat(results); // <* attribute="name"> for(var aIndex=0; aIndex < attributes.length; aIndex++) { var attribute_name = attributes[aIndex]; - results = jQuery('*:vis[' + attribute_name + '="' + name + '"]', document.body).parents(thing).toArray(); + results = jqCucu('*:vis[' + attribute_name + '="' + name + '"]', document.body).parents(thing).toArray(); if (cucu.debug) { console.log('<* attibute="name">', results); } elements = elements.concat(results); } @@ -171,7 +171,7 @@ /* * <*>name */ - results = jQuery('*:vis:has_text("' + name + '")', document.body).children(thing + ':vis').toArray(); + results = jqCucu('*:vis:has_text("' + name + '")', document.body).children(thing + ':vis').toArray(); if (cucu.debug) { console.log('<*>name', results); } elements = elements.concat(results); } @@ -182,7 +182,7 @@ var thing = things[tIndex]; // <*>name - results = jQuery('*:vis:' + matcher + '("' + name + '")', document.body).next(thing + ':vis').toArray(); + results = jqCucu('*:vis:' + matcher + '("' + name + '")', document.body).next(thing + ':vis').toArray(); if (cucu.debug) { console.log('<*>name', results); } elements = elements.concat(results); } @@ -194,7 +194,7 @@ var thing = things[tIndex]; // <*>name - results = jQuery('*:vis:' + matcher + '("' + name + '")', document.body).prev(thing).toArray(); + results = jqCucu('*:vis:' + matcher + '("' + name + '")', document.body).prev(thing).toArray(); if (cucu.debug) { console.log('<*>name', results); } elements = elements.concat(results); } @@ -210,14 +210,14 @@ var thing = things[tIndex]; // <*>name...... - results = jQuery('*:vis:' + matcher + '("' + name + '")', document.body).nextAll(thing + ':vis').toArray(); + results = jqCucu('*:vis:' + matcher + '("' + name + '")', document.body).nextAll(thing + ':vis').toArray(); if (cucu.debug) { console.log('<*>name......', results); } elements = elements.concat(results); // <...><*>name...<...> // XXX: this rule is horribly complicated and I'd rather see it gone // basically: common great grandpranet - results = jQuery('*:vis:' + matcher + '("' + name + '")', document.body).nextAll().find(thing + ':vis').toArray(); + results = jqCucu('*:vis:' + matcher + '("' + name + '")', document.body).nextAll().find(thing + ':vis').toArray(); if (cucu.debug) { console.log('<...><*>name...<...>', results); } elements = elements.concat(results); } @@ -229,13 +229,13 @@ var thing = things[tIndex]; // next siblings: ...<*>name... - results = jQuery('*:vis:' + matcher + '("' + name + '")', document.body).prevAll(thing).toArray(); + results = jqCucu('*:vis:' + matcher + '("' + name + '")', document.body).prevAll(thing).toArray(); if (cucu.debug) { console.log('...<*>name...', results); } elements = elements.concat(results); // <...>...<...><*>name // XXX: this rule is horribly complicated and I'd rather see it gone - results = jQuery('*:vis:' + matcher + '("' + name + '")', document.body).prevAll().find(thing + ':vis').toArray(); + results = jqCucu('*:vis:' + matcher + '("' + name + '")', document.body).prevAll().find(thing + ':vis').toArray(); if (cucu.debug) { console.log('<...>...<...><*>name', results); } elements = elements.concat(results); } diff --git a/src/cucu/steps/text_steps.py b/src/cucu/steps/text_steps.py index ff705583..58456546 100644 --- a/src/cucu/steps/text_steps.py +++ b/src/cucu/steps/text_steps.py @@ -1,9 +1,7 @@ -from functools import partial - from cucu import fuzzy, helpers, step -from cucu.browser.frames import search_text_in_all_frames -from cucu.fuzzy.core import load_jquery_lib +from cucu.browser.frames import try_in_frames_until_success from cucu.steps import step_utils +from cucu.utils import text_in_current_frame def find_text(ctx, name, index=0): @@ -38,14 +36,14 @@ def find_text(ctx, name, index=0): ) def search_for_regex_to_page_and_save(ctx, regex, name, variable): ctx.check_browser_initialized() - ctx.browser.execute(load_jquery_lib()) - text = ctx.browser.execute( - 'return jQuery("body").children(":visible").text();' - ) - search_function = partial( - step_utils.search_and_save, regex=regex, name=name, variable=variable - ) - search_text_in_all_frames(ctx.browser, search_function, value=text) + + def search_for_regex_in_frame(): + text = text_in_current_frame(ctx.browser) + step_utils.search_and_save( + regex=regex, value=text, name=name, variable=variable + ) + + try_in_frames_until_success(ctx.browser, search_for_regex_in_frame) @step( @@ -53,22 +51,22 @@ def search_for_regex_to_page_and_save(ctx, regex, name, variable): ) def match_for_regex_to_page_and_save(ctx, regex, name, variable): ctx.check_browser_initialized() - ctx.browser.execute(load_jquery_lib()) - text = ctx.browser.execute( - 'return jQuery("body").children(":visible").text();' - ) - search_function = partial( - step_utils.match_and_save, regex=regex, name=name, variable=variable - ) - search_text_in_all_frames(ctx.browser, search_function, value=text) + + def match_for_regex_in_frame(): + text = text_in_current_frame(ctx.browser) + step_utils.match_and_save( + regex=regex, value=text, name=name, variable=variable + ) + + try_in_frames_until_success(ctx.browser, match_for_regex_in_frame) @step('I should see text matching the regex "{regex}" on the current page') def search_for_regex_on_page(ctx, regex): ctx.check_browser_initialized() - ctx.browser.execute(load_jquery_lib()) - text = ctx.browser.execute( - 'return jQuery("body").children(":visible").text();' - ) - search_function = partial(step_utils.search, regex=regex) - search_text_in_all_frames(ctx.browser, search_function, value=text) + + def search_for_regex_in_frame(): + text = text_in_current_frame(ctx.browser) + step_utils.search(regex=regex, value=text) + + try_in_frames_until_success(ctx.browser, search_for_regex_in_frame) diff --git a/src/cucu/utils.py b/src/cucu/utils.py index 1e680852..2c4577df 100644 --- a/src/cucu/utils.py +++ b/src/cucu/utils.py @@ -3,6 +3,7 @@ the src/cucu/__init__.py """ import logging +import pkgutil from tabulate import DataRow, TableFormat, tabulate from tenacity import ( @@ -11,11 +12,10 @@ stop_after_delay, wait_fixed, ) -from tenacity import ( - retry as retrying, -) +from tenacity import retry as retrying from cucu import logger +from cucu.browser.core import Browser from cucu.config import CONFIG GHERKIN_TABLEFORMAT = TableFormat( @@ -123,3 +123,24 @@ def new_decorator(*args, **kwargs): return func(*args, **kwargs) return new_decorator + + +def load_jquery_lib(): + """ + load jquery library + """ + jquery_lib = pkgutil.get_data("cucu", "external/jquery/jquery-3.5.1.min.js") + return jquery_lib.decode("utf8") + + +def text_in_current_frame(browser: Browser) -> str: + """ + Utility to get all the visible text of the current frame. + + Args: + browser (Browser): the browser session switched to the desired frame + """ + browser.execute(load_jquery_lib()) + browser.execute("window.jqCucu = jQuery.noConflict(true);") + text = browser.execute('return jqCucu("body").children(":visible").text();') + return text