Skip to content

Commit

Permalink
[WIP] process TextInput event on typing (closes DevExpress#1956)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexKamaev committed Apr 19, 2018
1 parent 5bccb81 commit d9ca271
Show file tree
Hide file tree
Showing 6 changed files with 299 additions and 29 deletions.
81 changes: 56 additions & 25 deletions src/client/automation/playback/type/type-text.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
});
};

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

Expand All @@ -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);
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 ? 12 : 0;

$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
5 changes: 3 additions & 2 deletions test/functional/fixtures/regression/gh-1054/test.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
describe('[Regression](GH-1054)', function () {
describe.only('[Regression](GH-1054)', function () {
describe('Target element should contain the first symbol of text input when the input event raised at the first time', function () {
it('Typing in the input element with "replace" option', function () {
return runTests('testcafe-fixtures/index.test.js', 'Type text in the input');
});

// NOTE: IE11 does not raise Input event on contenteditable element
it('Typing in the content editable element with "replace" option', function () {
return runTests('testcafe-fixtures/index.test.js', 'Type text in the content editable element');
return runTests('testcafe-fixtures/index.test.js', 'Type text in the content editable element', { skip: [ 'ie' ] });
});
});
});
105 changes: 105 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,105 @@
<html>
<head><title>Edit test</title></head>
<style>
h2 {
margin-top: 20px;
}
div {
border: 1px solid black;
min-height: 10px;
}
</style>
<script type="text/javascript">
function onTextInput (event) {
var needPreventDefault = true;

if (isTargetElement(event.target, 'contentEditableWithModify'))
changeNodeValueOnTextInput(event);

if (isTargetElement(event.target, 'contentEditableWithReplace')) {
replaceEditableElementOnTextInput(event);

needPreventDefault = false;
}

if(needPreventDefault)
event.preventDefault();
}

function changeNodeValueOnTextInput(event) {
document.getElementById('contentEditableWithModify').childNodes[0].childNodes[0].nodeValue += event.data;
}

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

window.preventNextElementReplacement = true;

var div = document.getElementById('contentEditableWithReplace');
var paragraph = div.childNodes[0];
var textNode = paragraph.childNodes[0];
var newParagraph = document.createElement("P");

newParagraph.innerHTML = 'X';

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

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

function isTargetElement(el, id) {
while (el) {
if (el.id === id)
return true;

el = el.parentNode;
}

return null;
}

document.addEventListener('textInput', onTextInput, true);
document.addEventListener('textinput', onTextInput, true);
document.addEventListener('input', onInput, true);
</script>
<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>
</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', '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 });
});
});
Loading

0 comments on commit d9ca271

Please sign in to comment.