Skip to content

Commit

Permalink
BeforeInput is fired with a wrong text at a wrong time on IE (#7107)
Browse files Browse the repository at this point in the history
  • Loading branch information
msmania authored and sophiebits committed Sep 13, 2016
1 parent f3569a2 commit a64ca9b
Show file tree
Hide file tree
Showing 2 changed files with 214 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -345,11 +345,12 @@ function getNativeBeforeInputChars(topLevelType: TopLevelTypes, nativeEvent) {
function getFallbackBeforeInputChars(topLevelType: TopLevelTypes, nativeEvent) {
// If we are currently composing (IME) and using a fallback to do so,
// try to extract the composed characters from the fallback object.
// If composition event is available, we extract a string only at
// compositionevent, otherwise extract it at fallback events.
if (currentComposition) {
if (
topLevelType === 'topCompositionEnd' ||
isFallbackCompositionEnd(topLevelType, nativeEvent)
) {
if (topLevelType === 'topCompositionEnd'
|| (!canUseCompositionEvent
&& isFallbackCompositionEnd(topLevelType, nativeEvent))) {
var chars = currentComposition.getData();
FallbackCompositionState.release(currentComposition);
currentComposition = null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @emails react-core
*/

'use strict';

var React = require('React');
var ReactTestUtils = require('ReactTestUtils');

var EventMapping = {
compositionstart : 'topCompositionStart',
compositionend : 'topCompositionEnd',
keyup : 'topKeyUp',
keydown : 'topKeyDown',
textInput : 'topTextInput',
textinput : null, // Not defined now
};

describe('BeforeInputEventPlugin', function() {
var ModuleCache;

function simulateIE11() {
document.documentMode = 11;
window.CompositionEvent = {};
delete window.TextEvent;
}

function simulateWebkit() {
delete document.documentMode;
window.CompositionEvent = {};
window.TextEvent = {};
}

function initialize(simulator) {
// Need to delete cached modules before executing simulator
jest.resetModuleRegistry();

// Initialize variables in the scope of BeforeInputEventPlugin
simulator();

// Modules which have dependency on BeforeInputEventPlugin are stored
// in ModuleCache so that we can use these modules ouside test functions.
this.ReactDOM = require('ReactDOM');
this.ReactDOMComponentTree = require('ReactDOMComponentTree');
this.SyntheticCompositionEvent = require('SyntheticCompositionEvent');
this.SyntheticInputEvent = require('SyntheticInputEvent');
this.BeforeInputEventPlugin = require('BeforeInputEventPlugin');
}

function extract(node, eventType, optionalData) {
var evt = document.createEvent('HTMLEvents');
evt.initEvent(eventType, true, true);
evt = Object.assign(evt, optionalData);
return ModuleCache.BeforeInputEventPlugin.extractEvents(
EventMapping[eventType],
ModuleCache.ReactDOMComponentTree.getInstanceFromNode(node),
evt,
node
);
}

function setElementText(node) {
return (args) => node.innerHTML = args;
}

function accumulateEvents(node, events) {
// We don't use accumulateInto module to apply partial application.
return function() {
var newArgs = [node].concat(Array.prototype.slice.call(arguments));
var newEvents = extract.apply(this, newArgs);
Array.prototype.push.apply(events, newEvents);
};
}

function EventMismatchError(idx, message) {
this.name = 'EventMismatchError';
this.message = '[' + idx + '] ' + message;
}
EventMismatchError.prototype = Object.create(Error.prototype);

function verifyEvents(actualEvents, expectedEvents) {
expect(actualEvents.length).toBe(expectedEvents.length);
expectedEvents.forEach(function(expected, idx) {
var actual = actualEvents[idx];
expect(function() {
if (actual === null && expected.type === null) {
// Both are null. Expected.
} else if (actual === null) {
throw new EventMismatchError(idx, 'Expected not to be null');
} else if (expected.type === null
|| !(actual instanceof expected.type)) {
throw new EventMismatchError(idx, 'Unexpected type: ' + actual);
} else {
// Type match.
Object.keys(expected.data).forEach(function(expectedKey) {
if (!(expectedKey in actual)) {
throw new EventMismatchError(idx, 'KeyNotFound: ' + expectedKey);
} else if (actual[expectedKey] !== expected.data[expectedKey]) {
throw new EventMismatchError(idx,
'ValueMismatch: ' + actual[expectedKey]);
}
});
}
}).not.toThrow();
});
}

// IE fires an event named `textinput` with all lowercase characters,
// instead of a standard name `textInput`. As of now, React does not have
// a corresponding topEvent to IE's textinput, but both events are added to
// this scenario data for future use.
var Scenario_Composition = [
{run: accumulateEvents, arg: ['compositionstart', {data: ''}]},
{run: accumulateEvents, arg: ['textInput', {data: 'A'}]},
{run: accumulateEvents, arg: ['textinput', {data: 'A'}]},
{run: accumulateEvents, arg: ['keyup', {keyCode: 65}]},
{run: setElementText, arg: ['ABC']},
{run: accumulateEvents, arg: ['textInput', {data: 'abc'}]},
{run: accumulateEvents, arg: ['textinput', {data: 'abc'}]},
{run: accumulateEvents, arg: ['keyup', {keyCode: 32}]},
{run: setElementText, arg: ['XYZ']},
{run: accumulateEvents, arg: ['textInput', {data: 'xyz'}]},
{run: accumulateEvents, arg: ['textinput', {data: 'xyz'}]},
{run: accumulateEvents, arg: ['keyup', {keyCode: 32}]},
{run: accumulateEvents, arg: ['compositionend', {data: 'Hello'}]},
];

/* Defined expected results as a factory of result data because we need
lazy evaluation for event modules.
Event modules are reloaded to simulate a different platform per testcase.
If we define expected results as a simple dictionary here, the comparison
of 'instanceof' fails after module cache is reset. */

// Webkit behavior is simple. We expect SyntheticInputEvent at each
// textInput, SyntheticCompositionEvent at composition, and nothing from
// keyUp.
var Expected_Webkit = () => [
{type: ModuleCache.SyntheticCompositionEvent, data: {}}, {type: null},
{type: null}, {type: ModuleCache.SyntheticInputEvent, data: {data: 'A'}},
{type: null}, {type: null}, // textinput of A
{type: null}, {type: null}, // keyUp of 65
{type: null}, {type: ModuleCache.SyntheticInputEvent, data: {data: 'abc'}},
{type: null}, {type: null}, // textinput of abc
{type: null}, {type: null}, // keyUp of 32
{type: null}, {type: ModuleCache.SyntheticInputEvent, data: {data: 'xyz'}},
{type: null}, {type: null}, // textinput of xyz
{type: null}, {type: null}, // keyUp of 32
{type: ModuleCache.SyntheticCompositionEvent, data: {data: 'Hello'}},
{type: null},
];

// For IE11, we use fallback data instead of IE's textinput events.
// We expect no SyntheticInputEvent from textinput. Fallback beforeInput is
// expected to be triggered at compositionend with a text of the target
// element, not event data.
var Expected_IE11 = () => [
{type: ModuleCache.SyntheticCompositionEvent, data: {}}, {type: null},
{type: null}, {type: null}, // textInput of A
{type: null}, {type: null}, // textinput of A
{type: null}, {type: null}, // keyUp of 65
{type: null}, {type: null}, // textInput of abc
{type: null}, {type: null}, // textinput of abc

// fallbackData should NOT be set at keyUp with any of END_KEYCODES
{type: null}, {type: null}, // keyUp of 32

{type: null}, {type: null}, // textInput of xyz
{type: null}, {type: null}, // textinput of xyz
{type: null}, {type: null}, // keyUp of 32

// fallbackData is retrieved from the element, which is XYZ,
// at a time of compositionend
{type: ModuleCache.SyntheticCompositionEvent, data: {}},
{type: ModuleCache.SyntheticInputEvent, data: {data: 'XYZ'}},
];

function TestEditableReactComponent(Emulator, Scenario, ExpectedResult) {
ModuleCache = new initialize(Emulator);

var EditableDiv = React.createClass({
render: () => (<div contentEditable="true" />),
});
var rendered = ReactTestUtils.renderIntoDocument(<EditableDiv />);

var node = ModuleCache.ReactDOM.findDOMNode(rendered);
var events = [];

Scenario.forEach((el) =>
el.run.call(this, node, events).apply(this, el.arg));
verifyEvents(events, ExpectedResult());
}

it('extract onBeforeInput from native textinput events', function() {
TestEditableReactComponent(
simulateWebkit, Scenario_Composition, Expected_Webkit);
});

it('extract onBeforeInput from fallback objects', function() {
TestEditableReactComponent(
simulateIE11, Scenario_Composition, Expected_IE11);
});
});

0 comments on commit a64ca9b

Please sign in to comment.