Skip to content

Commit 78e197a

Browse files
authored
Merge pull request #288 from digitronik/enhance_click
Some extra click methods arguments for playwright
2 parents 2c303f0 + efc0e0c commit 78e197a

4 files changed

Lines changed: 192 additions & 31 deletions

File tree

src/widgetastic/browser.py

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from typing import Type
2828
from typing import TYPE_CHECKING
2929
from typing import Union
30+
from typing import Literal
3031
import warnings
3132

3233
from cached_property import cached_property
@@ -846,8 +847,12 @@ def highlight(self, locator: LocatorAlias, *args, **kwargs) -> None:
846847
def click(
847848
self,
848849
locator: LocatorAlias,
849-
button: str = "left",
850-
no_wait_after: bool = False,
850+
button: Optional[Literal["left", "middle", "right"]] = "left",
851+
click_count: Optional[int] = None,
852+
delay: Optional[float] = None,
853+
force: Optional[bool] = None,
854+
no_wait_after: Optional[bool] = None,
855+
timeout: Optional[float] = None,
851856
*args,
852857
**kwargs,
853858
) -> None:
@@ -856,27 +861,40 @@ def click(
856861
Args:
857862
locator: Element locator to click on
858863
button: Mouse button to click with ("left", "right", or "middle")
864+
click_count: defaults to 1
865+
delay: Time to wait between `mousedown` and `mouseup` in milliseconds. Defaults to 0.
866+
force: Whether to bypass the actionability checks
859867
no_wait_after: If True, don't wait for page events after click
860-
ignore_ajax: If True, expect blocking dialogs (passed via kwargs)
861-
"""
862-
# Validate button parameter
863-
valid_buttons = ["left", "right", "middle"]
864-
if button not in valid_buttons:
865-
raise ValueError(
866-
f"Invalid button '{button}'. Must be one of: {', '.join(valid_buttons)}"
867-
)
868+
timeout: Maximum time in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout.
868869
870+
Deprecated:
871+
ignore_ajax: Deprecated parameter. Use `no_wait_after=True` instead.
872+
"""
869873
self.logger.debug("click: %r with %s button", locator, button)
874+
875+
# Handle deprecated ignore_ajax parameter
870876
ignore_ajax = kwargs.pop("ignore_ajax", False)
877+
if ignore_ajax:
878+
warnings.warn(
879+
"The 'ignore_ajax' parameter is deprecated and will be removed in a future version. "
880+
"Use 'no_wait_after=True' instead.",
881+
DeprecationWarning,
882+
stacklevel=2,
883+
)
884+
no_wait_after = True
885+
871886
el = self.element(locator, *args, **kwargs)
872887
self.plugin.before_click(el, locator)
873888

874-
# If ignore_ajax is True, it's a signal that a blocking dialog is expected.
875-
# We pass no_wait_after=True to prevent a timeout.
876-
if ignore_ajax or no_wait_after:
877-
el.click(button=button, no_wait_after=True)
878-
else:
879-
el.click(button=button)
889+
el.click(
890+
button=button,
891+
click_count=click_count,
892+
delay=delay,
893+
force=force,
894+
no_wait_after=no_wait_after,
895+
timeout=timeout,
896+
)
897+
if not ignore_ajax and not no_wait_after:
880898
try:
881899
self.plugin.ensure_page_safe()
882900
except TimedOutError:
@@ -890,13 +908,29 @@ def double_click(self, locator: LocatorAlias, *args, **kwargs) -> None:
890908
locator: Element locator to double-click on
891909
*args: Additional arguments passed to element() method
892910
**kwargs: Additional keyword arguments passed to element() method
911+
912+
Deprecated:
913+
ignore_ajax: Deprecated parameter. Use `no_wait_after=True` instead.
893914
"""
894915
self.logger.debug("double_click: %r", locator)
916+
917+
# Handle deprecated ignore_ajax parameter
895918
ignore_ajax = kwargs.pop("ignore_ajax", False)
919+
if ignore_ajax:
920+
warnings.warn(
921+
"The 'ignore_ajax' parameter is deprecated and will be removed in a future version. "
922+
"Use 'no_wait_after=True' instead.",
923+
DeprecationWarning,
924+
stacklevel=2,
925+
)
926+
927+
no_wait_after = kwargs.pop("no_wait_after", False)
928+
896929
el = self.element(locator, *args, **kwargs)
897930
self.plugin.before_click(el, locator)
898-
el.dblclick()
899-
if not ignore_ajax:
931+
el.dblclick(no_wait_after=no_wait_after)
932+
933+
if not ignore_ajax and not no_wait_after:
900934
try:
901935
self.plugin.ensure_page_safe()
902936
except TimedOutError:

src/widgetastic/widget/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -719,7 +719,7 @@ def click(self, handle_alert=None):
719719
Args:
720720
handle_alert: Special alert handling. None - no handling, True - accept, False - dismiss
721721
"""
722-
self.browser.click(self, ignore_ajax=(handle_alert is not None))
722+
self.browser.click(self, no_wait_after=(handle_alert is not None))
723723
if handle_alert is not None:
724724
self.browser.handle_alert(cancel=not handle_alert, wait=2.0, squash=True)
725725

testing/html/testing_page.html

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -319,14 +319,18 @@ <h2 class="foo bar wt-invisible" style="display: none;" id="invisible">This is i
319319

320320
<div class="widget-group">
321321
<div class="widget-title">Interactive Buttons</div>
322-
<div class="widget-description">Buttons with state changes and click handlers</div>
323-
<button id="a_button" onclick="this.className += ' clicked'">Click Me (State Changes)</button>
324-
<div class="code-example">Button adds 'clicked' class when pressed</div>
322+
<div class="widget-description">Buttons with state changes, click handlers, and enhanced click parameters</div>
323+
<button id="a_button" onclick="handleClickCount(event)">Click Me (State Changes)</button>
324+
<div id="click_count_result" style="margin-left: 10px; font-weight: bold; color: #007bff; display: inline;">Clicks: 0</div>
325+
<div class="code-example">Button tracks click count and adds 'clicked' class (tests click_count parameter)</div>
326+
325327
<button id='disabled_button' disabled>Disabled Button</button>
326328
<div class="code-example">Button in disabled state - cannot be clicked</div>
327-
<button id="multi_button" onmousedown="handleButtonClick(event)">Multi-Click Test</button>
328-
<div id="click_result" style="margin-left: 10px; font-weight: bold; color: #007bff;">Ready</div>
329-
<div class="code-example">Button detects left, right, and middle mouse clicks</div>
329+
330+
<button id="multi_button" onmousedown="handleButtonClick(event)" onmouseup="handleMouseUp()">Multi-Click Test</button>
331+
<div id="click_result" style="margin-left: 10px; font-weight: bold; color: #007bff; display: inline;">Ready</div>
332+
<div id="click_delay_result" style="margin-left: 10px; font-weight: bold; color: #007bff; display: inline;">Delay: 0ms</div>
333+
<div class="code-example">Button detects mouse buttons and measures click delay (tests button, delay, force, and timeout parameters)</div>
330334
</div>
331335
</div>
332336
</div>
@@ -721,6 +725,46 @@ <h2 class="foo bar wt-invisible" style="display: none;" id="invisible">This is i
721725
document.getElementById("alert_out").innerHTML = txt;
722726
}
723727

