diff --git a/src/client/automation/playback/type/type-text.js b/src/client/automation/playback/type/type-text.js index 1f0d99fad89..4da58ca14bf 100644 --- a/src/client/automation/playback/type/type-text.js +++ b/src/client/automation/playback/type/type-text.js @@ -3,6 +3,7 @@ import testCafeCore from '../../deps/testcafe-core'; import nextTick from '../../utils/next-tick'; var browserUtils = hammerhead.utils.browser; +var eventSandbox = hammerhead.sandbox.event; var eventSimulator = hammerhead.eventSandbox.eventSimulator; var listeners = hammerhead.eventSandbox.listeners; @@ -64,6 +65,26 @@ function _typeTextInElementNode (elementNode, text, offset) { textSelection.selectByNodesAndOffsets(selectPosition, selectPosition); } +function _typeTextInChildTextNode (element, selection, text) { + let startNode = selection.startPos.node; + + // NOTE: startNode could be moved or deleted on textInput event. Need ensure startNode. + if (!domUtils.isElementContainsNode(element, startNode)) { + selection = _excludeInvisibleSymbolsFromSelection(_getSelectionInElement(element)); + startNode = selection.startPos.node; + } + + const startOffset = selection.startPos.offset; + const endOffset = selection.endPos.offset; + const nodeValue = startNode.nodeValue; + const selectPosition = { node: startNode, offset: startOffset + text.length }; + + startNode.nodeValue = nodeValue.substring(0, startOffset) + text + + nodeValue.substring(endOffset, nodeValue.length); + + textSelection.selectByNodesAndOffsets(selectPosition, selectPosition); +} + function _excludeInvisibleSymbolsFromSelection (selection) { var startNode = selection.startPos.node; var startOffset = selection.startPos.offset; @@ -84,26 +105,68 @@ function _excludeInvisibleSymbolsFromSelection (selection) { return selection; } +// NOTE: Typing can be prevented in Chrome/Edge but can not be prevented in IE11 or Firefox +// Firefox does not support TextInput event +// Safari supports the TextInput event but has a bug: e.data is added to the node value. +// So in Safari we need to call preventDefault in the last textInput handler but not prevent the Input event + +function simulateTextInput (element, text) { + let forceInputInSafari; + + function onSafariTextInput (e) { + e.preventDefault(); + + forceInputInSafari = true; + } + + function onSafariPreventTextInput (e) { + if (e.type === 'textInput') + forceInputInSafari = false; + } + + if (browserUtils.isSafari) { + listeners.addInternalEventListener(window, ['textInput'], onSafariTextInput); + eventSandbox.on(eventSandbox.EVENT_PREVENTED_EVENT, onSafariPreventTextInput); + } + + const isInputEventRequired = browserUtils.isFirefox || eventSimulator.textInput(element, text) || forceInputInSafari; + + if (browserUtils.isSafari) { + listeners.removeInternalEventListener(window, ['textInput'], onSafariTextInput); + eventSandbox.off(eventSandbox.EVENT_PREVENTED_EVENT, onSafariPreventTextInput); + } + + return isInputEventRequired || browserUtils.isIE11; +} + function _typeTextToContentEditable (element, text) { - var currentSelection = _getSelectionInElement(element); - var startNode = currentSelection.startPos.node; - var endNode = currentSelection.endPos.node; + var currentSelection = _getSelectionInElement(element); + var startNode = currentSelection.startPos.node; + var endNode = currentSelection.endPos.node; + var needProcessInput = true; + var needRaiseInputEvent = true; // NOTE: some browsers raise the 'input' event after the element // content is changed, but in others we should do it manually. - var inputEventRaised = false; var onInput = () => { - inputEventRaised = true; + needRaiseInputEvent = false; + }; + + // NOTE: IE11 does not raise input event when type to contenteditable + + var beforeContentChanged = () => { + needProcessInput = simulateTextInput(element, text); + needRaiseInputEvent = needProcessInput && !browserUtils.isIE11; }; var afterContentChanged = () => { nextTick() .then(() => { - if (!inputEventRaised) + if (needRaiseInputEvent) eventSimulator.input(element); - listeners.removeInternalEventListener(window, 'input', onInput); + listeners.removeInternalEventListener(window, ['input'], onInput); }); }; @@ -126,27 +189,16 @@ function _typeTextToContentEditable (element, text) { if (!startNode || !domUtils.isContentEditableElement(startNode) || !domUtils.isRenderedNode(startNode)) return; - // 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, currentSelection.startPos.offset); + beforeContentChanged(); - afterContentChanged(); - return; + if (needProcessInput) { + // 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); + else + _typeTextInChildTextNode(element, _excludeInvisibleSymbolsFromSelection(currentSelection), text); } - currentSelection = _excludeInvisibleSymbolsFromSelection(currentSelection); - startNode = currentSelection.startPos.node; - - var startOffset = currentSelection.startPos.offset; - var endOffset = currentSelection.endPos.offset; - var nodeValue = startNode.nodeValue; - var selectPosition = { node: startNode, offset: startOffset + text.length }; - - startNode.nodeValue = nodeValue.substring(0, startOffset) + text + - nodeValue.substring(endOffset, nodeValue.length); - - textSelection.selectByNodesAndOffsets(selectPosition, selectPosition); - afterContentChanged(); } @@ -156,6 +208,10 @@ function _typeTextToTextEditable (element, text) { var startSelection = textSelection.getSelectionStart(element); var endSelection = textSelection.getSelectionEnd(element); var isInputTypeNumber = domUtils.isInputElement(element) && element.type === 'number'; + var needProcessInput = simulateTextInput(element, text); + + if (!needProcessInput) + return; // NOTE: the 'maxlength' attribute doesn't work in all browsers. IE still doesn't support input with the 'number' type var elementMaxLength = !browserUtils.isIE && isInputTypeNumber ? null : parseInt(element.maxLength, 10); diff --git a/test/client/fixtures/automation/content-editable/api-actions-content-editable-test/index-test.js b/test/client/fixtures/automation/content-editable/api-actions-content-editable-test/index-test.js index 42a25d3ab26..6592c5f01dd 100644 --- a/test/client/fixtures/automation/content-editable/api-actions-content-editable-test/index-test.js +++ b/test/client/fixtures/automation/content-editable/api-actions-content-editable-test/index-test.js @@ -439,6 +439,9 @@ $(document).ready(function () { var fixedText = 'Test' + String.fromCharCode(160) + 'me' + String.fromCharCode(160) + 'all!'; var inputEventRaisedCount = 0; + // NOTE IE11 does not raise input event on contenteditable element + var expectedInputEventRaisedCount = browserUtils.isIE11 ? 0 : 12; + $el = $('#2'); function onInput () { @@ -454,7 +457,7 @@ $(document).ready(function () { .then(function () { checkSelection($el, $el[0].childNodes[2], 4 + text.length, $el[0].childNodes[2], 4 + text.length); equal($.trim($el[0].childNodes[2].nodeValue), 'with' + fixedText + ' br'); - equal(inputEventRaisedCount, 12); + equal(inputEventRaisedCount, expectedInputEventRaisedCount); $el.unbind('input', onInput); startNext(); @@ -465,6 +468,9 @@ $(document).ready(function () { var text = 'Test'; var inputEventRaisedCount = 0; + // NOTE IE11 does not raise input event on contenteditable element + var expectedInputEventRaisedCount = !browserUtils.isIE11 ? 4 : 0; + $el = $('#8'); function onInput () { @@ -479,7 +485,7 @@ $(document).ready(function () { .run() .then(function () { equal($.trim($el[0].textContent), text); - equal(inputEventRaisedCount, 4); + equal(inputEventRaisedCount, expectedInputEventRaisedCount); $el.unbind('input', onInput); startNext(); diff --git a/test/functional/fixtures/regression/gh-1054/testcafe-fixtures/index.test.js b/test/functional/fixtures/regression/gh-1054/testcafe-fixtures/index.test.js index 1500059d8a0..8e78ce69072 100644 --- a/test/functional/fixtures/regression/gh-1054/testcafe-fixtures/index.test.js +++ b/test/functional/fixtures/regression/gh-1054/testcafe-fixtures/index.test.js @@ -1,10 +1,12 @@ import { ClientFunction, Selector } from 'testcafe'; +import userAgent from 'useragent'; fixture `Check the target element value when the first input event raised` .page('http://localhost:3000/fixtures/regression/gh-1054/pages/index.html'); const getFirstValue = ClientFunction(() => window.storedValue); +const getUserAgent = ClientFunction(() => navigator.userAgent); test('Type text in the input', async t => { await t @@ -19,5 +21,9 @@ test('Type text in the content editable element', async t => { .typeText('div', 'text', { replace: true }) .expect(Selector('div').textContent).eql('text'); - await t.expect(await getFirstValue()).eql('t'); + var userAgentStr = await getUserAgent(); + var isIE = userAgent.is(userAgentStr).ie; + + if (!isIE) + await t.expect(await getFirstValue()).eql('t'); }); diff --git a/test/functional/fixtures/regression/gh-1956/pages/index.html b/test/functional/fixtures/regression/gh-1956/pages/index.html new file mode 100644 index 00000000000..f903cbbd489 --- /dev/null +++ b/test/functional/fixtures/regression/gh-1956/pages/index.html @@ -0,0 +1,100 @@ + +Edit test + + +

Prevent Input event on TextInput when type to input element

+
+        Chrome/Edge. Typing is prevented and Input event is not raised
+        IE11/Firefox. Typing is not prevented. Input event is raised
+    
+ +

Prevent Input event and typing on simple ContentEditable div

+
+        Chrome/Edge. Typing is prevented. Input event is not raised
+        IE11/Firefox. Typing is not prevented.
+        Input event is raised in firefox but is not raised in IE11 - it's a IE11 bug
+    
+
+

Prevent Input event on TextInput event when type to element node

+
+        Not for IE11 because preventDefault will not prevent typing
+        Not for Firefox because Firefox does not support TextInput event
+    
+


+

Modify text node of ContentEditable div on TextInput event and prevent Input event

+
+        Not for IE11 because is's not possible to prevent typing in IE11
+        Not for Firefox because Firefox does not support TextInput event
+    
+

A

+

Type to ContentEditable div when selected node was replaced on TextInput event

+
+        Not for IE11 because this test emulates behavior from https://github.com/DevExpress/testcafe/issues/1956.
+        This behavior is different in IE11
+        Not for Firefox because Firefox does not support TextInput event
+    
+

B

+ + + \ No newline at end of file diff --git a/test/functional/fixtures/regression/gh-1956/test.js b/test/functional/fixtures/regression/gh-1956/test.js new file mode 100644 index 00000000000..97616caa660 --- /dev/null +++ b/test/functional/fixtures/regression/gh-1956/test.js @@ -0,0 +1,63 @@ +var expect = require('chai').expect; + +var browsersWithLimitations = [ 'ie', 'firefox', 'firefox-osx' ]; + +describe('Should support TextInput event[Regression](GH-1956)', function () { + it('Prevent Input event on TextInput when type to input element', function () { + return runTests('testcafe-fixtures/index.js', + 'Prevent Input event on TextInput when type to input element', + { skip: browsersWithLimitations }); + }); + + it('Prevent Input event on TextInput when type to input element IE11/Firefox', function () { + return runTests('testcafe-fixtures/index.js', + 'Prevent Input event on TextInput when type to input element IE11/Firefox', + { only: [ 'ie', 'firefox', 'firefox-osx' ], shouldFail: true }) + .catch(function (errs) { + var errors = [ errs['ie'], errs['firefox'] ].filter(err => err); + + errors.forEach(err => { + expect(err[0]).contains('Input event has raised'); + }); + }); + }); + + it('Prevent Input event on TextInput when type to ContentEditable div', function () { + return runTests('testcafe-fixtures/index.js', + 'Prevent Input event on TextInput when type to ContentEditable div', + { skip: browsersWithLimitations }); + }); + + it('Prevent Input event on TextInput when type to ContentEditable div IE11', function () { + return runTests('testcafe-fixtures/index.js', + 'Prevent Input event on TextInput when type to ContentEditable div IE11/Firefox', + { only: [ 'ie' ] }); + }); + + it('Prevent Input event on TextInput when type to ContentEditable div Firefox', function () { + return runTests('testcafe-fixtures/index.js', + 'Prevent Input event on TextInput when type to ContentEditable div IE11/Firefox', + { only: [ 'firefox', 'firefox-osx' ], shouldFail: true }) + .catch(function (errs) { + expect(errs[0]).contains('Input event has raised'); + }); + }); + + it('Modify text node of ContentEditable div on TextInput event and prevent Input event', function () { + return runTests('testcafe-fixtures/index.js', + 'Modify text node of ContentEditable div on TextInput event and prevent Input event', + { skip: browsersWithLimitations }); + }); + + it('Type to ContentEditable div when selected node was replaced on TextInput event', function () { + return runTests('testcafe-fixtures/index.js', + 'Type to ContentEditable div when selected node was replaced on TextInput event', + { skip: browsersWithLimitations }); + }); + + it('Prevent Input event on TextInput when type to element node', function () { + return runTests('testcafe-fixtures/index.js', + 'Prevent Input event on TextInput when type to element node', + { skip: browsersWithLimitations }); + }); +}); diff --git a/test/functional/fixtures/regression/gh-1956/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-1956/testcafe-fixtures/index.js new file mode 100644 index 00000000000..32d86c7f6ef --- /dev/null +++ b/test/functional/fixtures/regression/gh-1956/testcafe-fixtures/index.js @@ -0,0 +1,64 @@ +import { Selector } from 'testcafe'; + +fixture('GH-1956 - Should support TextInput event') + .page `http://localhost:3000/fixtures/regression/gh-1956/pages/index.html`; + +const simpleInput = Selector('#simpleInput'); +const simpleContentEditable = Selector('#simpleContentEditable'); +const contentEditableWithElementNode = Selector('#contentEditableWithElementNode'); +const contentEditableWithModify = Selector('#contentEditableWithModify'); +const contentEditableWithReplace = Selector('#contentEditableWithReplace'); + +// NOTE: Chrome/Edge/Safari. Typing is prevented and Input event is not raised +test('Prevent Input event on TextInput when type to input element', async t => { + await t + .typeText(simpleInput, 'Hello') + .expect(simpleInput.value).eql(''); +}); + +// NOTE: IE11/Firefox. Typing is not prevented. Input event is raised +test('Prevent Input event on TextInput when type to input element IE11/Firefox', async t => { + await t + .typeText(simpleInput, 'Hello') + .expect(simpleInput.value).eql('Hello'); +}); + +// NOTE: Chrome/Edge/Safari. Typing is prevented. Input event is not raised +test('Prevent Input event on TextInput when type to ContentEditable div', async t => { + await t + .typeText(simpleContentEditable, 'Hello') + .expect(simpleContentEditable.textContent).eql(''); +}); + +// NOTE: IE11/Firefox. Typing is not prevented. +// Input event is raised in firefox but is not raised in IE11 - it's a IE11 bug +test('Prevent Input event on TextInput when type to ContentEditable div IE11/Firefox', async t => { + await t + .typeText(simpleContentEditable, 'Hello') + .expect(simpleContentEditable.textContent).eql('Hello'); +}); + +// NOTE: Not for IE11 because preventDefault will not prevent typing +// Not for Firefox because Firefox does not support TextInput event +test('Prevent Input event on TextInput when type to element node', async t => { + await t + .typeText(contentEditableWithElementNode, 'Hello') + .expect(contentEditableWithElementNode.textContent).eql(''); +}); + +// NOTE: Not for IE11 because is's not possible to prevent typing in IE11 +// Not for Firefox because Firefox does not support TextInput event +test('Modify text node of ContentEditable div on TextInput event and prevent Input event', async t => { + await t + .typeText(contentEditableWithModify, 'Hello') + .expect(contentEditableWithModify.textContent).eql('AHello'); +}); + +// NOTE: Not for IE11 because this test emulates behavior from https://github.com/DevExpress/testcafe/issues/1956. +// This behavior is different in IE11 +// Not for Firefox because Firefox does not support TextInput event +test('Type to ContentEditable div when selected node was replaced on TextInput event', async t => { + await t + .typeText(contentEditableWithReplace, 'Hello') + .expect(contentEditableWithReplace.textContent).eql('HelloX'); +});