@samitouri / QOS-React-2 / commits / a64ca9b697

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

tokikuch committed Sep 13, 2016 at 11:15 UTC a64ca9b697d0808a2966465fea2e658e4f310080
2 files changed +214 -4
src/renderers/dom/client/eventPlugins/BeforeInputEventPlugin.js
+5 -4
@@ -345,11 +345,12 @@ function getNativeBeforeInputChars(topLevelType: TopLevelTypes, nativeEvent) {
345 function getFallbackBeforeInputChars(topLevelType: TopLevelTypes, nativeEvent) {
346 // If we are currently composing (IME) and using a fallback to do so,
347 // try to extract the composed characters from the fallback object.
348 + // If composition event is available, we extract a string only at
349 + // compositionevent, otherwise extract it at fallback events.
350 if (currentComposition) {
349 - if (
350 - topLevelType === 'topCompositionEnd' ||
351 - isFallbackCompositionEnd(topLevelType, nativeEvent)
352 - ) {
351 + if (topLevelType === 'topCompositionEnd'
352 + || (!canUseCompositionEvent
353 + && isFallbackCompositionEnd(topLevelType, nativeEvent))) {
354 var chars = currentComposition.getData();
355 FallbackCompositionState.release(currentComposition);
356 currentComposition = null;
src/renderers/dom/client/eventPlugins/__tests__/BeforeInputEventPlugin-test.js new
+209
@@ -0,0 +1,209 @@
1 +/**
2 + * Copyright 2013-present, Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + *
9 + * @emails react-core
10 + */
11 +
12 +'use strict';
13 +
14 +var React = require('React');
15 +var ReactTestUtils = require('ReactTestUtils');
16 +
17 +var EventMapping = {
18 + compositionstart : 'topCompositionStart',
19 + compositionend : 'topCompositionEnd',
20 + keyup : 'topKeyUp',
21 + keydown : 'topKeyDown',
22 + textInput : 'topTextInput',
23 + textinput : null, // Not defined now
24 +};
25 +
26 +describe('BeforeInputEventPlugin', function() {
27 + var ModuleCache;
28 +
29 + function simulateIE11() {
30 + document.documentMode = 11;
31 + window.CompositionEvent = {};
32 + delete window.TextEvent;
33 + }
34 +
35 + function simulateWebkit() {
36 + delete document.documentMode;
37 + window.CompositionEvent = {};
38 + window.TextEvent = {};
39 + }
40 +
41 + function initialize(simulator) {
42 + // Need to delete cached modules before executing simulator
43 + jest.resetModuleRegistry();
44 +
45 + // Initialize variables in the scope of BeforeInputEventPlugin
46 + simulator();
47 +
48 + // Modules which have dependency on BeforeInputEventPlugin are stored
49 + // in ModuleCache so that we can use these modules ouside test functions.
50 + this.ReactDOM = require('ReactDOM');
51 + this.ReactDOMComponentTree = require('ReactDOMComponentTree');
52 + this.SyntheticCompositionEvent = require('SyntheticCompositionEvent');
53 + this.SyntheticInputEvent = require('SyntheticInputEvent');
54 + this.BeforeInputEventPlugin = require('BeforeInputEventPlugin');
55 + }
56 +
57 + function extract(node, eventType, optionalData) {
58 + var evt = document.createEvent('HTMLEvents');
59 + evt.initEvent(eventType, true, true);
60 + evt = Object.assign(evt, optionalData);
61 + return ModuleCache.BeforeInputEventPlugin.extractEvents(
62 + EventMapping[eventType],
63 + ModuleCache.ReactDOMComponentTree.getInstanceFromNode(node),
64 + evt,
65 + node
66 + );
67 + }
68 +
69 + function setElementText(node) {
70 + return (args) => node.innerHTML = args;
71 + }
72 +
73 + function accumulateEvents(node, events) {
74 + // We don't use accumulateInto module to apply partial application.
75 + return function() {
76 + var newArgs = [node].concat(Array.prototype.slice.call(arguments));
77 + var newEvents = extract.apply(this, newArgs);
78 + Array.prototype.push.apply(events, newEvents);
79 + };
80 + }
81 +
82 + function EventMismatchError(idx, message) {
83 + this.name = 'EventMismatchError';
84 + this.message = '[' + idx + '] ' + message;
85 + }
86 + EventMismatchError.prototype = Object.create(Error.prototype);
87 +
88 + function verifyEvents(actualEvents, expectedEvents) {
89 + expect(actualEvents.length).toBe(expectedEvents.length);
90 + expectedEvents.forEach(function(expected, idx) {
91 + var actual = actualEvents[idx];
92 + expect(function() {
93 + if (actual === null && expected.type === null) {
94 + // Both are null. Expected.
95 + } else if (actual === null) {
96 + throw new EventMismatchError(idx, 'Expected not to be null');
97 + } else if (expected.type === null
98 + || !(actual instanceof expected.type)) {
99 + throw new EventMismatchError(idx, 'Unexpected type: ' + actual);
100 + } else {
101 + // Type match.
102 + Object.keys(expected.data).forEach(function(expectedKey) {
103 + if (!(expectedKey in actual)) {
104 + throw new EventMismatchError(idx, 'KeyNotFound: ' + expectedKey);
105 + } else if (actual[expectedKey] !== expected.data[expectedKey]) {
106 + throw new EventMismatchError(idx,
107 + 'ValueMismatch: ' + actual[expectedKey]);
108 + }
109 + });
110 + }
111 + }).not.toThrow();
112 + });
113 + }
114 +
115 + // IE fires an event named `textinput` with all lowercase characters,
116 + // instead of a standard name `textInput`. As of now, React does not have
117 + // a corresponding topEvent to IE's textinput, but both events are added to
118 + // this scenario data for future use.
119 + var Scenario_Composition = [
120 + {run: accumulateEvents, arg: ['compositionstart', {data: ''}]},
121 + {run: accumulateEvents, arg: ['textInput', {data: 'A'}]},
122 + {run: accumulateEvents, arg: ['textinput', {data: 'A'}]},
123 + {run: accumulateEvents, arg: ['keyup', {keyCode: 65}]},
124 + {run: setElementText, arg: ['ABC']},
125 + {run: accumulateEvents, arg: ['textInput', {data: 'abc'}]},
126 + {run: accumulateEvents, arg: ['textinput', {data: 'abc'}]},
127 + {run: accumulateEvents, arg: ['keyup', {keyCode: 32}]},
128 + {run: setElementText, arg: ['XYZ']},
129 + {run: accumulateEvents, arg: ['textInput', {data: 'xyz'}]},
130 + {run: accumulateEvents, arg: ['textinput', {data: 'xyz'}]},
131 + {run: accumulateEvents, arg: ['keyup', {keyCode: 32}]},
132 + {run: accumulateEvents, arg: ['compositionend', {data: 'Hello'}]},
133 + ];
134 +
135 + /* Defined expected results as a factory of result data because we need
136 + lazy evaluation for event modules.
137 + Event modules are reloaded to simulate a different platform per testcase.
138 + If we define expected results as a simple dictionary here, the comparison
139 + of 'instanceof' fails after module cache is reset. */
140 +
141 + // Webkit behavior is simple. We expect SyntheticInputEvent at each
142 + // textInput, SyntheticCompositionEvent at composition, and nothing from
143 + // keyUp.
144 + var Expected_Webkit = () => [
145 + {type: ModuleCache.SyntheticCompositionEvent, data: {}}, {type: null},
146 + {type: null}, {type: ModuleCache.SyntheticInputEvent, data: {data: 'A'}},
147 + {type: null}, {type: null}, // textinput of A
148 + {type: null}, {type: null}, // keyUp of 65
149 + {type: null}, {type: ModuleCache.SyntheticInputEvent, data: {data: 'abc'}},
150 + {type: null}, {type: null}, // textinput of abc
151 + {type: null}, {type: null}, // keyUp of 32
152 + {type: null}, {type: ModuleCache.SyntheticInputEvent, data: {data: 'xyz'}},
153 + {type: null}, {type: null}, // textinput of xyz
154 + {type: null}, {type: null}, // keyUp of 32
155 + {type: ModuleCache.SyntheticCompositionEvent, data: {data: 'Hello'}},
156 + {type: null},
157 + ];
158 +
159 + // For IE11, we use fallback data instead of IE's textinput events.
160 + // We expect no SyntheticInputEvent from textinput. Fallback beforeInput is
161 + // expected to be triggered at compositionend with a text of the target
162 + // element, not event data.
163 + var Expected_IE11 = () => [
164 + {type: ModuleCache.SyntheticCompositionEvent, data: {}}, {type: null},
165 + {type: null}, {type: null}, // textInput of A
166 + {type: null}, {type: null}, // textinput of A
167 + {type: null}, {type: null}, // keyUp of 65
168 + {type: null}, {type: null}, // textInput of abc
169 + {type: null}, {type: null}, // textinput of abc
170 +
171 + // fallbackData should NOT be set at keyUp with any of END_KEYCODES
172 + {type: null}, {type: null}, // keyUp of 32
173 +
174 + {type: null}, {type: null}, // textInput of xyz
175 + {type: null}, {type: null}, // textinput of xyz
176 + {type: null}, {type: null}, // keyUp of 32
177 +
178 + // fallbackData is retrieved from the element, which is XYZ,
179 + // at a time of compositionend
180 + {type: ModuleCache.SyntheticCompositionEvent, data: {}},
181 + {type: ModuleCache.SyntheticInputEvent, data: {data: 'XYZ'}},
182 + ];
183 +
184 + function TestEditableReactComponent(Emulator, Scenario, ExpectedResult) {
185 + ModuleCache = new initialize(Emulator);
186 +
187 + var EditableDiv = React.createClass({
188 + render: () => (<div contentEditable="true" />),
189 + });
190 + var rendered = ReactTestUtils.renderIntoDocument(<EditableDiv />);
191 +
192 + var node = ModuleCache.ReactDOM.findDOMNode(rendered);
193 + var events = [];
194 +
195 + Scenario.forEach((el) =>
196 + el.run.call(this, node, events).apply(this, el.arg));
197 + verifyEvents(events, ExpectedResult());
198 + }
199 +
200 + it('extract onBeforeInput from native textinput events', function() {
201 + TestEditableReactComponent(
202 + simulateWebkit, Scenario_Composition, Expected_Webkit);
203 + });
204 +
205 + it('extract onBeforeInput from fallback objects', function() {
206 + TestEditableReactComponent(
207 + simulateIE11, Scenario_Composition, Expected_IE11);
208 + });
209 +});