fix(android): restore development builds after Expo upgrade (#7476)

* fix(android): restore development builds after Expo upgrade

Port the native touch-cancellation patch to the gesture-handler API while retaining drawing order and surviving native gestures. Match the bundled Skia font scanner and Unicode paragraph construction APIs.

* test(e2e): hide the developer error toast instead of clicking it away

The locator handler that dismissed #error-toast could match a toast that closed before its
click; dispatchEvent then waited for the gone button until the test timed out. Hiding the toast
with CSS keeps it off app actions with no race.

* test(e2e): desktop browser-tabs hides developer toasts on its attached page

* test(cli): keep the Hub deploy fixture alive on malformed requests

An empty request body crashed the HTTP fixture outside its assertions. Return HTTP 400 for invalid JSON and exercise that request before the full validation and installation assertions.
This commit is contained in:
Mohamed Boudra
2026-10-21 10:48:01 +07:00
committed by GitHub
parent 461ea2ee9e
commit b36ff056ea
4 changed files with 40 additions and 11 deletions
@@ -7,6 +7,7 @@
#include <stdexcept>
#include <unordered_map>
#include "include/ports/SkFontMgr_android.h"
#include "include/ports/SkFontScanner_FreeType.h"
#include "modules/skunicode/include/SkUnicode.h"
namespace {
@@ -86,7 +87,7 @@ extern "C" JNIEXPORT jdoubleArray JNICALL METHOD(prepareDocument)(
const auto fontFamily = SkUnicode::convertUtf16ToUtf8(copyString(env, family));
const double copyMs = elapsed(copyStart);
const auto layoutStart = Clock::now();
paseo::diff::prepare(*document, SkFontMgr_New_Android(nullptr), fontFamily.c_str(), size, job->cancelled);
paseo::diff::prepare(*document, SkFontMgr_New_Android(nullptr, SkFontScanner_Make_FreeType()), fontFamily.c_str(), size, job->cancelled);
const double layoutMs = elapsed(layoutStart);
{
std::lock_guard lock(mutex);
@@ -13,6 +13,7 @@
#include "modules/skparagraph/include/Paragraph.h"
#include "modules/skparagraph/include/ParagraphBuilder.h"
#include "modules/skunicode/include/SkUnicode.h"
#include "modules/skunicode/include/SkUnicode_icu.h"
namespace paseo::diff {
namespace {
@@ -178,7 +179,7 @@ class Measurer {
return it->second;
}
std::unique_ptr<para::Paragraph> shape(const std::u16string& text) {
auto builder = para::ParagraphBuilder::make(style_, fonts_);
auto builder = para::ParagraphBuilder::make(style_, fonts_, SkUnicodes::ICU::Make());
builder->pushStyle(textStyle_);
const auto utf8 = SkUnicode::convertUtf16ToUtf8(text);
builder->addText(utf8.c_str(), utf8.size());
+14 -1
View File
@@ -17,7 +17,15 @@ const server = createServer((request, response) => {
body += chunk;
});
request.on("end", () => {
requests.push({ url: request.url, body: JSON.parse(body) });
let parsed: unknown;
try {
parsed = JSON.parse(body);
} catch {
response.writeHead(400);
response.end();
return;
}
requests.push({ url: request.url, body: parsed });
const validation = request.url?.endsWith("/validate") === true;
response.writeHead(validation ? 200 : 201, { "content-type": "application/json" });
response.end(
@@ -59,6 +67,11 @@ try {
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address() as AddressInfo;
const origin = `http://127.0.0.1:${address.port}`;
// A malformed request must not kill the fixture or satisfy a deployment assertion.
const malformed = await fetch(`${origin}/api/v1/configurations/validate`, { method: "POST" });
assert.equal(malformed.status, 400);
assert.equal(requests.length, 0);
const validate = await runLocalPaseo(
[
"hub",
@@ -1,8 +1,8 @@
diff --git a/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt b/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt
index 8d78aea..02e3ea6 100644
index 8d78aea..b84d19e 100644
--- a/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt
+++ b/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt
@@ -2,6 +2,7 @@ package com.swmansion.gesturehandler.core
@@ -2,11 +2,13 @@ package com.swmansion.gesturehandler.core
import android.graphics.Matrix
import android.graphics.PointF
@@ -10,7 +10,13 @@ index 8d78aea..02e3ea6 100644
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
@@ -28,6 +29,10 @@ class GestureHandlerOrchestrator(
import android.widget.EditText
import com.facebook.react.uimanager.RootView
+import com.facebook.react.views.view.ReactViewGroup
import com.swmansion.gesturehandler.react.RNGestureHandlerRootHelper
import com.swmansion.gesturehandler.react.RNGestureHandlerRootView
import com.swmansion.gesturehandler.react.isHoverAction
@@ -28,6 +30,10 @@ class GestureHandlerOrchestrator(
private val awaitingHandlers = arrayListOf<GestureHandler>()
private val preparedHandlers = arrayListOf<GestureHandler>()
@@ -21,7 +27,7 @@ index 8d78aea..02e3ea6 100644
// In `onHandlerStateChange` method we iterate through `awaitingHandlers`, but calling `tryActivate` may modify this list.
// To avoid `ConcurrentModificationException` we iterate through copy. There is one more problem though - if handler was
// removed from `awaitingHandlers`, it was still present in copy of original list. This hashset helps us identify which handlers
@@ -46,6 +51,7 @@ class GestureHandlerOrchestrator(
@@ -46,6 +52,7 @@ class GestureHandlerOrchestrator(
fun onTouchEvent(event: MotionEvent): Boolean {
isHandlingTouch = true
val action = event.actionMasked
@@ -29,7 +35,7 @@ index 8d78aea..02e3ea6 100644
if (action == MotionEvent.ACTION_DOWN ||
action == MotionEvent.ACTION_POINTER_DOWN ||
action == MotionEvent.ACTION_HOVER_MOVE
@@ -579,6 +585,107 @@ class GestureHandlerOrchestrator(
@@ -579,6 +586,115 @@ class GestureHandlerOrchestrator(
private fun shouldIgnoreSubtreeIfGestureHandlerRootView(view: View) =
view is RNGestureHandlerRootView && view != wrapperView && view.isRootViewEnabled()
@@ -87,6 +93,14 @@ index 8d78aea..02e3ea6 100644
+ handler is NativeViewGestureHandler &&
+ (handler.isActive || activeHandlers.none { shouldHandlerBeCancelledBy(handler, it) })
+
+ // Keep cancellation traversal in the same drawing order as Android touch dispatch.
+ private fun getChildInDrawingOrderAtIndex(parent: ViewGroup, index: Int): View =
+ if (parent is ReactViewGroup) {
+ parent.getChildAt(parent.getZIndexMappedChildIndex(index))
+ } else {
+ parent.getChildAt(index)
+ }
+
+ // Collects the view path under the point (topmost child first, like touch dispatch), leaf to root.
+ private fun collectViewsAtPoint(view: View, coords: FloatArray, out: MutableList<View>): Boolean {
+ val pointerEvents = viewConfigHelper.getPointerEventsConfigForView(view)
@@ -97,7 +111,7 @@ index 8d78aea..02e3ea6 100644
+ var found = false
+ if (view is ViewGroup && pointerEvents != PointerEventsConfig.BOX_ONLY) {
+ for (i in view.childCount - 1 downTo 0) {
+ val child = viewConfigHelper.getChildInDrawingOrderAtIndex(view, i)
+ val child = getChildInDrawingOrderAtIndex(view, i)
+ if (!canReceiveEvents(child)) {
+ continue
+ }
@@ -138,7 +152,7 @@ index 8d78aea..02e3ea6 100644
viewGroup: ViewGroup,
coords: FloatArray,
diff --git a/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt b/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt
index 629c155..e1909ff 100644
index 629c155..dc0790d 100644
--- a/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt
+++ b/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt
@@ -19,6 +19,7 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView:
@@ -157,7 +171,7 @@ index 629c155..e1909ff 100644
+ // On the transition into interception, cancel the native views the pointers landed on - the
+ // framework's ACTION_CANCEL never reaches them since RNGH ignores `onInterceptTouchEvent`.
+ if (shouldIntercept && !wasIntercepting) {
+ orchestrator!!.cancelTouchesInInterceptedViews(ev)
+ orchestrator!!.cancelTouchesInInterceptedViews(event)
+ }
+ wasIntercepting = shouldIntercept
+