diff --git a/src/client/automation/playback/type/type-text.js b/src/client/automation/playback/type/type-text.js index 4e2863e2b38..1f0d99fad89 100644 --- a/src/client/automation/playback/type/type-text.js +++ b/src/client/automation/playback/type/type-text.js @@ -49,13 +49,15 @@ function _updateSelectionAfterDeletionContent (element, selection) { return selection; } -function _typeTextInElementNode (elementNode, text) { +function _typeTextInElementNode (elementNode, text, offset) { var nodeForTyping = document.createTextNode(text); var textLength = text.length; var selectPosition = { node: nodeForTyping, offset: textLength }; if (domUtils.getTagName(elementNode) === 'br') elementNode.parentNode.insertBefore(nodeForTyping, elementNode); + else if (offset > 0) + elementNode.insertBefore(nodeForTyping, elementNode.childNodes[offset]); else elementNode.appendChild(nodeForTyping); @@ -126,7 +128,7 @@ function _typeTextToContentEditable (element, text) { // NOTE: we can type only to the text nodes; for nodes with the 'element-node' type, we use a special behavior if (domUtils.isElementNode(startNode)) { - _typeTextInElementNode(startNode, text); + _typeTextInElementNode(startNode, text, currentSelection.startPos.offset); afterContentChanged(); return; diff --git a/src/client/core/utils/content-editable.js b/src/client/core/utils/content-editable.js index bb639ead888..668a3171e6c 100644 --- a/src/client/core/utils/content-editable.js +++ b/src/client/core/utils/content-editable.js @@ -1,6 +1,6 @@ import * as domUtils from './dom'; import * as arrayUtils from './array'; - +import * as styleUtils from './style'; //nodes utils function getOwnFirstVisibleTextNode (el) { @@ -34,12 +34,20 @@ function getOwnPreviousVisibleSibling (el) { return sibling; } -function hasChildren (node) { - return node.childNodes && domUtils.getChildNodesLength(node.childNodes); +function isVisibleNode (node) { + return domUtils.isTextNode(node) || domUtils.isElementNode(node) && styleUtils.isElementVisible(node); +} + +function getVisibleChildren (node) { + return arrayUtils.filter(node.childNodes, isVisibleNode); +} + +function hasVisibleChildren (node) { + return arrayUtils.some(node.childNodes, isVisibleNode); } -function isElementWithChildren (node) { - return domUtils.isElementNode(node) || hasChildren(node); +function hasSelectableChildren (node) { + return arrayUtils.some(node.childNodes, child => isNodeSelectable(child, true)); } //NOTE: before such elements (like div or p) adds line breaks before and after it @@ -118,7 +126,7 @@ export function getFirstVisibleTextNode (el) { if (isVisibleTextNode(curNode)) return curNode; - else if (domUtils.isRenderedNode(curNode) && isElementWithChildren(curNode) && !isNotContentEditableElement) { + else if (domUtils.isRenderedNode(curNode) && hasVisibleChildren(curNode) && !isNotContentEditableElement) { child = getFirstVisibleTextNode(curNode); if (child) @@ -148,7 +156,7 @@ export function getLastTextNode (el, onlyVisible) { if (visibleTextNode) return curNode; - else if (domUtils.isRenderedNode(curNode) && isElementWithChildren(curNode) && !isNotContentEditableElement) { + else if (domUtils.isRenderedNode(curNode) && hasVisibleChildren(curNode) && !isNotContentEditableElement) { child = getLastTextNode(curNode, false); if (child) @@ -293,8 +301,10 @@ function getSelectedPositionInParentByOffset (node, offset) { // NOTE: we try to find text node while (!isSkippableNode(currentNode) && domUtils.isElementNode(currentNode)) { - if (hasChildren(currentNode)) - currentNode = currentNode.childNodes[isSearchForLastChild ? currentNode.childNodes.length - 1 : 0]; + const visibleChildren = getVisibleChildren(currentNode); + + if (visibleChildren.length) + currentNode = visibleChildren[isSearchForLastChild ? visibleChildren.length - 1 : 0]; else { //NOTE: if we didn't find a text node then always set offset to zero currentOffset = 0; @@ -321,7 +331,7 @@ function getSelectionStart (el, selection, inverseSelection) { }; //NOTE: window.getSelection() can't returns not rendered node like selected node, so we shouldn't check it - if ((domUtils.isTheSameNode(el, startNode) || domUtils.isElementNode(startNode)) && hasChildren(startNode)) + if ((domUtils.isTheSameNode(el, startNode) || domUtils.isElementNode(startNode)) && hasSelectableChildren(startNode)) correctedStartPosition = getSelectedPositionInParentByOffset(startNode, startOffset); return { @@ -340,7 +350,7 @@ function getSelectionEnd (el, selection, inverseSelection) { }; //NOTE: window.getSelection() can't returns not rendered node like selected node, so we shouldn't check it - if ((domUtils.isTheSameNode(el, endNode) || domUtils.isElementNode(endNode)) && hasChildren(endNode)) + if ((domUtils.isTheSameNode(el, endNode) || domUtils.isElementNode(endNode)) && hasSelectableChildren(endNode)) correctedEndPosition = getSelectedPositionInParentByOffset(endNode, endOffset); return { @@ -368,6 +378,37 @@ export function getSelectionEndPosition (el, selection, inverseSelection) { return calculatePositionByNodeAndOffset(el, correctedSelectionEnd); } +function getElementOffset (target) { + let offset = 0; + + const firstBreakElement = arrayUtils.find(target.childNodes, (node, index) => { + offset = index; + return domUtils.getTagName(node) === 'br'; + }); + + return firstBreakElement ? offset : 0; +} + +function isNodeSelectable (node, includeDescendants) { + if (styleUtils.isNotVisibleNode(node)) + return false; + + if (domUtils.isTextNode(node)) + return true; + + if (!domUtils.isElementNode(node)) + return false; + + if (hasSelectableChildren(node)) + return includeDescendants; + + const isContentEditableRoot = !domUtils.isContentEditableElement(node.parentNode); + const visibleChildren = getVisibleChildren(node); + const hasBreakLineElements = arrayUtils.some(visibleChildren, child => domUtils.getTagName(child) === 'br'); + + return isContentEditableRoot || hasBreakLineElements; +} + export function calculateNodeAndOffsetByPosition (el, offset) { var point = { node: null, @@ -397,13 +438,18 @@ export function calculateNodeAndOffsetByPosition (el, offset) { } } else if (domUtils.isElementNode(target)) { - if (point.offset === 0 && !getContentEditableValue(target).length) { - point.node = target; + if (!isVisibleNode(target)) + return point; + + if (point.offset === 0 && isNodeSelectable(target, false)) { + point.node = target; + point.offset = getElementOffset(target); + return point; } if (!point.node && (isNodeBlockWithBreakLine(el, target) || isNodeAfterNodeBlockWithBreakLine(el, target))) point.offset--; - else if (!childNodesLength && domUtils.isElementNode(target) && domUtils.getTagName(target) === 'br') + else if (!childNodesLength && domUtils.getTagName(target) === 'br') point.offset--; } @@ -499,23 +545,22 @@ export function getLastVisiblePosition (el) { return 0; } -//contents util -export function getContentEditableValue (target) { - var elementValue = ''; +function getContentEditableNodes (target) { + var result = []; var childNodes = target.childNodes; var childNodesLength = domUtils.getChildNodesLength(childNodes); - if (isSkippableNode(target)) - return elementValue; - - if (!childNodesLength && domUtils.isTextNode(target)) - return target.nodeValue; - else if (childNodesLength === 1 && domUtils.isTextNode(childNodes[0])) - return childNodes[0].nodeValue; + if (!isSkippableNode(target) && !childNodesLength && domUtils.isTextNode(target)) + result.push(target); arrayUtils.forEach(childNodes, node => { - elementValue += getContentEditableValue(node); + result = result.concat(getContentEditableNodes(node)); }); - return elementValue; + return result; +} + +// contents util +export function getContentEditableValue (target) { + return arrayUtils.map(getContentEditableNodes(target), node => node.nodeValue).join(''); } diff --git a/src/client/core/utils/style.js b/src/client/core/utils/style.js index 7e9ca57fa89..0b84da2ef04 100644 --- a/src/client/core/utils/style.js +++ b/src/client/core/utils/style.js @@ -12,6 +12,7 @@ export var getElementPadding = hammerhead.utils.style.getElementPadding; export var getElementScroll = hammerhead.utils.style.getElementScroll; export var getOptionHeight = hammerhead.utils.style.getOptionHeight; export var getSelectElementSize = hammerhead.utils.style.getSelectElementSize; +export var isElementVisible = hammerhead.utils.style.isElementVisible; export var isSelectVisibleChild = hammerhead.utils.style.isVisibleChild; export var getWidth = hammerhead.utils.style.getWidth; export var getHeight = hammerhead.utils.style.getHeight; @@ -40,10 +41,10 @@ var getAncestorsAndSelf = function (node) { return getAncestors(node).concat([node]); }; -var isVisibilityHiddenTextNode = function (textNode) { - var el = domUtils.isTextNode(textNode) ? textNode.parentNode : null; +var isVisibilityHiddenNode = function (node) { + var ancestors = getAncestorsAndSelf(node); - return el && get(el, 'visibility') === 'hidden'; + return some(ancestors, ancestor => domUtils.isElementNode(ancestor) && get(ancestor, 'visibility') === 'hidden'); }; var isHiddenNode = function (node) { @@ -53,7 +54,7 @@ var isHiddenNode = function (node) { }; export function isNotVisibleNode (node) { - return !domUtils.isRenderedNode(node) || isHiddenNode(node) || isVisibilityHiddenTextNode(node); + return !domUtils.isRenderedNode(node) || isHiddenNode(node) || isVisibilityHiddenNode(node); } export function getScrollableParents (element) { @@ -135,22 +136,6 @@ export function hasDimensions (el) { return el && !(el.offsetHeight <= 0 && el.offsetWidth <= 0); } -export function isElementHidden (el) { - //NOTE: it's like jquery ':hidden' selector - if (get(el, 'display') === 'none' || !hasDimensions(el) || el.type && el.type === 'hidden') - return true; - - var elements = domUtils.findDocument(el).querySelectorAll('*'); - var hiddenElements = []; - - for (var i = 0; i < elements.length; i++) { - if (get(elements[i], 'display') === 'none' || !hasDimensions(elements[i])) - hiddenElements.push(elements[i]); - } - - return domUtils.containsElement(hiddenElements, el); -} - export function set (el, style, value) { if (typeof style === 'string') styleUtils.set(el, style, value); diff --git a/src/client/core/utils/text-selection.js b/src/client/core/utils/text-selection.js index ccbfeb50269..699328ed731 100644 --- a/src/client/core/utils/text-selection.js +++ b/src/client/core/utils/text-selection.js @@ -349,8 +349,14 @@ export function selectByNodesAndOffsets (startPos, endPos, needFocus) { var startNodeLength = startNode.nodeValue ? startNode.length : 0; var endNodeLength = endNode.nodeValue ? endNode.length : 0; - var startOffset = Math.min(startNodeLength, startPos.offset); - var endOffset = Math.min(endNodeLength, endPos.offset); + var startOffset = startPos.offset; + var endOffset = endPos.offset; + + if (!domUtils.isElementNode(startNode) || !startOffset) + startOffset = Math.min(startNodeLength, startPos.offset); + + if (!domUtils.isElementNode(endNode) || !endOffset) + endOffset = Math.min(endNodeLength, endPos.offset); var parentElement = contentEditable.findContentEditableParent(startNode); var inverse = isInverseSelectionContentEditable(parentElement, startPos, endPos); diff --git a/test/functional/fixtures/regression/gh-2205/pages/index.html b/test/functional/fixtures/regression/gh-2205/pages/index.html new file mode 100644 index 00000000000..5e6abd48491 --- /dev/null +++ b/test/functional/fixtures/regression/gh-2205/pages/index.html @@ -0,0 +1,101 @@ + + + + + Title + + + + +

Display: none

+
+
+
+ +

Visibility: hidden

+
+
+
+ +

Two hidden divs inside

+
+
test
+
+ +
Type here...
+
Type here...
+ + + + \ No newline at end of file diff --git a/test/functional/fixtures/regression/gh-2205/test.js b/test/functional/fixtures/regression/gh-2205/test.js new file mode 100644 index 00000000000..e947b52eb4e --- /dev/null +++ b/test/functional/fixtures/regression/gh-2205/test.js @@ -0,0 +1,5 @@ +describe('[Regression](GH-2205)', function () { + it('Should type in div if it has an invisible child with contententeditable=false', function () { + return runTests('testcafe-fixtures/index.js'); + }); +}); diff --git a/test/functional/fixtures/regression/gh-2205/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-2205/testcafe-fixtures/index.js new file mode 100644 index 00000000000..2c4409867d3 --- /dev/null +++ b/test/functional/fixtures/regression/gh-2205/testcafe-fixtures/index.js @@ -0,0 +1,26 @@ +import { Selector } from 'testcafe'; + +fixture `GH-2205 - Should type in div if it has an invisible child with contententeditable=false` + .page `http://localhost:3000/fixtures/regression/gh-2205/pages/index.html`; + +async function typeAndCheck (t, editorId) { + const editor = Selector(editorId); + + await t + .click(editor) + .typeText(editor, 'Hello') + .expect(editor.innerText).contains('Hello'); +} + +test(`Click on div with display:none placeholder`, async t => { + await typeAndCheck(t, '#editor1'); +}); + +test(`Click on div with visibility:hidden placeholder`, async t => { + await typeAndCheck(t, '#editor2'); +}); + +test(`Click on div with two invisible placeholders`, async t => { + await typeAndCheck(t, '#editor3'); +}); +