Skip to content

Commit

Permalink
QE-12169 Use a unique name for the jQuery used by cucu (#341)
Browse files Browse the repository at this point in the history
Some cucu functionalities, including fuzzy find, use jQuery library. in
these situations, cucu loads its own version of jQuery. However, if the
webpage already uses jQuery, the cucu jQuery will interfere with the
jQuery used by the webpage.
  • Loading branch information
ddl-xin authored Jun 23, 2023
1 parent caa43b8 commit 18067b9
Show file tree
Hide file tree
Showing 8 changed files with 109 additions and 94 deletions.
2 changes: 1 addition & 1 deletion features/cli/internals.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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 \<module\>
.*File ".*\/src\/cucu\/steps\/text_steps.py", line 30, in \<module\>
[\s\S]*
"""

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>", "Cedric Young <[email protected]>"]
Expand Down
75 changes: 35 additions & 40 deletions src/cucu/browser/frames.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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
2 changes: 0 additions & 2 deletions src/cucu/browser/selenium.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 4 additions & 1 deletion src/cucu/fuzzy/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand Down
40 changes: 20 additions & 20 deletions src/cucu/fuzzy/fuzzy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
);
Expand Down Expand Up @@ -76,18 +76,18 @@
/*
* <thing>name</thing>
*/
results = jQuery(thing + ':vis:' + matcher + '("' + name + '")', document.body).toArray();
results = jqCucu(thing + ':vis:' + matcher + '("' + name + '")', document.body).toArray();
if (cucu.debug) { console.log('<thing>name</thing>', results); }
elements = elements.concat(results);

// <thing attribute="name"></thing>
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('<thing attibute="name"></thing>', 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('<thing attibute*="name"></thing>', results); }
}
elements = elements.concat(results);
Expand All @@ -107,12 +107,12 @@

// <thing value="name"></thing>
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('<thing value="name"></thing>', 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('<thing value*="name"></thing>', results); }
Expand All @@ -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 = [];
Expand All @@ -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</*>...<thing id=...></thing>', results); }
elements = elements.concat(results);
}
Expand All @@ -149,14 +149,14 @@
/*
* <thing><*>...name...</*></thing>
*/
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('<thing><*>...name...</*></thing>', results); }
elements = elements.concat(results);

// <thing><* attribute="name"></*></thing>
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('<thing><* attibute="name"></*></thing>', results); }
elements = elements.concat(results);
}
Expand All @@ -171,7 +171,7 @@
/*
* <*><thing></thing>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('<*><thing></thing>name</*>', results); }
elements = elements.concat(results);
}
Expand All @@ -182,7 +182,7 @@
var thing = things[tIndex];

// <*>name</*><thing/>
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</*><thing/>', results); }
elements = elements.concat(results);
}
Expand All @@ -194,7 +194,7 @@
var thing = things[tIndex];

// <thing/><*>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('<thing/><*>name</*>', results); }
elements = elements.concat(results);
}
Expand All @@ -210,14 +210,14 @@
var thing = things[tIndex];

// <*>name</*>...<thing>...
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</*>...<thing>...', results); }
elements = elements.concat(results);

// <...><*>name</*></...>...<...><thing></...>
// 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</*></...>...<...><thing></...>', results); }
elements = elements.concat(results);
}
Expand All @@ -229,13 +229,13 @@
var thing = things[tIndex];

// next siblings: <thing>...<*>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('<thing>...<*>name</*>...', results); }
elements = elements.concat(results);

// <...><thing></...>...<...><*>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('<...><thin></...>...<...><*>name</*></...>', results); }
elements = elements.concat(results);
}
Expand Down
50 changes: 24 additions & 26 deletions src/cucu/steps/text_steps.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -38,37 +36,37 @@ 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(
'I match the regex "{regex}" on the current page and save the group "{name}" to the variable "{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)
27 changes: 24 additions & 3 deletions src/cucu/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
the src/cucu/__init__.py
"""
import logging
import pkgutil

from tabulate import DataRow, TableFormat, tabulate
from tenacity import (
Expand All @@ -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(
Expand Down Expand Up @@ -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

0 comments on commit 18067b9

Please sign in to comment.