Skip to content

Commit 7ea17c8

Browse files
DevmateUtgenCppGeneralFBOrg Botfacebook-github-bot
authored andcommitted
xplat/js/react-native-github/packages/react-native/ReactAndroid/src/main/jni/react/fabric/EventEmitterWrapper.cpp
Differential Revision: D113548144
1 parent 8e995e8 commit 7ea17c8

1 file changed

Lines changed: 247 additions & 0 deletions

File tree

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include <react/fabric/EventEmitterWrapper.h>
9+
10+
#include <react/renderer/core/EventBeat.h>
11+
#include <react/renderer/core/EventDispatcher.h>
12+
#include <react/renderer/core/EventListener.h>
13+
#include <react/renderer/core/EventQueueProcessor.h>
14+
#include <react/renderer/core/RawEvent.h>
15+
#include <react/renderer/runtimescheduler/RuntimeScheduler.h>
16+
#include <react/timing/primitives.h>
17+
18+
#include <gtest/gtest.h>
19+
20+
#include <chrono>
21+
#include <memory>
22+
#include <string>
23+
24+
/*
25+
* Pure-C++ unit tests for `EventEmitterWrapper`, the JNI adapter that bridges
26+
* Java-side event dispatch to the C++ `EventEmitter`. The three public methods
27+
* (`dispatchEvent`, `dispatchUniqueEvent`, `dispatchEventSynchronously`) are
28+
* plain C++ member functions; the only JNI-coupled argument is the
29+
* `NativeMap* payload`, which the wrapper explicitly treats as optional. By
30+
* passing `nullptr` for the payload we exercise the full forwarding logic
31+
* without any attached JavaVM.
32+
*
33+
* Forwarding is observed by wiring the wrapper to a real `EventEmitter` backed
34+
* by a real `EventDispatcher`, and installing an `EventListener` on that
35+
* dispatcher. `EventDispatcher::dispatchEvent`/`dispatchUniqueEvent` invoke the
36+
* listener chain synchronously *before* enqueueing; a listener that returns
37+
* `true` interrupts default dispatch, letting us capture the fully-formed
38+
* `RawEvent` (normalized type, category, uniqueness, timestamp) without needing
39+
* a `jsi::Runtime` or a real event beat to flush the queue.
40+
*
41+
* `EventEmitterWrapper` derives from `jni::HybridClass`, but with the default
42+
* base its C++ part is just a `detail::BaseHybridClass` (a class with a virtual
43+
* destructor and no JNI state), so instances can be constructed directly on the
44+
* stack host-side.
45+
*/
46+
namespace facebook::react {
47+
namespace {
48+
49+
// Snapshot of the RawEvent that reached the dispatcher's listener chain.
50+
struct DispatchRecord {
51+
bool dispatched{false};
52+
std::string type;
53+
RawEvent::Category category{RawEvent::Category::Unspecified};
54+
bool isUnique{false};
55+
HighResTimeStamp timestamp{HighResTimeStamp::now()};
56+
};
57+
58+
// EventBeat that records synchronous-flush requests. The base `request()` and
59+
// `requestSynchronous()` only flip atomic flags and never dereference the
60+
// `RuntimeScheduler` (that happens in `induce()`, which the interrupt-based
61+
// listener path never triggers), so overriding `requestSynchronous()` to count
62+
// invocations lets us assert that `dispatchEventSynchronously` routes through
63+
// `EventDispatcher::experimental_flushSync`.
64+
class RecordingEventBeat : public EventBeat {
65+
public:
66+
RecordingEventBeat(
67+
std::shared_ptr<OwnerBox> ownerBox,
68+
RuntimeScheduler& runtimeScheduler,
69+
int& syncFlushCount)
70+
: EventBeat(std::move(ownerBox), runtimeScheduler),
71+
syncFlushCount_(syncFlushCount) {}
72+
73+
void requestSynchronous() const override {
74+
++syncFlushCount_;
75+
}
76+
77+
private:
78+
int& syncFlushCount_;
79+
};
80+
81+
} // namespace
82+
83+
class EventEmitterWrapperTest : public ::testing::Test {
84+
protected:
85+
void SetUp() override {
86+
// A no-op runtime executor is sufficient: it is only invoked when the
87+
// event beat is induced, which never happens because the listener
88+
// interrupts dispatch before anything is enqueued.
89+
runtimeScheduler_ = std::make_unique<RuntimeScheduler>(RuntimeExecutor{});
90+
91+
record_ = std::make_shared<DispatchRecord>();
92+
93+
EventQueueProcessor eventProcessor(
94+
EventPipe{},
95+
EventPipeConclusion{},
96+
StatePipe{},
97+
std::weak_ptr<EventLogger>{});
98+
99+
auto eventBeat = std::make_unique<RecordingEventBeat>(
100+
std::make_shared<EventBeat::OwnerBox>(),
101+
*runtimeScheduler_,
102+
syncFlushCount_);
103+
104+
dispatcher_ = std::make_shared<EventDispatcher>(
105+
eventProcessor,
106+
std::move(eventBeat),
107+
StatePipe{},
108+
std::weak_ptr<EventLogger>{});
109+
110+
auto record = record_;
111+
listener_ =
112+
std::make_shared<EventListener>([record](const RawEvent& event) {
113+
record->dispatched = true;
114+
record->type = event.type;
115+
record->category = event.category;
116+
record->isUnique = event.isUnique;
117+
record->timestamp = event.eventStartTimeStamp;
118+
// Interrupt default dispatch so the event is never enqueued/flushed.
119+
return true;
120+
});
121+
dispatcher_->addListener(listener_);
122+
123+
emitter_ = std::make_shared<EventEmitter>(
124+
/*eventTarget=*/nullptr, EventDispatcher::Weak(dispatcher_));
125+
}
126+
127+
// Returns the milliseconds-since-steady-clock-epoch encoded in a timestamp
128+
// produced by the wrapper, so tests can assert the millis->HighResTimeStamp
129+
// conversion preserves the value and unit.
130+
static int64_t millisSinceEpoch(HighResTimeStamp timestamp) {
131+
return std::chrono::duration_cast<std::chrono::milliseconds>(
132+
timestamp.toChronoSteadyClockTimePoint().time_since_epoch())
133+
.count();
134+
}
135+
136+
std::unique_ptr<RuntimeScheduler> runtimeScheduler_;
137+
std::shared_ptr<DispatchRecord> record_;
138+
std::shared_ptr<EventDispatcher> dispatcher_;
139+
std::shared_ptr<const EventListener> listener_;
140+
SharedEventEmitter emitter_;
141+
int syncFlushCount_{0};
142+
};
143+
144+
/*
145+
* `dispatchEvent` must (a) normalize the raw JS event name to its "top" form,
146+
* (b) forward the integer category verbatim as a `RawEvent::Category`, and
147+
* (c) convert the Java uptime-millis timestamp into a HighResTimeStamp that
148+
* represents the same number of milliseconds. It must NOT force a synchronous
149+
* flush.
150+
*
151+
* Bug this catches: mis-casting the category (e.g. hardcoding a value), or a
152+
* unit error in the timestamp conversion (treating millis as nanos/seconds).
153+
*/
154+
TEST_F(
155+
EventEmitterWrapperTest,
156+
dispatchEventForwardsNormalizedNameCategoryAndTimestamp) {
157+
EventEmitterWrapper wrapper(emitter_);
158+
constexpr jlong kEventTimestampMillis = 1234;
159+
160+
wrapper.dispatchEvent(
161+
"onScroll",
162+
/*payload=*/nullptr,
163+
static_cast<int>(RawEvent::Category::Continuous),
164+
kEventTimestampMillis);
165+
166+
EXPECT_TRUE(record_->dispatched);
167+
EXPECT_EQ("topScroll", record_->type);
168+
EXPECT_EQ(RawEvent::Category::Continuous, record_->category);
169+
EXPECT_FALSE(record_->isUnique);
170+
EXPECT_EQ(kEventTimestampMillis, millisSinceEpoch(record_->timestamp));
171+
// Asynchronous events must not trigger a synchronous flush.
172+
EXPECT_EQ(0, syncFlushCount_);
173+
}
174+
175+
/*
176+
* `dispatchUniqueEvent` must forward through
177+
* `EventEmitter::dispatchUniqueEvent`, which marks the RawEvent as unique and
178+
* tags it as `Continuous`. Uniqueness is what lets the event queue coalesce
179+
* repeated events (e.g. onLayout) for the same target.
180+
*
181+
* Bug this catches: routing a unique event through the non-unique dispatch path
182+
* would drop the `isUnique` flag and defeat coalescing.
183+
*/
184+
TEST_F(EventEmitterWrapperTest, dispatchUniqueEventMarksEventUnique) {
185+
EventEmitterWrapper wrapper(emitter_);
186+
constexpr jlong kEventTimestampMillis = 5000;
187+
188+
wrapper.dispatchUniqueEvent(
189+
"onLayout", /*payload=*/nullptr, kEventTimestampMillis);
190+
191+
EXPECT_TRUE(record_->dispatched);
192+
EXPECT_EQ("topLayout", record_->type);
193+
EXPECT_TRUE(record_->isUnique);
194+
EXPECT_EQ(RawEvent::Category::Continuous, record_->category);
195+
EXPECT_EQ(kEventTimestampMillis, millisSinceEpoch(record_->timestamp));
196+
}
197+
198+
/*
199+
* `dispatchEventSynchronously` must (a) force the `Discrete` category
200+
* regardless of the caller, and (b) route through
201+
* `EventEmitter::experimental_flushSync`, which asks the event beat for a
202+
* synchronous flush. This is what makes synchronous events (e.g. controlled
203+
* text input) observe their effects before returning to Java.
204+
*
205+
* Bug this catches: dropping the synchronous flush (making the call behave like
206+
* an ordinary async dispatch) or using the wrong category.
207+
*/
208+
TEST_F(
209+
EventEmitterWrapperTest,
210+
dispatchEventSynchronouslyUsesDiscreteCategoryAndFlushesSync) {
211+
EventEmitterWrapper wrapper(emitter_);
212+
213+
wrapper.dispatchEventSynchronously(
214+
"onChange", /*params=*/nullptr, /*eventTimestamp=*/42);
215+
216+
EXPECT_TRUE(record_->dispatched);
217+
EXPECT_EQ("topChange", record_->type);
218+
EXPECT_EQ(RawEvent::Category::Discrete, record_->category);
219+
EXPECT_EQ(1, syncFlushCount_);
220+
}
221+
222+
/*
223+
* A wrapper can be constructed without a valid `EventEmitter` (the source
224+
* comments call this "marginal, but possible"). In that state every dispatch
225+
* method must black-hole the event: no crash, and nothing is forwarded.
226+
*
227+
* Bug this catches: removing the `eventEmitter != nullptr` guard would
228+
* dereference a null shared_ptr and crash instead of no-op'ing.
229+
*/
230+
TEST_F(EventEmitterWrapperTest, dispatchOnNullEventEmitterIsNoop) {
231+
EventEmitterWrapper wrapper(/*eventEmitter=*/nullptr);
232+
233+
wrapper.dispatchEvent(
234+
"onScroll",
235+
/*payload=*/nullptr,
236+
static_cast<int>(RawEvent::Category::Discrete),
237+
/*eventTimestamp=*/100);
238+
wrapper.dispatchUniqueEvent(
239+
"onLayout", /*payload=*/nullptr, /*eventTimestamp=*/100);
240+
wrapper.dispatchEventSynchronously(
241+
"onChange", /*params=*/nullptr, /*eventTimestamp=*/100);
242+
243+
EXPECT_FALSE(record_->dispatched);
244+
EXPECT_EQ(0, syncFlushCount_);
245+
}
246+
247+
} // namespace facebook::react

0 commit comments

Comments
 (0)