jiuyiUniapp/service/node_modules/react-native/ReactCommon/jsinspector-modern/tests/JsiIntegrationTest.cpp

858 lines
33 KiB
C++
Raw Normal View History

2025-02-13 09:59:20 +08:00
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include <folly/Format.h>
#include <folly/executors/ManualExecutor.h>
#include <folly/executors/QueuedImmediateExecutor.h>
#include "JsiIntegrationTest.h"
#include "engines/JsiIntegrationTestGenericEngineAdapter.h"
#include "engines/JsiIntegrationTestHermesEngineAdapter.h"
using namespace ::testing;
using folly::sformat;
namespace facebook::react::jsinspector_modern {
////////////////////////////////////////////////////////////////////////////////
// Some tests are specific to Hermes's CDP capabilities and some are not.
// We'll use JsiIntegrationHermesTest as an alias for Hermes-specific tests
// and JsiIntegrationPortableTest for the engine-agnostic ones.
/**
* The list of engine adapters for which engine-agnostic tests should pass.
*/
using AllEngines = Types<
JsiIntegrationTestHermesEngineAdapter,
JsiIntegrationTestGenericEngineAdapter>;
using AllHermesVariants = Types<JsiIntegrationTestHermesEngineAdapter>;
template <typename EngineAdapter>
using JsiIntegrationPortableTest = JsiIntegrationPortableTestBase<
EngineAdapter,
folly::QueuedImmediateExecutor>;
TYPED_TEST_SUITE(JsiIntegrationPortableTest, AllEngines);
template <typename EngineAdapter>
using JsiIntegrationHermesTest = JsiIntegrationPortableTestBase<
EngineAdapter,
folly::QueuedImmediateExecutor>;
/**
* Fixture class for tests that run on a ManualExecutor. Work scheduled
* on the executor is *not* run automatically; it must be manually advanced
* in the body of the test.
*/
template <typename EngineAdapter>
class JsiIntegrationHermesTestAsync : public JsiIntegrationPortableTestBase<
EngineAdapter,
folly::ManualExecutor> {
public:
void TearDown() override {
// Assert there are no pending tasks on the ManualExecutor.
auto tasksCleared = this->executor_.clear();
EXPECT_EQ(tasksCleared, 0)
<< "There were still pending tasks on executor_ at the end of the test. Use advance() or run() as needed.";
JsiIntegrationPortableTestBase<EngineAdapter, folly::ManualExecutor>::
TearDown();
}
};
TYPED_TEST_SUITE(JsiIntegrationHermesTest, AllHermesVariants);
TYPED_TEST_SUITE(JsiIntegrationHermesTestAsync, AllHermesVariants);
#pragma region AllEngines
TYPED_TEST(JsiIntegrationPortableTest, ConnectWithoutCrashing) {
this->connect();
}
TYPED_TEST(JsiIntegrationPortableTest, ErrorOnUnknownMethod) {
this->connect();
this->expectMessageFromPage(
JsonParsed(AllOf(AtJsonPtr("/id", 1), AtJsonPtr("/error/code", -32601))));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Foobar.unknownMethod"
})");
}
TYPED_TEST(JsiIntegrationPortableTest, ExecutionContextNotifications) {
this->connect();
InSequence s;
this->expectMessageFromPage(JsonEq(R"({
"method": "Runtime.executionContextCreated",
"params": {
"context": {
"id": 1,
"origin": "",
"name": "main"
}
}
})"));
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.enable"
})");
this->expectMessageFromPage(JsonEq(R"({
"method": "Runtime.executionContextDestroyed",
"params": {
"executionContextId": 1
}
})"));
this->expectMessageFromPage(JsonEq(R"({
"method": "Runtime.executionContextsCleared"
})"));
this->expectMessageFromPage(JsonEq(R"({
"method": "Runtime.executionContextCreated",
"params": {
"context": {
"id": 2,
"origin": "",
"name": "main"
}
}
})"));
// Simulate a reload triggered by the app (not by the debugger).
this->reload();
this->expectMessageFromPage(JsonEq(R"({
"method": "Runtime.executionContextDestroyed",
"params": {
"executionContextId": 2
}
})"));
this->expectMessageFromPage(JsonEq(R"({
"method": "Runtime.executionContextsCleared"
})"));
this->expectMessageFromPage(JsonEq(R"({
"method": "Runtime.executionContextCreated",
"params": {
"context": {
"id": 3,
"origin": "",
"name": "main"
}
}
})"));
this->expectMessageFromPage(JsonEq(R"({
"id": 2,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Page.reload"
})");
}
TYPED_TEST(JsiIntegrationPortableTest, AddBinding) {
this->connect();
InSequence s;
auto executionContextInfo = this->expectMessageFromPage(JsonParsed(
AllOf(AtJsonPtr("/method", "Runtime.executionContextCreated"))));
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.enable"
})");
ASSERT_TRUE(executionContextInfo->has_value());
auto executionContextId =
executionContextInfo->value()["params"]["context"]["id"];
this->expectMessageFromPage(JsonEq(R"({
"id": 2,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Runtime.addBinding",
"params": {"name": "foo"}
})");
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Runtime.bindingCalled"),
AtJsonPtr("/params/name", "foo"),
AtJsonPtr("/params/payload", "bar"),
AtJsonPtr("/params/executionContextId", executionContextId))));
this->eval("globalThis.foo('bar');");
}
TYPED_TEST(JsiIntegrationPortableTest, AddedBindingSurvivesReload) {
this->connect();
InSequence s;
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.addBinding",
"params": {"name": "foo"}
})");
this->reload();
// Get the new context ID by sending Runtime.enable now.
auto executionContextInfo = this->expectMessageFromPage(JsonParsed(
AllOf(AtJsonPtr("/method", "Runtime.executionContextCreated"))));
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.enable"
})");
ASSERT_TRUE(executionContextInfo->has_value());
auto executionContextId =
executionContextInfo->value()["params"]["context"]["id"];
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Runtime.bindingCalled"),
AtJsonPtr("/params/name", "foo"),
AtJsonPtr("/params/payload", "bar"),
AtJsonPtr("/params/executionContextId", executionContextId))));
this->eval("globalThis.foo('bar');");
}
TYPED_TEST(JsiIntegrationPortableTest, RemovedBindingRemainsInstalled) {
this->connect();
InSequence s;
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.addBinding",
"params": {"name": "foo"}
})");
this->expectMessageFromPage(JsonEq(R"({
"id": 2,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Runtime.removeBinding",
"params": {"name": "foo"}
})");
this->eval("globalThis.foo('bar');");
}
TYPED_TEST(JsiIntegrationPortableTest, RemovedBindingDoesNotSurviveReload) {
this->connect();
InSequence s;
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.addBinding",
"params": {"name": "foo"}
})");
this->expectMessageFromPage(JsonEq(R"({
"id": 2,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Runtime.removeBinding",
"params": {"name": "foo"}
})");
this->reload();
EXPECT_TRUE(this->eval("typeof globalThis.foo === 'undefined'").getBool());
}
TYPED_TEST(JsiIntegrationPortableTest, AddBindingClobbersExistingProperty) {
this->connect();
InSequence s;
this->eval(R"(
globalThis.foo = 'clobbered value';
)");
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.addBinding",
"params": {"name": "foo"}
})");
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Runtime.bindingCalled"),
AtJsonPtr("/params/name", "foo"),
AtJsonPtr("/params/payload", "bar"))));
this->eval("globalThis.foo('bar');");
}
TYPED_TEST(JsiIntegrationPortableTest, ExceptionDuringAddBindingIsIgnored) {
this->connect();
InSequence s;
this->eval(R"(
Object.defineProperty(globalThis, 'foo', {
get: function () { return 42; },
set: function () { throw new Error('nope'); },
});
)");
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.addBinding",
"params": {"name": "foo"}
})");
EXPECT_TRUE(this->eval("globalThis.foo === 42").getBool());
}
TYPED_TEST(JsiIntegrationPortableTest, FuseboxSetClientMetadata) {
this->connect();
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "FuseboxClient.setClientMetadata",
"params": {}
})");
}
TYPED_TEST(JsiIntegrationPortableTest, ReactNativeApplicationEnable) {
this->connect();
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->expectMessageFromPage(JsonEq(R"({
"method": "ReactNativeApplication.metadataUpdated",
"params": {
"integrationName": "JsiIntegrationTest"
}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "ReactNativeApplication.enable",
"params": {}
})");
}
TYPED_TEST(JsiIntegrationPortableTest, ReactNativeApplicationDisable) {
this->connect();
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "ReactNativeApplication.disable",
"params": {}
})");
}
#pragma endregion // AllEngines
#pragma region AllHermesVariants
TYPED_TEST(JsiIntegrationHermesTestAsync, HermesObjectsTableDoesNotMemoryLeak) {
// This is a regression test for T186157855 (CDPAgent leaking JSI data in
// RemoteObjectsTable past the Runtime's lifetime)
this->connect();
this->executor_.run();
InSequence s;
this->expectMessageFromPage(JsonParsed(
AllOf(AtJsonPtr("/method", "Runtime.executionContextCreated"))));
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.enable"
})");
this->executor_.run();
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Runtime.consoleAPICalled"),
AtJsonPtr("/params/args/0/objectId", "1"))));
this->eval(R"(console.log({a: 1});)");
this->executor_.run();
this->expectMessageFromPage(JsonEq(R"({
"method": "Runtime.executionContextDestroyed",
"params": {
"executionContextId": 1
}
})"));
this->expectMessageFromPage(JsonEq(R"({
"method": "Runtime.executionContextsCleared"
})"));
this->expectMessageFromPage(JsonEq(R"({
"method": "Runtime.executionContextCreated",
"params": {
"context": {
"id": 2,
"origin": "",
"name": "main"
}
}
})"));
// NOTE: Doesn't crash when Hermes checks for JSI value leaks
this->reload();
this->executor_.run();
}
TYPED_TEST(JsiIntegrationHermesTest, EvaluateExpression) {
this->connect();
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {
"result": {
"type": "number",
"value": 42
}
}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.evaluate",
"params": {"expression": "42"}
})");
}
TYPED_TEST(JsiIntegrationHermesTest, EvaluateExpressionInExecutionContext) {
this->connect();
InSequence s;
auto executionContextInfo = this->expectMessageFromPage(JsonParsed(
AllOf(AtJsonPtr("/method", "Runtime.executionContextCreated"))));
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.enable"
})");
ASSERT_TRUE(executionContextInfo->has_value());
auto executionContextId =
executionContextInfo->value()["params"]["context"]["id"].getInt();
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {
"result": {
"type": "number",
"value": 42
}
}
})"));
this->toPage_->sendMessage(sformat(
R"({{
"id": 1,
"method": "Runtime.evaluate",
"params": {{"expression": "42", "contextId": {0}}}
}})",
std::to_string(executionContextId)));
// Silence notifications about execution contexts.
this->expectMessageFromPage(JsonEq(R"({
"id": 2,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Runtime.disable"
})");
this->reload();
// Now the old execution context is stale.
this->expectMessageFromPage(
JsonParsed(AllOf(AtJsonPtr("/id", 3), AtJsonPtr("/error/code", -32600))));
this->toPage_->sendMessage(sformat(
R"({{
"id": 3,
"method": "Runtime.evaluate",
"params": {{"expression": "10000", "contextId": {0}}}
}})",
std::to_string(executionContextId)));
}
TYPED_TEST(JsiIntegrationHermesTest, ResolveBreakpointAfterEval) {
this->connect();
InSequence s;
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Debugger.enable"
})");
auto scriptInfo = this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Debugger.scriptParsed"),
AtJsonPtr("/params/url", "breakpointTest.js"))));
this->eval(R"( // line 0
globalThis.foo = function() { // line 1
Date.now(); // line 2
};
//# sourceURL=breakpointTest.js
)");
ASSERT_TRUE(scriptInfo->has_value());
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/id", 2),
AtJsonPtr("/result/locations/0/lineNumber", 2),
AtJsonPtr(
"/result/locations/0/scriptId",
scriptInfo->value()["params"]["scriptId"]))));
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Debugger.setBreakpointByUrl",
"params": {"lineNumber": 2, "url": "breakpointTest.js"}
})");
}
TYPED_TEST(JsiIntegrationHermesTest, ResolveBreakpointAfterReload) {
this->connect();
InSequence s;
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Debugger.enable"
})");
this->expectMessageFromPage(JsonParsed(AtJsonPtr("/id", 2)));
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Debugger.setBreakpointByUrl",
"params": {"lineNumber": 2, "url": "breakpointTest.js"}
})");
this->reload();
auto scriptInfo = this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Debugger.scriptParsed"),
AtJsonPtr("/params/url", "breakpointTest.js"))));
auto breakpointInfo = this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Debugger.breakpointResolved"),
AtJsonPtr("/params/location/lineNumber", 2))));
this->eval(R"( // line 0
globalThis.foo = function() { // line 1
Date.now(); // line 2
};
//# sourceURL=breakpointTest.js
)");
ASSERT_TRUE(breakpointInfo->has_value());
ASSERT_TRUE(scriptInfo->has_value());
EXPECT_EQ(
breakpointInfo->value()["params"]["location"]["scriptId"],
scriptInfo->value()["params"]["scriptId"]);
}
TYPED_TEST(JsiIntegrationHermesTest, CDPAgentReentrancyRegressionTest) {
this->connect();
InSequence s;
this->inspectorExecutor_([&]() {
// Tasks scheduled on our executor here will be executed when this lambda
// returns. This is integral to the bug we're trying to reproduce, so we
// place the EXPECT_* calls at the end of the lambda body to ensure the
// test fails if we get eager (unexpected) responses.
// 1. Cause CDPAgent to schedule a task to process the message. Originally,
// the task would be simultaneously scheduled on the JS executor, and as
// an interrupt on the JS interpreter. It's called via the executor
// regardless, since the interpreter is idle at the moment.
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.evaluate",
"params": {"expression": "Math.random(); /* Interrupts processed here. */ globalThis.x = 1 + 2"}
})");
// 2. Cause CDPAgent to schedule another task. If scheduled as an interrupt,
// this task will run _during_ the first task.
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Runtime.evaluate",
"params": {"expression": "globalThis.x = 3 + 4"}
})");
// This setup used to trigger three distinct bugs in CDPAgent:
// - The first task would be triggered twice due to a race condition
// between the executor and the interrupt handler. (D54771697)
// - The second task would deadlock due to the first task holding a lock
// preventing any other CDPAgent tasks from running. (D54838179)
// - The second task would complete first, returning `evaluate`
// responses out of order and (crucially) performing any JS side
// effects out of order. (D55250610)
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {
"result": {
"type": "number",
"value": 3
}
}
})"));
this->expectMessageFromPage(JsonEq(R"({
"id": 2,
"result": {
"result": {
"type": "number",
"value": 7
}
}
})"));
});
// Make sure the second task ran last.
EXPECT_EQ(this->eval("globalThis.x").getNumber(), 7);
}
TYPED_TEST(JsiIntegrationHermesTest, ScriptParsedExactlyOnce) {
// Regression test for T182003727 (multiple scriptParsed events for a single
// script under Hermes lazy compilation).
this->connect();
InSequence s;
this->eval(R"(
// NOTE: Triggers lazy compilation in Hermes when running with
// CompilationMode::ForceLazyCompilation.
(function foo(){var x = 2;})()
//# sourceURL=script.js
)");
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Debugger.scriptParsed"),
AtJsonPtr("/params/url", "script.js"))));
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Debugger.enable"
})");
}
TYPED_TEST(JsiIntegrationHermesTest, FunctionDescriptionIncludesName) {
// See
// https://github.com/facebookexperimental/rn-chrome-devtools-frontend/blob/9a23d4c7c4c2d1a3d9e913af38d6965f474c4284/front_end/ui/legacy/components/object_ui/ObjectPropertiesSection.ts#L311-L391
this->connect();
InSequence s;
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/id", 1),
AtJsonPtr("/result/result/type", "function"),
AtJsonPtr(
"/result/result/description",
DynamicString(StartsWith("function foo() {"))))));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.evaluate",
"params": {"expression": "(function foo() {Math.random()});"}
})");
}
TYPED_TEST(JsiIntegrationHermesTest, ReleaseRemoteObject) {
this->connect();
InSequence s;
// Create a remote object.
auto objectInfo = this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/id", 1),
AtJsonPtr("/result/result/type", "object"),
AtJsonPtr("/result/result/objectId", Not(IsEmpty())))));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.evaluate",
"params": {"expression": "[]"}
})");
ASSERT_TRUE(objectInfo->has_value());
auto objectId = objectInfo->value()["result"]["result"]["objectId"];
// Ensure we can get the properties of the object.
this->expectMessageFromPage(JsonParsed(
AllOf(AtJsonPtr("/id", 2), AtJsonPtr("/result/result", SizeIs(Gt(0))))));
this->toPage_->sendMessage(sformat(
R"({{
"id": 2,
"method": "Runtime.getProperties",
"params": {{"objectId": {}, "ownProperties": true}}
}})",
folly::toJson(objectId)));
// Release the object.
this->expectMessageFromPage(JsonEq(R"({
"id": 3,
"result": {}
})"));
this->toPage_->sendMessage(sformat(
R"({{
"id": 3,
"method": "Runtime.releaseObject",
"params": {{"objectId": {}, "ownProperties": true}}
}})",
folly::toJson(objectId)));
// Getting properties for a released object results in an error.
this->expectMessageFromPage(
JsonParsed(AllOf(AtJsonPtr("/id", 4), AtJsonPtr("/error/code", -32000))));
this->toPage_->sendMessage(sformat(
R"({{
"id": 4,
"method": "Runtime.getProperties",
"params": {{"objectId": {}, "ownProperties": true}}
}})",
folly::toJson(objectId)));
// Releasing an already released object is an error.
this->expectMessageFromPage(
JsonParsed(AllOf(AtJsonPtr("/id", 5), AtJsonPtr("/error/code", -32000))));
this->toPage_->sendMessage(sformat(
R"({{
"id": 5,
"method": "Runtime.releaseObject",
"params": {{"objectId": {}, "ownProperties": true}}
}})",
folly::toJson(objectId)));
}
TYPED_TEST(JsiIntegrationHermesTest, ReleaseRemoteObjectGroup) {
this->connect();
InSequence s;
// Create a remote object.
auto objectInfo = this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/id", 1),
AtJsonPtr("/result/result/type", "object"),
AtJsonPtr("/result/result/objectId", Not(IsEmpty())))));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.evaluate",
"params": {"expression": "[]", "objectGroup": "foo"}
})");
ASSERT_TRUE(objectInfo->has_value());
auto objectId = objectInfo->value()["result"]["result"]["objectId"];
// Ensure we can get the properties of the object.
this->expectMessageFromPage(JsonParsed(
AllOf(AtJsonPtr("/id", 2), AtJsonPtr("/result/result", SizeIs(Gt(0))))));
this->toPage_->sendMessage(sformat(
R"({{
"id": 2,
"method": "Runtime.getProperties",
"params": {{"objectId": {}, "ownProperties": true}}
}})",
folly::toJson(objectId)));
// Release the object group containing our object.
this->expectMessageFromPage(JsonEq(R"({
"id": 3,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 3,
"method": "Runtime.releaseObjectGroup",
"params": {"objectGroup": "foo"}
})");
// Getting properties for a released object results in an error.
this->expectMessageFromPage(
JsonParsed(AllOf(AtJsonPtr("/id", 4), AtJsonPtr("/error/code", -32000))));
this->toPage_->sendMessage(sformat(
R"({{
"id": 4,
"method": "Runtime.getProperties",
"params": {{"objectId": {}, "ownProperties": true}}
}})",
folly::toJson(objectId)));
// Releasing an already released object group is a no-op.
this->expectMessageFromPage(JsonEq(R"({
"id": 5,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 5,
"method": "Runtime.releaseObjectGroup",
"params": {"objectGroup": "foo"}
})");
}
#pragma endregion // AllHermesVariants
} // namespace facebook::react::jsinspector_modern