Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BeforeInput is fired with a wrong text at a wrong time on IE #7107

Merged
merged 1 commit into from
Sep 13, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
});
});