feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,50 @@
/*
* 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/dynamic.h>
#include <folly/json.h>
#include <folly/json_pointer.h>
#include <gmock/gmock.h>
#include "FollyDynamicMatchers.h"
namespace facebook::folly_dynamic_matchers_utils {
std::string as_string(std::string value) {
return value;
}
std::string as_string(folly::dynamic value) {
return value.asString();
}
std::string explain_error(
folly::dynamic::json_pointer_resolution_error<folly::dynamic const> error) {
using err_code = folly::dynamic::json_pointer_resolution_error_code;
switch (error.error_code) {
case err_code::key_not_found:
return "key not found";
case err_code::index_out_of_bounds:
return "index out of bounds";
case err_code::append_requested:
return "append requested";
case err_code::index_not_numeric:
return "array index is not numeric";
case err_code::index_has_leading_zero:
return "leading zero not allowed when indexing arrays";
case err_code::element_not_object_or_array:
return "element is neither an object nor an array";
case err_code::json_pointer_out_of_bounds:
return "JSON pointer out of bounds";
case err_code::other:
return "unknown error";
default:
assert(false && "unhandled error code");
return "<unhandled error code>";
}
}
} // namespace facebook::folly_dynamic_matchers_utils

View File

@@ -0,0 +1,80 @@
/*
* 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.
*/
#pragma once
#include <folly/dynamic.h>
#include <folly/json.h>
#include <folly/json_pointer.h>
#include <gmock/gmock.h>
namespace facebook {
namespace folly_dynamic_matchers_utils {
std::string as_string(std::string value);
std::string as_string(folly::dynamic value);
std::string explain_error(
folly::dynamic::json_pointer_resolution_error<folly::dynamic const> error);
} // namespace folly_dynamic_matchers_utils
// GTest / GMock compatible matchers for `folly::dynamic` values.
/**
* Parses a JSON string into a folly::dynamic, then matches it against the
* given matcher.
*/
MATCHER_P(
JsonParsed,
innerMatcher,
std::string{"parsed as JSON "} +
testing::DescribeMatcher<folly::dynamic>(innerMatcher, negation)) {
using namespace ::testing;
using namespace folly_dynamic_matchers_utils;
const auto& json = arg;
folly::dynamic parsed = folly::parseJson(as_string(json));
return ExplainMatchResult(innerMatcher, parsed, result_listener);
}
/**
* Given a folly::dynamic argument, asserts that it is deeply equal to the
* result of parsing the given JSON string.
*/
MATCHER_P(
JsonEq,
expected,
std::string{"deeply equals "} +
folly::toPrettyJson(folly::parseJson(expected))) {
using namespace ::testing;
return ExplainMatchResult(
JsonParsed(Eq(folly::parseJson(expected))), arg, result_listener);
}
/**
* A higher-order matcher that applies an inner matcher to the value at a
* particular JSON Pointer location within a folly::dynamic.
*/
MATCHER_P2(
AtJsonPtr,
jsonPointer,
innerMatcher,
std::string{"value at "} + jsonPointer + " " +
testing::DescribeMatcher<folly::dynamic>(innerMatcher, negation)) {
using namespace ::testing;
using namespace folly_dynamic_matchers_utils;
auto resolved_ptr = arg.try_get_ptr(folly::json_pointer::parse(jsonPointer));
if (resolved_ptr.hasValue()) {
return ExplainMatchResult(
innerMatcher, *resolved_ptr.value().value, result_listener);
}
*result_listener << "has no value at " << jsonPointer << " because of error: "
<< explain_error(resolved_ptr.error());
return false;
}
} // namespace facebook

View File