728+
// Click count tracking for #a_button
729+
var clickCountData = {
730+
count: 0,
731+
lastClick: 0
732+
};
733+
734+
function handleClickCount(event) {
735+
var now = Date.now();
736+
// Reset count if more than 500ms since last click
737+
if (now - clickCountData.lastClick > 500) {
738+
clickCountData.count = 0;
739+
}
740+
clickCountData.count++;
741+
clickCountData.lastClick = now;
742+
743+
document.getElementById("click_count_result").textContent = "Clicks: " + clickCountData.count;
744+
745+
// Add specific class based on click count
746+
if (clickCountData.count === 2) {
747+
event.target.className = event.target.className.replace(/\s*single-clicked/g, '').replace(/\s*clicked/g, '') + ' double-clicked';
748+
} else if (clickCountData.count === 3) {
749+
event.target.className = event.target.className.replace(/\s*double-clicked/g, '') + ' triple-clicked';
750+
} else {
751+
event.target.className = event.target.className.replace(/\s*(double|triple)-clicked/g, '') + ' single-clicked clicked';
752+
}
753+
}
754+
755+
function resetClickCount() {
756+
clickCountData.count = 0;
757+
clickCountData.lastClick = 0;
758+
var btn = document.getElementById("a_button");
759+
if (btn) btn.className = "";
760+
document.getElementById("click_count_result").textContent = "Clicks: 0";
761+
}
762+
763+
// Click delay tracking for #multi_button
764+
var clickDelayData = {
765+
mousedownTime: 0
766+
};
767+
724768
function handleButtonClick(event) {
725769
var result = document.getElementById("click_result");
726770
var clickType = "";
@@ -740,8 +784,22 @@ <h2 class="foo bar wt-invisible" style="display: none;" id="invisible">This is i
740784
}
741785

742786
result.textContent = clickType;
787+
clickDelayData.mousedownTime = Date.now();
743788
event.preventDefault();
744789
}
790+
791+
function handleMouseUp() {
792+
if (clickDelayData.mousedownTime > 0) {
793+
var delay = Date.now() - clickDelayData.mousedownTime;
794+
document.getElementById("click_delay_result").textContent = "Delay: " + delay + "ms";
795+
clickDelayData.mousedownTime = 0;
796+
}
797+
}
798+
799+
function resetClickDelay() {
800+
clickDelayData.mousedownTime = 0;
801+
document.getElementById("click_delay_result").textContent = "Delay: 0ms";
802+
}
745803
</script>
746804

747805
<!-- Form Elements & Checkboxes Section -->

testing/test_browser.py

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44
from datetime import datetime
55
from pathlib import Path
66

7+
import playwright
78
import pytest
89

