Skip to content

Commit

Permalink
TextInput event is not raised on typing (#closes 1956) (DevExpress#2303)
Browse files Browse the repository at this point in the history
* [WIP] process TextInput event on typing (closes DevExpress#1956)

* fix textInput dispatch in safari

* fix textInput dispatch in safari

* fix review remarks

* fix user agent module
  • Loading branch information
AlexKamaev authored and kirovboris committed Dec 18, 2019
1 parent 0700835 commit 0c82b1b
Show file tree
Hide file tree
Showing 6 changed files with 323 additions and 28 deletions.
106 changes: 81 additions & 25 deletions src/client/automation/playback/type/type-text.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand All @@ -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);
});
};

Expand All @@ -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();
}

Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand All @@ -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();
Expand All @@ -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 () {
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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');
});
100 changes: 100 additions & 0 deletions test/functional/fixtures/regression/gh-1956/pages/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<html>
<head><title>Edit test</title></head>
<style>
h2 {
margin-top: 20px;
}
div {
border: 1px solid black;
min-height: 10px;
}
</style>
<body>
<h2>Prevent Input event on TextInput when type to input element</h2>
<pre>
Chrome/Edge. Typing is prevented and Input event is not raised
IE11/Firefox. Typing is not prevented. Input event is raised
</pre>
<input id="simpleInput" />
<h2>Prevent Input event and typing on simple ContentEditable div</h2>
<pre>
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
</pre>
<div contenteditable="true" id="simpleContentEditable"></div>
<h2>Prevent Input event on TextInput event when type to element node</h2>
<pre>
Not for IE11 because preventDefault will not prevent typing
Not for Firefox because Firefox does not support TextInput event
</pre>
<div contenteditable="true" id="contentEditableWithElementNode"><p><br/></p></div>
<h2>Modify text node of ContentEditable div on TextInput event and prevent Input event</h2>
<pre>
Not for IE11 because is's not possible to prevent typing in IE11
Not for Firefox because Firefox does not support TextInput event
</pre>
<div contenteditable="true" id="contentEditableWithModify"><p>A</p></div>
<h2>Type to ContentEditable div when selected node was replaced on TextInput event</h2>
<pre>
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
</pre>
<div contenteditable="true" id="contentEditableWithReplace"><p>B</p></div>
</body>
<script type="text/javascript">
var contentEditableWithModifyEl = document.getElementById('contentEditableWithModify');
var contentEditableWithReplaceEl = document.getElementById('contentEditableWithReplace');

function onTextInput (event) {
var needPreventDefault = true;

if (isTargetElement(event.target, contentEditableWithModifyEl))
changeNodeValueOnTextInput(event);

if (isTargetElement(event.target, contentEditableWithReplaceEl)) {
replaceEditableElementOnTextInput(event);

needPreventDefault = false;
}

if(needPreventDefault)
event.preventDefault();
}

function changeNodeValueOnTextInput(event) {
contentEditableWithModifyEl.childNodes[0].childNodes[0].nodeValue += event.data;
}

function replaceEditableElementOnTextInput (event) {
if (window.preventNextElementReplacement)
return;

window.preventNextElementReplacement = true;

var paragraph = contentEditableWithReplaceEl.childNodes[0];
var textNode = paragraph.childNodes[0];
var newParagraph = document.createElement("P");

newParagraph.innerHTML = 'X';

contentEditableWithReplaceEl.removeChild(paragraph);
contentEditableWithReplaceEl.appendChild(newParagraph);
newParagraph.focus();
}

function onInput (event) {
if (!isTargetElement(event.target, contentEditableWithReplaceEl))
throw new Error('Input event has raised');
}

function isTargetElement(actualTarget, expectedTarget) {
return expectedTarget.contains(actualTarget);
}

document.addEventListener('textInput', onTextInput, true);
document.addEventListener('textinput', onTextInput, true);
document.addEventListener('input', onInput, true);
</script>
</html>
63 changes: 63 additions & 0 deletions test/functional/fixtures/regression/gh-1956/test.js
Original file line number Diff line number Diff line change
@@ -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 });
});
});
Loading

0 comments on commit 0c82b1b

Please sign in to comment.