@@ -0,0 +1,163 @@
/*
* 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.
*/
#pragma once
#include <folly/executors/ScheduledExecutor.h>
#include <gmock/gmock.h>
#include <jsinspector-modern/InspectorInterfaces.h>
#include <jsinspector-modern/InspectorPackagerConnection.h>
#include <jsinspector-modern/ReactCdp.h>
#include <chrono>
#include <functional>
#include <memory>
#include <string>
// Configurable mocks of various interfaces required by the inspector API.
namespace facebook::react::jsinspector_modern {
class MockWebSocket : public IWebSocket {
public:
MockWebSocket(
const std::string& url,
std::weak_ptr<IWebSocketDelegate> delegate)
: url{url}, delegate{delegate} {
EXPECT_TRUE(this->delegate.lock())
<< "Delegate should exist when provided to createWebSocket";
}
const std::string url;
std::weak_ptr<IWebSocketDelegate> delegate;
/**
* Convenience method to access the delegate from tests.
* \pre The delegate has not been destroyed.
*/
IWebSocketDelegate& getDelegate() {
auto delegateStrong = this->delegate.lock();
EXPECT_TRUE(delegateStrong);
return *delegateStrong;
}
// IWebSocket methods
MOCK_METHOD(void, send, (std::string_view message), (override));
};
class MockRemoteConnection : public IRemoteConnection {
public:
MockRemoteConnection() = default;
// IRemoteConnection methods
MOCK_METHOD(void, onMessage, (std::string message), (override));
MOCK_METHOD(void, onDisconnect, (), (override));
};
class MockLocalConnection : public ILocalConnection {
public:
explicit MockLocalConnection(
std::unique_ptr<IRemoteConnection> remoteConnection)
: remoteConnection_{std::move(remoteConnection)} {}
IRemoteConnection& getRemoteConnection() {
return *remoteConnection_;
}
std::unique_ptr<IRemoteConnection> dangerouslyReleaseRemoteConnection() {
return std::move(remoteConnection_);
}
// ILocalConnection methods
MOCK_METHOD(void, sendMessage, (std::string message), (override));
MOCK_METHOD(void, disconnect, (), (override));
private:
std::unique_ptr<IRemoteConnection> remoteConnection_;
};
class MockInspectorPackagerConnectionDelegate
: public InspectorPackagerConnectionDelegate {
public:
explicit MockInspectorPackagerConnectionDelegate(folly::Executor& executor)
: executor_(executor) {
using namespace testing;
ON_CALL(*this, scheduleCallback(_, _))
.WillByDefault(Invoke<>([this](auto callback, auto delay) {
if (auto scheduledExecutor =
dynamic_cast<folly::ScheduledExecutor*>(&executor_)) {
scheduledExecutor->scheduleAt(
callback, scheduledExecutor->now() + delay);
} else {
executor_.add(callback);
}
}));
}
// InspectorPackagerConnectionDelegate methods
MOCK_METHOD(
std::unique_ptr<IWebSocket>,
connectWebSocket,
(const std::string& url, std::weak_ptr<IWebSocketDelegate> delegate),
(override));
MOCK_METHOD(
void,
scheduleCallback,
(std::function<void(void)> callback, std::chrono::milliseconds delayMs),
(override));
private:
folly::Executor& executor_;
};
class MockPageTargetDelegate : public PageTargetDelegate {
public:
// PageTargetDelegate methods
MOCK_METHOD(void, onReload, (const PageReloadRequest& request), (override));
};
class MockInstanceTargetDelegate : public InstanceTargetDelegate {};
class MockRuntimeTargetDelegate : public RuntimeTargetDelegate {
public:
// RuntimeTargetDelegate methods
MOCK_METHOD(
std::unique_ptr<RuntimeAgentDelegate>,
createAgentDelegate,
(FrontendChannel channel,
SessionState& sessionState,
std::unique_ptr<RuntimeAgentDelegate::ExportedState>
previouslyExportedState,
const ExecutionContextDescription&),
(override));
};
class MockRuntimeAgentDelegate : public RuntimeAgentDelegate {
public:
inline MockRuntimeAgentDelegate(
FrontendChannel frontendChannel,
SessionState& sessionState,
std::unique_ptr<RuntimeAgentDelegate::ExportedState>,
const ExecutionContextDescription& executionContextDescription)
: frontendChannel(std::move(frontendChannel)),
sessionState(sessionState),
executionContextDescription(executionContextDescription) {}
// RuntimeAgentDelegate methods
MOCK_METHOD(
bool,
handleRequest,
(const cdp::PreparsedRequest& req),
(override));
const FrontendChannel frontendChannel;
SessionState& sessionState;
const ExecutionContextDescription executionContextDescription;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,576 @@
/*
* 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/dynamic.h>
#include <folly/executors/QueuedImmediateExecutor.h>
#include <folly/json.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <jsinspector-modern/InspectorInterfaces.h>
#include <jsinspector-modern/PageTarget.h>
#include <memory>
#include "FollyDynamicMatchers.h"
#include "InspectorMocks.h"
#include "UniquePtrFactory.h"
#include "engines/JsiIntegrationTestGenericEngineAdapter.h"
#include "engines/JsiIntegrationTestHermesEngineAdapter.h"
using namespace ::testing;
using folly::sformat;
namespace facebook::react::jsinspector_modern {
namespace {
/**
* A text fixture class for the integration between the modern RN CDP backend
* and a JSI engine, mocking out the rest of RN. For simplicity, everything is
* single-threaded and "async" work is actually done through a queued immediate
* executor ( = run immediately and finish all queued sub-tasks before
* returning).
*
* The main limitation of the simpler threading model is that we can't cover
* breakpoints etc - since pausing during JS execution would prevent the test
* from making progress. Such functionality is better suited for a full RN+CDP
* integration test (using RN's own thread management) as well as for each
* engine's unit tests.
*
* \tparam EngineAdapter An adapter class that implements RuntimeTargetDelegate
* for a particular engine, plus exposes access to a RuntimeExecutor (based on
* the provided folly::Executor) and the corresponding jsi::Runtime.
*/
template <typename EngineAdapter>
class JsiIntegrationPortableTest : public Test, private PageTargetDelegate {
folly::QueuedImmediateExecutor immediateExecutor_;
protected:
JsiIntegrationPortableTest() : engineAdapter_{immediateExecutor_} {
instance_ = &page_->registerInstance(instanceTargetDelegate_);
runtimeTarget_ = &instance_->registerRuntime(
*engineAdapter_, engineAdapter_->getRuntimeExecutor());
}
~JsiIntegrationPortableTest() override {
toPage_.reset();
if (runtimeTarget_) {
EXPECT_TRUE(instance_);
instance_->unregisterRuntime(*runtimeTarget_);
runtimeTarget_ = nullptr;
}
if (instance_) {
page_->unregisterInstance(*instance_);
instance_ = nullptr;
}
}
void connect() {
ASSERT_FALSE(toPage_) << "Can only connect once in a JSI integration test.";
toPage_ = page_->connect(
remoteConnections_.make_unique(),
{.integrationName = "JsiIntegrationTest"});
// We'll always get an onDisconnect call when we tear
// down the test. Expect it in order to satisfy the strict mock.
EXPECT_CALL(*remoteConnections_[0], onDisconnect());
}
void reload() {
if (runtimeTarget_) {
ASSERT_TRUE(instance_);
instance_->unregisterRuntime(*runtimeTarget_);
runtimeTarget_ = nullptr;
}
if (instance_) {
page_->unregisterInstance(*instance_);
instance_ = nullptr;
}
// Recreate the engine (e.g. to wipe any state in the inner jsi::Runtime)
engineAdapter_.emplace(immediateExecutor_);
instance_ = &page_->registerInstance(instanceTargetDelegate_);
runtimeTarget_ = &instance_->registerRuntime(
*engineAdapter_, engineAdapter_->getRuntimeExecutor());
}
MockRemoteConnection& fromPage() {
assert(toPage_);
return *remoteConnections_[0];
}
VoidExecutor inspectorExecutor_ = [this](auto callback) {
immediateExecutor_.add(callback);
};
jsi::Value eval(std::string_view code) {
return engineAdapter_->getRuntime().evaluateJavaScript(
std::make_shared<jsi::StringBuffer>(std::string(code)), "<eval>");
}
/**
* Expect a message matching the provided gmock \c matcher and return a holder
* that will eventually contain the parsed JSON payload.
*/
template <typename Matcher>
std::shared_ptr<const std::optional<folly::dynamic>> expectMessageFromPage(
Matcher&& matcher) {
std::shared_ptr result =
std::make_shared<std::optional<folly::dynamic>>(std::nullopt);
EXPECT_CALL(fromPage(), onMessage(matcher))
.WillOnce(
([result](auto message) { *result = folly::parseJson(message); }))
.RetiresOnSaturation();
return result;
}
std::shared_ptr<PageTarget> page_ =
PageTarget::create(*this, inspectorExecutor_);
InstanceTarget* instance_{};
RuntimeTarget* runtimeTarget_{};
MockInstanceTargetDelegate instanceTargetDelegate_;
std::optional<EngineAdapter> engineAdapter_;
private:
UniquePtrFactory<StrictMock<MockRemoteConnection>> remoteConnections_;
protected:
// NOTE: Needs to be destroyed before page_.
std::unique_ptr<ILocalConnection> toPage_;
private:
// PageTargetDelegate methods
void onReload(const PageReloadRequest& request) override {
(void)request;
reload();
}
};
} // namespace
////////////////////////////////////////////////////////////////////////////////
// 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>;
TYPED_TEST_SUITE(JsiIntegrationPortableTest, AllEngines);
template <typename EngineAdapter>
using JsiIntegrationHermesTest = JsiIntegrationPortableTest<EngineAdapter>;
TYPED_TEST_SUITE(JsiIntegrationHermesTest, AllHermesVariants);
////////////////////////////////////////////////////////////////////////////////
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"
})");
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"
})");
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(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"
})");
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", -32000))));
this->toPage_->sendMessage(sformat(
R"({{
"id": 3,
"method": "Runtime.evaluate",
"params": {{"expression": "10000", "contextId": {0}}}
}})",
std::to_string(executionContextId)));
}
TYPED_TEST(JsiIntegrationHermesTest, ResolveBreakpointAfterReload) {
this->connect();
InSequence s;
this->expectMessageFromPage(JsonParsed(AtJsonPtr("/id", 1)));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Debugger.setBreakpointByUrl",
"params": {"lineNumber": 2, "url": "breakpointTest.js"}
})");
this->reload();
this->expectMessageFromPage(JsonEq(R"({
"id": 2,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Debugger.enable"
})");
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
)");
EXPECT_EQ(
breakpointInfo->value()["params"]["location"]["scriptId"],
scriptInfo->value()["params"]["scriptId"]);
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,517 @@
/*
* 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/dynamic.h>
#include <folly/executors/QueuedImmediateExecutor.h>
#include <folly/json.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <jsinspector-modern/InspectorInterfaces.h>
#include <jsinspector-modern/PageTarget.h>
#include <memory>
#include "FollyDynamicMatchers.h"
#include "InspectorMocks.h"
#include "UniquePtrFactory.h"
using namespace ::testing;
namespace facebook::react::jsinspector_modern {
namespace {
class PageTargetTest : public Test {
folly::QueuedImmediateExecutor immediateExecutor_;
protected:
PageTargetTest() {
EXPECT_CALL(runtimeTargetDelegate_, createAgentDelegate(_, _, _, _))
.WillRepeatedly(runtimeAgentDelegates_.lazily_make_unique<
FrontendChannel,
SessionState&,
std::unique_ptr<RuntimeAgentDelegate::ExportedState>,
const ExecutionContextDescription&>());
}
void connect() {
ASSERT_FALSE(toPage_) << "Can only connect once in a PageTargetTest.";
toPage_ = page_->connect(
remoteConnections_.make_unique(),
{.integrationName = "PageTargetTest"});
// We'll always get an onDisconnect call when we tear
// down the test. Expect it in order to satisfy the strict mock.
EXPECT_CALL(*remoteConnections_[0], onDisconnect());
}
MockPageTargetDelegate pageTargetDelegate_;
MockRemoteConnection& fromPage() {
assert(toPage_);
return *remoteConnections_[0];
}
VoidExecutor inspectorExecutor_ = [this](auto callback) {
immediateExecutor_.add(callback);
};
std::shared_ptr<PageTarget> page_ =
PageTarget::create(pageTargetDelegate_, inspectorExecutor_);
MockInstanceTargetDelegate instanceTargetDelegate_;
MockRuntimeTargetDelegate runtimeTargetDelegate_;
// We don't have access to a jsi::Runtime in these tests, so just use an
// executor that never runs the scheduled callbacks.
RuntimeExecutor runtimeExecutor_ = [](auto) {};
UniquePtrFactory<StrictMock<MockRuntimeAgentDelegate>> runtimeAgentDelegates_;
private:
UniquePtrFactory<StrictMock<MockRemoteConnection>> remoteConnections_;
protected:
// NOTE: Needs to be destroyed before page_.
std::unique_ptr<ILocalConnection> toPage_;
};
/**
* Simplified test harness focused on sending messages to and from a PageTarget.
*/
class PageTargetProtocolTest : public PageTargetTest {
public:
PageTargetProtocolTest() {
connect();
}
private:
// Protocol tests shouldn't manually call connect()
using PageTargetTest::connect;
};
} // namespace
TEST_F(PageTargetProtocolTest, UnrecognizedMethod) {
EXPECT_CALL(
fromPage(),
onMessage(JsonParsed(AllOf(
AtJsonPtr("/error/code", Eq(-32601)), AtJsonPtr("/id", Eq(1))))))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": "SomeUnrecognizedMethod",
"params": [1, 2]
})");
}
TEST_F(PageTargetProtocolTest, TypeErrorInMethodName) {
EXPECT_CALL(
fromPage(),
onMessage(JsonParsed(AllOf(
AtJsonPtr("/error/code", Eq(-32600)),
AtJsonPtr("/id", Eq(nullptr))))))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": 42,
"params": [1, 2]
})");
}
TEST_F(PageTargetProtocolTest, MissingId) {
EXPECT_CALL(
fromPage(),
onMessage(JsonParsed(AllOf(
AtJsonPtr("/error/code", Eq(-32600)),
AtJsonPtr("/id", Eq(nullptr))))))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"method": "SomeUnrecognizedMethod",
"params": [1, 2]
})");
}
TEST_F(PageTargetProtocolTest, MalformedJson) {
EXPECT_CALL(
fromPage(),
onMessage(JsonParsed(AllOf(
AtJsonPtr("/error/code", Eq(-32700)),
AtJsonPtr("/id", Eq(nullptr))))))
.RetiresOnSaturation();
toPage_->sendMessage("{");
}
TEST_F(PageTargetProtocolTest, InjectLogsToIdentifyBackend) {
InSequence s;
EXPECT_CALL(
fromPage(),
onMessage(JsonParsed(AllOf(
AtJsonPtr("/method", "Log.entryAdded"),
AtJsonPtr("/params/entry", Not(IsEmpty()))))))
.Times(2)
.RetiresOnSaturation();
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 1,
"result": {}
})")))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": "Log.enable"
})");
}
TEST_F(PageTargetProtocolTest, PageReloadMethod) {
InSequence s;
EXPECT_CALL(
pageTargetDelegate_,
onReload(Eq(PageTargetDelegate::PageReloadRequest{
.ignoreCache = std::nullopt,
.scriptToEvaluateOnLoad = std::nullopt})))
.RetiresOnSaturation();
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 1,
"result": {}
})")))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": "Page.reload"
})");
EXPECT_CALL(
pageTargetDelegate_,
onReload(Eq(PageTargetDelegate::PageReloadRequest{
.ignoreCache = true, .scriptToEvaluateOnLoad = "alert('hello');"})))
.RetiresOnSaturation();
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 2,
"result": {}
})")))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 2,
"method": "Page.reload",
"params": {
"ignoreCache": true,
"scriptToEvaluateOnLoad": "alert('hello');"
}
})");
}
TEST_F(PageTargetProtocolTest, RegisterUnregisterInstanceWithoutEvents) {
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
page_->unregisterInstance(instanceTarget);
}
TEST_F(PageTargetTest, ConnectToAlreadyRegisteredInstanceWithoutEvents) {
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
connect();
page_->unregisterInstance(instanceTarget);
}
TEST_F(PageTargetProtocolTest, RegisterUnregisterInstanceWithEvents) {
InSequence s;
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 1,
"result": {}
})")));
toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.enable"
})");
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"method": "Runtime.executionContextsCleared"
})")));
page_->unregisterInstance(instanceTarget);
}
TEST_F(PageTargetTest, ConnectToAlreadyRegisteredInstanceWithEvents) {
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
connect();
InSequence s;
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 1,
"result": {}
})")));
toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.enable"
})");
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"method": "Runtime.executionContextsCleared"
})")));
page_->unregisterInstance(instanceTarget);
}
TEST_F(PageTargetTest, ConnectToAlreadyRegisteredRuntimeWithEvents) {
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
auto& runtimeTarget =
instanceTarget.registerRuntime(runtimeTargetDelegate_, runtimeExecutor_);
connect();
InSequence s;
ASSERT_TRUE(runtimeAgentDelegates_[0]);
EXPECT_CALL(*runtimeAgentDelegates_[0], handleRequest(_))
.WillOnce(Return(true))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": "CustomRuntimeDomain.Foo",
"params": {
"expression": "42"
}
})");
static constexpr auto kFooResponse = R"({
"id": 1,
"result": {
"fooValue": 42
}
})";
EXPECT_CALL(fromPage(), onMessage(JsonEq(kFooResponse)))
.RetiresOnSaturation();
runtimeAgentDelegates_[0]->frontendChannel(kFooResponse);
instanceTarget.unregisterRuntime(runtimeTarget);
page_->unregisterInstance(instanceTarget);
}
TEST_F(PageTargetProtocolTest, RuntimeAgentDelegateLifecycle) {
{
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
auto& runtimeTarget = instanceTarget.registerRuntime(
runtimeTargetDelegate_, runtimeExecutor_);
EXPECT_TRUE(runtimeAgentDelegates_[0]);
instanceTarget.unregisterRuntime(runtimeTarget);
page_->unregisterInstance(instanceTarget);
}
EXPECT_FALSE(runtimeAgentDelegates_[0]);
{
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
auto& runtimeTarget = instanceTarget.registerRuntime(
runtimeTargetDelegate_, runtimeExecutor_);
EXPECT_TRUE(runtimeAgentDelegates_[1]);
instanceTarget.unregisterRuntime(runtimeTarget);
page_->unregisterInstance(instanceTarget);
}
EXPECT_FALSE(runtimeAgentDelegates_[1]);
}
TEST_F(PageTargetProtocolTest, MethodNotHandledByRuntimeAgentDelegate) {
InSequence s;
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
auto& runtimeTarget =
instanceTarget.registerRuntime(runtimeTargetDelegate_, runtimeExecutor_);
ASSERT_TRUE(runtimeAgentDelegates_[0]);
EXPECT_CALL(*runtimeAgentDelegates_[0], handleRequest(_))
.WillOnce(Return(false))
.RetiresOnSaturation();
EXPECT_CALL(
fromPage(), onMessage(JsonParsed(AtJsonPtr("/error/code", Eq(-32601)))))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": "CustomRuntimeDomain.Foo",
"params": {
"expression": "42"
}
})");
instanceTarget.unregisterRuntime(runtimeTarget);
page_->unregisterInstance(instanceTarget);
}
TEST_F(PageTargetProtocolTest, MethodHandledByRuntimeAgentDelegate) {
InSequence s;
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
auto& runtimeTarget =
instanceTarget.registerRuntime(runtimeTargetDelegate_, runtimeExecutor_);
ASSERT_TRUE(runtimeAgentDelegates_[0]);
EXPECT_CALL(*runtimeAgentDelegates_[0], handleRequest(_))
.WillOnce(Return(true))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": "CustomRuntimeDomain.Foo",
"params": {
"expression": "42"
}
})");
static constexpr auto kFooResponse = R"({
"id": 1,
"result": {
"fooValue": 42
}
})";
EXPECT_CALL(fromPage(), onMessage(JsonEq(kFooResponse)))
.RetiresOnSaturation();
runtimeAgentDelegates_[0]->frontendChannel(kFooResponse);
instanceTarget.unregisterRuntime(runtimeTarget);
page_->unregisterInstance(instanceTarget);
}
TEST_F(PageTargetProtocolTest, MessageRoutingWhileNoRuntimeAgentDelegate) {
InSequence s;
EXPECT_CALL(
fromPage(), onMessage(JsonParsed(AtJsonPtr("/error/code", Eq(-32601)))))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": "CustomRuntimeDomain.Foo",
"params": {
"expression": "42"
}
})");
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
auto& runtimeTarget =
instanceTarget.registerRuntime(runtimeTargetDelegate_, runtimeExecutor_);
ASSERT_TRUE(runtimeAgentDelegates_[0]);
EXPECT_CALL(*runtimeAgentDelegates_[0], handleRequest(_))
.WillOnce(Return(true))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 2,
"method": "CustomRuntimeDomain.Foo",
"params": {
"expression": "42"
}
})");
static constexpr auto kFooResponse = R"({
"id": 2,
"result": {
"fooValue": 42
}
})";
EXPECT_CALL(fromPage(), onMessage(JsonEq(kFooResponse)))
.RetiresOnSaturation();
runtimeAgentDelegates_[0]->frontendChannel(kFooResponse);
instanceTarget.unregisterRuntime(runtimeTarget);
page_->unregisterInstance(instanceTarget);
EXPECT_FALSE(runtimeAgentDelegates_[0]);
EXPECT_CALL(
fromPage(), onMessage(JsonParsed(AtJsonPtr("/error/code", Eq(-32601)))))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 3,
"method": "CustomRuntimeDomain.Foo",
"params": {
"expression": "42"
}
})");
}
TEST_F(PageTargetProtocolTest, InstanceWithNullRuntimeAgentDelegate) {
InSequence s;
EXPECT_CALL(runtimeTargetDelegate_, createAgentDelegate(_, _, _, _))
.WillRepeatedly(ReturnNull());
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
auto& runtimeTarget =
instanceTarget.registerRuntime(runtimeTargetDelegate_, runtimeExecutor_);
EXPECT_FALSE(runtimeAgentDelegates_[0]);
EXPECT_CALL(
fromPage(), onMessage(JsonParsed(AtJsonPtr("/error/code", Eq(-32601)))))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": "CustomRuntimeDomain.Foo",
"params": {
"expression": "42"
}
})");
instanceTarget.unregisterRuntime(runtimeTarget);
page_->unregisterInstance(instanceTarget);
}
TEST_F(PageTargetProtocolTest, RuntimeAgentDelegateHasAccessToSessionState) {
InSequence s;
// Send Runtime.enable before registering the Instance (which in turns creates
// the RuntimeAgentDelegate).
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 1,
"result": {}
})")))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.enable"
})");
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
EXPECT_CALL(
fromPage(),
onMessage(
JsonParsed(AtJsonPtr("/method", "Runtime.executionContextCreated"))))
.RetiresOnSaturation();
instanceTarget.registerRuntime(runtimeTargetDelegate_, runtimeExecutor_);
ASSERT_TRUE(runtimeAgentDelegates_[0]);
EXPECT_TRUE(runtimeAgentDelegates_[0]->sessionState.isRuntimeDomainEnabled);
// Send Runtime.disable while the RuntimeAgentDelegate exists - it receives
// the message and can also observe the updated state.
EXPECT_CALL(*runtimeAgentDelegates_[0], handleRequest(Eq(cdp::preparse(R"({
"id": 2,
"method": "Runtime.disable"
})"))));
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 2,
"result": {}
})")));
toPage_->sendMessage(R"({
"id": 2,
"method": "Runtime.disable"
})");
EXPECT_FALSE(runtimeAgentDelegates_[0]->sessionState.isRuntimeDomainEnabled);
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,110 @@
/*
* 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.
*/
#pragma once
#include <functional>
#include <memory>
namespace facebook {
/**
* A factory that creates objects of type T wrapped in unique_ptr, and provides
* non-owning access to those objects. Note that the factory MUST outlive the
* objects it creates.
*
* Example usage:
*
* struct Foo { virtual ~foo() = default; };
* UniquePtrFactory<Foo> objects;
* std::unique_ptr<Foo> object = objects.make_unique();
* assert(objects[0] == object.get());
* object.reset();
* assert(objects[0] == nullptr);
*
* See UniquePtrFactoryTest.cpp for more examples.
*/
template <typename T>
class UniquePtrFactory {
static_assert(
std::has_virtual_destructor_v<T>,
"T must have a virtual destructor");
public:
/**
* Creates a new object of type T, and returns a unique_ptr wrapping it.
*/
template <typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
size_t index = objectPtrs_.size();
auto ptr =
std::make_unique<Facade>(*this, index, std::forward<Args>(args)...);
objectPtrs_.push_back(ptr.get());
return ptr;
}
/**
* Returns a function that can be used to create objects of type T. The
* function may only be used while the factory is alive.
*/
template <typename... Args>
std::function<std::unique_ptr<T>(Args&&...)> lazily_make_unique() {
return [this](Args&&... args) {
return make_unique(std::forward<Args>(args)...);
};
}
/**
* Returns a pointer to the `index`th object created by this factory,
* or nullptr if the object has been destroyed (or not created yet).
*/
T* operator[](size_t index) {
return index >= objectPtrs_.size() ? nullptr : objectPtrs_[index];
}
/**
* Returns a pointer to the `index`th object created by this factory,
* or nullptr if the object has been destroyed (or not created yet).
*/
const T* operator[](size_t index) const {
return index >= objectPtrs_.size() ? nullptr : objectPtrs_[index];
}
/**
* Returns the total number of objects created by this factory, including
* those that have already been destroyed.
*/
size_t objectsVended() const {
return objectPtrs_.size();
}
private:
friend class Facade;
/**
* Extends T to clean up the reference in objectPtrs_ when the object is
* destroyed.
*/
class Facade : public T {
public:
template <typename... Args>
Facade(UniquePtrFactory& container, size_t index, Args&&... args)
: T(std::forward<Args>(args)...),
container_(container),
index_(index) {}
virtual ~Facade() override {
container_.objectPtrs_[index_] = nullptr;
}
UniquePtrFactory& container_;
size_t index_;
};
std::vector<T*> objectPtrs_;
};
} // namespace facebook

