summaryrefslogtreecommitdiffstats
path: root/layout/base/tests/marionette
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /layout/base/tests/marionette
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'layout/base/tests/marionette')
-rw-r--r--layout/base/tests/marionette/manifest.toml7
-rw-r--r--layout/base/tests/marionette/selection.py363
-rw-r--r--layout/base/tests/marionette/test_accessiblecaret_cursor_mode.py280
-rw-r--r--layout/base/tests/marionette/test_accessiblecaret_selection_mode.py805
4 files changed, 1455 insertions, 0 deletions
diff --git a/layout/base/tests/marionette/manifest.toml b/layout/base/tests/marionette/manifest.toml
new file mode 100644
index 0000000000..045e0648e4
--- /dev/null
+++ b/layout/base/tests/marionette/manifest.toml
@@ -0,0 +1,7 @@
+[DEFAULT]
+prefs = ["gfx.font_loader.delay=0"]
+run-if = ["buildapp == 'browser'"]
+
+["test_accessiblecaret_cursor_mode.py"]
+
+["test_accessiblecaret_selection_mode.py"]
diff --git a/layout/base/tests/marionette/selection.py b/layout/base/tests/marionette/selection.py
new file mode 100644
index 0000000000..dd391972fb
--- /dev/null
+++ b/layout/base/tests/marionette/selection.py
@@ -0,0 +1,363 @@
+# -*- coding: utf-8 -*-
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from marionette_driver.marionette import Actions, errors
+
+
+class CaretActions(Actions):
+ def __init__(self, marionette):
+ super(CaretActions, self).__init__(marionette)
+ self._reset_action_chain()
+
+ def _reset_action_chain(self):
+ # Release the action so that two consecutive clicks won't become a
+ # double-click.
+ self.release()
+
+ self.mouse_chain = self.sequence(
+ "pointer", "pointer_id", {"pointerType": "mouse"}
+ )
+ self.key_chain = self.sequence("key", "keyboard_id")
+
+ def move(self, element, x, y, duration=None):
+ """Queue a pointer_move action.
+
+ :param element: an element where its top-left corner is the origin of
+ the coordinates.
+ :param x: Destination x-axis coordinate in CSS pixels.
+ :param y: Destination y-axis coordinate in CSS pixels.
+ :param duration: Number of milliseconds over which to distribute the
+ move. If None, remote end defaults to 0.
+
+ """
+ rect = element.rect
+ el_x, el_y = rect["x"], rect["y"]
+
+ # Add the element's top-left corner (el_x, el_y) to make the coordinate
+ # relative to the viewport.
+ dest_x, dest_y = int(el_x + x), int(el_y + y)
+
+ self.mouse_chain.pointer_move(dest_x, dest_y, duration=duration)
+ return self
+
+ def click(self, element=None):
+ """Queue a click action.
+
+ If an element is given, move the pointer to that element first,
+ otherwise click current pointer coordinates.
+
+ :param element: Optional element to click.
+
+ """
+ self.mouse_chain.click(element=element)
+ return self
+
+ def flick(self, element, x1, y1, x2, y2, duration=200):
+ """Queue a flick gesture on the target element.
+
+ :param element: The element to perform the flick gesture on. Its
+ top-left corner is the origin of the coordinates.
+ :param x1: Starting x-axis coordinate of the flick in CSS Pixels.
+ :param y1: Starting y-axis coordinate of the flick in CSS Pixels.
+ :param x2: Ending x-axis coordinate of the flick in CSS Pixels.
+ :param y2: Ending y-axis coordinate of the flick in CSS Pixels.
+
+ """
+ self.move(element, x1, y1, duration=0)
+ self.mouse_chain.pointer_down()
+ self.move(element, x2, y2, duration=duration)
+ self.mouse_chain.pointer_up()
+ return self
+
+ def send_keys(self, keys):
+ """Perform a keyDown and keyUp action for each character in `keys`.
+
+ :param keys: String of keys to perform key actions with.
+
+ """
+ self.key_chain.send_keys(keys)
+ return self
+
+ def perform(self):
+ """Perform the action chain built so far to the server side for execution
+ and clears the current chain of actions.
+
+ Warning: This method performs all the mouse actions before all the key
+ actions!
+
+ """
+ self.mouse_chain.perform()
+ self.key_chain.perform()
+ self._reset_action_chain()
+
+
+class SelectionManager(object):
+ """Interface for manipulating the selection and carets of the element.
+
+ We call the blinking cursor (nsCaret) as cursor, and call AccessibleCaret as
+ caret for short.
+
+ Simple usage example:
+
+ ::
+
+ element = marionette.find_element(By.ID, 'input')
+ sel = SelectionManager(element)
+ sel.move_caret_to_front()
+
+ """
+
+ def __init__(self, element):
+ self.element = element
+
+ def _input_or_textarea(self):
+ """Return True if element is either <input> or <textarea>."""
+ return self.element.tag_name in ("input", "textarea")
+
+ def js_selection_cmd(self):
+ """Return a command snippet to get selection object.
+
+ If the element is <input> or <textarea>, return the selection object
+ associated with it. Otherwise, return the current selection object.
+
+ Note: "element" must be provided as the first argument to
+ execute_script().
+
+ """
+ if self._input_or_textarea():
+ # We must unwrap sel so that DOMRect could be returned to Python
+ # side.
+ return """var sel = arguments[0].editor.selection;"""
+ else:
+ return """var sel = window.getSelection();"""
+
+ def move_cursor_by_offset(self, offset, backward=False):
+ """Move cursor in the element by character offset.
+
+ :param offset: Move the cursor to the direction by offset characters.
+ :param backward: Optional, True to move backward; Default to False to
+ move forward.
+
+ """
+ cmd = (
+ self.js_selection_cmd()
+ + """
+ for (let i = 0; i < {0}; ++i) {{
+ sel.modify("move", "{1}", "character");
+ }}
+ """.format(
+ offset, "backward" if backward else "forward"
+ )
+ )
+
+ self.element.marionette.execute_script(
+ cmd, script_args=(self.element,), sandbox="system"
+ )
+
+ def move_cursor_to_front(self):
+ """Move cursor in the element to the front of the content."""
+ if self._input_or_textarea():
+ cmd = """arguments[0].setSelectionRange(0, 0);"""
+ else:
+ cmd = """var sel = window.getSelection();
+ sel.collapse(arguments[0].firstChild, 0);"""
+
+ self.element.marionette.execute_script(
+ cmd, script_args=(self.element,), sandbox=None
+ )
+
+ def move_cursor_to_end(self):
+ """Move cursor in the element to the end of the content."""
+ if self._input_or_textarea():
+ cmd = """var len = arguments[0].value.length;
+ arguments[0].setSelectionRange(len, len);"""
+ else:
+ cmd = """var sel = window.getSelection();
+ sel.collapse(arguments[0].lastChild, arguments[0].lastChild.length);"""
+
+ self.element.marionette.execute_script(
+ cmd, script_args=(self.element,), sandbox=None
+ )
+
+ def selection_rect_list(self, idx):
+ """Return the selection's DOMRectList object for the range at given idx.
+
+ If the element is either <input> or <textarea>, return the DOMRectList of
+ the range at given idx of the selection within the element. Otherwise,
+ return the DOMRectList of the of the range at given idx of current selection.
+
+ """
+ cmd = (
+ self.js_selection_cmd()
+ + """return sel.getRangeAt({}).getClientRects();""".format(idx)
+ )
+ return self.element.marionette.execute_script(
+ cmd, script_args=(self.element,), sandbox="system"
+ )
+
+ def range_count(self):
+ """Get selection's range count"""
+ cmd = self.js_selection_cmd() + """return sel.rangeCount;"""
+ return self.element.marionette.execute_script(
+ cmd, script_args=(self.element,), sandbox="system"
+ )
+
+ def _selection_location_helper(self, location_type):
+ """Return the start and end location of the selection in the element.
+
+ Return a tuple containing two pairs of (x, y) coordinates of the start
+ and end locations in the element. The coordinates are relative to the
+ top left-hand corner of the element. Both ltr and rtl directions are
+ considered.
+
+ """
+ range_count = self.range_count()
+ if range_count <= 0:
+ raise errors.MarionetteException(
+ "Expect at least one range object in Selection, but found nothing!"
+ )
+
+ # FIXME (Bug 1682382): We shouldn't need the retry for-loops if
+ # selection_rect_list() can reliably return a valid list.
+ retry_times = 3
+ for _ in range(retry_times):
+ try:
+ first_rect_list = self.selection_rect_list(0)
+ first_rect = first_rect_list["0"]
+ break
+ except KeyError:
+ continue
+ else:
+ raise errors.MarionetteException(
+ "Expect at least one rect in the first range, but found nothing!"
+ )
+
+ for _ in range(retry_times):
+ try:
+ # Making a selection over some non-selectable elements can
+ # create multiple ranges.
+ last_rect_list = (
+ first_rect_list
+ if range_count == 1
+ else self.selection_rect_list(range_count - 1)
+ )
+ last_list_length = last_rect_list["length"]
+ last_rect = last_rect_list[str(last_list_length - 1)]
+ break
+ except KeyError:
+ continue
+ else:
+ raise errors.MarionetteException(
+ "Expect at least one rect in the last range, but found nothing!"
+ )
+
+ origin_x, origin_y = self.element.rect["x"], self.element.rect["y"]
+
+ if self.element.get_property("dir") == "rtl": # such as Arabic
+ start_pos, end_pos = "right", "left"
+ else:
+ start_pos, end_pos = "left", "right"
+
+ # Calculate y offset according to different needs.
+ if location_type == "center":
+ start_y_offset = first_rect["height"] / 2.0
+ end_y_offset = last_rect["height"] / 2.0
+ elif location_type == "caret":
+ # Selection carets' tip are below the bottom of the two ends of the
+ # selection. Add 5px to y should be sufficient to locate them.
+ caret_tip_y_offset = 5
+ start_y_offset = first_rect["height"] + caret_tip_y_offset
+ end_y_offset = last_rect["height"] + caret_tip_y_offset
+ else:
+ start_y_offset = end_y_offset = 0
+
+ caret1_x = first_rect[start_pos] - origin_x
+ caret1_y = first_rect["top"] + start_y_offset - origin_y
+ caret2_x = last_rect[end_pos] - origin_x
+ caret2_y = last_rect["top"] + end_y_offset - origin_y
+
+ return ((caret1_x, caret1_y), (caret2_x, caret2_y))
+
+ def selection_location(self):
+ """Return the start and end location of the selection in the element.
+
+ Return a tuple containing two pairs of (x, y) coordinates of the start
+ and end of the selection. The coordinates are relative to the top
+ left-hand corner of the element. Both ltr and rtl direction are
+ considered.
+
+ """
+ return self._selection_location_helper("center")
+
+ def carets_location(self):
+ """Return a pair of the two carets' location.
+
+ Return a tuple containing two pairs of (x, y) coordinates of the two
+ carets' tip. The coordinates are relative to the top left-hand corner of
+ the element. Both ltr and rtl direction are considered.
+
+ """
+ return self._selection_location_helper("caret")
+
+ def cursor_location(self):
+ """Return the blanking cursor's center location within the element.
+
+ Return (x, y) coordinates of the cursor's center relative to the top
+ left-hand corner of the element.
+
+ """
+ return self._selection_location_helper("center")[0]
+
+ def first_caret_location(self):
+ """Return the first caret's location.
+
+ Return (x, y) coordinates of the first caret's tip relative to the top
+ left-hand corner of the element.
+
+ """
+ return self.carets_location()[0]
+
+ def second_caret_location(self):
+ """Return the second caret's location.
+
+ Return (x, y) coordinates of the second caret's tip relative to the top
+ left-hand corner of the element.
+
+ """
+ return self.carets_location()[1]
+
+ def select_all(self):
+ """Select all the content in the element."""
+ if self._input_or_textarea():
+ cmd = """var len = arguments[0].value.length;
+ arguments[0].focus();
+ arguments[0].setSelectionRange(0, len);"""
+ else:
+ cmd = """var range = document.createRange();
+ range.setStart(arguments[0].firstChild, 0);
+ range.setEnd(arguments[0].lastChild, arguments[0].lastChild.length);
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+ sel.addRange(range);"""
+
+ self.element.marionette.execute_script(
+ cmd, script_args=(self.element,), sandbox=None
+ )
+
+ @property
+ def content(self):
+ """Return all the content of the element."""
+ if self._input_or_textarea():
+ return self.element.get_property("value")
+ else:
+ return self.element.text
+
+ @property
+ def selected_content(self):
+ """Return the selected portion of the content in the element."""
+ cmd = self.js_selection_cmd() + """return sel.toString();"""
+ return self.element.marionette.execute_script(
+ cmd, script_args=(self.element,), sandbox="system"
+ )
diff --git a/layout/base/tests/marionette/test_accessiblecaret_cursor_mode.py b/layout/base/tests/marionette/test_accessiblecaret_cursor_mode.py
new file mode 100644
index 0000000000..a134b78f73
--- /dev/null
+++ b/layout/base/tests/marionette/test_accessiblecaret_cursor_mode.py
@@ -0,0 +1,280 @@
+# -*- coding: utf-8 -*-
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import string
+import sys
+import os
+
+# Add this directory to the import path.
+sys.path.append(os.path.dirname(__file__))
+
+from selection import (
+ CaretActions,
+ SelectionManager,
+)
+from marionette_driver.by import By
+from marionette_harness.marionette_test import (
+ MarionetteTestCase,
+ parameterized,
+)
+
+
+class AccessibleCaretCursorModeTestCase(MarionetteTestCase):
+ """Test cases for AccessibleCaret under cursor mode.
+
+ We call the blinking cursor (nsCaret) as cursor, and call AccessibleCaret as
+ caret for short.
+
+ """
+
+ # Element IDs.
+ _input_id = "input"
+ _input_padding_id = "input-padding"
+ _textarea_id = "textarea"
+ _textarea_one_line_id = "textarea-one-line"
+ _contenteditable_id = "contenteditable"
+
+ # Test html files.
+ _cursor_html = "layout/test_carets_cursor.html"
+
+ def setUp(self):
+ # Code to execute before every test is running.
+ super(AccessibleCaretCursorModeTestCase, self).setUp()
+ self.caret_tested_pref = "layout.accessiblecaret.enabled"
+ self.hide_carets_for_mouse = (
+ "layout.accessiblecaret.hide_carets_for_mouse_input"
+ )
+ self.prefs = {
+ self.caret_tested_pref: True,
+ self.hide_carets_for_mouse: False,
+ # To disable transition, or the caret may not be the desired
+ # location yet, we cannot press a caret successfully.
+ "layout.accessiblecaret.transition-duration": "0.0",
+ # Enabled hapticfeedback on all platforms. The tests shouldn't crash
+ # on platforms without hapticfeedback support.
+ "layout.accessiblecaret.hapticfeedback": True,
+ }
+ self.marionette.set_prefs(self.prefs)
+ self.actions = CaretActions(self.marionette)
+
+ def tearDown(self):
+ self.marionette.actions.release()
+ super(AccessibleCaretCursorModeTestCase, self).tearDown()
+
+ def open_test_html(self, test_html):
+ self.marionette.navigate(self.marionette.absolute_url(test_html))
+
+ @parameterized(_input_id, el_id=_input_id)
+ @parameterized(_textarea_id, el_id=_textarea_id)
+ @parameterized(_contenteditable_id, el_id=_contenteditable_id)
+ def test_move_cursor_to_the_right_by_one_character(self, el_id):
+ self.open_test_html(self._cursor_html)
+ el = self.marionette.find_element(By.ID, el_id)
+ sel = SelectionManager(el)
+ content_to_add = "!"
+ target_content = sel.content
+ target_content = target_content[:1] + content_to_add + target_content[1:]
+
+ # Get first caret (x, y) at position 1 and 2.
+ self.actions.click(element=el).perform()
+ sel.move_cursor_to_front()
+ cursor0_x, cursor0_y = sel.cursor_location()
+ first_caret0_x, first_caret0_y = sel.first_caret_location()
+ sel.move_cursor_by_offset(1)
+ first_caret1_x, first_caret1_y = sel.first_caret_location()
+
+ # Click the front of the input to make first caret appear.
+ self.actions.move(el, cursor0_x, cursor0_y).click().perform()
+
+ # Move first caret.
+ self.actions.flick(
+ el, first_caret0_x, first_caret0_y, first_caret1_x, first_caret1_y
+ ).perform()
+
+ self.actions.send_keys(content_to_add).perform()
+ self.assertEqual(target_content, sel.content)
+
+ @parameterized(_input_id, el_id=_input_id)
+ @parameterized(_textarea_id, el_id=_textarea_id)
+ @parameterized(_contenteditable_id, el_id=_contenteditable_id)
+ def test_move_cursor_to_end_by_dragging_caret_to_bottom_right_corner(self, el_id):
+ self.open_test_html(self._cursor_html)
+ el = self.marionette.find_element(By.ID, el_id)
+ sel = SelectionManager(el)
+ content_to_add = "!"
+ target_content = sel.content + content_to_add
+
+ # Click the front of the input to make first caret appear.
+ self.actions.click(element=el).perform()
+ sel.move_cursor_to_front()
+ self.actions.move(el, *sel.cursor_location()).click().perform()
+
+ # Move first caret to the bottom-right corner of the element.
+ src_x, src_y = sel.first_caret_location()
+ dest_x, dest_y = el.rect["width"], el.rect["height"]
+ self.actions.flick(el, src_x, src_y, dest_x, dest_y).perform()
+
+ self.actions.send_keys(content_to_add).perform()
+ self.assertEqual(target_content, sel.content)
+
+ @parameterized(_input_id, el_id=_input_id)
+ @parameterized(_textarea_id, el_id=_textarea_id)
+ @parameterized(_contenteditable_id, el_id=_contenteditable_id)
+ def test_move_cursor_to_front_by_dragging_caret_to_front(self, el_id):
+ self.open_test_html(self._cursor_html)
+ el = self.marionette.find_element(By.ID, el_id)
+ sel = SelectionManager(el)
+ content_to_add = "!"
+ target_content = content_to_add + sel.content
+
+ # Get first caret location at the front.
+ self.actions.click(element=el).perform()
+ sel.move_cursor_to_front()
+ dest_x, dest_y = sel.first_caret_location()
+
+ # Click to make first caret appear.
+ self.actions.click(element=el).perform()
+ sel.move_cursor_to_end()
+ self.actions.move(el, *sel.cursor_location()).click().perform()
+ src_x, src_y = sel.first_caret_location()
+
+ # Move first caret to the front of the input box.
+ self.actions.flick(el, src_x, src_y, dest_x, dest_y).perform()
+
+ self.actions.send_keys(content_to_add).perform()
+ self.assertEqual(target_content, sel.content)
+
+ def test_caret_not_appear_when_typing_in_scrollable_content(self):
+ self.open_test_html(self._cursor_html)
+ el = self.marionette.find_element(By.ID, self._input_id)
+ sel = SelectionManager(el)
+ content_to_add = "!"
+ non_target_content = content_to_add + sel.content + string.ascii_letters
+
+ self.actions.click(element=el).perform()
+ sel.move_cursor_to_end()
+
+ # Insert a long string to the end of the <input>, which triggers
+ # ScrollPositionChanged event.
+ el.send_keys(string.ascii_letters)
+
+ # The caret should not be visible. If it does appear wrongly due to the
+ # ScrollPositionChanged event, we can drag it to the front of the
+ # <input> to change the cursor position.
+ src_x, src_y = sel.first_caret_location()
+ dest_x, dest_y = 0, 0
+ self.actions.flick(el, src_x, src_y, dest_x, dest_y).perform()
+
+ # The content should not be inserted at the front of the <input>.
+ el.send_keys(content_to_add)
+
+ self.assertNotEqual(non_target_content, sel.content)
+
+ @parameterized(_input_id, el_id=_input_id)
+ @parameterized(_input_padding_id, el_id=_input_padding_id)
+ @parameterized(_textarea_one_line_id, el_id=_textarea_one_line_id)
+ @parameterized(_contenteditable_id, el_id=_contenteditable_id)
+ def test_caret_not_jump_when_dragging_to_editable_content_boundary(self, el_id):
+ self.open_test_html(self._cursor_html)
+ el = self.marionette.find_element(By.ID, el_id)
+ sel = SelectionManager(el)
+ content_to_add = "!"
+ non_target_content = sel.content + content_to_add
+
+ # Goal: the cursor position is not changed after dragging the caret down
+ # on the Y-axis.
+ self.actions.click(element=el).perform()
+ sel.move_cursor_to_front()
+ self.actions.move(el, *sel.cursor_location()).click().perform()
+ x, y = sel.first_caret_location()
+
+ # Drag the caret down by 50px, and insert '!'.
+ self.actions.flick(el, x, y, x, y + 50).perform()
+ self.actions.send_keys(content_to_add).perform()
+ self.assertNotEqual(non_target_content, sel.content)
+
+ @parameterized(_input_id, el_id=_input_id)
+ @parameterized(_input_padding_id, el_id=_input_padding_id)
+ @parameterized(_textarea_one_line_id, el_id=_textarea_one_line_id)
+ @parameterized(_contenteditable_id, el_id=_contenteditable_id)
+ def test_caret_not_jump_to_front_when_dragging_up_to_editable_content_boundary(
+ self, el_id
+ ):
+ self.open_test_html(self._cursor_html)
+ el = self.marionette.find_element(By.ID, el_id)
+ sel = SelectionManager(el)
+ content_to_add = "!"
+ non_target_content = content_to_add + sel.content
+
+ # Goal: the cursor position is not changed after dragging the caret down
+ # on the Y-axis.
+ self.actions.click(element=el).perform()
+ sel.move_cursor_to_end()
+ self.actions.move(el, *sel.cursor_location()).click().perform()
+ x, y = sel.first_caret_location()
+
+ # Drag the caret up by 40px, and insert '!'.
+ self.actions.flick(el, x, y, x, y - 40).perform()
+ self.actions.send_keys(content_to_add).perform()
+ self.assertNotEqual(non_target_content, sel.content)
+
+ def test_drag_caret_from_front_to_end_across_columns(self):
+ self.open_test_html("layout/test_carets_columns.html")
+ el = self.marionette.find_element(By.ID, "columns-inner")
+ sel = SelectionManager(el)
+ content_to_add = "!"
+ target_content = sel.content + content_to_add
+
+ # Goal: the cursor position can be changed by dragging the caret from
+ # the front to the end of the content.
+
+ # Click to make the cursor appear.
+ before_image_1 = self.marionette.find_element(By.ID, "before-image-1")
+ self.actions.click(element=before_image_1).perform()
+
+ # Click the front of the content to make first caret appear.
+ sel.move_cursor_to_front()
+ self.actions.move(el, *sel.cursor_location()).click().perform()
+ src_x, src_y = sel.first_caret_location()
+ dest_x, dest_y = el.rect["width"], el.rect["height"]
+
+ # Drag the first caret to the bottom-right corner of the element.
+ self.actions.flick(el, src_x, src_y, dest_x, dest_y).perform()
+
+ self.actions.send_keys(content_to_add).perform()
+ self.assertEqual(target_content, sel.content)
+
+ def test_move_cursor_to_front_by_dragging_caret_to_front_br_element(self):
+ self.open_test_html(self._cursor_html)
+ el = self.marionette.find_element(By.ID, self._contenteditable_id)
+ sel = SelectionManager(el)
+ content_to_add_1 = "!"
+ content_to_add_2 = "\n\n"
+ target_content = content_to_add_1 + content_to_add_2 + sel.content
+
+ # Goal: the cursor position can be changed by dragging the caret from
+ # the end of the content to the front br element. Because we cannot get
+ # caret location if it's on a br element, we need to get the first caret
+ # location then adding the new lines.
+
+ # Get first caret location at the front.
+ self.actions.click(element=el).perform()
+ sel.move_cursor_to_front()
+ dest_x, dest_y = sel.first_caret_location()
+
+ # Append new line to the front of the content.
+ el.send_keys(content_to_add_2)
+
+ # Click to make first caret appear.
+ self.actions.click(element=el).perform()
+ sel.move_cursor_to_end()
+ self.actions.move(el, *sel.cursor_location()).click().perform()
+ src_x, src_y = sel.first_caret_location()
+
+ # Move first caret to the front of the input box.
+ self.actions.flick(el, src_x, src_y, dest_x, dest_y).perform()
+
+ self.actions.send_keys(content_to_add_1).perform()
+ self.assertEqual(target_content, sel.content)
diff --git a/layout/base/tests/marionette/test_accessiblecaret_selection_mode.py b/layout/base/tests/marionette/test_accessiblecaret_selection_mode.py
new file mode 100644
index 0000000000..e3d815f62b
--- /dev/null
+++ b/layout/base/tests/marionette/test_accessiblecaret_selection_mode.py
@@ -0,0 +1,805 @@
+# -*- coding: utf-8 -*-
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import re
+import sys
+import os
+import unittest
+
+# Add this directory to the import path.
+sys.path.append(os.path.dirname(__file__))
+
+from selection import (
+ CaretActions,
+ SelectionManager,
+)
+from marionette_driver.by import By
+from marionette_driver.keys import Keys
+from marionette_harness.marionette_test import (
+ MarionetteTestCase,
+ SkipTest,
+ parameterized,
+)
+
+
+class AccessibleCaretSelectionModeTestCase(MarionetteTestCase):
+ """Test cases for AccessibleCaret under selection mode."""
+
+ # Element IDs.
+ _input_id = "input"
+ _input_padding_id = "input-padding"
+ _input_size_id = "input-size"
+ _textarea_id = "textarea"
+ _textarea2_id = "textarea2"
+ _textarea_disabled_id = "textarea-disabled"
+ _textarea_one_line_id = "textarea-one-line"
+ _textarea_rtl_id = "textarea-rtl"
+ _contenteditable_id = "contenteditable"
+ _contenteditable2_id = "contenteditable2"
+ _content_id = "content"
+ _content2_id = "content2"
+ _non_selectable_id = "non-selectable"
+
+ # Test html files.
+ _selection_html = "layout/test_carets_selection.html"
+ _multipleline_html = "layout/test_carets_multipleline.html"
+ _multiplerange_html = "layout/test_carets_multiplerange.html"
+ _longtext_html = "layout/test_carets_longtext.html"
+ _iframe_html = "layout/test_carets_iframe.html"
+ _iframe_scroll_html = "layout/test_carets_iframe_scroll.html"
+ _display_none_html = "layout/test_carets_display_none.html"
+ _svg_shapes_html = "layout/test_carets_svg_shapes.html"
+ _key_scroll_html = "layout/test_carets_key_scroll.html"
+
+ def setUp(self):
+ # Code to execute before every test is running.
+ super(AccessibleCaretSelectionModeTestCase, self).setUp()
+ self.carets_tested_pref = "layout.accessiblecaret.enabled"
+ self.prefs = {
+ "layout.word_select.eat_space_to_next_word": False,
+ self.carets_tested_pref: True,
+ # To disable transition, or the caret may not be the desired
+ # location yet, we cannot press a caret successfully.
+ "layout.accessiblecaret.transition-duration": "0.0",
+ # Enabled hapticfeedback on all platforms. The tests shouldn't crash
+ # on platforms without hapticfeedback support.
+ "layout.accessiblecaret.hapticfeedback": True,
+ }
+ self.marionette.set_prefs(self.prefs)
+ self.actions = CaretActions(self.marionette)
+
+ def tearDown(self):
+ self.marionette.actions.release()
+ super(AccessibleCaretSelectionModeTestCase, self).tearDown()
+
+ def open_test_html(self, test_html):
+ self.marionette.navigate(self.marionette.absolute_url(test_html))
+
+ def word_offset(self, text, ordinal):
+ "Get the character offset of the ordinal-th word in text."
+ tokens = re.split(r"(\S+)", text) # both words and spaces
+ spaces = tokens[0::2] # collect spaces at odd indices
+ words = tokens[1::2] # collect word at even indices
+
+ if ordinal >= len(words):
+ raise IndexError(
+ "Only %d words in text, but got ordinal %d" % (len(words), ordinal)
+ )
+
+ # Cursor position of the targeting word is behind the the first
+ # character in the word. For example, offset to 'def' in 'abc def' is
+ # between 'd' and 'e'.
+ offset = len(spaces[0]) + 1
+ offset += sum(len(words[i]) + len(spaces[i + 1]) for i in range(ordinal))
+ return offset
+
+ def test_word_offset(self):
+ text = " " * 3 + "abc" + " " * 3 + "def"
+
+ self.assertTrue(self.word_offset(text, 0), 4)
+ self.assertTrue(self.word_offset(text, 1), 10)
+ with self.assertRaises(IndexError):
+ self.word_offset(text, 2)
+
+ def word_location(self, el, ordinal):
+ """Get the location (x, y) of the ordinal-th word in el.
+
+ The ordinal starts from 0.
+
+ Note: this function has a side effect which changes focus to the
+ target element el.
+
+ """
+ sel = SelectionManager(el)
+ offset = self.word_offset(sel.content, ordinal)
+
+ # Move the blinking cursor to the word.
+ self.actions.click(element=el).perform()
+ sel.move_cursor_to_front()
+ sel.move_cursor_by_offset(offset)
+ x, y = sel.cursor_location()
+
+ return x, y
+
+ def rect_relative_to_window(self, el):
+ """Get element's bounding rectangle.
+
+ This function is similar to el.rect, but the coordinate is relative to
+ the top left corner of the window instead of the document.
+
+ """
+ return self.marionette.execute_script(
+ """
+ let rect = arguments[0].getBoundingClientRect();
+ return {x: rect.x, y:rect.y, width: rect.width, height: rect.height};
+ """,
+ script_args=[el],
+ )
+
+ def long_press_on_location(self, el, x=None, y=None):
+ """Long press the location (x, y) to select a word.
+
+ If no (x, y) are given, it will be targeted at the center of the
+ element. On Windows, those spaces after the word will also be selected.
+ This function sends synthesized eMouseLongTap to gecko.
+
+ """
+ rect = self.rect_relative_to_window(el)
+ target_x = rect["x"] + (x if x is not None else rect["width"] // 2)
+ target_y = rect["y"] + (y if y is not None else rect["height"] // 2)
+
+ self.marionette.execute_script(
+ """
+ let utils = window.windowUtils;
+ utils.sendTouchEventToWindow('touchstart', [0],
+ [arguments[0]], [arguments[1]],
+ [1], [1], [0], [1], [0], [0], [0], 0);
+ utils.sendMouseEventToWindow('mouselongtap', arguments[0], arguments[1],
+ 0, 1, 0);
+ utils.sendTouchEventToWindow('touchend', [0],
+ [arguments[0]], [arguments[1]],
+ [1], [1], [0], [1], [0], [0], [0], 0);
+ """,
+ script_args=[target_x, target_y],
+ sandbox="system",
+ )
+
+ def long_press_on_word(self, el, wordOrdinal):
+ x, y = self.word_location(el, wordOrdinal)
+ self.long_press_on_location(el, x, y)
+
+ def to_unix_line_ending(self, s):
+ """Changes all Windows/Mac line endings in s to UNIX line endings."""
+
+ return s.replace("\r\n", "\n").replace("\r", "\n")
+
+ @parameterized(_input_id, el_id=_input_id)
+ @parameterized(_textarea_id, el_id=_textarea_id)
+ @parameterized(_textarea_disabled_id, el_id=_textarea_disabled_id)
+ @parameterized(_textarea_rtl_id, el_id=_textarea_rtl_id)
+ @parameterized(_contenteditable_id, el_id=_contenteditable_id)
+ @parameterized(_content_id, el_id=_content_id)
+ def test_long_press_to_select_a_word(self, el_id):
+ self.open_test_html(self._selection_html)
+ el = self.marionette.find_element(By.ID, el_id)
+ self._test_long_press_to_select_a_word(el)
+
+ def _test_long_press_to_select_a_word(self, el):
+ sel = SelectionManager(el)
+ original_content = sel.content
+ words = original_content.split()
+ self.assertTrue(len(words) >= 2, "Expect at least two words in the content.")
+ target_content = words[0]
+
+ # Goal: Select the first word.
+ self.long_press_on_word(el, 0)
+
+ # Ignore extra spaces selected after the word.
+ self.assertEqual(target_content, sel.selected_content)
+
+ @parameterized(_input_id, el_id=_input_id)
+ @parameterized(_textarea_id, el_id=_textarea_id)
+ @parameterized(_textarea_disabled_id, el_id=_textarea_disabled_id)
+ @parameterized(_textarea_rtl_id, el_id=_textarea_rtl_id)
+ @parameterized(_contenteditable_id, el_id=_contenteditable_id)
+ @parameterized(_content_id, el_id=_content_id)
+ def test_drag_carets(self, el_id):
+ self.open_test_html(self._selection_html)
+ el = self.marionette.find_element(By.ID, el_id)
+ sel = SelectionManager(el)
+ original_content = sel.content
+ words = original_content.split()
+ self.assertTrue(len(words) >= 1, "Expect at least one word in the content.")
+
+ # Goal: Select all text after the first word.
+ target_content = original_content[len(words[0]) :]
+
+ # Get the location of the carets at the end of the content for later
+ # use.
+ self.actions.click(element=el).perform()
+ sel.select_all()
+ end_caret_x, end_caret_y = sel.second_caret_location()
+
+ self.long_press_on_word(el, 0)
+
+ # Drag the second caret to the end of the content.
+ (caret1_x, caret1_y), (caret2_x, caret2_y) = sel.carets_location()
+ self.actions.flick(el, caret2_x, caret2_y, end_caret_x, end_caret_y).perform()
+
+ # Drag the first caret to the previous position of the second caret.
+ self.actions.flick(el, caret1_x, caret1_y, caret2_x, caret2_y).perform()
+
+ self.assertEqual(target_content, sel.selected_content)
+
+ @parameterized(_input_id, el_id=_input_id)
+ @parameterized(_textarea_id, el_id=_textarea_id)
+ @parameterized(_textarea_disabled_id, el_id=_textarea_disabled_id)
+ @parameterized(_textarea_rtl_id, el_id=_textarea_rtl_id)
+ @parameterized(_contenteditable_id, el_id=_contenteditable_id)
+ @parameterized(_content_id, el_id=_content_id)
+ def test_drag_swappable_carets(self, el_id):
+ self.open_test_html(self._selection_html)
+ el = self.marionette.find_element(By.ID, el_id)
+ sel = SelectionManager(el)
+ original_content = sel.content
+ words = original_content.split()
+ self.assertTrue(len(words) >= 1, "Expect at least one word in the content.")
+
+ target_content1 = words[0]
+ target_content2 = original_content[len(words[0]) :]
+
+ # Get the location of the carets at the end of the content for later
+ # use.
+ self.actions.click(element=el).perform()
+ sel.select_all()
+ end_caret_x, end_caret_y = sel.second_caret_location()
+
+ self.long_press_on_word(el, 0)
+
+ # Drag the first caret to the end and back to where it was
+ # immediately. The selection range should not be collapsed.
+ caret1_x, caret1_y = sel.first_caret_location()
+ self.actions.flick(el, caret1_x, caret1_y, end_caret_x, end_caret_y).flick(
+ el, end_caret_x, end_caret_y, caret1_x, caret1_y
+ ).perform()
+ self.assertEqual(target_content1, sel.selected_content)
+
+ # Drag the first caret to the end.
+ caret1_x, caret1_y = sel.first_caret_location()
+ self.actions.flick(el, caret1_x, caret1_y, end_caret_x, end_caret_y).perform()
+ self.assertEqual(target_content2, sel.selected_content)
+
+ @parameterized(_input_id, el_id=_input_id)
+ # Bug 1855083: Skip textarea tests due to high frequent intermittent.
+ # @parameterized(_textarea_id, el_id=_textarea_id)
+ # @parameterized(_textarea_disabled_id, el_id=_textarea_disabled_id)
+ @parameterized(_textarea_rtl_id, el_id=_textarea_rtl_id)
+ @parameterized(_contenteditable_id, el_id=_contenteditable_id)
+ @parameterized(_content_id, el_id=_content_id)
+ def test_minimum_select_one_character(self, el_id):
+ self.open_test_html(self._selection_html)
+ el = self.marionette.find_element(By.ID, el_id)
+ self._test_minimum_select_one_character(el)
+
+ @parameterized(_textarea2_id, el_id=_textarea2_id)
+ @parameterized(_contenteditable2_id, el_id=_contenteditable2_id)
+ @parameterized(_content2_id, el_id=_content2_id)
+ def test_minimum_select_one_character2(self, el_id):
+ self.open_test_html(self._multipleline_html)
+ el = self.marionette.find_element(By.ID, el_id)
+ self._test_minimum_select_one_character(el)
+
+ def _test_minimum_select_one_character(self, el):
+ sel = SelectionManager(el)
+ original_content = sel.content
+ words = original_content.split()
+ self.assertTrue(len(words) >= 1, "Expect at least one word in the content.")
+
+ # Get the location of the carets at the end of the content for later
+ # use.
+ sel.select_all()
+ end_caret_x, end_caret_y = sel.second_caret_location()
+ self.actions.click(element=el).perform()
+
+ # Goal: Select the first character.
+ target_content = original_content[0]
+
+ self.long_press_on_word(el, 0)
+
+ # Drag the second caret to the end of the content.
+ (caret1_x, caret1_y), (caret2_x, caret2_y) = sel.carets_location()
+ self.actions.flick(el, caret2_x, caret2_y, end_caret_x, end_caret_y).perform()
+
+ # Drag the second caret to the position of the first caret.
+ (caret1_x, caret1_y), (caret2_x, caret2_y) = sel.carets_location()
+ self.actions.flick(el, caret2_x, caret2_y, caret1_x, caret1_y).perform()
+
+ self.assertEqual(target_content, sel.selected_content)
+
+ @parameterized(
+ _input_id + "_to_" + _textarea_id, el1_id=_input_id, el2_id=_textarea_id
+ )
+ @parameterized(
+ _input_id + "_to_" + _contenteditable_id,
+ el1_id=_input_id,
+ el2_id=_contenteditable_id,
+ )
+ @parameterized(
+ _input_id + "_to_" + _content_id, el1_id=_input_id, el2_id=_content_id
+ )
+ @parameterized(
+ _textarea_id + "_to_" + _input_id, el1_id=_textarea_id, el2_id=_input_id
+ )
+ @parameterized(
+ _textarea_id + "_to_" + _contenteditable_id,
+ el1_id=_textarea_id,
+ el2_id=_contenteditable_id,
+ )
+ @parameterized(
+ _textarea_id + "_to_" + _content_id, el1_id=_textarea_id, el2_id=_content_id
+ )
+ @parameterized(
+ _contenteditable_id + "_to_" + _input_id,
+ el1_id=_contenteditable_id,
+ el2_id=_input_id,
+ )
+ @parameterized(
+ _contenteditable_id + "_to_" + _textarea_id,
+ el1_id=_contenteditable_id,
+ el2_id=_textarea_id,
+ )
+ @parameterized(
+ _contenteditable_id + "_to_" + _content_id,
+ el1_id=_contenteditable_id,
+ el2_id=_content_id,
+ )
+ @parameterized(
+ _content_id + "_to_" + _input_id, el1_id=_content_id, el2_id=_input_id
+ )
+ @parameterized(
+ _content_id + "_to_" + _textarea_id, el1_id=_content_id, el2_id=_textarea_id
+ )
+ @parameterized(
+ _content_id + "_to_" + _contenteditable_id,
+ el1_id=_content_id,
+ el2_id=_contenteditable_id,
+ )
+ def test_long_press_changes_focus_from(self, el1_id, el2_id):
+ self.open_test_html(self._selection_html)
+ el1 = self.marionette.find_element(By.ID, el1_id)
+ el2 = self.marionette.find_element(By.ID, el2_id)
+
+ # Compute the content of the first word in el2.
+ sel = SelectionManager(el2)
+ original_content = sel.content
+ words = original_content.split()
+ target_content = words[0]
+
+ # Goal: Click to focus el1, and then select the first word on el2.
+
+ # We want to collect the location of the first word in el2 here
+ # since self.word_location() has the side effect which would
+ # change the focus.
+ x, y = self.word_location(el2, 0)
+
+ self.actions.click(element=el1).perform()
+ self.long_press_on_location(el2, x, y)
+ self.assertEqual(target_content, sel.selected_content)
+
+ @parameterized(_input_id, el_id=_input_id)
+ @parameterized(_textarea_id, el_id=_textarea_id)
+ @parameterized(_textarea_rtl_id, el_id=_textarea_rtl_id)
+ @parameterized(_contenteditable_id, el_id=_contenteditable_id)
+ def test_focus_not_changed_by_long_press_on_non_selectable(self, el_id):
+ self.open_test_html(self._selection_html)
+ el = self.marionette.find_element(By.ID, el_id)
+ non_selectable = self.marionette.find_element(By.ID, self._non_selectable_id)
+
+ # Goal: Focus remains on the editable element el after long pressing on
+ # the non-selectable element.
+ sel = SelectionManager(el)
+ self.long_press_on_word(el, 0)
+ self.long_press_on_location(non_selectable)
+ active_sel = SelectionManager(self.marionette.get_active_element())
+ self.assertEqual(sel.content, active_sel.content)
+
+ @parameterized(_input_id, el_id=_input_id)
+ @parameterized(_textarea_id, el_id=_textarea_id)
+ @parameterized(_textarea_disabled_id, el_id=_textarea_disabled_id)
+ @parameterized(_textarea_rtl_id, el_id=_textarea_rtl_id)
+ @parameterized(_contenteditable_id, el_id=_contenteditable_id)
+ @parameterized(_content_id, el_id=_content_id)
+ def test_handle_tilt_when_carets_overlap_each_other(self, el_id):
+ """Test tilt handling when carets overlap to each other.
+
+ Let the two carets overlap each other. If they are set to tilted
+ successfully, click on the tilted carets should not cause the selection
+ to be collapsed and the carets should be draggable.
+
+ """
+ self.open_test_html(self._selection_html)
+ el = self.marionette.find_element(By.ID, el_id)
+ sel = SelectionManager(el)
+ original_content = sel.content
+ words = original_content.split()
+ self.assertTrue(len(words) >= 1, "Expect at least one word in the content.")
+
+ # Goal: Select the first word.
+ self.long_press_on_word(el, 0)
+ target_content = sel.selected_content
+
+ # Drag the first caret to the position of the second caret to trigger
+ # carets overlapping.
+ (caret1_x, caret1_y), (caret2_x, caret2_y) = sel.carets_location()
+ self.actions.flick(el, caret1_x, caret1_y, caret2_x, caret2_y).perform()
+
+ # We make two hit tests targeting the left edge of the left tilted caret
+ # and the right edge of the right tilted caret. If either of the hits is
+ # missed, selection would be collapsed and both carets should not be
+ # draggable.
+ (caret3_x, caret3_y), (caret4_x, caret4_y) = sel.carets_location()
+
+ # The following values are from ua.css and all.js
+ caret_width = float(self.marionette.get_pref("layout.accessiblecaret.width"))
+ caret_margin_left = float(
+ self.marionette.get_pref("layout.accessiblecaret.margin-left")
+ )
+ tilt_right_margin_left = 0.41 * caret_width
+ tilt_left_margin_left = -0.39 * caret_width
+
+ left_caret_left_edge_x = caret3_x + caret_margin_left + tilt_left_margin_left
+ self.actions.move(el, left_caret_left_edge_x + 2, caret3_y).click().perform()
+
+ right_caret_right_edge_x = (
+ caret4_x + caret_margin_left + tilt_right_margin_left + caret_width
+ )
+ self.actions.move(el, right_caret_right_edge_x - 2, caret4_y).click().perform()
+
+ # Drag the first caret back to the initial selection, the first word.
+ self.actions.flick(el, caret3_x, caret3_y, caret1_x, caret1_y).perform()
+
+ self.assertEqual(target_content, sel.selected_content)
+
+ def test_drag_caret_over_non_selectable_field(self):
+ """Test dragging the caret over a non-selectable field.
+
+ The selected content should exclude non-selectable elements and the
+ second caret should appear in last range's position.
+
+ """
+ self.open_test_html(self._multiplerange_html)
+ body = self.marionette.find_element(By.ID, "bd")
+ sel3 = self.marionette.find_element(By.ID, "sel3")
+ sel4 = self.marionette.find_element(By.ID, "sel4")
+ sel6 = self.marionette.find_element(By.ID, "sel6")
+
+ # Select target element and get target caret location
+ self.long_press_on_word(sel4, 3)
+ sel = SelectionManager(body)
+ end_caret_x, end_caret_y = sel.second_caret_location()
+
+ self.long_press_on_word(sel6, 0)
+ end_caret2_x, end_caret2_y = sel.second_caret_location()
+
+ # Select start element
+ self.long_press_on_word(sel3, 3)
+
+ # Drag end caret to target location
+ (caret1_x, caret1_y), (caret2_x, caret2_y) = sel.carets_location()
+ self.actions.flick(
+ body, caret2_x, caret2_y, end_caret_x, end_caret_y, 1
+ ).perform()
+ self.assertEqual(
+ self.to_unix_line_ending(sel.selected_content.strip()),
+ "this 3\nuser can select this",
+ )
+
+ (caret1_x, caret1_y), (caret2_x, caret2_y) = sel.carets_location()
+ self.actions.flick(
+ body, caret2_x, caret2_y, end_caret2_x, end_caret2_y, 1
+ ).perform()
+ self.assertEqual(
+ self.to_unix_line_ending(sel.selected_content.strip()),
+ "this 3\nuser can select this 4\nuser can select this 5\nuser",
+ )
+
+ # Drag first caret to target location
+ (caret1_x, caret1_y), (caret2_x, caret2_y) = sel.carets_location()
+ self.actions.flick(
+ body, caret1_x, caret1_y, end_caret_x, end_caret_y, 1
+ ).perform()
+ self.assertEqual(
+ self.to_unix_line_ending(sel.selected_content.strip()),
+ "4\nuser can select this 5\nuser",
+ )
+
+ def test_drag_swappable_caret_over_non_selectable_field(self):
+ self.open_test_html(self._multiplerange_html)
+ body = self.marionette.find_element(By.ID, "bd")
+ sel3 = self.marionette.find_element(By.ID, "sel3")
+ sel4 = self.marionette.find_element(By.ID, "sel4")
+ sel = SelectionManager(body)
+
+ self.long_press_on_word(sel4, 3)
+ (
+ (end_caret1_x, end_caret1_y),
+ (end_caret2_x, end_caret2_y),
+ ) = sel.carets_location()
+
+ self.long_press_on_word(sel3, 3)
+ (caret1_x, caret1_y), (caret2_x, caret2_y) = sel.carets_location()
+
+ # Drag the first caret down, which will across the second caret.
+ self.actions.flick(
+ body, caret1_x, caret1_y, end_caret1_x, end_caret1_y
+ ).perform()
+ self.assertEqual(
+ self.to_unix_line_ending(sel.selected_content.strip()), "3\nuser can select"
+ )
+
+ # The old second caret becomes the first caret. Drag it down again.
+ self.actions.flick(
+ body, caret2_x, caret2_y, end_caret2_x, end_caret2_y
+ ).perform()
+ self.assertEqual(self.to_unix_line_ending(sel.selected_content.strip()), "this")
+
+ def test_drag_caret_to_beginning_of_a_line(self):
+ """Bug 1094056
+ Test caret visibility when caret is dragged to beginning of a line
+ """
+ self.open_test_html(self._multiplerange_html)
+ body = self.marionette.find_element(By.ID, "bd")
+ sel1 = self.marionette.find_element(By.ID, "sel1")
+ sel2 = self.marionette.find_element(By.ID, "sel2")
+
+ # Select the first word in the second line
+ self.long_press_on_word(sel2, 0)
+ sel = SelectionManager(body)
+ (
+ (start_caret_x, start_caret_y),
+ (end_caret_x, end_caret_y),
+ ) = sel.carets_location()
+
+ # Select target word in the first line
+ self.long_press_on_word(sel1, 2)
+
+ # Drag end caret to the beginning of the second line
+ (caret1_x, caret1_y), (caret2_x, caret2_y) = sel.carets_location()
+ self.actions.flick(
+ body, caret2_x, caret2_y, start_caret_x, start_caret_y
+ ).perform()
+
+ # Drag end caret back to the target word
+ self.actions.flick(
+ body, start_caret_x, start_caret_y, caret2_x, caret2_y
+ ).perform()
+
+ self.assertEqual(self.to_unix_line_ending(sel.selected_content), "select")
+
+ def test_select_word_inside_an_iframe(self):
+ """Bug 1088552
+ The scroll offset in iframe should be taken into consideration properly.
+ In this test, we scroll content in the iframe to the bottom to cause a
+ huge offset. If we use the right coordinate system, selection should
+ work. Otherwise, it would be hard to trigger select word.
+ """
+ self.open_test_html(self._iframe_html)
+ iframe = self.marionette.find_element(By.ID, "frame")
+
+ # switch to inner iframe and scroll to the bottom
+ self.marionette.switch_to_frame(iframe)
+ self.marionette.execute_script('document.getElementById("bd").scrollTop += 999')
+
+ # long press to select bottom text
+ body = self.marionette.find_element(By.ID, "bd")
+ sel = SelectionManager(body)
+ self._bottomtext = self.marionette.find_element(By.ID, "bottomtext")
+ self.long_press_on_location(self._bottomtext)
+
+ self.assertNotEqual(self.to_unix_line_ending(sel.selected_content), "")
+
+ def test_select_word_inside_an_unfocused_iframe(self):
+ """Bug 1306634: Test we can long press to select a word in an unfocused iframe."""
+ self.open_test_html(self._iframe_html)
+
+ el = self.marionette.find_element(By.ID, self._input_id)
+ sel = SelectionManager(el)
+
+ # First, we select the first word in the input of the parent document.
+ el_first_word = sel.content.split()[0] # first world is "ABC"
+ self.long_press_on_word(el, 0)
+ self.assertEqual(el_first_word, sel.selected_content)
+
+ # Then, we long press on the center of the iframe. It should select a
+ # word inside of the document, not the placehoder in the parent
+ # document.
+ iframe = self.marionette.find_element(By.ID, "frame")
+ self.long_press_on_location(iframe)
+ self.marionette.switch_to_frame(iframe)
+ body = self.marionette.find_element(By.ID, "bd")
+ sel = SelectionManager(body)
+ self.assertNotEqual("", sel.selected_content)
+
+ def test_carets_initialized_in_display_none(self):
+ """Test AccessibleCaretEventHub is properly initialized on a <html> with
+ display: none.
+
+ """
+ self.open_test_html(self._display_none_html)
+ html = self.marionette.find_element(By.ID, "html")
+ content = self.marionette.find_element(By.ID, "content")
+
+ # Remove 'display: none' from <html>
+ self.marionette.execute_script(
+ 'arguments[0].style.display = "unset";', script_args=[html]
+ )
+
+ # If AccessibleCaretEventHub is initialized successfully, select a word
+ # should work.
+ self._test_long_press_to_select_a_word(content)
+
+ @unittest.skip("Bug 1855083: High frequent intermittent.")
+ def test_long_press_to_select_when_partial_visible_word_is_selected(self):
+ self.open_test_html(self._selection_html)
+ el = self.marionette.find_element(By.ID, self._input_size_id)
+ sel = SelectionManager(el)
+
+ original_content = sel.content
+ words = original_content.split()
+
+ # We cannot use self.long_press_on_word() for the second long press
+ # on the first word because it has side effect that changes the
+ # cursor position. We need to save the location of the first word to
+ # be used later.
+ word0_x, word0_y = self.word_location(el, 0)
+
+ # Long press on the second word.
+ self.long_press_on_word(el, 1)
+ self.assertEqual(words[1], sel.selected_content)
+
+ # Long press on the first word.
+ self.long_press_on_location(el, word0_x, word0_y)
+ self.assertEqual(words[0], sel.selected_content)
+
+ # If the second caret is visible, it can be dragged to the position
+ # of the first caret. After that, selection will contain only the
+ # first character.
+ (caret1_x, caret1_y), (caret2_x, caret2_y) = sel.carets_location()
+ self.actions.flick(el, caret2_x, caret2_y, caret1_x, caret1_y).perform()
+ self.assertEqual(words[0][0], sel.selected_content)
+
+ @parameterized(_input_id, el_id=_input_id)
+ @parameterized(_input_padding_id, el_id=_input_padding_id)
+ @parameterized(_textarea_one_line_id, el_id=_textarea_one_line_id)
+ @parameterized(_contenteditable_id, el_id=_contenteditable_id)
+ def test_carets_not_jump_when_dragging_to_editable_content_boundary(self, el_id):
+ self.open_test_html(self._selection_html)
+ el = self.marionette.find_element(By.ID, el_id)
+ sel = SelectionManager(el)
+ original_content = sel.content
+ words = original_content.split()
+ self.assertTrue(len(words) >= 3, "Expect at least three words in the content.")
+
+ # Goal: the selection is not changed after dragging the caret on the
+ # Y-axis.
+ target_content = words[1]
+
+ self.long_press_on_word(el, 1)
+ (caret1_x, caret1_y), (caret2_x, caret2_y) = sel.carets_location()
+
+ # Drag the first caret up by 40px.
+ self.actions.flick(el, caret1_x, caret1_y, caret1_x, caret1_y - 40).perform()
+ self.assertEqual(target_content, sel.selected_content)
+
+ # Drag the second caret down by 50px.
+ self.actions.flick(el, caret2_x, caret2_y, caret2_x, caret2_y + 50).perform()
+ self.assertEqual(target_content, sel.selected_content)
+
+ def test_carets_should_not_appear_when_long_pressing_svg_shapes(self):
+ self.open_test_html(self._svg_shapes_html)
+
+ rect = self.marionette.find_element(By.ID, "rect")
+ text = self.marionette.find_element(By.ID, "text")
+
+ sel = SelectionManager(text)
+ num_words_in_text = len(sel.content.split())
+
+ # Goal: the carets should not appear when long-pressing on the
+ # unselectable SVG rect.
+
+ # Get the position of the end of last word in text, i.e. the
+ # position of the second caret when selecting the last word.
+ self.long_press_on_word(text, num_words_in_text - 1)
+ (_, _), (x2, y2) = sel.carets_location()
+
+ # Long press to select the unselectable SVG rect.
+ self.long_press_on_location(rect)
+ (_, _), (x, y) = sel.carets_location()
+
+ # Drag the second caret to (x2, y2).
+ self.actions.flick(text, x, y, x2, y2).perform()
+
+ # If the carets should appear on the rect, the selection will be
+ # extended to cover all the words in text. Assert this should not
+ # happen.
+ self.assertNotEqual(sel.content, sel.selected_content.strip())
+
+ def test_select_word_scroll_then_drag_caret(self):
+ """Bug 1657256: Test select word, scroll page up , and then drag the second
+ caret down to cover "EEEEEE".
+
+ Note the selection should be extended to just cover "EEEEEE", not extend
+ to other lines below "EEEEEE".
+
+ """
+
+ self.open_test_html(self._iframe_scroll_html)
+ iframe = self.marionette.find_element(By.ID, "iframe")
+
+ # Switch to the inner iframe.
+ self.marionette.switch_to_frame(iframe)
+ body = self.marionette.find_element(By.ID, "bd")
+ sel = SelectionManager(body)
+
+ # Select "EEEEEE" to get the y position of the second caret. This is the
+ # y position we are going to drag the caret to.
+ content2 = self.marionette.find_element(By.ID, self._content2_id)
+ self.long_press_on_word(content2, 0)
+ (_, _), (x, y2) = sel.carets_location()
+
+ # Step 1: Select "DDDDDD".
+ content = self.marionette.find_element(By.ID, self._content_id)
+ self.long_press_on_word(content, 0)
+ (_, _), (_, y1) = sel.carets_location()
+
+ # The y-axis offset of the second caret needed to extend the selection.
+ y_offset = y2 - y1
+
+ # Step 2: Scroll the page upwards by 40px.
+ scroll_offset = 40
+ self.marionette.execute_script(
+ "document.documentElement.scrollTop += arguments[0]",
+ script_args=[scroll_offset],
+ )
+
+ # Step 3: Drag the second caret down.
+ self.actions.flick(
+ body, x, y1 - scroll_offset, x, y1 - scroll_offset + y_offset
+ ).perform()
+
+ self.assertEqual("DDDDDD EEEEEE", sel.selected_content)
+
+ @unittest.skip("Bug 1855083: High frequent intermittent.")
+ def test_carets_not_show_after_key_scroll_down_and_up(self):
+ self.open_test_html(self._key_scroll_html)
+ html = self.marionette.find_element(By.ID, "html")
+ sel = SelectionManager(html)
+
+ # Select "BBBBB" to get the position of the second caret. This is the
+ # position to which we are going to drag the caret in the step 3.
+ content2 = self.marionette.find_element(By.ID, self._content2_id)
+ self.long_press_on_word(content2, 0)
+ (_, _), (x2, y2) = sel.carets_location()
+
+ # Step 1: Select "AAAAA".
+ content = self.marionette.find_element(By.ID, self._content_id)
+ self.long_press_on_word(content, 0)
+ (_, _), (x1, y1) = sel.carets_location()
+
+ # Step 2: Scroll the page down and up.
+ #
+ # FIXME: The selection highlight on "AAAAA" often disappears after page
+ # up, which is the root cause of the intermittent. Longer pause between
+ # the page down and page up might help, but it's not a great solution.
+ self.actions.key_chain.send_keys(Keys.PAGE_DOWN).pause(1000).send_keys(
+ Keys.PAGE_UP
+ ).perform()
+ self.assertEqual("AAAAA", sel.selected_content)
+
+ # Step 3: The carets shouldn't show up after scrolling the page. We're
+ # attempting to drag the second caret down so that if the bug occurs, we
+ # can drag the second caret to extend the selection to "BBBBB".
+ self.actions.flick(html, x1, y1, x2, y2).perform()
+ self.assertNotEqual("AAAAA\nBBBBB", sel.selected_content)