2121from .exceptions import (
2222 NoSuchElementException , UnexpectedAlertPresentException , MoveTargetOutOfBoundsException ,
2323 StaleElementReferenceException , NoAlertPresentException , LocatorNotImplemented )
24- from .log import create_widget_logger , null_logger
24+ from .log import create_widget_logger , logged , null_logger
25+ from .utils import repeat_once_on_exceptions
2526from .xpath import normalize_space
2627
2728
@@ -256,6 +257,7 @@ def elements(
256257
257258 return result
258259
260+ @repeat_once_on_exceptions (NoSuchElementException , check_safe = True )
259261 def element (self , locator , * args , ** kwargs ):
260262 """Returns one :py:class:`selenium.webdriver.remote.webelement.WebElement`
261263
@@ -267,6 +269,8 @@ def element(self, locator, *args, **kwargs):
267269 Raises:
268270 :py:class:`selenium.common.exceptions.NoSuchElementException`
269271 """
272+ if 'check_safe' not in kwargs :
273+ kwargs ['check_safe' ] = False
270274 try :
271275 vcheck = self ._locator_force_visibility_check (locator )
272276 if vcheck is not None :
@@ -281,18 +285,18 @@ def element(self, locator, *args, **kwargs):
281285 else :
282286 return elements [0 ]
283287 except IndexError :
284- raise NoSuchElementException ('Could not find an element {}' .format (repr ( locator ) ))
288+ raise NoSuchElementException ('Could not find an element {!r }' .format (locator ))
285289
286290 def perform_click (self ):
287291 """Clicks the left mouse button at the current mouse position."""
288292 ActionChains (self .selenium ).click ().perform ()
289293
294+ @logged (log_args = True , only_after = True , debug_only = True , log_full_exception = False )
290295 def click (self , locator , * args , ** kwargs ):
291296 """Clicks at a specific element using two separate events (mouse move, mouse click).
292297
293298 Args: See :py:meth:`elements`
294299 """
295- self .logger .debug ('click: %r' , locator )
296300 ignore_ajax = kwargs .pop ('ignore_ajax' , False )
297301 el = self .move_to_element (locator , * args , ** kwargs )
298302 self .plugin .before_click (el )
@@ -308,12 +312,12 @@ def click(self, locator, *args, **kwargs):
308312 except (StaleElementReferenceException , UnexpectedAlertPresentException ):
309313 pass
310314
315+ @logged (log_args = True , only_after = True , debug_only = True , log_full_exception = False )
311316 def raw_click (self , locator , * args , ** kwargs ):
312317 """Clicks at a specific element using the direct event.
313318
314319 Args: See :py:meth:`elements`
315320 """
316- self .logger .debug ('raw_click: %r' , locator )
317321 ignore_ajax = kwargs .pop ('ignore_ajax' , False )
318322 el = self .element (locator , * args , ** kwargs )
319323 self .plugin .before_click (el )
@@ -356,6 +360,10 @@ def is_displayed(self, locator, *args, **kwargs):
356360 # Just in case
357361 return False
358362
363+ @logged (log_args = True , only_after = True , debug_only = True , log_full_exception = False )
364+ @repeat_once_on_exceptions (
365+ StaleElementReferenceException , MoveTargetOutOfBoundsException ,
366+ check_safe = True )
359367 def move_to_element (self , locator , * args , ** kwargs ):
360368 """Moves the mouse cursor to the middle of the element represented by the locator.
361369
@@ -364,23 +372,31 @@ def move_to_element(self, locator, *args, **kwargs):
364372
365373 Args: See :py:meth:`elements`
366374
375+ Keywords:
376+ workaround: Default True, tells whether it can or can not perform the JS workaround.
377+
367378 Returns:
368379 :py:class:`selenium.webdriver.remote.webelement.WebElement`
369380 """
370- self .logger .debug ('move_to_element: %r' , locator )
381+ if 'check_safe' not in kwargs :
382+ kwargs ['check_safe' ] = False
383+ workaround = kwargs .pop ('workaround' , True )
371384 el = self .element (locator , * args , ** kwargs )
372385 if el .tag_name == "option" :
373386 # Instead of option, let's move on its parent <select> if possible
374- parent = self .element (".." , parent = el )
387+ parent = self .element (".." , parent = el , check_safe = False )
375388 if parent .tag_name == "select" :
376- self .move_to_element (parent )
389+ self .move_to_element (parent , workaround = workaround )
377390 return el
378391 move_to = ActionChains (self .selenium ).move_to_element (el )
379392 try :
380393 move_to .perform ()
381394 except MoveTargetOutOfBoundsException :
395+ if not workaround :
396+ # No workaround, reraise
397+ raise
382398 # ff workaround
383- self .execute_script ("arguments[0].scrollIntoView();" , el )
399+ self .execute_script ("arguments[0].scrollIntoView();" , el , silent = True )
384400 try :
385401 move_to .perform ()
386402 except MoveTargetOutOfBoundsException : # This has become desperate now.
@@ -399,20 +415,24 @@ def drag_and_drop(self, source, target):
399415 self .logger .debug ('drag_and_drop %r to %r' , source , target )
400416 ActionChains (self .selenium ).drag_and_drop (self .element (source ), self .element (target ))
401417
418+ @logged (log_args = True , only_after = True , debug_only = True , log_full_exception = False )
402419 def move_by_offset (self , x , y ):
403- self .logger .debug ('move_by_offset X:%r Y:%r' , x , y )
404- ActionChains (self .selenium ).move_by_offset (x , y )
420+ """Moves mouse pointer by given values."""
421+ ActionChains (self .selenium ).move_by_offset (x , y ).perform ()
422+ self .plugin .ensure_page_safe ()
405423
406424 def execute_script (self , script , * args , ** kwargs ):
407425 """Executes a script."""
408426 if not kwargs .pop ('silent' , False ):
409- self .logger .debug ('execute_script: %r ' , script )
427+ self .logger .debug ('execute_script(%r) ' , script )
410428 return self .selenium .execute_script (dedent (script ), * args , ** kwargs )
411429
412430 def refresh (self ):
413431 """Triggers a page refresh."""
414432 return self .selenium .refresh ()
415433
434+ @logged (
435+ log_args = True , log_result = True , only_after = True , debug_only = True , log_full_exception = False )
416436 def classes (self , locator , * args , ** kwargs ):
417437 """Return a list of classes attached to the element.
418438
@@ -421,11 +441,11 @@ def classes(self, locator, *args, **kwargs):
421441 Returns:
422442 A :py:class:`set` of strings with classes.
423443 """
424- result = set (self .execute_script (
444+ return set (self .execute_script (
425445 "return arguments[0].classList;" , self .element (locator , * args , ** kwargs ), silent = True ))
426- self .logger .debug ('css classes for %r => %r' , locator , result )
427- return result
428446
447+ @logged (
448+ log_args = True , log_result = True , only_after = True , debug_only = True , log_full_exception = False )
429449 def tag (self , * args , ** kwargs ):
430450 """Returns the tag name of the element represented by the locator passed.
431451
@@ -436,7 +456,9 @@ def tag(self, *args, **kwargs):
436456 """
437457 return self .element (* args , ** kwargs ).tag_name
438458
439- def text (self , * args , ** kwargs ):
459+ @logged (
460+ log_args = True , log_result = True , only_after = True , debug_only = True , log_full_exception = False )
461+ def text (self , locator , * args , ** kwargs ):
440462 """Returns the text inside the element represented by the locator passed.
441463
442464 The returned text is normalized with :py:func:`widgetastic.xpath.normalize_space` as defined
@@ -448,45 +470,56 @@ def text(self, *args, **kwargs):
448470 :py:class:`str` with the text
449471 """
450472 try :
451- text = self .element ( * args , ** kwargs ).text
473+ text = self .move_to_element ( locator , * args , ** dict ( kwargs , workaround = False ) ).text
452474 except MoveTargetOutOfBoundsException :
453475 text = ''
454476
455477 if not text :
456478 # It is probably invisible
457479 text = self .execute_script (
458480 'return arguments[0].textContent || arguments[0].innerText;' ,
459- self .element (* args , ** kwargs ))
481+ self .element (locator , * args , ** kwargs ),
482+ silent = True )
460483 if text is None :
461484 text = ''
462485
463486 return normalize_space (text )
464487
465- def get_attribute (self , attr , * args , ** kwargs ):
466- return self .element (* args , ** kwargs ).get_attribute (attr )
488+ @logged (
489+ log_args = True , log_result = True , only_after = True , debug_only = True , log_full_exception = False )
490+ def get_attribute (self , attr , locator , * args , ** kwargs ):
491+ """Get attribute value from an element."""
492+ return self .element (locator , * args , ** kwargs ).get_attribute (attr )
467493
468- def set_attribute (self , attr , value , * args , ** kwargs ):
494+ @logged (log_args = True , only_after = True , debug_only = True , log_full_exception = False )
495+ def set_attribute (self , attr , value , locator , * args , ** kwargs ):
469496 return self .execute_script (
470497 "arguments[0].setAttribute(arguments[1], arguments[2]);" ,
471- self .element (* args , ** kwargs ), attr , value )
498+ self .element (locator , * args , ** kwargs ), attr , value )
472499
473- def size_of (self , * args , ** kwargs ):
500+ @logged (
501+ log_args = True , log_result = True , only_after = True , debug_only = True , log_full_exception = False )
502+ def size_of (self , locator , * args , ** kwargs ):
474503 """Returns element's size as a tuple of width/height."""
475- size = self .element (* args , ** kwargs ).size
504+ size = self .element (locator , * args , ** kwargs ).size
476505 return Size (size ['width' ], size ['height' ])
477506
507+ @logged (log_args = True , only_after = True , debug_only = True , log_full_exception = False )
478508 def clear (self , locator , * args , ** kwargs ):
479509 """Clears a text input with given locator."""
480- self .logger .debug ('clear: %r' , locator )
481510 el = self .element (locator , * args , ** kwargs )
482511 self .plugin .before_keyboard_input (el , None )
483512 result = el .clear ()
513+ self .plugin .ensure_page_safe ()
484514 self .plugin .after_keyboard_input (el , None )
485515 return result
486516
487- def is_selected (self , * args , ** kwargs ):
488- return self .element (* args , ** kwargs ).is_selected ()
517+ @logged (
518+ log_args = True , log_result = True , only_after = True , debug_only = True , log_full_exception = False )
519+ def is_selected (self , locator , * args , ** kwargs ):
520+ return self .element (locator , * args , ** kwargs ).is_selected ()
489521
522+ @logged (log_args = True , debug_only = True , log_full_exception = False )
490523 def send_keys (self , text , locator , * args , ** kwargs ):
491524 """Sends keys to the element. Detects the file inputs automatically.
492525
@@ -508,8 +541,9 @@ def send_keys(self, text, locator, *args, **kwargs):
508541 self .selenium .file_detector = LocalFileDetector ()
509542 el = self .move_to_element (locator , * args , ** kwargs )
510543 self .plugin .before_keyboard_input (el , text )
511- self .logger .debug ('send_keys %r to %r' , text , locator )
512544 result = el .send_keys (text )
545+ # Ensure the page input was safe
546+ self .plugin .ensure_page_safe ()
513547 if Keys .ENTER not in text :
514548 try :
515549 self .plugin .after_keyboard_input (el , text )
@@ -537,6 +571,8 @@ def get_alert(self):
537571 return self .selenium .switch_to_alert ()
538572
539573 @property
574+ @logged (
575+ log_args = True , log_result = True , only_after = True , debug_only = True , log_full_exception = False )
540576 def alert_present (self ):
541577 """Checks whether there is any alert present.
542578
@@ -551,6 +587,7 @@ def alert_present(self):
551587 else :
552588 return True
553589
590+ @logged (log_args = True , log_full_exception = False )
554591 def dismiss_any_alerts (self ):
555592 """Loops until there are no further alerts present to dismiss.
556593
@@ -564,6 +601,7 @@ def dismiss_any_alerts(self):
564601 except NoAlertPresentException : # Just in case. alert_present should be reliable
565602 pass
566603
604+ @logged (log_args = True , log_result = True , log_full_exception = False )
567605 def handle_alert (self , cancel = False , wait = 30.0 , squash = False , prompt = None , check_present = False ):
568606 """Handles an alert popup.
569607
0 commit comments