Skip to content

Commit

Permalink
QE-13818 Helper template for 2 interacting elements, added draggables (
Browse files Browse the repository at this point in the history
…#381)

### Helper function

I was looking to implement steps in cucu for draggable elements and I
looked a little deeper at the helper code and found that there is a
potential enhancement that cucu could have, templatized steps for 2
interacting elements.

I started by implementing draggables as a proof of concept as a custom
step on e2e-test branch. I then transferred that code over to cucu
framework and had it working without any helper function implementation.

That same code is now embedded in a helper function called
`define_interaction_on_thing_with_name_steps` (final name pending)
basically this is what it does... It intertwines 3 functions.

1. find function for finding the element you want to make the action
with
2. find function for the finding the element you want the above element
to interact with
3. Action function that defines what happens between the two found
elements, in my case I found a draggable element, I found another
generic element and I performed a drag action between them.

It will be easiest to start on the `src/cucu/steps/draggable_steps.py`
and then see how the helper function from here `src/cucu/helpers.py` is
being used to allow for modularity across different applications.

### Draggable function

After going through some trials on different browsers I noticed that the
implementations for ActionChains was not consistent enough to be used in
cucu. I then pivoted and created a JS_DRAG_DROP global variable that is
then referenced in the `drag_element_to_element` function.

This took a bit of refining to get right. Waits needs to be put in place
to prevent the browser from executing too fast for the Webdriver to
realize anything has happened. And with that we need to stay synchronous
with python code that is being executed in tandem. That is where the
Webdriver wait comes in.

I setup a Webdriver wait to keep python executions and javascript
executions in sync.

`WebDriverWait(driver, 10).until(lambda driver:
driver.execute_script("return window.dragAndDropCompleted;"))`

This allows the drag and drop to be very consistent across all browser
types.
  • Loading branch information
ddl-michael-noonan authored Oct 31, 2023
1 parent 656ecc7 commit 8829462
Show file tree
Hide file tree
Showing 9 changed files with 499 additions and 5 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pygls.log
report
results
.monitor.png
geckodriver.log

# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project closely adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 0.173.0
- Add - functions and steps for dragging and dropping elements
- Add - add helper function for two interacting elements

## 0.172.0
- Add repository and homepage fields to solve an issue with pip/poetry failing to install this package.

Expand Down
53 changes: 53 additions & 0 deletions data/www/draggables.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html>
<head>
<style>
.draggable {
width: 100px;
height: 100px;
background-color: lightblue;
text-align: center;
padding: 10px;
margin: 10px;
}

.target {
width: 400px;
height: 500px;
background-color: lightgray;
margin: 10px;
float: left;
}
</style>
</head>
<body>
<div id="draggable1" class="draggable" draggable="true" ondragstart="dragStart(event)">Drag Me 1</div>
<div id="draggable2" class="draggable" draggable="true" ondragstart="dragStart(event)">Drag Me 2</div>
<div id="draggable3" class="draggable" draggable="true" ondragstart="dragStart(event)">Drag Me 3</div>


<div id="target1" class="target" ondragover="dragOver(event)" ondrop="drop(event)">Drop Here 1</div>
<div id="target2" class="target" ondragover="dragOver(event)" ondrop="drop(event)">Drop Here 2</div>
<div id="target3" class="target" ondragover="dragOver(event)" ondrop="drop(event)">Drop Here 3</div>
<div id="target4" class="target" >Drop Here 4</div>

<script>
function dragStart(e) {
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("Text", e.target.getAttribute("id"));
}

function dragOver(e) {
e.preventDefault();
e.stopPropagation();
}

function drop(e) {
e.stopPropagation();
e.preventDefault();
var data = e.dataTransfer.getData("Text");
e.target.appendChild(document.getElementById(data));
}
</script>
</body>
</html>
36 changes: 36 additions & 0 deletions features/browser/draggables.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
Feature: Draggables
As a developer I want to make sure the test writer can interact with different
draggable elements

Background: HTML page with draggables
Given I start a webserver at directory "data/www" and save the port to the variable "PORT"
And I open a browser at the url "http://{HOST_ADDRESS}:{PORT}/draggables.html"

Scenario: User can drag element
When I wait to see the text "Drag"
Then I should immediately see the "2nd" element "Drag Me" is draggable
And I drag the element "Drag Me 1" to the element "Drop Here 2"
And I drag the element "Drag Me 2" to the element "Drop Here 1"
And I drag the element "Drag Me 3" to the element "Drop Here 3"
And I should immediately see the "2nd" element "Drop Here" is not draggable

@negative
Scenario: User cannot drag element
When I wait to see the text "Drag"
Then I should immediately see the element "Drop Here 2" is not draggable
And I expect the following step to fail with "Unable to find the element \"Nonexistent drop\""
"""
When I drag the element "Drag Me 1" to the element "Nonexistent drop"
"""
And I expect the following step to fail with "Unable to find the element \"Nonexistent drag\""
"""
When I drag the element "Nonexistent drag" to the element "Drop Here 1"
"""
And I expect the following step to fail with "Unable to find the element \"Nonexistent drag\", Unable to find the element \"Nonexistent drop\""
"""
When I drag the element "Nonexistent drag" to the element "Nonexistent drop"
"""
And I expect the following step to fail with "Drag element Drag Me 1 position did not change"
"""
When I drag the element "Drag Me 1" to the element "Drop Here 4"
"""
34 changes: 34 additions & 0 deletions features/browser/duplicate_draggables.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
Feature: Duplicate Draggables
As a developer I want to make sure the test writer can interact with duplicate
draggable elements.

Background: HTML page with draggables
Given I start a webserver at directory "data/www" and save the port to the variable "PORT"
And I open a browser at the url "http://{HOST_ADDRESS}:{PORT}/draggables.html"

Scenario: User can drag nth element
When I wait to see the text "Drag"
Then I should immediately see the "2nd" element "Drag Me" is draggable
And I drag the "1st" element "Drag Me" to the "2nd" element "Drop Here"
And I drag the "2nd" element "Drag Me" to the "1st" element "Drop Here"
And I should immediately see the "2nd" element "Drop Here" is not draggable

@negative
Scenario: User cannot drag nth elements
Then I should immediately see the "3rd" element "Drop Here" is not draggable
And I expect the following step to fail with "Unable to find the \"3rd\" element \"Nonexistent drop\""
"""
When I drag the "2nd" element "Drag Me" to the "3rd" element "Nonexistent drop"
"""
And I expect the following step to fail with "Unable to find the \"2nd\" element \"Nonexistent drag\""
"""
When I drag the "2nd" element "Nonexistent drag" to the "3rd" element "Drop Here"
"""
And I expect the following step to fail with "Unable to find the \"2nd\" element \"Nonexistent drag\", Unable to find the \"3rd\" element \"Nonexistent drop\""
"""
When I drag the "2nd" element "Nonexistent drag" to the "3rd" element "Nonexistent drop"
"""
And I expect the following step to fail with "Drag element Drag Me 1 position did not change"
"""
When I drag the "1st" element "Drag Me" to the "4th" element "Drop Here"
"""
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.172.0"
version = "0.173.0"
license = "MIT"
description = "Easy BDD web testing"
authors = ["Domino Data Lab <[email protected]>"]
Expand Down
208 changes: 204 additions & 4 deletions src/cucu/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ def define_action_on_thing_with_name_steps(
parameters:
thing(string): name of the thing we're creating the steps for such
as button, dialog, etc.
action(stirng): the name of the action being performed, such as:
action(string): the name of the action being performed, such as:
click, disable, etc.
find_func(function): function that returns the desired element:
Expand Down Expand Up @@ -552,8 +552,8 @@ def base_should_see_the_in_state(ctx, thing, name, index=0):
logger.debug(f'{thing} {name} was in desired state "{state}"')

@step(f'I should immediately see the {thing} "{{name}}" is {state}')
def should_immedieately_see_the_in_state(ctx, thing, name, state, index=0):
base_should_see_the_in_state(ctx, thing, name, state, index=0)
def should_immediately_see_the_in_state(ctx, name, index=0):
base_should_see_the_in_state(ctx, thing, name, index=0)

@step(f'I should see the {thing} "{{name}}" is {state}')
def should_see_the_in_state(ctx, name):
Expand All @@ -579,7 +579,7 @@ def wait_up_to_seconds_to_see_the_in_state(ctx, seconds, name):
if with_nth:

@step(
f'I should_immediately see the "{{nth:nth}}" {thing} "{{name}}" is {state}'
f'I should immediately see the "{{nth:nth}}" {thing} "{{name}}" is {state}'
)
def base_should_see_the_nth_in_state(ctx, nth, name):
base_should_see_the_in_state(ctx, thing, name, index=nth)
Expand Down Expand Up @@ -669,3 +669,203 @@ def run_if_not_visibile(ctx, name):
retry_after_s=float(CONFIG["CUCU_SHORT_UI_RETRY_AFTER_S"]),
wait_up_to_s=float(CONFIG["CUCU_SHORT_UI_WAIT_TIMEOUT_S"]),
)(ctx, name)


def define_two_thing_interaction_steps(
action: str,
action_func,
thing_1,
thing_1_find_func,
preposition: str,
thing_2,
thing_2_find_func,
with_nth=False,
):
"""
defines steps with with the following signatures:
I {action} the {thing_1} "{name_1}" {preposition} the {thing_2} "{name_2}"
I wait to {action} the {thing_1} "{name_1}" {preposition} the {thing_2} "{name_2}"
I wait up to "{seconds}" seconds to {action} the {thing_1} "{name_1}" {preposition} the {thing_2} "{name_2}"
...
I {action} the {thing_1} "{name_1}" {preposition} the {thing_2} "{name_2}" if they both exist
when with_nth=True we also define:
I {action} the "{nth_1}" {thing_1} "{name_1}" {preposition} the "{nth_2}" {thing_2} "{name_2}"
I wait to {action} the "{nth_1}" {thing_1} "{name_1}" {preposition} the "{nth_2}" {thing_2} "{name_2}"
I wait up to "{seconds}" seconds to {action} the "{nth_1}" {thing_1} "{name_1}" {preposition} the "{nth_2}" {thing_2} "{name_2}"
...
I {action} the "{nth_1}" {thing_1} "{name_1}" {preposition} the "{nth_2}" {thing_2} "{name_2}" if they both exist
parameters:
action(string): the name of the action being performed, such as:
click, disable, etc.
action_func(function): function that performs the desired action:
def action_func(ctx, element, ):
'''
ctx(object): behave context object
element(object): the element found
'''
thing_1(string): name of the thing we're creating the steps for such
as button, dialog, etc.
thing_1_find_func(function): function that returns the desired element:
def thing_1_find_func(ctx, name_1, index_1=):
'''
ctx(object): behave context object
name_1(string):name of the thing to find
index_1(int): when there are multiple elements
with the same name and you've
specified with_nth=True
'''
preposition(string): preposition to help with readability as there are
many different prepositions that would be valid for
a desired action
thing_2(string): name of the thing that is being interacted with
from the defined action
thing_2_find_func(function): function that returns the interacted element:
def thing_2_find_func(ctx, name_2, index_2=):
'''
ctx(object): behave context object
name_2(string): name of the thing to find
index_1(int): when there are multiple elements
with the same name and you've
specified with_nth=True
'''
with_nth(bool): when set to True we'll define the expanded set of
"nth" steps. default: False
"""

# undecorated def for reference below
def base_action_the(
ctx,
thing_1,
name_1,
thing_2,
name_2,
index_1=0,
index_2=0,
):
prefix_1 = nth_to_ordinal(index_1)
prefix_2 = nth_to_ordinal(index_2)

element_1 = thing_1_find_func(ctx, name_1, index_1)
element_2 = thing_2_find_func(ctx, name_2, index_2)

if element_1 is None or element_2 is None:
error_message = []
if element_1 is None:
error_message.append(
f'Unable to find the {prefix_1}{thing_1} "{name_1}"'
)
if element_2 is None:
error_message.append(
f'Unable to find the {prefix_2}{thing_2} "{name_2}"'
)

raise RuntimeError(", ".join(error_message))

else:
action_func(ctx, element_1, element_2)
logger.debug(
f'Successfully executed {action} {prefix_1}{thing_1} "{name_1}" {preposition} {prefix_2}{thing_2} "{name_2}"'
)

@step(
f'I immediately {action} the {thing_1} "{{name_1}}" {preposition} the {thing_2} "{{name_2}}"'
)
def immediately_action_the(ctx, name_1, name_2):
base_action_the(ctx, thing_1, name_1, thing_2, name_2)

@step(
f'I {action} the {thing_1} "{{name_1}}" {preposition} the {thing_2} "{{name_2}}"'
)
def action_the(ctx, name_1, name_2):
retry(
base_action_the,
retry_after_s=float(CONFIG["CUCU_SHORT_UI_RETRY_AFTER_S"]),
wait_up_to_s=float(CONFIG["CUCU_SHORT_UI_WAIT_TIMEOUT_S"]),
)(ctx, thing_1, name_1, thing_2, name_2)

@step(
f'I wait to {action} the {thing_1} "{{name_1}}" {preposition} the {thing_2} "{{name_2}}"'
)
def wait_to_action_the(ctx, name_1, name_2):
retry(base_action_the)(ctx, thing_1, name_1, thing_2, name_2)

@step(
f'I wait up to "{{seconds}}" seconds to {action} the {thing_1} "{{name_1}}" {preposition} the {thing_2} "{{name_2}}"'
)
def wait_up_to_seconds_to_action_the(ctx, seconds, name_1, name_2):
seconds = float(seconds)
retry(base_action_the, wait_up_to_s=seconds)(
ctx, thing_1, name_1, thing_2, name_2
)

if with_nth:

@step(
f'I immediately {action} the "{{nth_1:nth}}" {thing_1} "{{name_1}}" {preposition} the "{{nth_2:nth}}" {thing_2} "{{name_2}}"'
)
def immediately_action_the_nth_i_nth(ctx, nth_1, name_1, nth_2, name_2):
base_action_the(
ctx,
thing_1,
name_1,
thing_2,
name_2,
index_1=nth_1,
index_2=nth_2,
)

@step(
f'I {action} the "{{nth_1:nth}}" {thing_1} "{{name_1}}" {preposition} the "{{nth_2:nth}}" {thing_2} "{{name_2}}"'
)
def action_the_nth_i_nth(ctx, nth_1, name_1, nth_2, name_2):
retry(
base_action_the,
retry_after_s=float(CONFIG["CUCU_SHORT_UI_RETRY_AFTER_S"]),
wait_up_to_s=float(CONFIG["CUCU_SHORT_UI_WAIT_TIMEOUT_S"]),
)(
ctx,
thing_1,
name_1,
thing_2,
name_2,
index_1=nth_1,
index_2=nth_2,
)

@step(
f'I wait to {action} the "{{nth_1:nth}}" {thing_1} "{{name_1}}" {preposition} the "{{nth_2:nth}}" {thing_2} "{{name_2}}"'
)
def wait_to_action_the_nth_ith(ctx, nth_1, name_1, nth_2, name_2):
retry(base_action_the)(
ctx,
thing_1,
name_1,
thing_2,
name_2,
index_1=nth_1,
index_2=nth_2,
)

@step(
f'I wait up to "{{seconds}}" seconds to {action} the "{{nth_1:nth}}" {thing_1} "{{name_1}}" {preposition} the "{{nth_2:nth}}" {thing_2} "{{name_2}}"'
)
def wait_up_to_action_the_nth_i_nth(
ctx, seconds, nth_1, name_1, nth_2, name_2
):
seconds = float(seconds)
retry(base_action_the, wait_up_to_s=seconds)(
ctx,
thing_1,
name_1,
thing_2,
name_2,
index_1=nth_1,
index_2=nth_2,
)
1 change: 1 addition & 0 deletions src/cucu/steps/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import cucu.steps.button_steps
import cucu.steps.command_steps
import cucu.steps.checkbox_steps
import cucu.steps.draggable_steps
import cucu.steps.dropdown_steps
import cucu.steps.filesystem_steps
import cucu.steps.image_steps
Expand Down
Loading

0 comments on commit 8829462

Please sign in to comment.