From ff39d6ab3aea9ed586e645fe572051e614f38a0a Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Wed, 8 Apr 2026 02:36:48 -0700 Subject: [PATCH] Add Page.captureScreenshot CDP support (#56307) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/56307 Implement the `Page.captureScreenshot` CDP command in the inspector backend, allowing DevTools to capture an on-demand screenshot of the current app view. This is a minimal implementation supporting only the `format` (jpeg/png/webp) and `quality` (0-100, jpeg only) parameters, returning base64-encoded image data. The feature is gated behind a new `fuseboxCaptureScreenshotEnabled` React Native feature flag, wired through `InspectorFlags` following the same pattern as frame recording. **Motivation** Improve agent verification / user feedback during AI sessions. We've proven that screenshots (in performance traces) are useful for understanding how components render on screen, and exposing this as an on-demand CDP method is relatively cheap today vs the larger task of modelling the DOM (Elements panel). **Changes** - New `fuseboxCaptureScreenshotEnabled` feature flag + `InspectorFlags` wiring (C++, Android JNI, Kotlin) - `HostTargetDelegate::captureScreenshot()` virtual method with async callback - C++ CDP handler in `HostAgent` — parses `format`/`quality`, delegates to platform, sends async CDP response (gated by flag) - iOS (`RCTHost.mm`): Captures key window via `UIGraphicsImageRenderer` + `drawViewHierarchyInRect`, encodes to PNG/JPEG - Android (`ReactHostImpl.kt`): Captures decor view via `Bitmap` + `Canvas` + `View.draw()`, encodes to PNG/JPEG/WebP - 4 C++ tests: success, failure, param forwarding, flag-disabled rejection Changelog: [Internal] Reviewed By: hoxyq Differential Revision: D99099930 --- .../react/devsupport/InspectorFlags.kt | 2 + .../featureflags/ReactNativeFeatureFlags.kt | 8 ++- .../ReactNativeFeatureFlagsCxxAccessor.kt | 12 +++- .../ReactNativeFeatureFlagsCxxInterop.kt | 4 +- .../ReactNativeFeatureFlagsDefaults.kt | 4 +- .../ReactNativeFeatureFlagsLocalAccessor.kt | 13 +++- .../ReactNativeFeatureFlagsProvider.kt | 4 +- .../facebook/react/runtime/ReactHostImpl.kt | 38 ++++++++++ .../jni/react/devsupport/JInspectorFlags.cpp | 11 +++ .../jni/react/devsupport/JInspectorFlags.h | 1 + .../JReactNativeFeatureFlagsCxxInterop.cpp | 16 ++++- .../JReactNativeFeatureFlagsCxxInterop.h | 5 +- .../runtime/jni/JReactHostInspectorTarget.cpp | 14 ++++ .../runtime/jni/JReactHostInspectorTarget.h | 9 +++ .../jsinspector-modern/HostAgent.cpp | 36 ++++++++++ .../jsinspector-modern/HostTarget.h | 25 +++++++ .../jsinspector-modern/InspectorFlags.cpp | 6 ++ .../jsinspector-modern/InspectorFlags.h | 6 ++ .../tests/HostTargetTest.cpp | 12 ++++ .../jsinspector-modern/tests/InspectorMocks.h | 5 +- .../utils/InspectorFlagOverridesGuard.cpp | 5 ++ .../tests/utils/InspectorFlagOverridesGuard.h | 1 + .../featureflags/ReactNativeFeatureFlags.cpp | 6 +- .../featureflags/ReactNativeFeatureFlags.h | 7 +- .../ReactNativeFeatureFlagsAccessor.cpp | 72 ++++++++++++------- .../ReactNativeFeatureFlagsAccessor.h | 6 +- .../ReactNativeFeatureFlagsDefaults.h | 6 +- .../ReactNativeFeatureFlagsDynamicProvider.h | 11 ++- .../ReactNativeFeatureFlagsProvider.h | 3 +- .../NativeReactNativeFeatureFlags.cpp | 7 +- .../NativeReactNativeFeatureFlags.h | 4 +- .../platform/ios/ReactCommon/RCTHost.mm | 59 +++++++++++++++ .../ReactNativeFeatureFlags.config.js | 11 +++ .../featureflags/ReactNativeFeatureFlags.js | 7 +- .../specs/NativeReactNativeFeatureFlags.js | 3 +- .../api-snapshots/ReactAndroidDebugCxx.api | 10 +++ .../api-snapshots/ReactAndroidReleaseCxx.api | 10 +++ .../api-snapshots/ReactAppleDebugCxx.api | 7 ++ .../api-snapshots/ReactAppleReleaseCxx.api | 7 ++ .../api-snapshots/ReactCommonDebugCxx.api | 7 ++ .../api-snapshots/ReactCommonReleaseCxx.api | 7 ++ 41 files changed, 439 insertions(+), 48 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/InspectorFlags.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/InspectorFlags.kt index 608f24a8641b..f9e38dda2c5f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/InspectorFlags.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/InspectorFlags.kt @@ -19,6 +19,8 @@ internal object InspectorFlags { SoLoader.loadLibrary("react_devsupportjni") } + @DoNotStrip @JvmStatic external fun getScreenshotCaptureEnabled(): Boolean + @DoNotStrip @JvmStatic external fun getFuseboxEnabled(): Boolean @DoNotStrip @JvmStatic external fun getIsProfilingBuild(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt index ed73611cd0d6..e025a325bd74 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<84bfc7b7a1b239c514b6d7c38dd91283>> + * @generated SignedSource<> */ /** @@ -408,6 +408,12 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun fuseboxNetworkInspectionEnabled(): Boolean = accessor.fuseboxNetworkInspectionEnabled() + /** + * Enable Page.captureScreenshot CDP method support in the React Native DevTools CDP backend. This flag is global and should not be changed across React Host lifetimes. + */ + @JvmStatic + public fun fuseboxScreenshotCaptureEnabled(): Boolean = accessor.fuseboxScreenshotCaptureEnabled() + /** * Hides offscreen VirtualViews on iOS by setting hidden = YES to avoid extra cost of views */ diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt index 86fe46d41f98..2028a5d436d7 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<1b59188082b9222b22b5cb0585cd166f>> + * @generated SignedSource<<668885665a149d50bdff92bd96a297f0>> */ /** @@ -83,6 +83,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces private var fuseboxEnabledReleaseCache: Boolean? = null private var fuseboxFrameRecordingEnabledCache: Boolean? = null private var fuseboxNetworkInspectionEnabledCache: Boolean? = null + private var fuseboxScreenshotCaptureEnabledCache: Boolean? = null private var hideOffscreenVirtualViewsOnIOSCache: Boolean? = null private var overrideBySynchronousMountPropsAtMountingAndroidCache: Boolean? = null private var perfIssuesEnabledCache: Boolean? = null @@ -677,6 +678,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun fuseboxScreenshotCaptureEnabled(): Boolean { + var cached = fuseboxScreenshotCaptureEnabledCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.fuseboxScreenshotCaptureEnabled() + fuseboxScreenshotCaptureEnabledCache = cached + } + return cached + } + override fun hideOffscreenVirtualViewsOnIOS(): Boolean { var cached = hideOffscreenVirtualViewsOnIOSCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt index 0bbcc7d1b000..a73f9e0367be 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<2fd657c62d07ed766a9241cb1c14d98d>> + * @generated SignedSource<> */ /** @@ -154,6 +154,8 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun fuseboxNetworkInspectionEnabled(): Boolean + @DoNotStrip @JvmStatic public external fun fuseboxScreenshotCaptureEnabled(): Boolean + @DoNotStrip @JvmStatic public external fun hideOffscreenVirtualViewsOnIOS(): Boolean @DoNotStrip @JvmStatic public external fun overrideBySynchronousMountPropsAtMountingAndroid(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt index 10441aeec331..d453d31e9991 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<6348c0cc9285f2bac6df9986155b584f>> */ /** @@ -149,6 +149,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun fuseboxNetworkInspectionEnabled(): Boolean = true + override fun fuseboxScreenshotCaptureEnabled(): Boolean = false + override fun hideOffscreenVirtualViewsOnIOS(): Boolean = false override fun overrideBySynchronousMountPropsAtMountingAndroid(): Boolean = false diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt index b576851f9e89..a7e5f3728a02 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<251381892c7d15310d61b35913c5cba6>> + * @generated SignedSource<> */ /** @@ -87,6 +87,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc private var fuseboxEnabledReleaseCache: Boolean? = null private var fuseboxFrameRecordingEnabledCache: Boolean? = null private var fuseboxNetworkInspectionEnabledCache: Boolean? = null + private var fuseboxScreenshotCaptureEnabledCache: Boolean? = null private var hideOffscreenVirtualViewsOnIOSCache: Boolean? = null private var overrideBySynchronousMountPropsAtMountingAndroidCache: Boolean? = null private var perfIssuesEnabledCache: Boolean? = null @@ -744,6 +745,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc return cached } + override fun fuseboxScreenshotCaptureEnabled(): Boolean { + var cached = fuseboxScreenshotCaptureEnabledCache + if (cached == null) { + cached = currentProvider.fuseboxScreenshotCaptureEnabled() + accessedFeatureFlags.add("fuseboxScreenshotCaptureEnabled") + fuseboxScreenshotCaptureEnabledCache = cached + } + return cached + } + override fun hideOffscreenVirtualViewsOnIOS(): Boolean { var cached = hideOffscreenVirtualViewsOnIOSCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt index f10b331f32d8..8370d4532b38 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<7e47ac680222281a813e65484e7f8e39>> + * @generated SignedSource<> */ /** @@ -149,6 +149,8 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun fuseboxNetworkInspectionEnabled(): Boolean + @DoNotStrip public fun fuseboxScreenshotCaptureEnabled(): Boolean + @DoNotStrip public fun hideOffscreenVirtualViewsOnIOS(): Boolean @DoNotStrip public fun overrideBySynchronousMountPropsAtMountingAndroid(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt index 9f398f300c58..5f8b3dd9c18a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt @@ -12,6 +12,7 @@ import android.content.Context import android.content.Intent import android.nfc.NfcAdapter import android.os.Bundle +import androidx.core.graphics.createBitmap import com.facebook.common.logging.FLog import com.facebook.infer.annotation.Assertions import com.facebook.infer.annotation.ThreadConfined @@ -447,6 +448,43 @@ public class ReactHostImpl( InspectorNetworkHelper.loadNetworkResource(url, listener) } + @DoNotStrip + private fun captureScreenshot(format: String, quality: Int): String? { + val activity = currentActivity ?: return null + val window = activity.window ?: return null + val decorView = window.decorView.rootView + + val width = decorView.width + val height = decorView.height + if (width <= 0 || height <= 0) { + return null + } + + val bitmap = createBitmap(width, height) + val canvas = android.graphics.Canvas(bitmap) + decorView.draw(canvas) + + val outputStream = java.io.ByteArrayOutputStream() + val compressFormat = + when (format) { + "jpeg" -> android.graphics.Bitmap.CompressFormat.JPEG + "webp" -> + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { + android.graphics.Bitmap.CompressFormat.WEBP_LOSSY + } else { + @Suppress("DEPRECATION") android.graphics.Bitmap.CompressFormat.WEBP + } + else -> android.graphics.Bitmap.CompressFormat.PNG + } + val compressQuality = if (quality in 0..100) quality else 80 + + bitmap.compress(compressFormat, compressQuality, outputStream) + bitmap.recycle() + + val bytes = outputStream.toByteArray() + return android.util.Base64.encodeToString(bytes, android.util.Base64.NO_WRAP) + } + /** * Entrypoint to destroy the ReactInstance. If the ReactInstance is reloading, will wait until * reload is finished, before destroying. diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/devsupport/JInspectorFlags.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/devsupport/JInspectorFlags.cpp index 87548175823b..95cf9636f43f 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/devsupport/JInspectorFlags.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/devsupport/JInspectorFlags.cpp @@ -11,6 +11,12 @@ namespace facebook::react::jsinspector_modern { +bool JInspectorFlags::getScreenshotCaptureEnabled( + jni::alias_ref /*unused*/) { + auto& inspectorFlags = InspectorFlags::getInstance(); + return inspectorFlags.getScreenshotCaptureEnabled(); +} + bool JInspectorFlags::getFuseboxEnabled(jni::alias_ref /*unused*/) { auto& inspectorFlags = InspectorFlags::getInstance(); return inspectorFlags.getFuseboxEnabled(); @@ -28,6 +34,11 @@ bool JInspectorFlags::getFrameRecordingEnabled( } void JInspectorFlags::registerNatives() { + javaClassLocal()->registerNatives({ + makeNativeMethod( + "getScreenshotCaptureEnabled", + JInspectorFlags::getScreenshotCaptureEnabled), + }); javaClassLocal()->registerNatives({ makeNativeMethod("getFuseboxEnabled", JInspectorFlags::getFuseboxEnabled), }); diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/devsupport/JInspectorFlags.h b/packages/react-native/ReactAndroid/src/main/jni/react/devsupport/JInspectorFlags.h index 6b6ff607541d..1056132c7c68 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/devsupport/JInspectorFlags.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/devsupport/JInspectorFlags.h @@ -18,6 +18,7 @@ class JInspectorFlags : public jni::JavaClass { public: static constexpr auto kJavaDescriptor = "Lcom/facebook/react/devsupport/InspectorFlags;"; + static bool getScreenshotCaptureEnabled(jni::alias_ref /*unused*/); static bool getFuseboxEnabled(jni::alias_ref /*unused*/); static bool getIsProfilingBuild(jni::alias_ref /*unused*/); static bool getFrameRecordingEnabled(jni::alias_ref /*unused*/); diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp index 38c8d2acf482..3c2ad65cb436 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<7e78fc846fe46b5dc7d451817db90ec1>> + * @generated SignedSource<<367bb35175543888c27d2e26a049289c>> */ /** @@ -417,6 +417,12 @@ class ReactNativeFeatureFlagsJavaProvider return method(javaProvider_); } + bool fuseboxScreenshotCaptureEnabled() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("fuseboxScreenshotCaptureEnabled"); + return method(javaProvider_); + } + bool hideOffscreenVirtualViewsOnIOS() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("hideOffscreenVirtualViewsOnIOS"); @@ -892,6 +898,11 @@ bool JReactNativeFeatureFlagsCxxInterop::fuseboxNetworkInspectionEnabled( return ReactNativeFeatureFlags::fuseboxNetworkInspectionEnabled(); } +bool JReactNativeFeatureFlagsCxxInterop::fuseboxScreenshotCaptureEnabled( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::fuseboxScreenshotCaptureEnabled(); +} + bool JReactNativeFeatureFlagsCxxInterop::hideOffscreenVirtualViewsOnIOS( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::hideOffscreenVirtualViewsOnIOS(); @@ -1242,6 +1253,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "fuseboxNetworkInspectionEnabled", JReactNativeFeatureFlagsCxxInterop::fuseboxNetworkInspectionEnabled), + makeNativeMethod( + "fuseboxScreenshotCaptureEnabled", + JReactNativeFeatureFlagsCxxInterop::fuseboxScreenshotCaptureEnabled), makeNativeMethod( "hideOffscreenVirtualViewsOnIOS", JReactNativeFeatureFlagsCxxInterop::hideOffscreenVirtualViewsOnIOS), diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h index 7b1e44ea7586..78ccfb6c1e24 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<022cea241825b693de81b5f1f6d7d638>> + * @generated SignedSource<> */ /** @@ -219,6 +219,9 @@ class JReactNativeFeatureFlagsCxxInterop static bool fuseboxNetworkInspectionEnabled( facebook::jni::alias_ref); + static bool fuseboxScreenshotCaptureEnabled( + facebook::jni::alias_ref); + static bool hideOffscreenVirtualViewsOnIOS( facebook::jni::alias_ref); diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.cpp index 709b5a0dd7b2..d8498aeea988 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.cpp @@ -145,6 +145,20 @@ void JReactHostInspectorTarget::loadNetworkResource( } } +std::optional JReactHostInspectorTarget::captureScreenshot( + const jsinspector_modern::HostTargetDelegate::PageCaptureScreenshotRequest& + request) { + if (auto javaReactHostImplStrong = javaReactHostImpl_->get()) { + std::string format = request.format.value_or("png"); + int quality = request.quality.value_or(-1); + auto result = javaReactHostImplStrong->captureScreenshot(format, quality); + if (result) { + return result->toStdString(); + } + } + return std::nullopt; +} + HostTarget* JReactHostInspectorTarget::getInspectorTarget() { return inspectorTarget_ ? inspectorTarget_.get() : nullptr; } diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.h b/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.h index 761fbb6b8926..d2389940308b 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.h @@ -145,6 +145,13 @@ struct JReactHostImpl : public jni::JavaClass { "loadNetworkResource"); return method(self(), jni::make_jstring(url), listener); } + + jni::local_ref captureScreenshot(const std::string &format, int quality) const + { + auto method = javaClassStatic()->getMethod(jni::local_ref, jint)>( + "captureScreenshot"); + return method(self(), jni::make_jstring(format), static_cast(quality)); + } }; /** @@ -275,6 +282,8 @@ class JReactHostInspectorTarget : public jni::HybridClass executor) override; + std::optional captureScreenshot( + const jsinspector_modern::HostTargetDelegate::PageCaptureScreenshotRequest &request) override; jsinspector_modern::HostTargetTracingDelegate *getTracingDelegate() override; private: diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp b/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp index 8fd200024d3a..d1ef2ab54f05 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp @@ -198,6 +198,42 @@ class HostAgent::Impl final { .shouldSendOKResponse = true, }; } + if (InspectorFlags::getInstance().getScreenshotCaptureEnabled()) { + if (req.method == "Page.captureScreenshot") { + std::optional format; + std::optional quality; + + if (req.params.isObject()) { + if (req.params.count("format") != 0u) { + format = req.params.at("format").asString(); + } + if (req.params.count("quality") != 0u) { + quality = static_cast(req.params.at("quality").asInt()); + } + } + + auto base64Data = targetController_.getDelegate().captureScreenshot( + {.format = format, .quality = quality}); + + if (base64Data.has_value()) { + frontendChannel_( + cdp::jsonResult( + req.id, + folly::dynamic::object("data", std::move(*base64Data)))); + } else { + frontendChannel_( + cdp::jsonError( + req.id, + cdp::ErrorCode::InternalError, + "Failed to capture screenshot")); + } + + return { + .isFinishedHandlingRequest = true, + .shouldSendOKResponse = false, + }; + } + } if (req.method == "Overlay.setPausedInDebuggerMessage") { auto message = req.params.isObject() && (req.params.count("message") != 0u) diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h b/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h index c7b8281bb89d..1030b90879f9 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h @@ -16,6 +16,7 @@ #include "ScopedExecutor.h" #include "WeakList.h" +#include #include #include #include @@ -134,6 +135,19 @@ class HostTargetDelegate : public LoadNetworkResourceDelegate { } }; + struct PageCaptureScreenshotRequest { + /** + * Image compression format. Defaults to "png". + * Allowed values: "jpeg", "png", "webp". + */ + std::optional format; + + /** + * Compression quality from range [0..100] (jpeg only). + */ + std::optional quality; + }; + virtual ~HostTargetDelegate() override; /** @@ -181,6 +195,17 @@ class HostTargetDelegate : public LoadNetworkResourceDelegate { "LoadNetworkResourceDelegate.loadNetworkResource is not implemented by this host target delegate."); } + /** + * Called when the debugger requests a screenshot of the current page via + * @cdp Page.captureScreenshot. The delegate should capture the current + * view, encode it to the requested format, and return base64-encoded + * image data. Return std::nullopt on failure. + */ + virtual std::optional captureScreenshot(const PageCaptureScreenshotRequest & /*request*/) + { + return std::nullopt; + } + /** * An optional delegate that will be used by HostTarget to notify about tracing-related events. */ diff --git a/packages/react-native/ReactCommon/jsinspector-modern/InspectorFlags.cpp b/packages/react-native/ReactCommon/jsinspector-modern/InspectorFlags.cpp index af4f90b53a27..c2c31ee13d28 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/InspectorFlags.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/InspectorFlags.cpp @@ -21,6 +21,10 @@ bool InspectorFlags::getAssertSingleHostState() const { return loadFlagsAndAssertUnchanged().assertSingleHostState; } +bool InspectorFlags::getScreenshotCaptureEnabled() const { + return loadFlagsAndAssertUnchanged().screenshotCaptureEnabled; +} + bool InspectorFlags::getFrameRecordingEnabled() const { return loadFlagsAndAssertUnchanged().frameRecordingEnabled; } @@ -58,6 +62,8 @@ const InspectorFlags::Values& InspectorFlags::loadFlagsAndAssertUnchanged() InspectorFlags::Values newValues = { .assertSingleHostState = ReactNativeFeatureFlags::fuseboxAssertSingleHostState(), + .screenshotCaptureEnabled = + ReactNativeFeatureFlags::fuseboxScreenshotCaptureEnabled(), .frameRecordingEnabled = ReactNativeFeatureFlags::fuseboxFrameRecordingEnabled(), .fuseboxEnabled = diff --git a/packages/react-native/ReactCommon/jsinspector-modern/InspectorFlags.h b/packages/react-native/ReactCommon/jsinspector-modern/InspectorFlags.h index 47c683e972c8..f1f1353e5e95 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/InspectorFlags.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/InspectorFlags.h @@ -36,6 +36,11 @@ class InspectorFlags { */ bool getIsProfilingBuild() const; + /** + * Flag determining if Page.captureScreenshot CDP method is enabled. + */ + bool getScreenshotCaptureEnabled() const; + /** * Flag determining if frame recording (timings + screenshots) is enabled. */ @@ -66,6 +71,7 @@ class InspectorFlags { private: struct Values { bool assertSingleHostState; + bool screenshotCaptureEnabled; bool frameRecordingEnabled; bool fuseboxEnabled; bool isProfilingBuild; diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tests/HostTargetTest.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tests/HostTargetTest.cpp index 38175d4110c6..a4bca34e82b7 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tests/HostTargetTest.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/HostTargetTest.cpp @@ -1579,4 +1579,16 @@ TEST_F(HostTargetTest, TracingDelegateIsNotifiedOnDirectTracingCall) { page_->stopTracing(); } +TEST_F(HostTargetProtocolTest, CaptureScreenshotNotSupportedWhenFlagDisabled) { + EXPECT_CALL( + fromPage(), + onMessage(JsonParsed(AllOf( + AtJsonPtr("/error/code", Eq(-32601)), AtJsonPtr("/id", Eq(1)))))) + .RetiresOnSaturation(); + toPage_->sendMessage(R"({ + "id": 1, + "method": "Page.captureScreenshot" + })"); +} + } // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tests/InspectorMocks.h b/packages/react-native/ReactCommon/jsinspector-modern/tests/InspectorMocks.h index 122b233f24a3..b50582b771f9 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tests/InspectorMocks.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/InspectorMocks.h @@ -87,13 +87,13 @@ class MockInspectorPackagerConnectionDelegate : public InspectorPackagerConnecti explicit MockInspectorPackagerConnectionDelegate(folly::Executor &executor) : executor_(executor) { using namespace testing; - ON_CALL(*this, scheduleCallback(_, _)).WillByDefault(Invoke<>([this](auto callback, auto delay) { + ON_CALL(*this, scheduleCallback(_, _)).WillByDefault([this](auto callback, auto delay) { if (auto scheduledExecutor = dynamic_cast(&executor_)) { scheduledExecutor->scheduleAt(callback, scheduledExecutor->now() + delay); } else { executor_.add(callback); } - })); + }); EXPECT_CALL(*this, scheduleCallback(_, _)).Times(AnyNumber()); } @@ -137,6 +137,7 @@ class MockHostTargetDelegate : public HostTargetDelegate { loadNetworkResource, (const LoadNetworkResourceRequest ¶ms, ScopedExecutor executor), (override)); + MOCK_METHOD(std::optional, captureScreenshot, (const PageCaptureScreenshotRequest &request), (override)); HostTargetTracingDelegate *getTracingDelegate() override { diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tests/utils/InspectorFlagOverridesGuard.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tests/utils/InspectorFlagOverridesGuard.cpp index ad5558056499..17a83c994139 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tests/utils/InspectorFlagOverridesGuard.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/utils/InspectorFlagOverridesGuard.cpp @@ -26,6 +26,11 @@ class ReactNativeFeatureFlagsOverrides const InspectorFlagOverrides& overrides) : overrides_(overrides) {} + bool fuseboxScreenshotCaptureEnabled() override { + return overrides_.screenshotCaptureEnabled.value_or( + ReactNativeFeatureFlagsDefaults::fuseboxScreenshotCaptureEnabled()); + } + bool fuseboxEnabledRelease() override { return overrides_.fuseboxEnabledRelease.value_or( ReactNativeFeatureFlagsDefaults::fuseboxEnabledRelease()); diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tests/utils/InspectorFlagOverridesGuard.h b/packages/react-native/ReactCommon/jsinspector-modern/tests/utils/InspectorFlagOverridesGuard.h index cf4c971248d2..12122b3f2d72 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tests/utils/InspectorFlagOverridesGuard.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/utils/InspectorFlagOverridesGuard.h @@ -19,6 +19,7 @@ namespace facebook::react::jsinspector_modern { struct InspectorFlagOverrides { // NOTE: Keep these entries in sync with ReactNativeFeatureFlagsOverrides in // the implementation file. + std::optional screenshotCaptureEnabled; std::optional enableNetworkEventReporting; std::optional frameRecordingEnabled; std::optional fuseboxEnabledRelease; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp index cd83dad26433..aa083fd0c8c0 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<470390f6f44bd822b286329f5f103cbe>> + * @generated SignedSource<<252f40100280324a528cced3c2c40176>> */ /** @@ -278,6 +278,10 @@ bool ReactNativeFeatureFlags::fuseboxNetworkInspectionEnabled() { return getAccessor().fuseboxNetworkInspectionEnabled(); } +bool ReactNativeFeatureFlags::fuseboxScreenshotCaptureEnabled() { + return getAccessor().fuseboxScreenshotCaptureEnabled(); +} + bool ReactNativeFeatureFlags::hideOffscreenVirtualViewsOnIOS() { return getAccessor().hideOffscreenVirtualViewsOnIOS(); } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index 17701176c436..63bb8ee1500d 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<8196f0d040b6f43e3bdc875f40b0041d>> + * @generated SignedSource<> */ /** @@ -354,6 +354,11 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool fuseboxNetworkInspectionEnabled(); + /** + * Enable Page.captureScreenshot CDP method support in the React Native DevTools CDP backend. This flag is global and should not be changed across React Host lifetimes. + */ + RN_EXPORT static bool fuseboxScreenshotCaptureEnabled(); + /** * Hides offscreen VirtualViews on iOS by setting hidden = YES to avoid extra cost of views */ diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp index d9efdc017239..8bbde6f64045 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<5339552fb4a1112256f8d5b2c15750e5>> + * @generated SignedSource<<41b8a301ce219afd038cea6f62ab7a52>> */ /** @@ -1163,6 +1163,24 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxNetworkInspectionEnabled() { return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::fuseboxScreenshotCaptureEnabled() { + auto flagValue = fuseboxScreenshotCaptureEnabled_.load(); + + if (!flagValue.has_value()) { + // This block is not exclusive but it is not necessary. + // If multiple threads try to initialize the feature flag, we would only + // be accessing the provider multiple times but the end state of this + // instance and the returned flag value would be the same. + + markFlagAsAccessed(63, "fuseboxScreenshotCaptureEnabled"); + + flagValue = currentProvider_->fuseboxScreenshotCaptureEnabled(); + fuseboxScreenshotCaptureEnabled_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::hideOffscreenVirtualViewsOnIOS() { auto flagValue = hideOffscreenVirtualViewsOnIOS_.load(); @@ -1172,7 +1190,7 @@ bool ReactNativeFeatureFlagsAccessor::hideOffscreenVirtualViewsOnIOS() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(63, "hideOffscreenVirtualViewsOnIOS"); + markFlagAsAccessed(64, "hideOffscreenVirtualViewsOnIOS"); flagValue = currentProvider_->hideOffscreenVirtualViewsOnIOS(); hideOffscreenVirtualViewsOnIOS_ = flagValue; @@ -1190,7 +1208,7 @@ bool ReactNativeFeatureFlagsAccessor::overrideBySynchronousMountPropsAtMountingA // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(64, "overrideBySynchronousMountPropsAtMountingAndroid"); + markFlagAsAccessed(65, "overrideBySynchronousMountPropsAtMountingAndroid"); flagValue = currentProvider_->overrideBySynchronousMountPropsAtMountingAndroid(); overrideBySynchronousMountPropsAtMountingAndroid_ = flagValue; @@ -1208,7 +1226,7 @@ bool ReactNativeFeatureFlagsAccessor::perfIssuesEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(65, "perfIssuesEnabled"); + markFlagAsAccessed(66, "perfIssuesEnabled"); flagValue = currentProvider_->perfIssuesEnabled(); perfIssuesEnabled_ = flagValue; @@ -1226,7 +1244,7 @@ bool ReactNativeFeatureFlagsAccessor::perfMonitorV2Enabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(66, "perfMonitorV2Enabled"); + markFlagAsAccessed(67, "perfMonitorV2Enabled"); flagValue = currentProvider_->perfMonitorV2Enabled(); perfMonitorV2Enabled_ = flagValue; @@ -1244,7 +1262,7 @@ double ReactNativeFeatureFlagsAccessor::preparedTextCacheSize() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(67, "preparedTextCacheSize"); + markFlagAsAccessed(68, "preparedTextCacheSize"); flagValue = currentProvider_->preparedTextCacheSize(); preparedTextCacheSize_ = flagValue; @@ -1262,7 +1280,7 @@ bool ReactNativeFeatureFlagsAccessor::preventShadowTreeCommitExhaustion() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(68, "preventShadowTreeCommitExhaustion"); + markFlagAsAccessed(69, "preventShadowTreeCommitExhaustion"); flagValue = currentProvider_->preventShadowTreeCommitExhaustion(); preventShadowTreeCommitExhaustion_ = flagValue; @@ -1280,7 +1298,7 @@ bool ReactNativeFeatureFlagsAccessor::shouldPressibilityUseW3CPointerEventsForHo // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(69, "shouldPressibilityUseW3CPointerEventsForHover"); + markFlagAsAccessed(70, "shouldPressibilityUseW3CPointerEventsForHover"); flagValue = currentProvider_->shouldPressibilityUseW3CPointerEventsForHover(); shouldPressibilityUseW3CPointerEventsForHover_ = flagValue; @@ -1298,7 +1316,7 @@ bool ReactNativeFeatureFlagsAccessor::shouldTriggerResponderTransferOnScrollAndr // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(70, "shouldTriggerResponderTransferOnScrollAndroid"); + markFlagAsAccessed(71, "shouldTriggerResponderTransferOnScrollAndroid"); flagValue = currentProvider_->shouldTriggerResponderTransferOnScrollAndroid(); shouldTriggerResponderTransferOnScrollAndroid_ = flagValue; @@ -1316,7 +1334,7 @@ bool ReactNativeFeatureFlagsAccessor::skipActivityIdentityAssertionOnHostPause() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(71, "skipActivityIdentityAssertionOnHostPause"); + markFlagAsAccessed(72, "skipActivityIdentityAssertionOnHostPause"); flagValue = currentProvider_->skipActivityIdentityAssertionOnHostPause(); skipActivityIdentityAssertionOnHostPause_ = flagValue; @@ -1334,7 +1352,7 @@ bool ReactNativeFeatureFlagsAccessor::syncAndroidClipToPaddingWithOverflow() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(72, "syncAndroidClipToPaddingWithOverflow"); + markFlagAsAccessed(73, "syncAndroidClipToPaddingWithOverflow"); flagValue = currentProvider_->syncAndroidClipToPaddingWithOverflow(); syncAndroidClipToPaddingWithOverflow_ = flagValue; @@ -1352,7 +1370,7 @@ bool ReactNativeFeatureFlagsAccessor::traceTurboModulePromiseRejectionsOnAndroid // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(73, "traceTurboModulePromiseRejectionsOnAndroid"); + markFlagAsAccessed(74, "traceTurboModulePromiseRejectionsOnAndroid"); flagValue = currentProvider_->traceTurboModulePromiseRejectionsOnAndroid(); traceTurboModulePromiseRejectionsOnAndroid_ = flagValue; @@ -1370,7 +1388,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommit( // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(74, "updateRuntimeShadowNodeReferencesOnCommit"); + markFlagAsAccessed(75, "updateRuntimeShadowNodeReferencesOnCommit"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommit(); updateRuntimeShadowNodeReferencesOnCommit_ = flagValue; @@ -1388,7 +1406,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommitT // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(75, "updateRuntimeShadowNodeReferencesOnCommitThread"); + markFlagAsAccessed(76, "updateRuntimeShadowNodeReferencesOnCommitThread"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommitThread(); updateRuntimeShadowNodeReferencesOnCommitThread_ = flagValue; @@ -1406,7 +1424,7 @@ bool ReactNativeFeatureFlagsAccessor::useAlwaysAvailableJSErrorHandling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(76, "useAlwaysAvailableJSErrorHandling"); + markFlagAsAccessed(77, "useAlwaysAvailableJSErrorHandling"); flagValue = currentProvider_->useAlwaysAvailableJSErrorHandling(); useAlwaysAvailableJSErrorHandling_ = flagValue; @@ -1424,7 +1442,7 @@ bool ReactNativeFeatureFlagsAccessor::useFabricInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(77, "useFabricInterop"); + markFlagAsAccessed(78, "useFabricInterop"); flagValue = currentProvider_->useFabricInterop(); useFabricInterop_ = flagValue; @@ -1442,7 +1460,7 @@ bool ReactNativeFeatureFlagsAccessor::useLISAlgorithmInDifferentiator() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(78, "useLISAlgorithmInDifferentiator"); + markFlagAsAccessed(79, "useLISAlgorithmInDifferentiator"); flagValue = currentProvider_->useLISAlgorithmInDifferentiator(); useLISAlgorithmInDifferentiator_ = flagValue; @@ -1460,7 +1478,7 @@ bool ReactNativeFeatureFlagsAccessor::useNativeViewConfigsInBridgelessMode() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(79, "useNativeViewConfigsInBridgelessMode"); + markFlagAsAccessed(80, "useNativeViewConfigsInBridgelessMode"); flagValue = currentProvider_->useNativeViewConfigsInBridgelessMode(); useNativeViewConfigsInBridgelessMode_ = flagValue; @@ -1478,7 +1496,7 @@ bool ReactNativeFeatureFlagsAccessor::useNestedScrollViewAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(80, "useNestedScrollViewAndroid"); + markFlagAsAccessed(81, "useNestedScrollViewAndroid"); flagValue = currentProvider_->useNestedScrollViewAndroid(); useNestedScrollViewAndroid_ = flagValue; @@ -1496,7 +1514,7 @@ bool ReactNativeFeatureFlagsAccessor::useSharedAnimatedBackend() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(81, "useSharedAnimatedBackend"); + markFlagAsAccessed(82, "useSharedAnimatedBackend"); flagValue = currentProvider_->useSharedAnimatedBackend(); useSharedAnimatedBackend_ = flagValue; @@ -1514,7 +1532,7 @@ bool ReactNativeFeatureFlagsAccessor::useTraitHiddenOnAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(82, "useTraitHiddenOnAndroid"); + markFlagAsAccessed(83, "useTraitHiddenOnAndroid"); flagValue = currentProvider_->useTraitHiddenOnAndroid(); useTraitHiddenOnAndroid_ = flagValue; @@ -1532,7 +1550,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModuleInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(83, "useTurboModuleInterop"); + markFlagAsAccessed(84, "useTurboModuleInterop"); flagValue = currentProvider_->useTurboModuleInterop(); useTurboModuleInterop_ = flagValue; @@ -1550,7 +1568,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModules() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(84, "useTurboModules"); + markFlagAsAccessed(85, "useTurboModules"); flagValue = currentProvider_->useTurboModules(); useTurboModules_ = flagValue; @@ -1568,7 +1586,7 @@ bool ReactNativeFeatureFlagsAccessor::useUnorderedMapInDifferentiator() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(85, "useUnorderedMapInDifferentiator"); + markFlagAsAccessed(86, "useUnorderedMapInDifferentiator"); flagValue = currentProvider_->useUnorderedMapInDifferentiator(); useUnorderedMapInDifferentiator_ = flagValue; @@ -1586,7 +1604,7 @@ double ReactNativeFeatureFlagsAccessor::viewCullingOutsetRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(86, "viewCullingOutsetRatio"); + markFlagAsAccessed(87, "viewCullingOutsetRatio"); flagValue = currentProvider_->viewCullingOutsetRatio(); viewCullingOutsetRatio_ = flagValue; @@ -1604,7 +1622,7 @@ bool ReactNativeFeatureFlagsAccessor::viewTransitionEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(87, "viewTransitionEnabled"); + markFlagAsAccessed(88, "viewTransitionEnabled"); flagValue = currentProvider_->viewTransitionEnabled(); viewTransitionEnabled_ = flagValue; @@ -1622,7 +1640,7 @@ double ReactNativeFeatureFlagsAccessor::virtualViewPrerenderRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(88, "virtualViewPrerenderRatio"); + markFlagAsAccessed(89, "virtualViewPrerenderRatio"); flagValue = currentProvider_->virtualViewPrerenderRatio(); virtualViewPrerenderRatio_ = flagValue; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h index 2a0ec606eb4d..36021f0899fb 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<01254f701f0bf8f552fb7721ec6acfbf>> + * @generated SignedSource<> */ /** @@ -95,6 +95,7 @@ class ReactNativeFeatureFlagsAccessor { bool fuseboxEnabledRelease(); bool fuseboxFrameRecordingEnabled(); bool fuseboxNetworkInspectionEnabled(); + bool fuseboxScreenshotCaptureEnabled(); bool hideOffscreenVirtualViewsOnIOS(); bool overrideBySynchronousMountPropsAtMountingAndroid(); bool perfIssuesEnabled(); @@ -132,7 +133,7 @@ class ReactNativeFeatureFlagsAccessor { std::unique_ptr currentProvider_; bool wasOverridden_; - std::array, 89> accessedFeatureFlags_; + std::array, 90> accessedFeatureFlags_; std::atomic> commonTestFlag_; std::atomic> cdpInteractionMetricsEnabled_; @@ -197,6 +198,7 @@ class ReactNativeFeatureFlagsAccessor { std::atomic> fuseboxEnabledRelease_; std::atomic> fuseboxFrameRecordingEnabled_; std::atomic> fuseboxNetworkInspectionEnabled_; + std::atomic> fuseboxScreenshotCaptureEnabled_; std::atomic> hideOffscreenVirtualViewsOnIOS_; std::atomic> overrideBySynchronousMountPropsAtMountingAndroid_; std::atomic> perfIssuesEnabled_; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h index 29e5f9f3d547..f63db0e1490e 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<2d8d43862bd76c356d61de752636ae1f>> + * @generated SignedSource<<21c6a4ee6b9c64c4b136798e558dff8f>> */ /** @@ -279,6 +279,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { return true; } + bool fuseboxScreenshotCaptureEnabled() override { + return false; + } + bool hideOffscreenVirtualViewsOnIOS() override { return false; } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h index af609fa40ce0..ab8d10761aaf 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<5c71e9a2d1a426217755ab1454250d63>> + * @generated SignedSource<<7b7ce6d46da0c344cbde4f99f5a6188c>> */ /** @@ -612,6 +612,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef return ReactNativeFeatureFlagsDefaults::fuseboxNetworkInspectionEnabled(); } + bool fuseboxScreenshotCaptureEnabled() override { + auto value = values_["fuseboxScreenshotCaptureEnabled"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::fuseboxScreenshotCaptureEnabled(); + } + bool hideOffscreenVirtualViewsOnIOS() override { auto value = values_["hideOffscreenVirtualViewsOnIOS"]; if (!value.isNull()) { diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h index b5464e06d4e2..9e5846cd98c8 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<7e831019de014113ff1f7835b2b4c0d5>> */ /** @@ -88,6 +88,7 @@ class ReactNativeFeatureFlagsProvider { virtual bool fuseboxEnabledRelease() = 0; virtual bool fuseboxFrameRecordingEnabled() = 0; virtual bool fuseboxNetworkInspectionEnabled() = 0; + virtual bool fuseboxScreenshotCaptureEnabled() = 0; virtual bool hideOffscreenVirtualViewsOnIOS() = 0; virtual bool overrideBySynchronousMountPropsAtMountingAndroid() = 0; virtual bool perfIssuesEnabled() = 0; diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp index 510a6f104396..8dd1d88313ae 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<938108cbabe89175f0bd23ac9fb9d404>> */ /** @@ -359,6 +359,11 @@ bool NativeReactNativeFeatureFlags::fuseboxNetworkInspectionEnabled( return ReactNativeFeatureFlags::fuseboxNetworkInspectionEnabled(); } +bool NativeReactNativeFeatureFlags::fuseboxScreenshotCaptureEnabled( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::fuseboxScreenshotCaptureEnabled(); +} + bool NativeReactNativeFeatureFlags::hideOffscreenVirtualViewsOnIOS( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::hideOffscreenVirtualViewsOnIOS(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h index ae69d409833d..061d4dde7727 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<0d116ce9085dc407beaff5b926fa2166>> + * @generated SignedSource<<30d3bbe294fb92c10cd6c1aed5ca0272>> */ /** @@ -162,6 +162,8 @@ class NativeReactNativeFeatureFlags bool fuseboxNetworkInspectionEnabled(jsi::Runtime& runtime); + bool fuseboxScreenshotCaptureEnabled(jsi::Runtime& runtime); + bool hideOffscreenVirtualViewsOnIOS(jsi::Runtime& runtime); bool overrideBySynchronousMountPropsAtMountingAndroid(jsi::Runtime& runtime); diff --git a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.mm b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.mm index b536dcb90b7e..5ff41abd1484 100644 --- a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.mm +++ b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.mm @@ -141,6 +141,65 @@ void loadNetworkResource(const RCTInspectorLoadNetworkResourceRequest ¶ms, R [networkHelper_ loadNetworkResourceWithParams:params executor:executor]; } +#if TARGET_OS_IPHONE && defined(REACT_NATIVE_DEBUGGER_ENABLED) + std::optional captureScreenshot(const PageCaptureScreenshotRequest &request) override + { + UIWindow *keyWindow = nil; + for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) { + if (scene.activationState == UISceneActivationStateForegroundActive && + [scene isKindOfClass:[UIWindowScene class]]) { + auto *windowScene = (UIWindowScene *)scene; + for (UIWindow *win in windowScene.windows) { + if (win.isKeyWindow) { + keyWindow = win; + break; + } + } + } + if (keyWindow != nil) { + break; + } + } + + if (keyWindow == nil) { + return std::nullopt; + } + + UIView *rootView = keyWindow.rootViewController.view != nil ? keyWindow.rootViewController.view : keyWindow; + CGSize viewSize = rootView.bounds.size; + + UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat defaultFormat]; + format.scale = 1.0; + UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:viewSize format:format]; + + UIImage *image = [renderer imageWithActions:^(UIGraphicsImageRendererContext *context) { + [rootView drawViewHierarchyInRect:CGRectMake(0, 0, viewSize.width, viewSize.height) afterScreenUpdates:NO]; + }]; + + if (image == nil) { + return std::nullopt; + } + + NSData *encodedData = nil; + std::string formatStr = request.format.value_or("png"); + + if (formatStr == "jpeg") { + CGFloat quality = request.quality.has_value() ? (*request.quality / 100.0) : 0.8; + encodedData = UIImageJPEGRepresentation(image, quality); + } else { + // Default to PNG for "png" and "webp" (WebP encoding not available via UIKit) + encodedData = UIImagePNGRepresentation(image); + } + + if (encodedData == nil) { + return std::nullopt; + } + + NSString *base64String = [encodedData base64EncodedStringWithOptions:0]; + return std::string([base64String UTF8String]); + } +#endif + #if TARGET_OS_IPHONE && defined(REACT_NATIVE_DEBUGGER_ENABLED) jsinspector_modern::HostTargetTracingDelegate *getTracingDelegate() override { diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index 92c06660d51c..80c553320fff 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -720,6 +720,17 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'none', }, + fuseboxScreenshotCaptureEnabled: { + defaultValue: false, + metadata: { + dateAdded: '2026-04-01', + description: + 'Enable Page.captureScreenshot CDP method support in the React Native DevTools CDP backend. This flag is global and should not be changed across React Host lifetimes.', + expectedReleaseValue: true, + purpose: 'experimentation', + }, + ossReleaseStage: 'none', + }, hideOffscreenVirtualViewsOnIOS: { defaultValue: false, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index a7469d796e4f..a59b9a32297b 100644 --- a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<82da4884f8309101012c4257acd6ca92>> + * @generated SignedSource<<0d7e830593d2da938af59f9a215e2380>> * @flow strict * @noformat */ @@ -109,6 +109,7 @@ export type ReactNativeFeatureFlags = $ReadOnly<{ fuseboxEnabledRelease: Getter, fuseboxFrameRecordingEnabled: Getter, fuseboxNetworkInspectionEnabled: Getter, + fuseboxScreenshotCaptureEnabled: Getter, hideOffscreenVirtualViewsOnIOS: Getter, overrideBySynchronousMountPropsAtMountingAndroid: Getter, perfIssuesEnabled: Getter, @@ -448,6 +449,10 @@ export const fuseboxFrameRecordingEnabled: Getter = createNativeFlagGet * Enable network inspection support in the React Native DevTools CDP backend. Requires `enableBridgelessArchitecture`. This flag is global and should not be changed across React Host lifetimes. */ export const fuseboxNetworkInspectionEnabled: Getter = createNativeFlagGetter('fuseboxNetworkInspectionEnabled', true); +/** + * Enable Page.captureScreenshot CDP method support in the React Native DevTools CDP backend. This flag is global and should not be changed across React Host lifetimes. + */ +export const fuseboxScreenshotCaptureEnabled: Getter = createNativeFlagGetter('fuseboxScreenshotCaptureEnabled', false); /** * Hides offscreen VirtualViews on iOS by setting hidden = YES to avoid extra cost of views */ diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js index 69f12715be37..e585ba89d337 100644 --- a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<28137681d5a69686dafedd9d64cdcade>> + * @generated SignedSource<<7a3aa334314a39c580979d81b8921046>> * @flow strict * @noformat */ @@ -88,6 +88,7 @@ export interface Spec extends TurboModule { +fuseboxEnabledRelease?: () => boolean; +fuseboxFrameRecordingEnabled?: () => boolean; +fuseboxNetworkInspectionEnabled?: () => boolean; + +fuseboxScreenshotCaptureEnabled?: () => boolean; +hideOffscreenVirtualViewsOnIOS?: () => boolean; +overrideBySynchronousMountPropsAtMountingAndroid?: () => boolean; +perfIssuesEnabled?: () => boolean; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 63d4be65497d..40df46e03489 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -2823,6 +2823,7 @@ class facebook::react::JReactHostInspectorTarget : public jni::HybridClass base64Data)>& callback) override; public virtual void onReload(const facebook::react::jsinspector_modern::HostTargetDelegate::PageReloadRequest& request) override; public virtual void onSetPausedInDebuggerMessage(const facebook::react::jsinspector_modern::HostTargetDelegate::OverlaySetPausedInDebuggerMessageRequest& request) override; public virtual void unstable_onPerfIssueAdded(const facebook::react::jsinspector_modern::PerfIssuePayload&) override; @@ -7250,6 +7251,7 @@ struct facebook::react::JNativeModule : public facebook::jni::JavaClass { public jni::local_ref reload(const std::string& reason); public jni::local_ref> getHostMetadata() const; + public jni::local_ref captureScreenshot(const std::string& format, int quality) const; public static constexpr auto kJavaDescriptor; public void loadNetworkResource(const std::string& url, jni::local_ref listener) const; public void setPausedInDebuggerMessage(std::optional message); @@ -10393,6 +10395,7 @@ class facebook::react::jsinspector_modern::HostTargetDelegate : public facebook: public facebook::react::jsinspector_modern::HostTargetDelegate& operator=(facebook::react::jsinspector_modern::HostTargetDelegate&&) = delete; public virtual facebook::react::jsinspector_modern::HostTargetMetadata getMetadata() = 0; public virtual facebook::react::jsinspector_modern::HostTargetTracingDelegate* getTracingDelegate(); + public virtual void captureScreenshot(const facebook::react::jsinspector_modern::HostTargetDelegate::PageCaptureScreenshotRequest&, const std::function base64Data)>& callback); public virtual void loadNetworkResource(const facebook::react::jsinspector_modern::LoadNetworkResourceRequest&, facebook::react::jsinspector_modern::ScopedExecutor) override; public virtual void onReload(const facebook::react::jsinspector_modern::HostTargetDelegate::PageReloadRequest& request) = 0; public virtual void onSetPausedInDebuggerMessage(const facebook::react::jsinspector_modern::HostTargetDelegate::OverlaySetPausedInDebuggerMessageRequest& request) = 0; @@ -10405,6 +10408,11 @@ struct facebook::react::jsinspector_modern::HostTargetDelegate::OverlaySetPaused public std::optional message; } +struct facebook::react::jsinspector_modern::HostTargetDelegate::PageCaptureScreenshotRequest { + public std::optional quality; + public std::optional format; +} + struct facebook::react::jsinspector_modern::HostTargetDelegate::PageReloadRequest { public bool operator==(const facebook::react::jsinspector_modern::HostTargetDelegate::PageReloadRequest& rhs) const; public std::optional ignoreCache; @@ -10490,6 +10498,7 @@ class facebook::react::jsinspector_modern::InspectorFlags { public bool getIsProfilingBuild() const; public bool getNetworkInspectionEnabled() const; public bool getPerfIssuesEnabled() const; + public bool getScreenshotCaptureEnabled() const; public static facebook::react::jsinspector_modern::InspectorFlags& getInstance(); public void dangerouslyDisableFuseboxForTest(); public void dangerouslyResetFlags(); @@ -10594,6 +10603,7 @@ class facebook::react::jsinspector_modern::JInspectorFlags : public facebook::jn public static bool getFrameRecordingEnabled(jni::alias_ref); public static bool getFuseboxEnabled(jni::alias_ref); public static bool getIsProfilingBuild(jni::alias_ref); + public static bool getScreenshotCaptureEnabled(jni::alias_ref); public static constexpr auto kJavaDescriptor; public static void registerNatives(); } diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index 75f6199fdc5e..28f040b53ad4 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -2820,6 +2820,7 @@ class facebook::react::JReactHostInspectorTarget : public jni::HybridClass base64Data)>& callback) override; public virtual void onReload(const facebook::react::jsinspector_modern::HostTargetDelegate::PageReloadRequest& request) override; public virtual void onSetPausedInDebuggerMessage(const facebook::react::jsinspector_modern::HostTargetDelegate::OverlaySetPausedInDebuggerMessageRequest& request) override; public virtual void unstable_onPerfIssueAdded(const facebook::react::jsinspector_modern::PerfIssuePayload&) override; @@ -7241,6 +7242,7 @@ struct facebook::react::JNativeModule : public facebook::jni::JavaClass { public jni::local_ref reload(const std::string& reason); public jni::local_ref> getHostMetadata() const; + public jni::local_ref captureScreenshot(const std::string& format, int quality) const; public static constexpr auto kJavaDescriptor; public void loadNetworkResource(const std::string& url, jni::local_ref listener) const; public void setPausedInDebuggerMessage(std::optional message); @@ -10249,6 +10251,7 @@ class facebook::react::jsinspector_modern::HostTargetDelegate : public facebook: public facebook::react::jsinspector_modern::HostTargetDelegate& operator=(facebook::react::jsinspector_modern::HostTargetDelegate&&) = delete; public virtual facebook::react::jsinspector_modern::HostTargetMetadata getMetadata() = 0; public virtual facebook::react::jsinspector_modern::HostTargetTracingDelegate* getTracingDelegate(); + public virtual void captureScreenshot(const facebook::react::jsinspector_modern::HostTargetDelegate::PageCaptureScreenshotRequest&, const std::function base64Data)>& callback); public virtual void loadNetworkResource(const facebook::react::jsinspector_modern::LoadNetworkResourceRequest&, facebook::react::jsinspector_modern::ScopedExecutor) override; public virtual void onReload(const facebook::react::jsinspector_modern::HostTargetDelegate::PageReloadRequest& request) = 0; public virtual void onSetPausedInDebuggerMessage(const facebook::react::jsinspector_modern::HostTargetDelegate::OverlaySetPausedInDebuggerMessageRequest& request) = 0; @@ -10261,6 +10264,11 @@ struct facebook::react::jsinspector_modern::HostTargetDelegate::OverlaySetPaused public std::optional message; } +struct facebook::react::jsinspector_modern::HostTargetDelegate::PageCaptureScreenshotRequest { + public std::optional quality; + public std::optional format; +} + struct facebook::react::jsinspector_modern::HostTargetDelegate::PageReloadRequest { public bool operator==(const facebook::react::jsinspector_modern::HostTargetDelegate::PageReloadRequest& rhs) const; public std::optional ignoreCache; @@ -10346,6 +10354,7 @@ class facebook::react::jsinspector_modern::InspectorFlags { public bool getIsProfilingBuild() const; public bool getNetworkInspectionEnabled() const; public bool getPerfIssuesEnabled() const; + public bool getScreenshotCaptureEnabled() const; public static facebook::react::jsinspector_modern::InspectorFlags& getInstance(); public void dangerouslyDisableFuseboxForTest(); public void dangerouslyResetFlags(); @@ -10450,6 +10459,7 @@ class facebook::react::jsinspector_modern::JInspectorFlags : public facebook::jn public static bool getFrameRecordingEnabled(jni::alias_ref); public static bool getFuseboxEnabled(jni::alias_ref); public static bool getIsProfilingBuild(jni::alias_ref); + public static bool getScreenshotCaptureEnabled(jni::alias_ref); public static constexpr auto kJavaDescriptor; public static void registerNatives(); } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index dd938f249e2c..1a7d1e0d1926 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -12734,6 +12734,7 @@ class facebook::react::jsinspector_modern::HostTargetDelegate : public facebook: public facebook::react::jsinspector_modern::HostTargetDelegate& operator=(facebook::react::jsinspector_modern::HostTargetDelegate&&) = delete; public virtual facebook::react::jsinspector_modern::HostTargetMetadata getMetadata() = 0; public virtual facebook::react::jsinspector_modern::HostTargetTracingDelegate* getTracingDelegate(); + public virtual void captureScreenshot(const facebook::react::jsinspector_modern::HostTargetDelegate::PageCaptureScreenshotRequest&, const std::function base64Data)>& callback); public virtual void loadNetworkResource(const facebook::react::jsinspector_modern::LoadNetworkResourceRequest&, facebook::react::jsinspector_modern::ScopedExecutor) override; public virtual void onReload(const facebook::react::jsinspector_modern::HostTargetDelegate::PageReloadRequest& request) = 0; public virtual void onSetPausedInDebuggerMessage(const facebook::react::jsinspector_modern::HostTargetDelegate::OverlaySetPausedInDebuggerMessageRequest& request) = 0; @@ -12746,6 +12747,11 @@ struct facebook::react::jsinspector_modern::HostTargetDelegate::OverlaySetPaused public std::optional message; } +struct facebook::react::jsinspector_modern::HostTargetDelegate::PageCaptureScreenshotRequest { + public std::optional quality; + public std::optional format; +} + struct facebook::react::jsinspector_modern::HostTargetDelegate::PageReloadRequest { public bool operator==(const facebook::react::jsinspector_modern::HostTargetDelegate::PageReloadRequest& rhs) const; public std::optional ignoreCache; @@ -12831,6 +12837,7 @@ class facebook::react::jsinspector_modern::InspectorFlags { public bool getIsProfilingBuild() const; public bool getNetworkInspectionEnabled() const; public bool getPerfIssuesEnabled() const; + public bool getScreenshotCaptureEnabled() const; public static facebook::react::jsinspector_modern::InspectorFlags& getInstance(); public void dangerouslyDisableFuseboxForTest(); public void dangerouslyResetFlags(); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index c02ab2b15443..6bcf2227ae3d 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -12600,6 +12600,7 @@ class facebook::react::jsinspector_modern::HostTargetDelegate : public facebook: public facebook::react::jsinspector_modern::HostTargetDelegate& operator=(facebook::react::jsinspector_modern::HostTargetDelegate&&) = delete; public virtual facebook::react::jsinspector_modern::HostTargetMetadata getMetadata() = 0; public virtual facebook::react::jsinspector_modern::HostTargetTracingDelegate* getTracingDelegate(); + public virtual void captureScreenshot(const facebook::react::jsinspector_modern::HostTargetDelegate::PageCaptureScreenshotRequest&, const std::function base64Data)>& callback); public virtual void loadNetworkResource(const facebook::react::jsinspector_modern::LoadNetworkResourceRequest&, facebook::react::jsinspector_modern::ScopedExecutor) override; public virtual void onReload(const facebook::react::jsinspector_modern::HostTargetDelegate::PageReloadRequest& request) = 0; public virtual void onSetPausedInDebuggerMessage(const facebook::react::jsinspector_modern::HostTargetDelegate::OverlaySetPausedInDebuggerMessageRequest& request) = 0; @@ -12612,6 +12613,11 @@ struct facebook::react::jsinspector_modern::HostTargetDelegate::OverlaySetPaused public std::optional message; } +struct facebook::react::jsinspector_modern::HostTargetDelegate::PageCaptureScreenshotRequest { + public std::optional quality; + public std::optional format; +} + struct facebook::react::jsinspector_modern::HostTargetDelegate::PageReloadRequest { public bool operator==(const facebook::react::jsinspector_modern::HostTargetDelegate::PageReloadRequest& rhs) const; public std::optional ignoreCache; @@ -12697,6 +12703,7 @@ class facebook::react::jsinspector_modern::InspectorFlags { public bool getIsProfilingBuild() const; public bool getNetworkInspectionEnabled() const; public bool getPerfIssuesEnabled() const; + public bool getScreenshotCaptureEnabled() const; public static facebook::react::jsinspector_modern::InspectorFlags& getInstance(); public void dangerouslyDisableFuseboxForTest(); public void dangerouslyResetFlags(); diff --git a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api index bc60935ffa82..9ebe1a5f7445 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -7508,6 +7508,7 @@ class facebook::react::jsinspector_modern::HostTargetDelegate : public facebook: public facebook::react::jsinspector_modern::HostTargetDelegate& operator=(facebook::react::jsinspector_modern::HostTargetDelegate&&) = delete; public virtual facebook::react::jsinspector_modern::HostTargetMetadata getMetadata() = 0; public virtual facebook::react::jsinspector_modern::HostTargetTracingDelegate* getTracingDelegate(); + public virtual void captureScreenshot(const facebook::react::jsinspector_modern::HostTargetDelegate::PageCaptureScreenshotRequest&, const std::function base64Data)>& callback); public virtual void loadNetworkResource(const facebook::react::jsinspector_modern::LoadNetworkResourceRequest&, facebook::react::jsinspector_modern::ScopedExecutor) override; public virtual void onReload(const facebook::react::jsinspector_modern::HostTargetDelegate::PageReloadRequest& request) = 0; public virtual void onSetPausedInDebuggerMessage(const facebook::react::jsinspector_modern::HostTargetDelegate::OverlaySetPausedInDebuggerMessageRequest& request) = 0; @@ -7520,6 +7521,11 @@ struct facebook::react::jsinspector_modern::HostTargetDelegate::OverlaySetPaused public std::optional message; } +struct facebook::react::jsinspector_modern::HostTargetDelegate::PageCaptureScreenshotRequest { + public std::optional quality; + public std::optional format; +} + struct facebook::react::jsinspector_modern::HostTargetDelegate::PageReloadRequest { public bool operator==(const facebook::react::jsinspector_modern::HostTargetDelegate::PageReloadRequest& rhs) const; public std::optional ignoreCache; @@ -7605,6 +7611,7 @@ class facebook::react::jsinspector_modern::InspectorFlags { public bool getIsProfilingBuild() const; public bool getNetworkInspectionEnabled() const; public bool getPerfIssuesEnabled() const; + public bool getScreenshotCaptureEnabled() const; public static facebook::react::jsinspector_modern::InspectorFlags& getInstance(); public void dangerouslyDisableFuseboxForTest(); public void dangerouslyResetFlags(); diff --git a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api index 40f2525cd2b9..6fe7e5d239f0 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -7499,6 +7499,7 @@ class facebook::react::jsinspector_modern::HostTargetDelegate : public facebook: public facebook::react::jsinspector_modern::HostTargetDelegate& operator=(facebook::react::jsinspector_modern::HostTargetDelegate&&) = delete; public virtual facebook::react::jsinspector_modern::HostTargetMetadata getMetadata() = 0; public virtual facebook::react::jsinspector_modern::HostTargetTracingDelegate* getTracingDelegate(); + public virtual void captureScreenshot(const facebook::react::jsinspector_modern::HostTargetDelegate::PageCaptureScreenshotRequest&, const std::function base64Data)>& callback); public virtual void loadNetworkResource(const facebook::react::jsinspector_modern::LoadNetworkResourceRequest&, facebook::react::jsinspector_modern::ScopedExecutor) override; public virtual void onReload(const facebook::react::jsinspector_modern::HostTargetDelegate::PageReloadRequest& request) = 0; public virtual void onSetPausedInDebuggerMessage(const facebook::react::jsinspector_modern::HostTargetDelegate::OverlaySetPausedInDebuggerMessageRequest& request) = 0; @@ -7511,6 +7512,11 @@ struct facebook::react::jsinspector_modern::HostTargetDelegate::OverlaySetPaused public std::optional message; } +struct facebook::react::jsinspector_modern::HostTargetDelegate::PageCaptureScreenshotRequest { + public std::optional quality; + public std::optional format; +} + struct facebook::react::jsinspector_modern::HostTargetDelegate::PageReloadRequest { public bool operator==(const facebook::react::jsinspector_modern::HostTargetDelegate::PageReloadRequest& rhs) const; public std::optional ignoreCache; @@ -7596,6 +7602,7 @@ class facebook::react::jsinspector_modern::InspectorFlags { public bool getIsProfilingBuild() const; public bool getNetworkInspectionEnabled() const; public bool getPerfIssuesEnabled() const; + public bool getScreenshotCaptureEnabled() const; public static facebook::react::jsinspector_modern::InspectorFlags& getInstance(); public void dangerouslyDisableFuseboxForTest(); public void dangerouslyResetFlags();