From cfae5dac4d8520a33f53eeca231d38c34a1a53ae Mon Sep 17 00:00:00 2001 From: "alexey.kamaev" Date: Wed, 18 Apr 2018 16:08:26 +0300 Subject: [PATCH] [WIP] process TextInput event on typing (closes #1956) --- package.json | 2 +- .../automation/playback/type/type-text.js | 81 +++++++++----- .../index-test.js | 10 +- .../regression/gh-1956/pages/index.html | 105 ++++++++++++++++++ .../fixtures/regression/gh-1956/test.js | 63 +++++++++++ .../gh-1956/testcafe-fixtures/index.js | 64 +++++++++++ 6 files changed, 297 insertions(+), 28 deletions(-) create mode 100644 test/functional/fixtures/regression/gh-1956/pages/index.html create mode 100644 test/functional/fixtures/regression/gh-1956/test.js create mode 100644 test/functional/fixtures/regression/gh-1956/testcafe-fixtures/index.js diff --git a/package.json b/package.json index 73192d9cd53..a19ca1c93ab 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ "source-map-support": "^0.4.0", "strip-bom": "^2.0.0", "testcafe-browser-tools": "1.5.1", - "testcafe-hammerhead": "13.3.1", + "testcafe-hammerhead": "13.4.0", "testcafe-legacy-api": "3.1.7", "testcafe-reporter-json": "^2.1.0", "testcafe-reporter-list": "^2.1.0", diff --git a/src/client/automation/playback/type/type-text.js b/src/client/automation/playback/type/type-text.js index 1f0d99fad89..55fae79be0e 100644 --- a/src/client/automation/playback/type/type-text.js +++ b/src/client/automation/playback/type/type-text.js @@ -64,6 +64,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 +104,44 @@ 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 support TextInput event but adds e.data to node value. So let's ignore it +function simulateTextInput (element, text) { + const isTextInputIgnoredByBrowser = [ browserUtils.isFirefox, browserUtils.isSafari ].some(browser => browser); + const isInputEventRequired = isTextInputIgnoredByBrowser || eventSimulator.textInput(element, text); + + return isInputEventRequired || browserUtils.isIE11 || browserUtils.isFirefox; +} + 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 +164,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 +183,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..6bed0f723d1 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 ? 12 : 0; + $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-1956/pages/index.html b/test/functional/fixtures/regression/gh-1956/pages/index.html new file mode 100644 index 00000000000..2af849c902f --- /dev/null +++ b/test/functional/fixtures/regression/gh-1956/pages/index.html @@ -0,0 +1,105 @@ + +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..66325503ece --- /dev/null +++ b/test/functional/fixtures/regression/gh-1956/test.js @@ -0,0 +1,63 @@ +var expect = require('chai').expect; + +var browsersWithLimitations = [ 'ie', 'safari', 'firefox', 'firefox-osx', 'ipad', 'iphone' ]; + +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..87acf1a0488 --- /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. 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. 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'); +});