|
| 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