Source code for nested_admin.tests.drag_drop

import re
import time

from selenium.webdriver.common.action_chains import ActionChains
from selenium.common.exceptions import WebDriverException

from .utils import (
    xpath_cls, xpath_item, is_integer, Position, Size, ElementRect)


if not hasattr(__builtins__, 'cmp'):
[docs] def cmp(a, b): return (a > b) - (a < b)
[docs]def sign(x): return x and (1, -1)[x < 0]
[docs]class DragAndDropAction(object): def __init__(self, test_case, from_indexes, to_indexes): self.test_case = test_case self.selenium = test_case.selenium if len(from_indexes) > len(to_indexes): self.target_is_empty = True else: self.target_is_empty = False self.from_indexes = self.test_case._normalize_indexes(from_indexes, named_models=False) self.to_indexes = self.test_case._normalize_indexes( to_indexes, is_group=self.target_is_empty, named_models=False) num_inlines_indexes = self.test_case._normalize_indexes( to_indexes, is_group=self.target_is_empty, named_models=True) if not is_integer(num_inlines_indexes[-1]): num_inlines_indexes[-1] = num_inlines_indexes[-1][0] self.target_num_items = self.test_case.get_num_inlines(num_inlines_indexes) if is_integer(self.to_indexes[-1]): self.to_indexes[-1] = [self.to_indexes[-1], 0] self.to_indexes = [tuple(i) for i in self.to_indexes] inline_models = self.test_case.models[1] for inline_index, item_index in self.to_indexes[:-1]: inline_models = inline_models[inline_index][1] self.target_num_inlines = len(inline_models) if self.from_indexes[:-1] == self.to_indexes[:-1]: if self.from_indexes[-1][1] < self.to_indexes[-1][1]: self.to_indexes[-1][1] += 1 self.test_case.assertEqual(len(to_indexes), len(from_indexes), "Depth of source and target must be the same") @property def viewport_size(self): if not hasattr(self, '_viewport_size'): dimensions = self.selenium.execute_script(""" return { width: (window.innerWidth || document.documentElement.clientWidth), height: (window.innerHeight || document.documentElement.clientHeight) }""") self._viewport_size = Size(**dimensions) return self._viewport_size # def get_element_rect(self, element): # return Rect()
[docs] def get_mouse_position(self): pos_dict = self.selenium.execute_script(""" return (function(m) { return m && {x: m.clientX, y: m.clientY}; })(DJNesting.lastMouseMove)""") return Position(**pos_dict)
@property def source(self): if not hasattr(self, '_source'): source_item = self.test_case.get_item(indexes=self.from_indexes) if source_item.tag_name == 'div': drag_handler_xpath = "h3" elif self.test_case.has_grappelli: drag_handler_xpath = "/".join([ "*[%s]" % xpath_cls("djn-tr"), "*[%s]" % xpath_cls("djn-td"), "*[%s]" % xpath_cls("tools"), "/*[%s]" % xpath_cls("djn-drag-handler"), ]) else: drag_handler_xpath = "/".join([ "*[%s]" % xpath_cls("djn-tr"), "*[%s]" % xpath_cls("is-sortable"), "*[%s]" % xpath_cls("djn-drag-handler"), ]) self._source = source_item.find_element_by_xpath(drag_handler_xpath) return self._source @property def target(self): if not hasattr(self, '_target'): if len(self.to_indexes) > 1: target_inline_parent = self.test_case.get_item(self.to_indexes[:-1]) else: target_inline_parent = self.selenium target_xpath = ".//*[%s][%d]//*[%s]" % ( xpath_cls('djn-group'), self.to_indexes[-1][0] + 1, xpath_cls("djn-items")) if self.target_num_items != self.to_indexes[-1][1]: target_xpath += "/*[%(item_pred)s][%(item_pos)d]" % { 'item_pred': xpath_item(), 'item_pos': self.to_indexes[-1][1] + 1, } self._target = target_inline_parent.find_element_by_xpath(target_xpath) return self._target
[docs] def initialize_drag(self): source = self.source if self.test_case.has_suit and self.test_case.browser == 'chrome' and source.tag_name == 'h3': source = source.find_element_by_css_selector('b') try: source.click() except WebDriverException: self.selenium.execute_script(""" var el = arguments[0], top = el.getBoundingClientRect().top; if (top <= 15) { document.documentElement.scrollTop += (top - 16); } else { el.scrollIntoView(); } """, source) source.click() (ActionChains(self.selenium) .move_to_element_with_offset(source, 5, 5) .click_and_hold() .perform()) time.sleep(0.05) ActionChains(self.selenium).move_by_offset(0, -15).perform() time.sleep(0.05) ActionChains(self.selenium).move_by_offset(0, 15).perform() with self.test_case.visible_selector('.ui-sortable-helper') as el: return el
[docs] def release(self): ActionChains(self.selenium).release().perform()
def _match_helper_with_target(self, helper_element, target_element): ActionChains(self.selenium).move_by_offset(0, -15).perform() desired_pos = tuple(self.to_indexes) # True if aiming for the bottom of the target target_bottom = bool(0 < self.to_indexes[-1][1] == (self.target_num_items - 1) and self.to_indexes[-1][0] == (self.target_num_inlines - 1) and cmp(desired_pos[:-1], self.current_position[:-1]) > -1) helper = ElementRect(helper_element) target = ElementRect(target_element, aliases={ 'y': ('bottom' if target_bottom else 'top')}) mouse_pos = self.get_mouse_position() viewport_height = self.viewport_size.height dy = target.y - helper.y inline_height = max(target.height, helper.height) increment = max(15, min(viewport_height // 3, (2 * inline_height) // 3, abs(dy) // 2)) max_iter = 50 i = 0 prev_pos_diff = None direction = None direction_flip = 1 flip_count = 0 flip_multiplier = max(2, inline_height // 18) while i < max_iter: curr_pos = self.current_position pos_diff = cmp(desired_pos, curr_pos) if pos_diff == 0: break if prev_pos_diff is None: prev_pos_diff = pos_diff elif pos_diff != prev_pos_diff: # We switched directions, lower the increment increment = min(increment, 15) prev_pos_diff = pos_diff if direction is None: direction = pos_diff target.refresh() helper.refresh() target_y = getattr(target, 'bottom' if direction == 1 else 'top') dy = target_y - helper.y if sign(dy) != sign(pos_diff) * direction_flip and abs(dy) > inline_height: flip_count += 1 if flip_count > 3: increment = 10 elif flip_count > 5: increment = 5 else: increment = max(abs(dy // 2), flip_count * flip_multiplier) direction_flip *= -1 direction = pos_diff * direction_flip inc = increment * direction mouse_pos = self.get_mouse_position() mouse_edge_dy = mouse_pos.y if direction < 0 else viewport_height - mouse_pos.y if mouse_edge_dy <= abs(inc): self.selenium.execute_script( 'document.documentElement.scrollTop += arguments[0]', inc + direction) (ActionChains(self.selenium) .move_by_offset(0, -direction) .perform()) else: ActionChains(self.selenium).move_by_offset(0, inc).perform() i += 1 return helper_element def _num_preceding_siblings(self, ctx, condition): """ For an unknown reason, evaluating XPath expressions of the form preceding-sibling::*[not(contains(@attr, 'value'))] Where 'value' is contained in at least one of the preceding siblings, is extraordinarily slow. So we just grab all siblings and iterate through the elements in python. """ siblings = ctx.find_element_by_xpath('parent::*').find_elements_by_xpath('*') count = 0 for el in siblings: if el.id == ctx.id: break if condition(el): count += 1 return count def _num_preceding_djn_items(self, ctx): def is_djn_item(el): classes = set(re.split(r'\s+', el.get_attribute('class'))) return (classes & {'djn-item'} and not(classes & {'djn-no-drag', 'djn-thead', 'djn-item-dragging'})) return self._num_preceding_siblings(ctx, condition=is_djn_item) def _num_preceding_djn_groups(self, ctx): def is_djn_group(el): return "djn-group" in re.split(r'\s+', el.get_attribute('class')) return self._num_preceding_siblings(ctx, condition=is_djn_group) @property def current_position(self): placeholder = self.selenium.find_element_by_css_selector( '.ui-sortable-placeholder') pos = [] ctx = None ancestor_xpath = 'ancestor::*[%s][1]' % xpath_cls("djn-item") for i in range(0, len(self.to_indexes)): if ctx is None: ctx = placeholder else: ctx = ctx.find_element_by_xpath(ancestor_xpath) item_index = self._num_preceding_djn_items(ctx) ctx = ctx.find_element_by_xpath('ancestor::*[%s][1]' % xpath_cls("djn-group")) inline_index = self._num_preceding_djn_groups(ctx) pos.insert(0, (inline_index, item_index)) return tuple(pos)
[docs] def move_to_target(self, screenshot_hack=False): target = self.target helper = self.initialize_drag() if screenshot_hack and 'phantomjs' in self.test_case.browser: # I don't know why, but saving a screenshot fixes a weird bug # in phantomjs self.selenium.save_screenshot('/dev/null') helper = self._match_helper_with_target(helper, target) self.release()