View File

@@ -0,0 +1,84 @@
/*
* 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 <gtest/gtest.h>
#include "UniquePtrFactory.h"
using namespace ::testing;
namespace {
struct Foo {
explicit Foo(int v) : value(v) {}
// Required for UniquePtrFactory
virtual ~Foo() = default;
int value{0};
};
} // namespace
namespace facebook {
TEST(UniquePtrFactoryTest, KitchenSink) {
UniquePtrFactory<Foo> fooObjects;
EXPECT_EQ(fooObjects[0], nullptr)
<< "objects should be nullptr before being created";
EXPECT_EQ(fooObjects.objectsVended(), 0);
auto foo0 = fooObjects.make_unique(100);
EXPECT_EQ(foo0.get(), fooObjects[0]);
EXPECT_EQ(fooObjects.objectsVended(), 1);
auto foo1 = fooObjects.make_unique(200);
EXPECT_EQ(foo1.get(), fooObjects[1]);
EXPECT_EQ(fooObjects.objectsVended(), 2);
foo0.reset();
EXPECT_EQ(fooObjects[0], nullptr)
<< "objects should be nullptr after being destroyed";
EXPECT_EQ(fooObjects.objectsVended(), 2)
<< "objectsVended should never decrease";
EXPECT_EQ(foo1.get(), fooObjects[1])
<< "foo1 should not be affected by foo0 being reset";
foo1.reset();
EXPECT_EQ(fooObjects[1], nullptr)
<< "objects should be nullptr after being destroyed";
EXPECT_EQ(fooObjects.objectsVended(), 2);
auto foo2 = fooObjects.make_unique(300);
EXPECT_EQ(foo2.get(), fooObjects[2]);
EXPECT_EQ(fooObjects.objectsVended(), 3);
}
TEST(UniquePtrFactoryTest, LazilyMakeUnique) {
UniquePtrFactory<Foo> fooObjects;
EXPECT_EQ(fooObjects[0], nullptr)
<< "objects should be nullptr before being created";
EXPECT_EQ(fooObjects.objectsVended(), 0);
auto makeFoo = fooObjects.lazily_make_unique<int>();
EXPECT_EQ(fooObjects[0], nullptr)
<< "an object should not be created until makeFoo is called";
EXPECT_EQ(fooObjects.objectsVended(), 0);
auto foo0 = makeFoo(100);
EXPECT_EQ(foo0.get(), fooObjects[0]);
EXPECT_EQ(fooObjects.objectsVended(), 1);
auto foo1 = makeFoo(200);
EXPECT_EQ(foo1.get(), fooObjects[1]);
EXPECT_EQ(fooObjects.objectsVended(), 2);
}
} // namespace facebook

View File

@@ -0,0 +1,91 @@
/*
* 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 <jsinspector-modern/WeakList.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <vector>
using namespace ::testing;
namespace facebook::react::jsinspector_modern {
TEST(WeakListTest, Size) {
WeakList<int> list;
EXPECT_EQ(list.size(), 0);
auto p1 = std::make_shared<int>(1);
list.insert(p1);
EXPECT_EQ(list.size(), 1);
auto p2 = std::make_shared<int>(2);
list.insert(p2);
EXPECT_EQ(list.size(), 2);
p1.reset();
EXPECT_EQ(list.size(), 1);
p2.reset();
EXPECT_EQ(list.size(), 0);
}
TEST(WeakListTest, Empty) {
WeakList<int> list;
EXPECT_EQ(list.empty(), true);
auto p1 = std::make_shared<int>(1);
list.insert(p1);
EXPECT_EQ(list.empty(), false);
auto p2 = std::make_shared<int>(2);
list.insert(p2);
EXPECT_EQ(list.empty(), false);
p1.reset();
EXPECT_EQ(list.empty(), false);
p2.reset();
EXPECT_EQ(list.empty(), true);
}
TEST(WeakListTest, ForEach) {
WeakList<int> list;
auto p1 = std::make_shared<int>(1);
list.insert(p1);
auto p2 = std::make_shared<int>(2);
list.insert(p2);
auto p3 = std::make_shared<int>(3);
list.insert(p3);
p2.reset();
std::vector<int> visited;
list.forEach([&visited](const int& value) { visited.push_back(value); });
EXPECT_THAT(visited, ElementsAre(1, 3));
}
TEST(WeakListTest, ElementsAreAliveDuringCallback) {
WeakList<int> list;
auto p1 = std::make_shared<int>(1);
// A separate weak_ptr to observe the lifetime of `p1`.
std::weak_ptr wp1 = p1;
list.insert(p1);
std::vector<int> visited;
list.forEach([&](const int& value) {
p1.reset();
EXPECT_FALSE(wp1.expired());
visited.push_back(value);
});
EXPECT_TRUE(wp1.expired());
EXPECT_THAT(visited, ElementsAre(1));
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,48 @@
/*
* 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 <jsinspector-modern/FallbackRuntimeAgentDelegate.h>
#include <folly/executors/QueuedImmediateExecutor.h>
#include <hermes/hermes.h>
#include "JsiIntegrationTestGenericEngineAdapter.h"
using facebook::hermes::makeHermesRuntime;
namespace facebook::react::jsinspector_modern {
JsiIntegrationTestGenericEngineAdapter::JsiIntegrationTestGenericEngineAdapter(
folly::Executor& jsExecutor)
: runtime_{hermes::makeHermesRuntime()}, jsExecutor_{jsExecutor} {}
std::unique_ptr<RuntimeAgentDelegate>
JsiIntegrationTestGenericEngineAdapter::createAgentDelegate(
FrontendChannel frontendChannel,
SessionState& sessionState,
std::unique_ptr<RuntimeAgentDelegate::ExportedState>,
const ExecutionContextDescription&) {
return std::unique_ptr<jsinspector_modern::RuntimeAgentDelegate>(
new FallbackRuntimeAgentDelegate(
frontendChannel,
sessionState,
"Generic engine (" + runtime_->description() + ")"));
}
jsi::Runtime& JsiIntegrationTestGenericEngineAdapter::getRuntime()
const noexcept {
return *runtime_;
}
RuntimeExecutor JsiIntegrationTestGenericEngineAdapter::getRuntimeExecutor()
const noexcept {
return [&jsExecutor = jsExecutor_, &runtime = getRuntime()](auto fn) {
jsExecutor.add([fn, &runtime]() { fn(runtime); });
};
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,44 @@
/*
* 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.
*/
#pragma once
#include <jsinspector-modern/RuntimeTarget.h>
#include <folly/executors/QueuedImmediateExecutor.h>
#include <jsi/jsi.h>
#include <memory>
namespace facebook::react::jsinspector_modern {
/**
* An engine adapter for JsiIntegrationTest that represents a generic
* JSI-compatible engine, with no engine-specific CDP support. Uses Hermes under
* the hood, without Hermes's CDP support.
*/
class JsiIntegrationTestGenericEngineAdapter : public RuntimeTargetDelegate {
public:
explicit JsiIntegrationTestGenericEngineAdapter(folly::Executor& jsExecutor);
virtual std::unique_ptr<RuntimeAgentDelegate> createAgentDelegate(
FrontendChannel frontendChannel,
SessionState& sessionState,
std::unique_ptr<RuntimeAgentDelegate::ExportedState>
previouslyExportedState,
const ExecutionContextDescription& executionContextDescription) override;
jsi::Runtime& getRuntime() const noexcept;
RuntimeExecutor getRuntimeExecutor() const noexcept;
private:
std::unique_ptr<jsi::Runtime> runtime_;
folly::Executor& jsExecutor_;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,58 @@
/*
* 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/executors/QueuedImmediateExecutor.h>
#include <hermes/inspector-modern/chrome/HermesRuntimeAgentDelegate.h>
#include "JsiIntegrationTestHermesEngineAdapter.h"
using facebook::hermes::makeHermesRuntime;
namespace facebook::react::jsinspector_modern {
JsiIntegrationTestHermesEngineAdapter::JsiIntegrationTestHermesEngineAdapter(
folly::Executor& jsExecutor)
: runtime_{hermes::makeHermesRuntime()}, jsExecutor_{jsExecutor} {}
std::unique_ptr<RuntimeAgentDelegate>
JsiIntegrationTestHermesEngineAdapter::createAgentDelegate(
FrontendChannel frontendChannel,
SessionState& sessionState,
std::unique_ptr<RuntimeAgentDelegate::ExportedState>
previouslyExportedState,
const ExecutionContextDescription& executionContextDescription) {
return std::unique_ptr<jsinspector_modern::RuntimeAgentDelegate>(
new HermesRuntimeAgentDelegate(
frontendChannel,
sessionState,
std::move(previouslyExportedState),
executionContextDescription,
runtime_,
getRuntimeExecutor()));
}
jsi::Runtime& JsiIntegrationTestHermesEngineAdapter::getRuntime()
const noexcept {
return *runtime_;
}
RuntimeExecutor JsiIntegrationTestHermesEngineAdapter::getRuntimeExecutor()
const noexcept {
auto& jsExecutor = jsExecutor_;
return [runtimeWeak = std::weak_ptr(runtime_), &jsExecutor](auto fn) {
jsExecutor.add([runtimeWeak, fn]() {
auto runtime = runtimeWeak.lock();
if (!runtime) {
return;
}
fn(*runtime);
});
};
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,44 @@
/*
* 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.
*/
#pragma once
#include <jsinspector-modern/RuntimeTarget.h>
#include <folly/executors/QueuedImmediateExecutor.h>
#include <hermes/hermes.h>
#include <jsi/jsi.h>
#include <memory>
namespace facebook::react::jsinspector_modern {
/**
* An engine adapter for JsiIntegrationTest that uses Hermes (and Hermes's
* CDP support).
*/
class JsiIntegrationTestHermesEngineAdapter : public RuntimeTargetDelegate {
public:
explicit JsiIntegrationTestHermesEngineAdapter(folly::Executor& jsExecutor);
virtual std::unique_ptr<RuntimeAgentDelegate> createAgentDelegate(
FrontendChannel frontendChannel,
SessionState& sessionState,
std::unique_ptr<RuntimeAgentDelegate::ExportedState>
previouslyExportedState,
const ExecutionContextDescription& executionContextDescription) override;
jsi::Runtime& getRuntime() const noexcept;
RuntimeExecutor getRuntimeExecutor() const noexcept;
private:
std::shared_ptr<facebook::hermes::HermesRuntime> runtime_;
folly::Executor& jsExecutor_;
};
} // namespace facebook::react::jsinspector_modern