910
from widgetastic.browser import BrowserParentWrapper
1011
from widgetastic.exceptions import LocatorNotImplemented
1112
from widgetastic.exceptions import NoSuchElementException
1213
from widgetastic.widget import Text
1314
from widgetastic.widget import View
14-
from playwright.sync_api import Locator
1515

1616

1717
@pytest.fixture()
@@ -273,7 +273,10 @@ def test_wait_for_element_visible(browser):
273273
# Click on the button
274274
browser.click("#invisible_appear_button")
275275
try:
276-
assert isinstance(browser.wait_for_element("#invisible_appear_p", visible=True), Locator)
276+
assert isinstance(
277+
browser.wait_for_element("#invisible_appear_p", visible=True),
278+
playwright.sync_api.Locator,
279+
)
277280
except NoSuchElementException:
278281
pytest.fail("NoSuchElementException raised when webelement expected")
279282

@@ -603,7 +606,7 @@ def test_click_with_ignore_ajax(browser):
603606

604607
def test_click_with_invalid_button(browser):
605608
"""Test click method with invalid button parameter raises ValueError."""
606-
with pytest.raises(ValueError, match="Invalid button 'invalid'"):
609+
with pytest.raises(playwright._impl._errors.Error):
607610
browser.click("#a_button", button="invalid")
608611

609612

@@ -655,19 +658,85 @@ def test_mouse_click(browser, button):
655658
assert result == expected_result
656659

657660

661+
def test_click_with_click_count(browser):
662+
"""Test click method with click_count=<number> parameter."""
663+
for click_count in [1, 2, 3]:
664+
# Reset first
665+
browser.execute_script("resetClickCount()")
666+
667+
browser.click("#a_button", click_count=click_count)
668+
669+
# Verify click count was detected
670+
result = browser.text("#click_count_result")
671+
assert f"Clicks: {click_count}" in result
672+
assert "clicked" in browser.classes("#a_button").pop()
673+
674+
675+
def test_click_with_delay_parameter(browser):
676+
"""Test click method with delay parameter between mousedown and mouseup."""
677+
for delay_ms in [100, 0]:
678+
browser.execute_script("resetClickDelay()")
679+
680+
browser.click("#multi_button", delay=delay_ms)
681+
# Give a moment for the handler to process
682+
time.sleep(0.05)
683+
684+
# Verify delay was applied (allow some tolerance for execution time)
685+
result = browser.text("#click_delay_result")
686+
assert "Delay:" in result
687+
688+
# Extract the delay value from result
689+
delay_value = int(result.split(":")[1].strip().replace("ms", ""))
690+
print(delay_value)
691+
# Allow 10ms tolerance for browser timing variations
692+
assert delay_value >= delay_ms
693+
assert delay_value <= delay_ms + 10
694+
695+
696+
def test_click_with_force_parameter(browser):
697+
"""Test click method with force parameter (True and False)."""
698+
# force parameter bypasses Playwright's actionability checks
699+
# We test that the parameter is properly passed through
700+
browser.execute_script("resetClickCount()")
701+
702+
# Test force=True works
703+
browser.click("#a_button", force=True)
704+
assert "clicked" in browser.classes("#a_button")
705+
706+
browser.refresh()
707+
browser.execute_script("resetClickCount()")
708+
709+
# Test force=False works (default behavior)
710+
browser.click("#a_button", force=False)
711+
assert "clicked" in browser.classes("#a_button")
712+
713+
714+
def test_click_with_timeout_parameter(browser):
715+
"""Test click method with timeout parameter."""
716+
# Test that timeout parameter is accepted and doesn't break normal clicks
717+
browser.click("#a_button", timeout=0)
718+
assert "clicked" in browser.classes("#a_button")
719+
720+
browser.refresh()
721+
browser.execute_script("resetClickCount()")
722+
723+
browser.click("#a_button", timeout=100)
724+
assert "clicked" in browser.classes("#a_button")
725+
726+
658727
def test_double_click_method(browser):
659728
"""Test double_click method."""
660729
initial_classes = browser.classes("#a_button")
661730
browser.double_click("#a_button")
662731
final_classes = browser.classes("#a_button")
663-
assert "clicked" in final_classes
732+
assert "double-clicked" in final_classes
664733
assert "clicked" not in initial_classes
665734

666735

667736
def test_double_click_with_ignore_ajax(browser):
668737
"""Test double_click method with ignore_ajax parameter."""
669738
browser.double_click("#a_button", ignore_ajax=True)
670-
assert "clicked" in browser.classes("#a_button")
739+
assert "double-clicked" in browser.classes("#a_button")
671740

672741

673742
def test_double_click_with_timed_out_error_in_ensure_page_safe(browser, monkeypatch):
@@ -703,7 +772,7 @@ def mock_after_click_safe_timeout(el, locator):
703772

704773
# Verify the timeout handler was called
705774
assert timeout_called is True
706-
assert "clicked" in browser.classes("#a_button")
775+
assert "double-clicked" in browser.classes("#a_button")
707776

708777

709778
def test_raw_click(browser):

0 commit comments

Comments
 (0)