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 13, 2018
1 parent 0f83b12 commit 17b3c67
Show file tree
Hide file tree
Showing 5 changed files with 281 additions and 26 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
78 changes: 53 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,41 @@ function _excludeInvisibleSymbolsFromSelection (selection) {
return selection;
}

// NOTE: typing can be prevented in Chrome/Edge but can not be prevented in IE11 or Firefox
function simulateTextInput (element, text) {
const isInputEventRequired = 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 +161,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 +180,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
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>
58 changes: 58 additions & 0 deletions test/functional/fixtures/regression/gh-1956/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
var expect = require('chai').expect;

describe('Should support TextInput event[Regression](GH-1956)', function () {
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: [ 'firefox', 'ie' ] });
});

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: [ 'firefox', 'ie' ] });
});

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: [ 'firefox', 'ie' ] });
});

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: [ 'ie', 'firefox' ] });
});

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' ], shouldFail: true })
.catch(function (errs) {
expect(errs['ie'][0]).contains('Input event has raised');
expect(errs['firefox'][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: [ 'ie', 'firefox' ] });
});

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' ], shouldFail: true })
.catch(function (errs) {
expect(errs[0]).contains('Input event has raised');
});
});
});
Original file line number Diff line number Diff line change
@@ -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');
});

0 comments on commit 17b3c67

Please sign in to comment.