first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

View File

@@ -0,0 +1,916 @@
/*
* 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 "JsiIntegrationTest.h"
#include "engines/JsiIntegrationTestHermesEngineAdapter.h"
#include "prelude.js.h"
#include <utility>
using namespace ::testing;
namespace facebook::react::jsinspector_modern {
namespace {
struct Params {
/**
* Whether to evaluate the prelude.js script (containing RN's console
* polyfill) after setting up the Runtime.
*/
bool withConsolePolyfill{false};
/**
* Whether to install the global nativeLoggingHook function after setting up
* the Runtime (before the prelude if any).
*/
bool withNativeLoggingHook{false};
/**
* Whether to enable the Runtime domain at the start of the test (and expect
* live consoleAPICalled notifications), or enable it at the *end* of the test
* (and expect buffered notifications at that point).
*/
bool runtimeEnabledAtStart{false};
};
} // namespace
/**
* A test fixture for the Console API.
*/
class ConsoleApiTest : public JsiIntegrationPortableTestBase<
JsiIntegrationTestHermesEngineAdapter,
folly::QueuedImmediateExecutor>,
public WithParamInterface<Params> {
protected:
void SetUp() override {
JsiIntegrationPortableTestBase::SetUp();
connect();
EXPECT_CALL(
fromPage(),
onMessage(
JsonParsed(AllOf(AtJsonPtr("/method", "Debugger.scriptParsed")))))
.Times(AnyNumber())
.WillRepeatedly(Invoke<>([this](const std::string& message) {
auto params = folly::parseJson(message);
// Store the script ID and URL for later use.
scriptUrlsById_.emplace(
params.at("params").at("scriptId").getString(),
params.at("params").at("url").getString());
}));
this->expectMessageFromPage(JsonEq(R"({
"id": 0,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 0,
"method": "Debugger.enable"
})");
if (GetParam().runtimeEnabledAtStart) {
enableRuntimeDomain();
}
}
void TearDown() override {
if (!GetParam().runtimeEnabledAtStart) {
enableRuntimeDomain();
}
JsiIntegrationPortableTestBase::TearDown();
}
/**
* Expect a console API call to be reported with parameters matching \param
* paramsMatcher.
*/
void expectConsoleApiCall(
Matcher<folly::dynamic> paramsMatcher,
std::source_location location = std::source_location::current()) {
if (runtimeEnabled_) {
expectConsoleApiCallImpl(std::move(paramsMatcher), location);
} else {
expectedConsoleApiCalls_.emplace_back(paramsMatcher, location);
}
}
/**
* Expect a console API call to be reported with parameters matching \param
* paramsMatcher, only if the Runtime domain is currently enabled ( = the call
* is reported in real time).
*/
void expectConsoleApiCallImmediate(
Matcher<folly::dynamic> paramsMatcher,
std::source_location location = std::source_location::current()) {
if (runtimeEnabled_) {
expectConsoleApiCallImpl(std::move(paramsMatcher), location);
}
}
/**
* Expect a console API call to be reported with parameters matching \param
* paramsMatcher, only if the Runtime domain is currently disabled ( = the
* call will be buffered and reported later upon enabling the domain).
*/
void expectConsoleApiCallBuffered(
const Matcher<folly::dynamic>& paramsMatcher,
std::source_location location = std::source_location::current()) {
if (!runtimeEnabled_) {
expectedConsoleApiCalls_.emplace_back(paramsMatcher, location);
}
}
bool isRuntimeDomainEnabled() const {
return runtimeEnabled_;
}
void clearExpectedConsoleApiCalls() {
expectedConsoleApiCalls_.clear();
}
template <typename InnerMatcher>
Matcher<folly::dynamic> ScriptIdMapsTo(InnerMatcher urlMatcher) {
return ResultOf(
[this](const auto& id) { return getScriptUrlById(id.getString()); },
urlMatcher);
}
private:
std::optional<std::string> getScriptUrlById(const std::string& scriptId) {
auto it = scriptUrlsById_.find(scriptId);
if (it == scriptUrlsById_.end()) {
return std::nullopt;
}
return it->second;
}
void expectConsoleApiCallImpl(
Matcher<folly::dynamic> paramsMatcher,
std::source_location location) {
this->expectMessageFromPage(
JsonParsed(AllOf(
AtJsonPtr("/method", "Runtime.consoleAPICalled"),
AtJsonPtr("/params", std::move(paramsMatcher)))),
location);
}
void enableRuntimeDomain() {
InSequence s;
auto executionContextInfo = this->expectMessageFromPage(JsonParsed(
AllOf(AtJsonPtr("/method", "Runtime.executionContextCreated"))));
if (!runtimeEnabled_) {
for (auto& [call, location] : expectedConsoleApiCalls_) {
expectConsoleApiCallImpl(call, location);
}
expectedConsoleApiCalls_.clear();
}
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.enable"
})");
ASSERT_TRUE(executionContextInfo->has_value());
runtimeEnabled_ = true;
}
void loadMainBundle() override {
auto params = GetParam();
if (params.withNativeLoggingHook) {
// The presence or absence of nativeLoggingHook affects the console
// polyfill's behaviour.
eval(
R"(
if (!globalThis.nativeLoggingHook) {
globalThis.nativeLoggingHook = function(level, message) {
print(level + ': ' + message);
};
}
)");
} else {
// Ensure that we run without nativeLoggingHook even if it was installed
// elsewhere.
eval(R"(
delete globalThis.nativeLoggingHook;
)");
}
if (params.withConsolePolyfill) {
eval(preludeJsCode);
}
}
std::vector<std::pair<Matcher<folly::dynamic>, std::source_location>>
expectedConsoleApiCalls_;
bool runtimeEnabled_{false};
std::unordered_map<std::string, std::string> scriptUrlsById_;
};
class ConsoleApiTestWithPreExistingConsole : public ConsoleApiTest {
void setupRuntimeBeforeRegistration(jsi::Runtime& /*unused*/) override {
eval(R"(
globalThis.__console_messages__ = [];
globalThis.console = {
log: function(...args) {
globalThis.__console_messages__.push({
type: 'log',
args,
});
},
warn: function(...args) {
globalThis.__console_messages__.push({
type: 'warn',
args,
});
},
};
)");
}
};
TEST_P(ConsoleApiTest, testConsoleLog) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "log"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "hello"
}, {
"type": "string",
"value": "world"
}])"_json)));
eval("console.log('hello', 'world');");
}
TEST_P(ConsoleApiTest, testConsoleDebug) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "debug"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "hello fusebox"
}])"_json)));
eval("console.debug('hello fusebox');");
}
TEST_P(ConsoleApiTest, testConsoleInfo) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "info"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "you should know this"
}])"_json)));
eval("console.info('you should know this');");
}
TEST_P(ConsoleApiTest, testConsoleError) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "error"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "uh oh"
}])"_json)));
eval("console.error('uh oh');");
}
TEST_P(ConsoleApiTest, testConsoleLogWithErrorObject) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "log"),
AtJsonPtr("/args/0/type", "object"),
AtJsonPtr("/args/0/subtype", "error"),
AtJsonPtr("/args/0/className", "Error"),
AtJsonPtr(
"/args/0/description",
"Error: wut\n"
" at secondFunction (<eval>:6:28)\n"
" at firstFunction (<eval>:3:21)\n"
" at anonymous (<eval>:8:18)\n"
" at global (<eval>:9:5)")));
eval(R"((() => {
function firstFunction() {
secondFunction();
}
function secondFunction() {
console.log(new Error('wut'));
}
firstFunction();
})())");
}
TEST_P(ConsoleApiTest, testConsoleLogWithArrayOfErrors) {
InSequence s;
expectConsoleApiCallImmediate(AllOf(
AtJsonPtr("/type", "log"),
AtJsonPtr("/args/0/type", "object"),
AtJsonPtr("/args/0/subtype", "array"),
AtJsonPtr("/args/0/description", "Array(2)"),
AtJsonPtr("/args/0/preview/description", "Array(2)"),
AtJsonPtr("/args/0/preview/type", "object"),
AtJsonPtr("/args/0/preview/subtype", "array"),
AtJsonPtr("/args/0/preview/properties/0/type", "object"),
AtJsonPtr("/args/0/preview/properties/0/subtype", "error"),
AtJsonPtr(
"/args/0/preview/properties/0/value",
"Error: wut\n"
" at typicallyUrlsAreLongAndWillHitTheAbbreviationLimit (<eval>:6:29)\n"
" at reallyLon…")));
expectConsoleApiCallBuffered(AllOf(AtJsonPtr("/type", "log")));
eval(R"((() => {
function reallyLongFunctionNameToAssertMaxLengthOfAbbreviatedString() {
typicallyUrlsAreLongAndWillHitTheAbbreviationLimit();
}
function typicallyUrlsAreLongAndWillHitTheAbbreviationLimit() {
console.log([new Error('wut'), new TypeError('why')]);
}
reallyLongFunctionNameToAssertMaxLengthOfAbbreviatedString();
})())");
}
TEST_P(ConsoleApiTest, testConsoleWarn) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "warning"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "careful"
}])"_json)));
eval("console.warn('careful');");
}
TEST_P(ConsoleApiTest, testConsoleDir) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "dir"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "something"
}])"_json)));
eval("console.dir('something');");
}
TEST_P(ConsoleApiTest, testConsoleDirxml) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "dirxml"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "pretend this is a DOM element"
}])"_json)));
eval("console.dirxml('pretend this is a DOM element');");
}
TEST_P(ConsoleApiTest, testConsoleTable) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "table"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "pretend this is a complex object"
}])"_json)));
eval("console.table('pretend this is a complex object');");
}
TEST_P(ConsoleApiTest, testConsoleTrace) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "trace"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "trace trace"
}])"_json)));
eval("console.trace('trace trace');");
}
TEST_P(ConsoleApiTest, testConsoleClear) {
InSequence s;
expectConsoleApiCall(
AllOf(AtJsonPtr("/type", "clear"), AtJsonPtr("/args", "[]"_json)));
eval("console.clear();");
}
TEST_P(ConsoleApiTest, testConsoleClearAfterOtherCall) {
InSequence s;
if (isRuntimeDomainEnabled()) {
// This should only be delivered if console notifications are enabled, not
// when they're being cached for later.
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "log"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "hello"
}])"_json)));
}
expectConsoleApiCall(
AllOf(AtJsonPtr("/type", "clear"), AtJsonPtr("/args", "[]"_json)));
eval("console.log('hello');");
eval("console.clear();");
}
TEST_P(ConsoleApiTest, testConsoleGroup) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "startGroup"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "group title"
}])"_json)));
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "log"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "in group"
}])"_json)));
expectConsoleApiCall(
AllOf(AtJsonPtr("/type", "endGroup"), AtJsonPtr("/args", "[]"_json)));
eval("console.group('group title');");
eval("console.log('in group');");
eval("console.groupEnd();");
}
TEST_P(ConsoleApiTest, testConsoleGroupCollapsed) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "startGroupCollapsed"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "group collapsed title"
}])"_json)));
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "log"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "in group collapsed"
}])"_json)));
expectConsoleApiCall(
AllOf(AtJsonPtr("/type", "endGroup"), AtJsonPtr("/args", "[]"_json)));
eval("console.groupCollapsed('group collapsed title');");
eval("console.log('in group collapsed');");
eval("console.groupEnd();");
}
TEST_P(ConsoleApiTest, testConsoleAssert) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "assert"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "Assertion failed: something is bad"
}])"_json)));
eval("console.assert(true, 'everything is good');");
eval("console.assert(false, 'something is bad');");
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "assert"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "Assertion failed"
}])"_json)));
eval("console.assert();");
}
TEST_P(ConsoleApiTest, testConsoleCount) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "count"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "default: 1"
}])"_json)));
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "count"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "default: 2"
}])"_json)));
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "count"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "default: 3"
}])"_json)));
eval("console.count();");
eval("console.count('default');");
eval("console.count();");
eval("console.countReset();");
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "count"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "default: 1"
}])"_json)));
eval("console.count();");
eval("console.countReset('default');");
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "count"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "default: 1"
}])"_json)));
eval("console.count();");
}
TEST_P(ConsoleApiTest, testConsoleCountLabel) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "count"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "foo: 1"
}])"_json)));
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "count"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "foo: 2"
}])"_json)));
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "count"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "foo: 3"
}])"_json)));
eval("console.count('foo');");
eval("console.count('foo');");
eval("console.count('foo');");
eval("console.countReset('foo');");
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "count"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "foo: 1"
}])"_json)));
eval("console.count('foo');");
}
TEST_P(ConsoleApiTest, testConsoleCountResetInvalidLabel) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "warning"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "Count for 'default' does not exist"
}])"_json)));
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "warning"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "Count for 'default' does not exist"
}])"_json)));
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "warning"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "Count for 'foo' does not exist"
}])"_json)));
eval("console.countReset();");
eval("console.countReset('default');");
eval("console.countReset('foo');");
}
// TODO(moti): Tests for console.timeEnd() and timeLog() that actually check the
// output (with mocked system clock?)
TEST_P(ConsoleApiTest, testConsoleTimeExistingLabel) {
eval("console.time();");
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "warning"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "Timer 'default' already exists"
}])"_json)));
eval("console.time('default');");
eval("console.time('foo');");
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "warning"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "Timer 'foo' already exists"
}])"_json)));
eval("console.time('foo');");
}
TEST_P(ConsoleApiTest, testConsoleTimeInvalidLabel) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "warning"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "Timer 'default' does not exist"
}])"_json)));
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "warning"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "Timer 'default' does not exist"
}])"_json)));
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "warning"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "Timer 'foo' does not exist"
}])"_json)));
eval("console.timeEnd();");
eval("console.timeEnd('default');");
eval("console.timeEnd('foo');");
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "warning"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "Timer 'default' does not exist"
}])"_json)));
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "warning"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "Timer 'default' does not exist"
}])"_json)));
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "warning"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "Timer 'foo' does not exist"
}])"_json)));
eval("console.timeLog();");
eval("console.timeLog('default');");
eval("console.timeLog('foo');");
}
TEST_P(ConsoleApiTest, testConsoleSilentlyClearedOnReload) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "log"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "hello"
}])"_json)));
eval("console.log('hello');");
// If there are any expectations we haven't checked yet, clear them
clearExpectedConsoleApiCalls();
// Reloading generates some Runtime events
if (isRuntimeDomainEnabled()) {
expectMessageFromPage(JsonParsed(
AllOf(AtJsonPtr("/method", "Runtime.executionContextDestroyed"))));
expectMessageFromPage(JsonParsed(
AllOf(AtJsonPtr("/method", "Runtime.executionContextsCleared"))));
expectMessageFromPage(JsonParsed(
AllOf(AtJsonPtr("/method", "Runtime.executionContextCreated"))));
}
reload();
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "log"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "world"
}])"_json)));
eval("console.log('world');");
}
TEST_P(ConsoleApiTestWithPreExistingConsole, testPreExistingConsoleObject) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "log"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "hello"
}])"_json)));
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "warning"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "world"
}])"_json)));
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "table"),
AtJsonPtr(
"/args",
R"([{
"type": "number",
"value": 42
}])"_json)));
eval("console.log('hello');");
eval("console.warn('world');");
// NOTE: not present in the pre-existing console object
eval("console.table(42);");
auto& runtime = engineAdapter_->getRuntime();
EXPECT_THAT(
eval("JSON.stringify(globalThis.__console_messages__)")
.asString(runtime)
.utf8(runtime),
JsonEq(
R"([{
"type": "log",
"args": [
"hello"
]
}, {
"type": "warn",
"args": [
"world"
]
}])"));
}
TEST_P(ConsoleApiTest, testConsoleLogStack) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "log"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "hello"
}])"_json),
AtJsonPtr(
"/stackTrace/callFrames",
AllOf(
Each(AtJsonPtr(
"/url",
Conditional(
GetParam().withConsolePolyfill,
AnyOf("script.js", "prelude.js"),
"script.js"))),
// A relatively weak assertion: we expect at least one frame tying
// the call to the `console.log` line.
Contains(AllOf(
AtJsonPtr("/functionName", "global"),
AtJsonPtr("/url", "script.js"),
AtJsonPtr("/lineNumber", 1),
AtJsonPtr("/scriptId", ScriptIdMapsTo("script.js"))))))));
eval(R"( // line 0
console.log('hello'); // line 1
//# sourceURL=script.js
)");
}
TEST_P(ConsoleApiTest, testConsoleLogTwice) {
InSequence s;
expectConsoleApiCall(
AllOf(AtJsonPtr("/type", "log"), AtJsonPtr("/args/0/value", "hello")));
eval("console.log('hello');");
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "log"), AtJsonPtr("/args/0/value", "hello again")));
eval("console.log('hello again');");
}
TEST_P(ConsoleApiTest, testConsoleLogWithObjectPreview) {
InSequence s;
expectConsoleApiCallImmediate(AllOf(
AtJsonPtr("/type", "log"),
AtJsonPtr("/args/0/preview/type", "object"),
AtJsonPtr("/args/0/preview/overflow", false),
AtJsonPtr("/args/0/preview/properties/0/name", "string"),
AtJsonPtr("/args/0/preview/properties/0/type", "string"),
AtJsonPtr("/args/0/preview/properties/0/value", "hello")));
expectConsoleApiCallBuffered(AllOf(AtJsonPtr("/type", "log")));
eval("console.log({ string: 'hello' });");
}
static const auto paramValues = testing::Values(
Params{
.withConsolePolyfill = true,
.withNativeLoggingHook = false,
.runtimeEnabledAtStart = false,
},
Params{
.withConsolePolyfill = false,
.withNativeLoggingHook = false,
.runtimeEnabledAtStart = false,
},
Params{
.withConsolePolyfill = true,
.withNativeLoggingHook = false,
.runtimeEnabledAtStart = true,
},
Params{
.withConsolePolyfill = false,
.withNativeLoggingHook = false,
.runtimeEnabledAtStart = true,
},
Params{
.withConsolePolyfill = true,
.withNativeLoggingHook = true,
.runtimeEnabledAtStart = false,
},
Params{
.withConsolePolyfill = false,
.withNativeLoggingHook = true,
.runtimeEnabledAtStart = false,
},
Params{
.withConsolePolyfill = true,
.withNativeLoggingHook = true,
.runtimeEnabledAtStart = true,
},
Params{
.withConsolePolyfill = false,
.withNativeLoggingHook = true,
.runtimeEnabledAtStart = true,
});
INSTANTIATE_TEST_SUITE_P(ConsoleApiTest, ConsoleApiTest, paramValues);
INSTANTIATE_TEST_SUITE_P(
ConsoleApiTestWithPreExistingConsole,
ConsoleApiTestWithPreExistingConsole,
paramValues);
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,143 @@
/*
* 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 "JsiIntegrationTest.h"
#include "engines/JsiIntegrationTestHermesEngineAdapter.h"
#include <jsinspector-modern/tracing/PerformanceTracer.h>
using namespace ::testing;
namespace facebook::react::jsinspector_modern {
/**
* A test fixture for the console.timeStamp API.
*/
class ConsoleTimeStampTest : public JsiIntegrationPortableTestBase<
JsiIntegrationTestHermesEngineAdapter,
folly::QueuedImmediateExecutor> {
protected:
size_t countNumberOfTimeStampEvents(
const std::vector<tracing::TraceEvent>& events) {
size_t count = 0;
for (const auto& event : events) {
if (event.name == "TimeStamp") {
count++;
}
}
return count;
}
};
TEST_F(ConsoleTimeStampTest, Installed) {
auto result = eval("typeof console.timeStamp");
auto& runtime = engineAdapter_->getRuntime();
EXPECT_EQ(result.asString(runtime).utf8(runtime), "function");
}
TEST_F(ConsoleTimeStampTest, RecordsEntriesWithJustLabel) {
auto& tracer = tracing::PerformanceTracer::getInstance();
EXPECT_TRUE(tracer.startTracing());
eval("console.timeStamp('test-label')");
auto events = tracer.stopTracing();
EXPECT_EQ(countNumberOfTimeStampEvents(*events), 1);
}
TEST_F(ConsoleTimeStampTest, RecordsEntriesWithSpecifiedTimestamps) {
auto& tracer = tracing::PerformanceTracer::getInstance();
EXPECT_TRUE(tracer.startTracing());
eval("console.timeStamp('test-range', 100, 200)");
auto events = tracer.stopTracing();
EXPECT_EQ(countNumberOfTimeStampEvents(*events), 1);
}
TEST_F(ConsoleTimeStampTest, RecordsEntriesWithMarkNames) {
auto& tracer = tracing::PerformanceTracer::getInstance();
EXPECT_TRUE(tracer.startTracing());
eval(
"console.timeStamp('test-string-timestamps', 'start-marker', 'end-marker')");
auto events = tracer.stopTracing();
EXPECT_EQ(countNumberOfTimeStampEvents(*events), 1);
}
TEST_F(ConsoleTimeStampTest, KeepsEntriesWithUnknownMarkNames) {
auto& tracer = tracing::PerformanceTracer::getInstance();
EXPECT_TRUE(tracer.startTracing());
eval(
"console.timeStamp('test-string-timestamps', 'start-marker', 'end-marker')");
auto events = tracer.stopTracing();
EXPECT_EQ(countNumberOfTimeStampEvents(*events), 1);
}
TEST_F(ConsoleTimeStampTest, DoesNotThrowIfNotTracing) {
auto& tracer = tracing::PerformanceTracer::getInstance();
EXPECT_FALSE(tracer.stopTracing());
// Call console.timeStamp - should be a no-op when not tracing
eval("console.timeStamp('test-no-tracing')");
}
TEST_F(ConsoleTimeStampTest, SurvivesInvalidArguments) {
auto& tracer = tracing::PerformanceTracer::getInstance();
EXPECT_TRUE(tracer.startTracing());
// Won't be logged, no label specified.
eval("console.timeStamp()");
// Will be logged - undefined will be stringified.
eval("console.timeStamp(undefined)");
// Will be logged - function will be stringified.
eval("console.timeStamp(() => {})");
// Will be logged - 123 will be stringified.
eval("console.timeStamp(123)");
// Will be logged - start will default to an empty string.
eval("console.timeStamp('label', {})");
auto events = tracer.stopTracing();
EXPECT_EQ(countNumberOfTimeStampEvents(*events), 4);
}
TEST_F(ConsoleTimeStampTest, InvalidTrackNameIsIgnored) {
auto& tracer = tracing::PerformanceTracer::getInstance();
EXPECT_TRUE(tracer.startTracing());
eval("console.timeStamp('label', 0, 1, {})");
auto events = tracer.stopTracing();
EXPECT_EQ(countNumberOfTimeStampEvents(*events), 1);
}
TEST_F(ConsoleTimeStampTest, InvalidTrackGroupIsIgnored) {
auto& tracer = tracing::PerformanceTracer::getInstance();
EXPECT_TRUE(tracer.startTracing());
eval("console.timeStamp('label', 0, 1, 'trackName', {})");
auto events = tracer.stopTracing();
EXPECT_EQ(countNumberOfTimeStampEvents(*events), 1);
}
TEST_F(ConsoleTimeStampTest, InvalidColorIsIgnored) {
auto& tracer = tracing::PerformanceTracer::getInstance();
EXPECT_TRUE(tracer.startTracing());
eval("console.timeStamp('test', 100, 200, 'FooTrack', 'BarGroup', 'red')");
auto events = tracer.stopTracing();
EXPECT_EQ(countNumberOfTimeStampEvents(*events), 1);
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,195 @@
/*
* 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 "JsiIntegrationTest.h"
#include "engines/JsiIntegrationTestHermesEngineAdapter.h"
using namespace ::testing;
namespace facebook::react::jsinspector_modern {
class DebuggerSessionObserverTest : public JsiIntegrationPortableTestBase<
JsiIntegrationTestHermesEngineAdapter,
folly::QueuedImmediateExecutor> {
protected:
void enableRuntimeDomain() {
InSequence s;
auto executionContextInfo = expectMessageFromPage(JsonParsed(
AllOf(AtJsonPtr("/method", "Runtime.executionContextCreated"))));
expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.enable"
})");
}
void enableLogDomain() {
InSequence s;
EXPECT_CALL(
fromPage(),
onMessage(JsonParsed(AllOf(
AtJsonPtr("/method", "Log.entryAdded"),
AtJsonPtr("/params/entry", Not(IsEmpty()))))))
.Times(AtLeast(1));
expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
toPage_->sendMessage(R"({
"id": 1,
"method": "Log.enable"
})");
}
void disableRuntimeDomain() {
InSequence s;
expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.disable"
})");
}
void disableLogDomain() {
InSequence s;
expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
toPage_->sendMessage(R"({
"id": 1,
"method": "Log.disable"
})");
}
};
TEST_F(DebuggerSessionObserverTest, InstallsGlobalObserverObjectByDefault) {
EXPECT_TRUE(eval("__DEBUGGER_SESSION_OBSERVER__ != null").asBool());
}
TEST_F(
DebuggerSessionObserverTest,
WillNotEmitStatusUpdateUnlessBothRuntimeAndLogDomainsAreEnabled) {
EXPECT_FALSE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
connect();
EXPECT_FALSE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
enableRuntimeDomain();
EXPECT_FALSE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
enableLogDomain();
EXPECT_TRUE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
}
TEST_F(
DebuggerSessionObserverTest,
UpdatesTheStatusOnceRuntimeDomainIsDisabled) {
connect();
enableLogDomain();
enableRuntimeDomain();
EXPECT_TRUE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
disableRuntimeDomain();
EXPECT_FALSE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
}
TEST_F(DebuggerSessionObserverTest, UpdatesTheStatusOnceLogDomainIsDisabled) {
connect();
enableLogDomain();
enableRuntimeDomain();
EXPECT_TRUE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
disableLogDomain();
EXPECT_FALSE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
}
TEST_F(
DebuggerSessionObserverTest,
NotifiesSubscribersWhichWereSubscribedBeforeSessionInitialization) {
eval(
R"(
var latestStatus = undefined;
__DEBUGGER_SESSION_OBSERVER__.subscribers.add(updatedStatus => {
latestStatus = updatedStatus;
});
)");
EXPECT_TRUE(eval("latestStatus").isUndefined());
connect();
enableLogDomain();
enableRuntimeDomain();
EXPECT_TRUE(eval("latestStatus").asBool());
disableLogDomain();
EXPECT_FALSE(eval("latestStatus").asBool());
}
TEST_F(DebuggerSessionObserverTest, testTwoConcurrentConnections) {
connect();
auto secondary = connectSecondary();
EXPECT_CALL(fromPage(), onMessage(_)).Times(AnyNumber());
EXPECT_CALL(secondary.fromPage(), onMessage(_)).Times(AnyNumber());
// No domains enabled to start with
EXPECT_FALSE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
toPage_->sendMessage(R"({"id": 1, "method": "Runtime.enable"})");
// Primary: Runtime, Secondary: None
EXPECT_FALSE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
secondary.toPage().sendMessage(R"({"id": 2, "method": "Log.enable"})");
// Primary: Runtime, Secondary: Log
EXPECT_FALSE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
secondary.toPage().sendMessage(R"({"id": 3, "method": "Runtime.enable"})");
// Primary: Runtime, Secondary: [Runtime, Log]
EXPECT_TRUE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
toPage_->sendMessage(R"({"id": 4, "method": "Log.enable"})");
// Primary: [Runtime, Log], Secondary: [Runtime, Log]
EXPECT_TRUE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
toPage_->sendMessage(R"({"id": 5, "method": "Runtime.disable"})");
// Primary: Log, Secondary: [Runtime, Log]
EXPECT_TRUE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
secondary.toPage().sendMessage(R"({"id": 6, "method": "Log.disable"})");
// Primary: Log, Secondary: Runtime
EXPECT_FALSE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
secondary.toPage().sendMessage(R"({"id": 7, "method": "Runtime.disable"})");
// Primary: Log, Secondary: None
EXPECT_FALSE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
toPage_->sendMessage(R"({"id": 8, "method": "Log.disable"})");
// Primary: None, Secondary: None
EXPECT_FALSE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
}
} // namespace facebook::react::jsinspector_modern

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(const folly::dynamic& value) {
return value.asString();
}
std::string explain_error(
folly::dynamic::json_pointer_resolution_error<const folly::dynamic> 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,101 @@
/*
* 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(const folly::dynamic &value);
std::string explain_error(folly::dynamic::json_pointer_resolution_error<const folly::dynamic> 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;
}
/**
* A higher-order matcher that applies an inner matcher to the string value of
* a folly::dynamic.
*/
MATCHER_P(
DynamicString,
innerMatcher,
std::string{"string value "} + testing::DescribeMatcher<std::string>(innerMatcher, negation))
{
using namespace ::testing;
using namespace folly_dynamic_matchers_utils;
if (!arg.isString()) {
*result_listener << "is not a string";
return false;
}
return ExplainMatchResult(innerMatcher, arg.getString(), result_listener);
}
/**
* A user-defined literal for constructing a folly::dynamic from a JSON
* string. Not technically specific to GMock, but convenient to have in a test
* suite.
*/
inline folly::dynamic operator""_json(const char *s, size_t n)
{
return folly::parseJson(std::string{s, n});
}
} // namespace facebook

View File

@@ -0,0 +1,25 @@
/*
* 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 <gmock/gmock.h>
#pragma once
/**
* A variant of GMOCK_ON_CALL_IMPL that allows specifying the source location as
* a std::source_location parameter.
*/
#define GMOCK_ON_CALL_WITH_SOURCE_LOCATION_IMPL_(location, mock_expr, Setter, call) \
((mock_expr).gmock_##call)(::testing::internal::GetWithoutMatchers(), nullptr) \
.Setter((location).file_name(), (location).line(), #mock_expr, #call)
/**
* A variant of EXPECT_CALL that allows specifying the source location as a
* std::source_location parameter;
*/
#define EXPECT_CALL_WITH_SOURCE_LOCATION(location, obj, call) \
GMOCK_ON_CALL_WITH_SOURCE_LOCATION_IMPL_(location, obj, InternalExpectedAt, call)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,195 @@
/*
* 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);
}
}));
EXPECT_CALL(*this, scheduleCallback(_, _)).Times(AnyNumber());
}
// 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 MockHostTargetDelegate : public HostTargetDelegate {
public:
// HostTargetDelegate methods
HostTargetMetadata getMetadata() override
{
return {.integrationName = "MockHostTargetDelegate"};
}
MOCK_METHOD(void, onReload, (const PageReloadRequest &request), (override));
MOCK_METHOD(
void,
onSetPausedInDebuggerMessage,
(const OverlaySetPausedInDebuggerMessageRequest &request),
(override));
MOCK_METHOD(
void,
loadNetworkResource,
(const LoadNetworkResourceRequest &params, ScopedExecutor<NetworkRequestListener> executor),
(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 &,
RuntimeExecutor),
(override));
MOCK_METHOD(void, addConsoleMessage, (jsi::Runtime & runtime, ConsoleMessage message), (override));
MOCK_METHOD(bool, supportsConsole, (), (override, const));
MOCK_METHOD(
std::unique_ptr<StackTrace>,
captureStackTrace,
(jsi::Runtime & runtime, size_t framesToSkip),
(override));
MOCK_METHOD(void, enableSamplingProfiler, (), (override));
MOCK_METHOD(void, disableSamplingProfiler, (), (override));
MOCK_METHOD(tracing::RuntimeSamplingProfile, collectSamplingProfile, (), (override));
MOCK_METHOD(std::optional<folly::dynamic>, serializeStackTrace, (const StackTrace &stackTrace), (override));
inline MockRuntimeTargetDelegate()
{
using namespace testing;
// Silence "uninteresting mock function call" warnings for methods that
// don't have side effects.
EXPECT_CALL(*this, supportsConsole()).Times(AnyNumber());
}
};
class MockRuntimeAgentDelegate : public RuntimeAgentDelegate {
public:
inline MockRuntimeAgentDelegate(
FrontendChannel frontendChannel,
SessionState &sessionState,
std::unique_ptr<RuntimeAgentDelegate::ExportedState> /*unused*/,
ExecutionContextDescription executionContextDescription,
const RuntimeExecutor & /*runtimeExecutor*/)
: frontendChannel(std::move(frontendChannel)),
sessionState(sessionState),
executionContextDescription(std::move(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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,988 @@
/*
* 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 <fmt/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;
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, ReactNativeApplicationEnable) {
this->connect();
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->expectMessageFromPage(JsonEq(R"({
"method": "ReactNativeApplication.metadataUpdated",
"params": {
"integrationName": "JsiIntegrationTest",
"unstable_isProfilingBuild": false,
"unstable_networkInspectionEnabled": false
}
})"));
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 region JsiIntegrationPortableTestWithEarlyAddedBinding
// A test with some special setup to cover an edge case in the CDP backend:
// bindings can be added before there's even a Runtime in the session, and
// they should still work seamlessly once there is one.
TYPED_TEST_SUITE(JsiIntegrationPortableTestWithEarlyAddedBinding, AllEngines);
template <typename EngineAdapter>
class JsiIntegrationPortableTestWithEarlyAddedBinding
: public JsiIntegrationPortableTest<EngineAdapter> {
void setupRuntimeBeforeRegistration(jsi::Runtime& /*unused*/) override {
// Connect early, before the RuntimeTarget is created. This session will not
// have a RuntimeAgent yet.
this->connect();
// Add a binding.
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.addBinding",
"params": {"name": "foo"}
})");
// The binding is not installed yet, because the runtime is not registered
// with the CDP backend yet.
EXPECT_TRUE(this->eval("typeof globalThis.foo === 'undefined'").getBool());
}
};
TYPED_TEST(
JsiIntegrationPortableTestWithEarlyAddedBinding,
AddBindingBeforeRuntimeRegistered) {
// Now there is a RuntimeTarget / RuntimeAgent in the session, as per the
// normal test setup. Note that we're already connect()ed - not repeating that
// here.
// The binding is now installed, callable and working.
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Runtime.bindingCalled"),
AtJsonPtr("/params/name", "foo"),
AtJsonPtr("/params/payload", "bar"))));
this->eval("globalThis.foo('bar');");
}
#pragma endregion // JsiIntegrationPortableTestWithEarlyAddedBinding
#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(
fmt::format(
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(
fmt::format(
R"({{
"id": 3,
"method": "Runtime.evaluate",
"params": {{"expression": "10000", "contextId": {0}}}
}})",
std::to_string(executionContextId)));
}
#if !defined(HERMES_STATIC_HERMES)
// FIXME(T239924718): Breakpoint resolution in Static Hermes is broken for
// locations without column numbers under lazy compilation.
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"]);
}
#endif // !defined(HERMES_STATIC_HERMES)
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/facebook/react-native-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(
fmt::format(
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(
fmt::format(
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(
fmt::format(
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(
fmt::format(
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(
fmt::format(
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(
fmt::format(
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"}
})");
}
// A low-level test for captureStackTrace and serializeStackTrace in
// HermesRuntimeTargetDelegate. This functionality is not directly exposed
// to user code, but serves as a building block for higher-level CDP domains.
TYPED_TEST(JsiIntegrationHermesTest, testCaptureAndSerializeStackTrace) {
auto& runtimeTargetDelegate = this->dangerouslyGetRuntimeTargetDelegate();
auto& runtime = this->dangerouslyGetRuntime();
runtime.global().setProperty(
runtime,
"captureCdpStackTrace",
jsi::Function::createFromHostFunction(
runtime,
jsi::PropNameID::forAscii(runtime, "captureCdpStackTrace"),
0,
[&runtimeTargetDelegate](
jsi::Runtime& rt,
const jsi::Value& /* thisVal */,
const jsi::Value* /* args */,
size_t /* count */) -> jsi::Value {
auto stackTraceDynamic = runtimeTargetDelegate.serializeStackTrace(
*runtimeTargetDelegate.captureStackTrace(rt));
if (!stackTraceDynamic.has_value()) {
return jsi::Value::undefined();
}
return jsi::String::createFromUtf8(
rt, folly::toJson(*stackTraceDynamic));
}));
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", "stackTraceTest.js"))));
auto stackTrace = this->eval(R"( // line 0
function inner() { // line 1
return globalThis.captureCdpStackTrace(); // line 2
} // line 3
function outer() { // line 4
return inner(); // line 5
} // line 6
outer(); // line 7
//# sourceURL=stackTraceTest.js
)")
.getString(runtime)
.utf8(runtime);
ASSERT_TRUE(scriptInfo->has_value());
EXPECT_THAT(
stackTrace,
JsonParsed(AllOf(
AtJsonPtr("/callFrames/0/functionName", "inner"),
AtJsonPtr(
"/callFrames/0/scriptId",
scriptInfo->value()["params"]["scriptId"]),
AtJsonPtr("/callFrames/0/lineNumber", 2),
AtJsonPtr("/callFrames/0/columnNumber", 44),
AtJsonPtr("/callFrames/1/functionName", "outer"),
AtJsonPtr(
"/callFrames/1/scriptId",
scriptInfo->value()["params"]["scriptId"]),
AtJsonPtr("/callFrames/1/lineNumber", 5),
AtJsonPtr("/callFrames/1/columnNumber", 18),
AtJsonPtr("/callFrames/2/functionName", "global"),
AtJsonPtr(
"/callFrames/2/scriptId",
scriptInfo->value()["params"]["scriptId"]),
AtJsonPtr("/callFrames/2/lineNumber", 7),
AtJsonPtr("/callFrames/2/columnNumber", 9))));
}
#pragma endregion // AllHermesVariants
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,256 @@
/*
* 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 <gmock/gmock.h>
#include <gtest/gtest.h>
#include <jsinspector-modern/HostTarget.h>
#include <jsinspector-modern/InspectorInterfaces.h>
#include <memory>
#include <source_location>
#include "FollyDynamicMatchers.h"
#include "GmockHelpers.h"
#include "InspectorMocks.h"
#include "UniquePtrFactory.h"
#include "utils/InspectorFlagOverridesGuard.h"
namespace facebook::react::jsinspector_modern {
/**
* 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, typename Executor>
class JsiIntegrationPortableTestBase : public ::testing::Test, private HostTargetDelegate {
protected:
Executor executor_;
JsiIntegrationPortableTestBase(InspectorFlagOverrides overrides = {})
: inspectorFlagsGuard_(overrides), engineAdapter_{executor_}
{
}
void SetUp() override
{
// NOTE: Using SetUp() so we can call virtual methods like
// setupRuntimeBeforeRegistration().
page_ = HostTarget::create(*this, inspectorExecutor_);
instance_ = &page_->registerInstance(instanceTargetDelegate_);
setupRuntimeBeforeRegistration(engineAdapter_->getRuntime());
runtimeTarget_ =
&instance_->registerRuntime(engineAdapter_->getRuntimeTargetDelegate(), engineAdapter_->getRuntimeExecutor());
loadMainBundle();
}
~JsiIntegrationPortableTestBase() override
{
toPage_.reset();
if (runtimeTarget_ != nullptr) {
EXPECT_TRUE(instance_);
instance_->unregisterRuntime(*runtimeTarget_);
runtimeTarget_ = nullptr;
}
if (instance_ != nullptr) {
page_->unregisterInstance(*instance_);
instance_ = nullptr;
}
}
/**
* Noop in JsiIntegrationPortableTest, but can be overridden by derived
* fixture classes to load some code at startup and after each reload.
*/
virtual void loadMainBundle() {}
/**
* Noop in JsiIntegrationPortableTest, but can be overridden by derived
* fixture classes to set up the runtime before registering it with the
* CDP backend.
*/
virtual void setupRuntimeBeforeRegistration(jsi::Runtime & /*runtime*/) {}
void connect(std::source_location location = std::source_location::current())
{
ASSERT_FALSE(toPage_) << "Can only connect once in a JSI integration test.";
toPage_ = page_->connect(remoteConnections_.make_unique());
using namespace ::testing;
// Default to ignoring console messages originating inside the backend.
EXPECT_CALL_WITH_SOURCE_LOCATION(
location,
fromPage(),
onMessage(JsonParsed(AllOf(
AtJsonPtr("/method", "Runtime.consoleAPICalled"), AtJsonPtr("/params/context", "main#InstanceAgent")))))
.Times(AnyNumber());
// We'll always get an onDisconnect call when we tear
// down the test. Expect it in order to satisfy the strict mock.
EXPECT_CALL_WITH_SOURCE_LOCATION(location, *remoteConnections_[0], onDisconnect());
}
void reload()
{
if (runtimeTarget_ != nullptr) {
ASSERT_TRUE(instance_);
instance_->unregisterRuntime(*runtimeTarget_);
runtimeTarget_ = nullptr;
}
if (instance_ != nullptr) {
page_->unregisterInstance(*instance_);
instance_ = nullptr;
}
// Recreate the engine (e.g. to wipe any state in the inner jsi::Runtime)
engineAdapter_.emplace(executor_);
instance_ = &page_->registerInstance(instanceTargetDelegate_);
setupRuntimeBeforeRegistration(engineAdapter_->getRuntime());
runtimeTarget_ =
&instance_->registerRuntime(engineAdapter_->getRuntimeTargetDelegate(), engineAdapter_->getRuntimeExecutor());
loadMainBundle();
}
MockRemoteConnection &fromPage()
{
assert(toPage_);
return *remoteConnections_[0];
}
VoidExecutor inspectorExecutor_ = [this](auto callback) { executor_.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::source_location location = std::source_location::current())
{
using namespace ::testing;
ScopedTrace scope(location.file_name(), location.line(), "");
std::shared_ptr result = std::make_shared<std::optional<folly::dynamic>>(std::nullopt);
EXPECT_CALL_WITH_SOURCE_LOCATION(location, fromPage(), onMessage(matcher))
.WillOnce(([result](auto message) { *result = folly::parseJson(message); }))
.RetiresOnSaturation();
return result;
}
RuntimeTargetDelegate &dangerouslyGetRuntimeTargetDelegate()
{
return engineAdapter_->getRuntimeTargetDelegate();
}
jsi::Runtime &dangerouslyGetRuntime()
{
return engineAdapter_->getRuntime();
}
class SecondaryConnection {
public:
SecondaryConnection(
std::unique_ptr<ILocalConnection> toPage,
JsiIntegrationPortableTestBase<EngineAdapter, Executor> &test,
size_t remoteConnectionIndex)
: toPage_(std::move(toPage)), remoteConnectionIndex_(remoteConnectionIndex), test_(test)
{
}
ILocalConnection &toPage()
{
return *toPage_;
}
MockRemoteConnection &fromPage()
{
return *test_.remoteConnections_[remoteConnectionIndex_];
}
private:
std::unique_ptr<ILocalConnection> toPage_;
size_t remoteConnectionIndex_;
JsiIntegrationPortableTestBase<EngineAdapter, Executor> &test_;
};
SecondaryConnection connectSecondary(std::source_location location = std::source_location::current())
{
auto toPage = page_->connect(remoteConnections_.make_unique());
SecondaryConnection secondary{std::move(toPage), *this, remoteConnections_.objectsVended() - 1};
using namespace ::testing;
// Default to ignoring console messages originating inside the backend.
EXPECT_CALL_WITH_SOURCE_LOCATION(
location,
secondary.fromPage(),
onMessage(JsonParsed(AllOf(
AtJsonPtr("/method", "Runtime.consoleAPICalled"), AtJsonPtr("/params/context", "main#InstanceAgent")))))
.Times(AnyNumber());
// We'll always get an onDisconnect call when we tear
// down the test. Expect it in order to satisfy the strict mock.
EXPECT_CALL_WITH_SOURCE_LOCATION(location, secondary.fromPage(), onDisconnect());
return secondary;
}
std::shared_ptr<HostTarget> page_;
InstanceTarget *instance_{};
RuntimeTarget *runtimeTarget_{};
InspectorFlagOverridesGuard inspectorFlagsGuard_;
MockInstanceTargetDelegate instanceTargetDelegate_;
std::optional<EngineAdapter> engineAdapter_;
private:
UniquePtrFactory<::testing::StrictMock<MockRemoteConnection>> remoteConnections_;
protected:
// NOTE: Needs to be destroyed before page_.
std::unique_ptr<ILocalConnection> toPage_;
private:
// HostTargetDelegate methods
HostTargetMetadata getMetadata() override
{
return {.integrationName = "JsiIntegrationTest"};
}
void onReload(const PageReloadRequest &request) override
{
(void)request;
reload();
}
void onSetPausedInDebuggerMessage(const OverlaySetPausedInDebuggerMessageRequest & /*request*/) override {}
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,825 @@
/*
* 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 "JsiIntegrationTest.h"
#include "engines/JsiIntegrationTestHermesEngineAdapter.h"
#include <folly/executors/QueuedImmediateExecutor.h>
#include <jsinspector-modern/InspectorFlags.h>
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <react/networking/NetworkReporter.h>
using namespace ::testing;
namespace facebook::react::jsinspector_modern {
namespace {
struct NetworkReporterTestParams {
bool enableNetworkEventReporting;
};
} // namespace
/**
* A test fixture for the way the internal NetworkReporter API interacts with
* the CDP Network and Tracing domains.
*/
template <typename Params>
requires std::convertible_to<Params, NetworkReporterTestParams>
class NetworkReporterTestBase : public JsiIntegrationPortableTestBase<
JsiIntegrationTestHermesEngineAdapter,
folly::QueuedImmediateExecutor>,
public WithParamInterface<Params> {
protected:
NetworkReporterTestBase()
: JsiIntegrationPortableTestBase({
.networkInspectionEnabled = true,
.enableNetworkEventReporting =
WithParamInterface<Params>::GetParam()
.enableNetworkEventReporting,
}) {}
void SetUp() override {
JsiIntegrationPortableTestBase::SetUp();
connect();
EXPECT_CALL(
fromPage(),
onMessage(
JsonParsed(AllOf(AtJsonPtr("/method", "Debugger.scriptParsed")))))
.Times(AnyNumber())
.WillRepeatedly(Invoke<>([this](const std::string& message) {
auto params = folly::parseJson(message);
// Store the script ID and URL for later use.
scriptUrlsById_.emplace(
params.at("params").at("scriptId").getString(),
params.at("params").at("url").getString());
}));
}
template <typename InnerMatcher>
Matcher<folly::dynamic> ScriptIdMapsTo(InnerMatcher urlMatcher) {
return ResultOf(
[this](const auto& id) { return getScriptUrlById(id.getString()); },
urlMatcher);
}
void startTracing() {
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Tracing.start"
})");
}
/**
* Helper method to end tracing and collect all trace events from potentially
* multiple chunked Tracing.dataCollected messages.
* \returns A vector containing all collected trace events
*/
std::vector<folly::dynamic> endTracingAndCollectEvents() {
InSequence s;
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
std::vector<folly::dynamic> allTraceEvents;
EXPECT_CALL(
fromPage(),
onMessage(JsonParsed(AtJsonPtr("/method", "Tracing.dataCollected"))))
.Times(AtLeast(1))
.WillRepeatedly(Invoke([&allTraceEvents](const std::string& message) {
auto parsedMessage = folly::parseJson(message);
auto& events = parsedMessage.at("params").at("value");
allTraceEvents.insert(
allTraceEvents.end(),
std::make_move_iterator(events.begin()),
std::make_move_iterator(events.end()));
}));
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Tracing.tracingComplete"),
AtJsonPtr("/params/dataLossOccurred", false))));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Tracing.end"
})");
return allTraceEvents;
}
private:
std::optional<std::string> getScriptUrlById(const std::string& scriptId) {
auto it = scriptUrlsById_.find(scriptId);
if (it == scriptUrlsById_.end()) {
return std::nullopt;
}
return it->second;
}
std::unordered_map<std::string, std::string> scriptUrlsById_;
};
using NetworkReporterTest = NetworkReporterTestBase<NetworkReporterTestParams>;
TEST_P(NetworkReporterTest, testNetworkEnableDisable) {
InSequence s;
EXPECT_FALSE(NetworkReporter::getInstance().isDebuggingEnabled());
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Network.enable"
})");
EXPECT_TRUE(NetworkReporter::getInstance().isDebuggingEnabled());
this->expectMessageFromPage(JsonEq(R"({
"id": 2,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Network.disable"
})");
EXPECT_FALSE(NetworkReporter::getInstance().isDebuggingEnabled());
}
TEST_P(NetworkReporterTest, testGetMissingResponseBody) {
InSequence s;
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Network.enable"
})");
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/error/code", (int)cdp::ErrorCode::InternalError),
AtJsonPtr("/id", 2))));
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Network.getResponseBody",
"params": {
"requestId": "1234567890-no-such-request"
}
})");
this->expectMessageFromPage(JsonEq(R"({
"id": 3,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 3,
"method": "Network.disable"
})");
}
TEST_P(NetworkReporterTest, testRequestWillBeSentWithRedirect) {
InSequence s;
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Network.enable"
})");
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.requestWillBeSent"),
AtJsonPtr("/params/requestId", "test-request-1"),
AtJsonPtr("/params/loaderId", ""),
AtJsonPtr("/params/documentURL", "mobile"),
AtJsonPtr("/params/request/url", "https://example.com/redirected"),
AtJsonPtr("/params/request/method", "POST"),
AtJsonPtr("/params/request/headers/Content-Type", "application/json"),
AtJsonPtr("/params/timestamp", Gt(0)),
AtJsonPtr("/params/wallTime", Gt(0)),
AtJsonPtr("/params/initiator/type", "script"),
AtJsonPtr("/params/redirectHasExtraInfo", true),
AtJsonPtr("/params/redirectResponse", Not(IsEmpty())),
AtJsonPtr("/params/redirectResponse/url", "https://example.com/original"),
AtJsonPtr("/params/redirectResponse/status", 302),
AtJsonPtr(
"/params/redirectResponse/headers/Location",
"https://example.com/redirected"))));
RequestInfo requestInfo;
requestInfo.url = "https://example.com/redirected";
requestInfo.httpMethod = "POST";
requestInfo.headers = Headers{{"Content-Type", "application/json"}};
ResponseInfo redirectResponse;
redirectResponse.url = "https://example.com/original";
redirectResponse.statusCode = 302;
redirectResponse.headers =
Headers{{"Location", "https://example.com/redirected"}};
NetworkReporter::getInstance().reportRequestStart(
"test-request-1", requestInfo, 1024, redirectResponse);
this->expectMessageFromPage(JsonEq(R"({
"id": 2,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Network.disable"
})");
}
TEST_P(NetworkReporterTest, testRequestWillBeSentExtraInfoParameters) {
InSequence s;
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Network.enable"
})");
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.requestWillBeSentExtraInfo"),
AtJsonPtr("/params/requestId", "test-extra-info"),
AtJsonPtr("/params/headers/User-Agent", "TestAgent"),
AtJsonPtr("/params/headers/Accept-Language", "en-US"),
AtJsonPtr("/params/connectTiming/requestTime", Gt(0)))));
Headers extraHeaders = {
{"User-Agent", "TestAgent"}, {"Accept-Language", "en-US"}};
NetworkReporter::getInstance().reportConnectionTiming(
"test-extra-info", extraHeaders);
this->expectMessageFromPage(JsonEq(R"({
"id": 2,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Network.disable"
})");
}
TEST_P(NetworkReporterTest, testLoadingFailedCancelled) {
InSequence s;
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Network.enable"
})");
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.loadingFailed"),
AtJsonPtr("/params/requestId", "test-request-1"),
AtJsonPtr("/params/timestamp", Gt(0)),
AtJsonPtr("/params/type", "Other"),
AtJsonPtr("/params/errorText", "net::ERR_ABORTED"),
AtJsonPtr("/params/canceled", true))));
NetworkReporter::getInstance().reportRequestFailed("test-request-1", true);
this->expectMessageFromPage(JsonEq(R"({
"id": 2,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Network.disable"
})");
}
TEST_P(NetworkReporterTest, testLoadingFailedError) {
InSequence s;
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Network.enable"
})");
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.loadingFailed"),
AtJsonPtr("/params/requestId", "test-request-1"),
AtJsonPtr("/params/timestamp", Gt(0)),
AtJsonPtr("/params/type", "Other"),
AtJsonPtr("/params/errorText", "net::ERR_FAILED"),
AtJsonPtr("/params/canceled", false))));
NetworkReporter::getInstance().reportRequestFailed("test-request-1", false);
this->expectMessageFromPage(JsonEq(R"({
"id": 2,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Network.disable"
})");
}
TEST_P(NetworkReporterTest, testCompleteNetworkFlow) {
InSequence s;
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Network.enable"
})");
const std::string requestId = "complete-flow-request";
// Step 1: Request will be sent
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.requestWillBeSent"),
AtJsonPtr("/params/requestId", requestId),
AtJsonPtr("/params/loaderId", ""),
AtJsonPtr("/params/documentURL", "mobile"),
AtJsonPtr("/params/request/url", "https://api.example.com/users"),
AtJsonPtr("/params/request/method", "GET"),
AtJsonPtr("/params/request/headers/Accept", "application/json"),
AtJsonPtr("/params/timestamp", Gt(0)),
AtJsonPtr("/params/wallTime", Gt(0)),
AtJsonPtr("/params/initiator/type", "script"),
AtJsonPtr("/params/redirectHasExtraInfo", false))));
RequestInfo requestInfo;
requestInfo.url = "https://api.example.com/users";
requestInfo.httpMethod = "GET";
requestInfo.headers = Headers{{"Accept", "application/json"}};
NetworkReporter::getInstance().reportRequestStart(
requestId, requestInfo, 0, std::nullopt);
// Step 2: Connection timing
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.requestWillBeSentExtraInfo"),
AtJsonPtr("/params/requestId", requestId),
AtJsonPtr("/params/headers/Accept", "application/json"),
AtJsonPtr("/params/connectTiming/requestTime", Gt(0)))));
NetworkReporter::getInstance().reportConnectionTiming(
requestId, requestInfo.headers);
// Step 3: Response received
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.responseReceived"),
AtJsonPtr("/params/requestId", requestId),
AtJsonPtr("/params/loaderId", ""),
AtJsonPtr("/params/timestamp", Gt(0)),
AtJsonPtr("/params/type", "XHR"),
AtJsonPtr("/params/response/url", "https://api.example.com/users"),
AtJsonPtr("/params/response/status", 200),
AtJsonPtr("/params/response/statusText", "OK"),
AtJsonPtr("/params/response/headers/Content-Type", "application/json"),
AtJsonPtr("/params/response/headers/Content-Length", "1024"),
AtJsonPtr("/params/response/mimeType", "application/json"),
AtJsonPtr("/params/response/encodedDataLength", 1024),
AtJsonPtr("/params/hasExtraInfo", false))));
ResponseInfo responseInfo;
responseInfo.url = "https://api.example.com/users";
responseInfo.statusCode = 200;
responseInfo.headers =
Headers{{"Content-Type", "application/json"}, {"Content-Length", "1024"}};
NetworkReporter::getInstance().reportResponseStart(
requestId, responseInfo, 1024);
// Step 4: Data received (multiple chunks)
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.dataReceived"),
AtJsonPtr("/params/requestId", requestId),
AtJsonPtr("/params/timestamp", Gt(0)),
AtJsonPtr("/params/dataLength", 512),
AtJsonPtr("/params/encodedDataLength", 512))));
NetworkReporter::getInstance().reportDataReceived(requestId, 512, 512);
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.dataReceived"),
AtJsonPtr("/params/requestId", requestId),
AtJsonPtr("/params/timestamp", Gt(0)),
AtJsonPtr("/params/dataLength", 512),
AtJsonPtr("/params/encodedDataLength", 512))));
NetworkReporter::getInstance().reportDataReceived(requestId, 512, 512);
// Step 5: Loading finished
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.loadingFinished"),
AtJsonPtr("/params/requestId", requestId),
AtJsonPtr("/params/timestamp", Gt(0)),
AtJsonPtr("/params/encodedDataLength", 1024))));
NetworkReporter::getInstance().reportResponseEnd(requestId, 1024);
// Store and retrieve response body
NetworkReporter::getInstance().storeResponseBody(
requestId, R"({"users": [{"id": 1, "name": "John"}]})", false);
this->expectMessageFromPage(JsonEq(R"({
"id": 2,
"result": {
"body": "{\"users\": [{\"id\": 1, \"name\": \"John\"}]}",
"base64Encoded": false
}
})"));
this->toPage_->sendMessage(
fmt::format(
R"({{
"id": 2,
"method": "Network.getResponseBody",
"params": {{
"requestId": {0}
}}
}})",
folly::toJson(requestId)));
this->expectMessageFromPage(JsonEq(R"({
"id": 3,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 3,
"method": "Network.disable"
})");
}
TEST_P(NetworkReporterTest, testGetResponseBodyWithBase64) {
InSequence s;
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Network.enable"
})");
const std::string requestId = "base64-response-test";
// Store base64-encoded response body
NetworkReporter::getInstance().storeResponseBody(
requestId, "SGVsbG8gV29ybGQ=", true);
this->expectMessageFromPage(JsonEq(R"({
"id": 2,
"result": {
"body": "SGVsbG8gV29ybGQ=",
"base64Encoded": true
}
})"));
this->toPage_->sendMessage(
fmt::format(
R"({{
"id": 2,
"method": "Network.getResponseBody",
"params": {{
"requestId": {0}
}}
}})",
folly::toJson(requestId)));
this->expectMessageFromPage(JsonEq(R"({
"id": 3,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 3,
"method": "Network.disable"
})");
}
TEST_P(NetworkReporterTest, testNetworkEventsWhenDisabled) {
EXPECT_FALSE(NetworkReporter::getInstance().isDebuggingEnabled());
// NOTE: The test will automatically fail if any unexpected CDP messages are
// received as a result of the following calls.
RequestInfo requestInfo;
requestInfo.url = "https://example.com/disabled";
requestInfo.httpMethod = "GET";
NetworkReporter::getInstance().reportRequestStart(
"disabled-request", requestInfo, 0, std::nullopt);
ResponseInfo responseInfo;
responseInfo.url = "https://example.com/disabled";
responseInfo.statusCode = 200;
NetworkReporter::getInstance().reportConnectionTiming("disabled-request", {});
NetworkReporter::getInstance().reportResponseStart(
"disabled-request", responseInfo, 1024);
NetworkReporter::getInstance().reportDataReceived(
"disabled-request", 512, 512);
NetworkReporter::getInstance().reportResponseEnd("disabled-request", 1024);
NetworkReporter::getInstance().reportRequestFailed("disabled-request", false);
}
TEST_P(NetworkReporterTest, testRequestWillBeSentWithInitiator) {
InSequence s;
this->expectMessageFromPage(JsonEq(R"({
"id": 0,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 0,
"method": "Debugger.enable"
})");
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Network.enable"
})");
RequestInfo requestInfo;
requestInfo.url = "https://example.com/initiator";
requestInfo.httpMethod = "GET";
auto& runtime = engineAdapter_->getRuntime();
auto requestId = this->eval(R"( // line 0
function inner() { // line 1
return globalThis.__NETWORK_REPORTER__.createDevToolsRequestId(); // line 2
} // line 3
function outer() { // line 4
return inner(); // line 5
} // line 6
outer(); // line 7
//# sourceURL=initiatorTest.js
)")
.asString(runtime)
.utf8(runtime);
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.requestWillBeSent"),
AtJsonPtr("/params/requestId", requestId),
AtJsonPtr("/params/initiator/type", "script"),
AtJsonPtr(
"/params/initiator/stack/callFrames",
AllOf(
Each(AllOf(
AtJsonPtr("/url", "initiatorTest.js"),
AtJsonPtr(
"/scriptId", this->ScriptIdMapsTo("initiatorTest.js")))),
ElementsAre(
AllOf(
AtJsonPtr("/functionName", "inner"),
AtJsonPtr("/lineNumber", 2)),
AllOf(
AtJsonPtr("/functionName", "outer"),
AtJsonPtr("/lineNumber", 5)),
AllOf(
AtJsonPtr("/functionName", "global"),
AtJsonPtr("/lineNumber", 7))))))));
NetworkReporter::getInstance().reportRequestStart(
requestId, requestInfo, 0, std::nullopt);
this->expectMessageFromPage(JsonEq(R"({
"id": 2,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Network.disable"
})");
}
TEST_P(NetworkReporterTest, testCreateRequestIdWithoutNetworkDomain) {
InSequence s;
auto& runtime = engineAdapter_->getRuntime();
auto id1 = this->eval(R"(
globalThis.__NETWORK_REPORTER__.createDevToolsRequestId();
)")
.asString(runtime)
.utf8(runtime);
EXPECT_NE(id1, "");
auto id2 = this->eval(R"(
globalThis.__NETWORK_REPORTER__.createDevToolsRequestId();
)")
.asString(runtime)
.utf8(runtime);
EXPECT_NE(id2, "");
EXPECT_NE(id1, id2);
}
struct NetworkReporterTracingTestParams {
bool enableNetworkEventReporting;
bool enableNetworkDomain;
operator NetworkReporterTestParams() const {
return NetworkReporterTestParams{
.enableNetworkEventReporting = enableNetworkEventReporting,
};
}
};
using NetworkReporterTracingTest =
NetworkReporterTestBase<NetworkReporterTracingTestParams>;
TEST_P(
NetworkReporterTracingTest,
testReportsToTracingDomainPlusNetworkDomain) {
InSequence s;
this->startTracing();
if (GetParam().enableNetworkDomain) {
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Network.enable"
})");
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.requestWillBeSent"),
AtJsonPtr("/params/requestId", "trace-events-request"),
AtJsonPtr("/params/loaderId", ""),
AtJsonPtr("/params/documentURL", "mobile"),
AtJsonPtr("/params/request/url", "https://trace.example.com/events"),
AtJsonPtr("/params/request/method", "GET"),
AtJsonPtr("/params/request/headers/Accept", "application/json"),
AtJsonPtr("/params/timestamp", Gt(0)),
AtJsonPtr("/params/wallTime", Gt(0)),
AtJsonPtr("/params/initiator/type", "script"),
AtJsonPtr("/params/redirectHasExtraInfo", false))));
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.requestWillBeSentExtraInfo"),
AtJsonPtr("/params/requestId", "trace-events-request"),
AtJsonPtr("/params/associatedCookies", "[]"_json),
AtJsonPtr("/params/headers", "{}"_json),
AtJsonPtr("/params/connectTiming/requestTime", Gt(0)))));
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.responseReceived"),
AtJsonPtr("/params/requestId", "trace-events-request"),
AtJsonPtr("/params/loaderId", ""),
AtJsonPtr("/params/timestamp", Gt(0)),
AtJsonPtr("/params/type", "XHR"),
AtJsonPtr("/params/response/url", "https://trace.example.com/events"),
AtJsonPtr("/params/response/status", 200),
AtJsonPtr("/params/response/statusText", "OK"),
AtJsonPtr("/params/response/headers/Content-Type", "application/json"),
AtJsonPtr("/params/response/mimeType", "application/json"),
AtJsonPtr("/params/response/encodedDataLength", 1024),
AtJsonPtr("/params/hasExtraInfo", false))));
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.loadingFinished"),
AtJsonPtr("/params/requestId", "trace-events-request"),
AtJsonPtr("/params/timestamp", Gt(0)),
AtJsonPtr("/params/encodedDataLength", 1024))));
}
NetworkReporter::getInstance().reportRequestStart(
"trace-events-request",
{
.url = "https://trace.example.com/events",
.httpMethod = "GET",
.headers = Headers{{"Accept", "application/json"}},
},
0,
std::nullopt);
NetworkReporter::getInstance().reportConnectionTiming(
"trace-events-request", std::nullopt);
NetworkReporter::getInstance().reportResponseStart(
"trace-events-request",
{
.url = "https://trace.example.com/events",
.statusCode = 200,
.headers = Headers{{"Content-Type", "application/json"}},
},
1024);
NetworkReporter::getInstance().reportResponseEnd(
"trace-events-request", 1024);
auto allTraceEvents = endTracingAndCollectEvents();
EXPECT_THAT(
allTraceEvents,
Contains(AllOf(
AtJsonPtr("/name", "ResourceSendRequest"),
AtJsonPtr("/cat", "devtools.timeline"),
AtJsonPtr("/ph", "I"),
AtJsonPtr("/s", "t"),
AtJsonPtr("/tid", oscompat::getCurrentThreadId()),
AtJsonPtr("/pid", oscompat::getCurrentProcessId()),
AtJsonPtr("/args/data/initiator", "{}"_json),
AtJsonPtr("/args/data/requestId", "trace-events-request"),
AtJsonPtr("/args/data/url", "https://trace.example.com/events"),
AtJsonPtr("/args/data/requestMethod", "GET"),
AtJsonPtr("/args/data/priority", "VeryHigh"),
AtJsonPtr("/args/data/renderBlocking", "non_blocking"),
AtJsonPtr("/args/data/resourceType", "Other"))));
EXPECT_THAT(
allTraceEvents,
Contains(AllOf(
AtJsonPtr("/name", "ResourceReceiveResponse"),
AtJsonPtr("/cat", "devtools.timeline"),
AtJsonPtr("/ph", "I"),
AtJsonPtr("/s", "t"),
AtJsonPtr("/tid", oscompat::getCurrentThreadId()),
AtJsonPtr("/pid", oscompat::getCurrentProcessId()),
AtJsonPtr("/ts", Gt(0)),
AtJsonPtr("/args/data/requestId", "trace-events-request"),
AtJsonPtr("/args/data/statusCode", 200),
AtJsonPtr("/args/data/mimeType", "application/json"),
AtJsonPtr("/args/data/protocol", "h2"),
AtJsonPtr("/args/data/encodedDataLength", 1024),
AtJsonPtr(
"/args/data/headers",
R"([{ "name": "Content-Type", "value": "application/json" }])"_json),
AtJsonPtr(
"/args/data/timing",
AllOf(
AtJsonPtr("/requestTime", Ge(0)),
AtJsonPtr("/sendStart", Ge(0)),
AtJsonPtr("/sendEnd", Ge(0)),
AtJsonPtr("/receiveHeadersStart", Ge(0)),
AtJsonPtr("/receiveHeadersEnd", Ge(0)))))));
EXPECT_THAT(
allTraceEvents,
Contains(AllOf(
AtJsonPtr("/name", "ResourceFinish"),
AtJsonPtr("/cat", "devtools.timeline"),
AtJsonPtr("/ph", "I"),
AtJsonPtr("/s", "t"),
AtJsonPtr("/tid", oscompat::getCurrentThreadId()),
AtJsonPtr("/pid", oscompat::getCurrentProcessId()),
AtJsonPtr("/args/data/requestId", "trace-events-request"),
AtJsonPtr("/args/data/encodedDataLength", 1024),
AtJsonPtr("/args/data/decodedBodyLength", 0),
AtJsonPtr("/args/data/didFail", false))));
}
static const auto networkReporterTestParamValues = testing::Values(
NetworkReporterTestParams{.enableNetworkEventReporting = true},
NetworkReporterTestParams{
.enableNetworkEventReporting = false,
});
static const auto networkReporterTracingTestParamValues = testing::Values(
NetworkReporterTracingTestParams{
.enableNetworkEventReporting = true,
.enableNetworkDomain = true},
NetworkReporterTracingTestParams{
.enableNetworkEventReporting = true,
.enableNetworkDomain = false});
INSTANTIATE_TEST_SUITE_P(
NetworkReporterTest,
NetworkReporterTest,
networkReporterTestParamValues);
INSTANTIATE_TEST_SUITE_P(
NetworkReporterTracingTest,
NetworkReporterTracingTest,
networkReporterTracingTestParamValues);
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,229 @@
/*
* 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 "ReactInstanceIntegrationTest.h"
#include "FollyDynamicMatchers.h"
#include "UniquePtrFactory.h"
#include "prelude.js.h"
#include <folly/json.h>
#include <glog/logging.h>
#include <jsinspector-modern/InspectorFlags.h>
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <react/runtime/hermes/HermesInstance.h>
using namespace ::testing;
namespace facebook::react::jsinspector_modern {
#pragma region ReactInstanceIntegrationTest
ReactInstanceIntegrationTest::ReactInstanceIntegrationTest()
: runtime(nullptr),
instance(nullptr),
messageQueueThread(std::make_shared<MockMessageQueueThread>()),
errorHandler(std::make_shared<ErrorUtils>()),
testMode_(GetParam()) {}
void ReactInstanceIntegrationTest::SetUp() {
// Valdiate that the test mode in InspectorFlags is set correctly
EXPECT_EQ(
InspectorFlags::getInstance().getFuseboxEnabled(),
testMode_ == ReactInstanceIntegrationTestMode::FUSEBOX);
auto mockRegistry = std::make_unique<MockTimerRegistry>();
auto timerManager =
std::make_shared<react::TimerManager>(std::move(mockRegistry));
auto onJsError = [](jsi::Runtime& /*runtime*/,
const JsErrorHandler::ProcessedError& error) noexcept {
LOG(INFO) << "[jsErrorHandlingFunc called]";
LOG(INFO) << error << std::endl;
};
auto jsRuntimeFactory = std::make_unique<react::HermesInstance>();
std::unique_ptr<react::JSRuntime> runtime_ =
jsRuntimeFactory->createJSRuntime(nullptr, messageQueueThread, false);
jsi::Runtime* jsiRuntime = &runtime_->getRuntime();
// Error handler:
jsiRuntime->global().setProperty(
*jsiRuntime,
"ErrorUtils",
jsi::Object::createFromHostObject(*jsiRuntime, errorHandler));
std::shared_ptr<HostTarget> hostTargetIfModernCDP = nullptr;
if (InspectorFlags::getInstance().getFuseboxEnabled()) {
VoidExecutor inspectorExecutor = [this](auto callback) {
immediateExecutor_.add(callback);
};
hostTargetIfModernCDP =
HostTarget::create(hostTargetDelegate_, inspectorExecutor);
}
instance = std::make_unique<react::ReactInstance>(
std::move(runtime_),
messageQueueThread,
timerManager,
std::move(onJsError),
hostTargetIfModernCDP == nullptr ? nullptr : hostTargetIfModernCDP.get());
timerManager->setRuntimeExecutor(instance->getBufferedRuntimeExecutor());
// JS Environment:
initializeRuntime(preludeJsCode);
// Inspector:
auto& inspector = getInspectorInstance();
ASSERT_NE(hostTargetIfModernCDP, nullptr);
// Under modern CDP, the React host is responsible for adding itself as
// the root target on startup.
pageId_ = inspector.addPage(
"mock-description",
"mock-vm",
[hostTargetIfModernCDP](std::unique_ptr<IRemoteConnection> remote)
-> std::unique_ptr<ILocalConnection> {
auto localConnection =
hostTargetIfModernCDP->connect(std::move(remote));
return localConnection;
},
// TODO: Allow customisation of InspectorTargetCapabilities
{});
clientToVM_ =
inspector.connect(pageId_.value(), mockRemoteConnections_.make_unique());
ASSERT_NE(clientToVM_, nullptr);
// Default to ignoring console messages originating inside the backend.
EXPECT_CALL(
getRemoteConnection(),
onMessage(JsonParsed(AllOf(
AtJsonPtr("/method", "Runtime.consoleAPICalled"),
AtJsonPtr("/params/context", "main#InstanceAgent")))))
.Times(AnyNumber());
}
void ReactInstanceIntegrationTest::TearDown() {
clientToVM_->disconnect();
// Destroy the local connection.
clientToVM_.reset();
if (pageId_.has_value() &&
InspectorFlags::getInstance().getFuseboxEnabled()) {
// Under modern CDP, clean up the page we added in SetUp and destroy
// resources owned by HostTarget.
getInspectorInstance().removePage(pageId_.value());
}
pageId_.reset();
// Expect the remote connection to have been destroyed.
EXPECT_EQ(mockRemoteConnections_[0], nullptr);
// Make sure that any dangerous overriding is removed before the next test.
// Seemingly, we need both of these to cleanly reset and not break subsequent
// tests.
InspectorFlags::getInstance().dangerouslyResetFlags();
ReactNativeFeatureFlags::dangerouslyReset();
}
void ReactInstanceIntegrationTest::initializeRuntime(std::string_view script) {
react::ReactInstance::JSRuntimeFlags flags{
.isProfiling = false,
};
instance->initializeRuntime(flags, [](jsi::Runtime& rt) {
// NOTE: RN's console polyfill (included in prelude.js.h) depends on the
// native logging hook being installed, even if it's a noop.
facebook::react::bindNativeLogger(rt, [](auto, auto) {});
});
messageQueueThread->tick();
std::string init(script);
// JS calls no longer buffered after calling loadScript
instance->loadScript(std::make_unique<react::JSBigStdString>(init), "");
}
void ReactInstanceIntegrationTest::send(
const std::string& method,
const folly::dynamic& params) {
folly::dynamic request = folly::dynamic::object();
request["method"] = method;
request["id"] = id_++;
request["params"] = params;
sendJSONString(folly::toJson(request));
}
void ReactInstanceIntegrationTest::sendJSONString(const std::string& message) {
// The runtime must be initialized and connected to before messaging
clientToVM_->sendMessage(message);
}
jsi::Value ReactInstanceIntegrationTest::run(const std::string& script) {
auto runtimeExecutor = instance->getUnbufferedRuntimeExecutor();
auto ret = jsi::Value::undefined();
runtimeExecutor([script, &ret](jsi::Runtime& rt) {
ret = rt.evaluateJavaScript(
std::make_unique<jsi::StringBuffer>(script), "<test>");
});
messageQueueThread->flush();
while (verbose_ && errorHandler->size() > 0) {
LOG(INFO) << "Error: " << errorHandler->getLastError().getMessage();
}
return ret;
}
bool ReactInstanceIntegrationTest::verbose(bool isVerbose) {
const bool previous = verbose_;
verbose_ = isVerbose;
return previous;
}
#pragma endregion
TEST_P(ReactInstanceIntegrationTest, RuntimeEvalTest) {
auto val = run("1 + 2");
EXPECT_EQ(val.asNumber(), 3);
}
TEST_P(ReactInstanceIntegrationTest, ConsoleLog) {
EXPECT_CALL(
getRemoteConnection(),
onMessage(JsonParsed(
AtJsonPtr("/method", Eq("Runtime.executionContextCreated")))));
EXPECT_CALL(
getRemoteConnection(), onMessage(JsonParsed(AtJsonPtr("/id", Eq(1)))));
InSequence s;
EXPECT_CALL(
getRemoteConnection(),
onMessage(JsonParsed(AllOf(
AtJsonPtr("/params/args/0/value", Eq("Hello, World!")),
AtJsonPtr("/method", Eq("Runtime.consoleAPICalled"))))));
EXPECT_CALL(getRemoteConnection(), onDisconnect());
send("Runtime.enable");
run("console.log('Hello, World!');");
}
INSTANTIATE_TEST_SUITE_P(
ReactInstanceVaryingInspectorBackend,
ReactInstanceIntegrationTest,
::testing::Values(ReactInstanceIntegrationTestMode::FUSEBOX));
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,70 @@
/*
* 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 "InspectorMocks.h"
#include "ReactNativeMocks.h"
#include "UniquePtrFactory.h"
#include <folly/executors/QueuedImmediateExecutor.h>
#include <folly/json.h>
#include <gtest/gtest.h>
#include <jsinspector-modern/InspectorInterfaces.h>
#include <cassert>
#include <memory>
namespace facebook::react::jsinspector_modern {
using namespace ::testing;
enum ReactInstanceIntegrationTestMode {
FUSEBOX,
};
class ReactInstanceIntegrationTest : public Test,
public ::testing::WithParamInterface<ReactInstanceIntegrationTestMode> {
protected:
ReactInstanceIntegrationTest();
void SetUp() override;
void TearDown() override;
jsi::Value run(const std::string &script);
bool verbose(bool isVerbose);
void send(const std::string &method, const folly::dynamic &params = folly::dynamic::object());
void sendJSONString(const std::string &message);
jsi::Runtime *runtime;
std::unique_ptr<react::ReactInstance> instance;
std::shared_ptr<MockMessageQueueThread> messageQueueThread;
std::shared_ptr<ErrorUtils> errorHandler;
NiceMock<MockRemoteConnection> &getRemoteConnection()
{
EXPECT_EQ(mockRemoteConnections_.objectsVended(), 1);
auto rawPtr = mockRemoteConnections_[0];
assert(rawPtr);
return *rawPtr;
}
private:
void initializeRuntime(std::string_view script);
ReactInstanceIntegrationTestMode testMode_;
size_t id_ = 1;
bool verbose_ = false;
std::optional<int> pageId_;
UniquePtrFactory<NiceMock<MockRemoteConnection>> mockRemoteConnections_;
std::unique_ptr<ILocalConnection> clientToVM_;
folly::QueuedImmediateExecutor immediateExecutor_;
MockHostTargetDelegate hostTargetDelegate_;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,94 @@
/*
* 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 "ReactNativeMocks.h"
#include <glog/logging.h>
namespace facebook::react::jsinspector_modern {
//
// MockMessageQueueThread
//
void MockMessageQueueThread::runOnQueue(std::function<void()>&& func) {
callbackQueue_.push(func);
}
void MockMessageQueueThread::tick() {
if (!callbackQueue_.empty()) {
auto callback = callbackQueue_.front();
callback();
callbackQueue_.pop();
}
}
void MockMessageQueueThread::guardedTick() {
try {
tick();
} catch (const std::exception& e) {
// For easier debugging
FAIL() << e.what();
}
}
void MockMessageQueueThread::flush() {
while (!callbackQueue_.empty()) {
tick();
}
}
size_t MockMessageQueueThread::size() {
return callbackQueue_.size();
}
void MockMessageQueueThread::quitSynchronous() {
assert(false && "Not implemented");
}
void MockMessageQueueThread::runOnQueueSync(std::function<void()>&& callback) {
callback();
}
//
// ErrorUtils
//
jsi::Value ErrorUtils::get(jsi::Runtime& rt, const jsi::PropNameID& name) {
auto methodName = name.utf8(rt);
if (methodName == "reportFatalError") {
return jsi::Function::createFromHostFunction(
rt,
name,
1,
[this](
jsi::Runtime& runtime,
/* thisValue */ const jsi::Value&,
const jsi::Value* arguments,
size_t count) {
if (count >= 1) {
auto value = jsi::Value(runtime, arguments[0]);
auto error = jsi::JSError(runtime, std::move(value));
LOG(INFO) << "JSI Fatal: " << error.getMessage();
reportFatalError(std::move(error));
}
return jsi::Value::undefined();
});
} else {
throw std::runtime_error("Unknown method: " + methodName);
}
}
size_t ErrorUtils::size() {
return errors_.size();
}
jsi::JSError ErrorUtils::getLastError() {
auto error = errors_.back();
errors_.pop_back();
return error;
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,66 @@
/*
* 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 <gmock/gmock.h>
#include <gtest/gtest.h>
#include <queue>
#include <ReactCommon/RuntimeExecutor.h>
#include <jsi/jsi.h>
#include <react/runtime/ReactInstance.h>
namespace facebook::react::jsinspector_modern {
class MockTimerRegistry : public react::PlatformTimerRegistry {
public:
MOCK_METHOD2(createTimer, void(uint32_t, double));
MOCK_METHOD2(createRecurringTimer, void(uint32_t, double));
MOCK_METHOD1(deleteTimer, void(uint32_t));
};
class MockMessageQueueThread : public react::MessageQueueThread {
public:
void runOnQueue(std::function<void()> &&func) override;
// Unused
void runOnQueueSync(std::function<void()> &&callback) override;
// Unused
void quitSynchronous() override;
void tick();
void flush();
void guardedTick();
size_t size();
private:
std::queue<std::function<void()>> callbackQueue_;
};
class ErrorUtils : public jsi::HostObject {
public:
jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &name) override;
void reportFatalError(jsi::JSError &&error)
{
errors_.push_back(std::move(error));
}
size_t size();
jsi::JSError getLastError();
private:
std::vector<jsi::JSError> errors_;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,111 @@
/*
* 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,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 <jsinspector-modern/Utf8.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
using namespace ::testing;
namespace facebook::react::jsinspector_modern {
TEST(Utf8Test, TruncateToValidUtf8) {
auto buffer = std::vector<char>();
std::vector<std::pair<std::string, size_t>> expectedStringsUpToSizes;
// Construct a buffer with a concatenation of all code points, and a vector
// or "expectedStringsUpToSizes", pairs of valid UTF8 prefix strings and sizes
// "n" such that the string would be the expected truncation of the first "n"
// bytes of the buffer.
for (const std::string& codePoint : {
"a", // 1 byte
"é", // 2 bytes
"", // 3 bytes
"😀" // 4 bytes
}) {
auto partial = std::string(buffer.data(), buffer.size());
buffer.insert(buffer.end(), codePoint.begin(), codePoint.end());
expectedStringsUpToSizes.push_back(std::pair(partial, buffer.size()));
}
// The constructed buffer is 10 bytes long, comprised of 4 code points of
// varied size. Range over naive slices of length 0-9 ensuring that the
// truncated result matches the valid UTF8 substring of length <= n.
size_t n = 0;
for (const auto& expectedStringUpToSize : expectedStringsUpToSizes) {
auto nextSize = expectedStringUpToSize.second;
auto expectedString = expectedStringUpToSize.first;
for (; n < nextSize; ++n) {
// Take the first n bytes of the whole buffer, which may be slicing
// through the middle of a code point.
std::vector<char> slice(buffer.begin(), buffer.begin() + n);
truncateToValidUTF8(slice);
// Expect the final code point fragment has been discarded and that the
// contents are equal to expectedString, which is valid UTF8.
EXPECT_EQ(std::string(slice.begin(), slice.end()), expectedString);
}
}
// Finally verify that truncating the whole buffer, which is already valid
// UTF8, is a no-op.
auto wholeString = std::string(buffer.begin(), buffer.end());
truncateToValidUTF8(buffer);
EXPECT_EQ(std::string(buffer.begin(), buffer.end()), wholeString);
}
} // namespace facebook::react::jsinspector_modern

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,39 @@
/*
* 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/hermes.h>
#include "JsiIntegrationTestGenericEngineAdapter.h"
namespace facebook::react::jsinspector_modern {
JsiIntegrationTestGenericEngineAdapter::JsiIntegrationTestGenericEngineAdapter(
folly::Executor& jsExecutor)
: runtime_{hermes::makeHermesRuntime()},
jsExecutor_{jsExecutor},
runtimeTargetDelegate_{
"Generic engine (" + runtime_->description() + ")"} {}
RuntimeTargetDelegate&
JsiIntegrationTestGenericEngineAdapter::getRuntimeTargetDelegate() {
return runtimeTargetDelegate_;
}
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,45 @@
/*
* 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 "../utils/InspectorFlagOverridesGuard.h"
#include <jsinspector-modern/FallbackRuntimeTargetDelegate.h>
#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:
explicit JsiIntegrationTestGenericEngineAdapter(folly::Executor &jsExecutor);
static InspectorFlagOverrides getInspectorFlagOverrides() noexcept;
RuntimeTargetDelegate &getRuntimeTargetDelegate();
jsi::Runtime &getRuntime() const noexcept;
RuntimeExecutor getRuntimeExecutor() const noexcept;
private:
std::unique_ptr<jsi::Runtime> runtime_;
folly::Executor &jsExecutor_;
jsinspector_modern::FallbackRuntimeTargetDelegate runtimeTargetDelegate_;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,52 @@
/*
* 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 "JsiIntegrationTestHermesEngineAdapter.h"
namespace facebook::react::jsinspector_modern {
JsiIntegrationTestHermesEngineAdapter::JsiIntegrationTestHermesEngineAdapter(
folly::Executor& jsExecutor)
: runtime_{hermes::makeHermesRuntime(
::hermes::vm::RuntimeConfig::Builder()
.withCompilationMode(
::hermes::vm::CompilationMode::ForceLazyCompilation)
.build())},
jsExecutor_{jsExecutor},
runtimeTargetDelegate_{runtime_} {
// NOTE: In React Native, registerForProfiling is called by
// HermesInstance::unstable_initializeOnJsThread, called from
// ReactInstance::initializeRuntime. Ideally, we should figure out how to
// manages this from inside the CDP backend,
runtime_->registerForProfiling();
}
RuntimeTargetDelegate&
JsiIntegrationTestHermesEngineAdapter::getRuntimeTargetDelegate() {
return runtimeTargetDelegate_;
}
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,45 @@
/*
* 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 "../utils/InspectorFlagOverridesGuard.h"
#include <jsinspector-modern/RuntimeTarget.h>
#include <folly/executors/QueuedImmediateExecutor.h>
#include <hermes/hermes.h>
#include <hermes/inspector-modern/chrome/HermesRuntimeTargetDelegate.h>
#include <jsi/jsi.h>
#include <memory>
namespace facebook::react::jsinspector_modern {
/**
* An engine adapter for JsiIntegrationTest that uses Hermes (and Hermes'
* modern CDPAgent API).
*/
class JsiIntegrationTestHermesEngineAdapter {
public:
explicit JsiIntegrationTestHermesEngineAdapter(folly::Executor &jsExecutor);
static InspectorFlagOverrides getInspectorFlagOverrides() noexcept;
RuntimeTargetDelegate &getRuntimeTargetDelegate();
jsi::Runtime &getRuntime() const noexcept;
RuntimeExecutor getRuntimeExecutor() const noexcept;
private:
std::shared_ptr<facebook::hermes::HermesRuntime> runtime_;
folly::Executor &jsExecutor_;
HermesRuntimeTargetDelegate runtimeTargetDelegate_;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,636 @@
/**
* 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.
*
* @polyfill
* @nolint
* @format
*/
/* eslint-disable no-shadow, eqeqeq, curly, no-unused-vars, no-void, no-control-regex */
#pragma once
constexpr std::string_view preludeJsCode = R"___(
(function (global, __DEV__) {const inspect = (function () {
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
//
// https://github.com/joyent/node/blob/master/lib/util.js
function inspect(obj, opts) {
var ctx = {
seen: [],
formatValueCalls: 0,
stylize: stylizeNoColor,
};
return formatValue(ctx, obj, opts.depth);
}
function stylizeNoColor(str, styleType) {
return str;
}
function arrayToHash(array) {
var hash = {};
array.forEach(function (val, idx) {
hash[val] = true;
});
return hash;
}
function formatValue(ctx, value, recurseTimes) {
ctx.formatValueCalls++;
if (ctx.formatValueCalls > 200) {
return `[TOO BIG formatValueCalls ${ctx.formatValueCalls} exceeded limit of 200]`;
}
// Primitive types cannot have properties
var primitive = formatPrimitive(ctx, value);
if (primitive) {
return primitive;
}
// Look up the keys of the object.
var keys = Object.keys(value);
var visibleKeys = arrayToHash(keys);
// IE doesn't make error fields non-enumerable
// http://msdn.microsoft.com/en-us/library/ie/dww52sbt(v=vs.94).aspx
if (
isError(value) &&
(keys.indexOf('message') >= 0 || keys.indexOf('description') >= 0)
) {
return formatError(value);
}
// Some type of object without properties can be shortcutted.
if (keys.length === 0) {
if (isFunction(value)) {
var name = value.name ? ': ' + value.name : '';
return ctx.stylize('[Function' + name + ']', 'special');
}
if (isRegExp(value)) {
return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp');
}
if (isDate(value)) {
return ctx.stylize(Date.prototype.toString.call(value), 'date');
}
if (isError(value)) {
return formatError(value);
}
}
var base = '',
array = false,
braces = ['{', '}'];
// Make Array say that they are Array
if (isArray(value)) {
array = true;
braces = ['[', ']'];
}
// Make functions say that they are functions
if (isFunction(value)) {
var n = value.name ? ': ' + value.name : '';
base = ' [Function' + n + ']';
}
// Make RegExps say that they are RegExps
if (isRegExp(value)) {
base = ' ' + RegExp.prototype.toString.call(value);
}
// Make dates with properties first say the date
if (isDate(value)) {
base = ' ' + Date.prototype.toUTCString.call(value);
}
// Make error with message first say the error
if (isError(value)) {
base = ' ' + formatError(value);
}
if (keys.length === 0 && (!array || value.length == 0)) {
return braces[0] + base + braces[1];
}
if (recurseTimes < 0) {
if (isRegExp(value)) {
return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp');
} else {
return ctx.stylize('[Object]', 'special');
}
}
ctx.seen.push(value);
var output;
if (array) {
output = formatArray(ctx, value, recurseTimes, visibleKeys, keys);
} else {
output = keys.map(function (key) {
return formatProperty(
ctx,
value,
recurseTimes,
visibleKeys,
key,
array,
);
});
}
ctx.seen.pop();
return reduceToSingleString(output, base, braces);
}
function formatPrimitive(ctx, value) {
if (isUndefined(value)) return ctx.stylize('undefined', 'undefined');
if (isString(value)) {
var simple =
"'" +
JSON.stringify(value)
.replace(/^"|"$/g, '')
.replace(/'/g, "\\'")
.replace(/\\"/g, '"') +
"'";
return ctx.stylize(simple, 'string');
}
if (isNumber(value)) return ctx.stylize('' + value, 'number');
if (isBoolean(value)) return ctx.stylize('' + value, 'boolean');
// For some reason typeof null is "object", so special case here.
if (isNull(value)) return ctx.stylize('null', 'null');
}
function formatError(value) {
return '[' + Error.prototype.toString.call(value) + ']';
}
function formatArray(ctx, value, recurseTimes, visibleKeys, keys) {
var output = [];
for (var i = 0, l = value.length; i < l; ++i) {
if (hasOwnProperty(value, String(i))) {
output.push(
formatProperty(
ctx,
value,
recurseTimes,
visibleKeys,
String(i),
true,
),
);
} else {
output.push('');
}
}
keys.forEach(function (key) {
if (!key.match(/^\d+$/)) {
output.push(
formatProperty(ctx, value, recurseTimes, visibleKeys, key, true),
);
}
});
return output;
}
function formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) {
var name, str, desc;
desc = Object.getOwnPropertyDescriptor(value, key) || {value: value[key]};
if (desc.get) {
if (desc.set) {
str = ctx.stylize('[Getter/Setter]', 'special');
} else {
str = ctx.stylize('[Getter]', 'special');
}
} else {
if (desc.set) {
str = ctx.stylize('[Setter]', 'special');
}
}
if (!hasOwnProperty(visibleKeys, key)) {
name = '[' + key + ']';
}
if (!str) {
if (ctx.seen.indexOf(desc.value) < 0) {
if (isNull(recurseTimes)) {
str = formatValue(ctx, desc.value, null);
} else {
str = formatValue(ctx, desc.value, recurseTimes - 1);
}
if (str.indexOf('\n') > -1) {
if (array) {
str = str
.split('\n')
.map(function (line) {
return ' ' + line;
})
.join('\n')
.slice(2);
} else {
str =
'\n' +
str
.split('\n')
.map(function (line) {
return ' ' + line;
})
.join('\n');
}
}
} else {
str = ctx.stylize('[Circular]', 'special');
}
}
if (isUndefined(name)) {
if (array && key.match(/^\d+$/)) {
return str;
}
name = JSON.stringify('' + key);
if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) {
name = name.slice(1, name.length - 1);
name = ctx.stylize(name, 'name');
} else {
name = name
.replace(/'/g, "\\'")
.replace(/\\"/g, '"')
.replace(/(^"|"$)/g, "'");
name = ctx.stylize(name, 'string');
}
}
return name + ': ' + str;
}
function reduceToSingleString(output, base, braces) {
var numLinesEst = 0;
var length = output.reduce(function (prev, cur) {
numLinesEst++;
if (cur.indexOf('\n') >= 0) numLinesEst++;
return prev + cur.replace(/\u001b\[\d\d?m/g, '').length + 1;
}, 0);
if (length > 60) {
return (
braces[0] +
(base === '' ? '' : base + '\n ') +
' ' +
output.join(',\n ') +
' ' +
braces[1]
);
}
return braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1];
}
// NOTE: These type checking functions intentionally don't use `instanceof`
// because it is fragile and can be easily faked with `Object.create()`.
function isArray(ar) {
return Array.isArray(ar);
}
function isBoolean(arg) {
return typeof arg === 'boolean';
}
function isNull(arg) {
return arg === null;
}
function isNullOrUndefined(arg) {
return arg == null;
}
function isNumber(arg) {
return typeof arg === 'number';
}
function isString(arg) {
return typeof arg === 'string';
}
function isSymbol(arg) {
return typeof arg === 'symbol';
}
function isUndefined(arg) {
return arg === void 0;
}
function isRegExp(re) {
return isObject(re) && objectToString(re) === '[object RegExp]';
}
function isObject(arg) {
return typeof arg === 'object' && arg !== null;
}
function isDate(d) {
return isObject(d) && objectToString(d) === '[object Date]';
}
function isError(e) {
return (
isObject(e) &&
(objectToString(e) === '[object Error]' || e instanceof Error)
);
}
function isFunction(arg) {
return typeof arg === 'function';
}
function objectToString(o) {
return Object.prototype.toString.call(o);
}
function hasOwnProperty(obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop);
}
return inspect;
})();
const OBJECT_COLUMN_NAME = '(index)';
const LOG_LEVELS = {
trace: 0,
info: 1,
warn: 2,
error: 3,
};
const INSPECTOR_LEVELS = [];
INSPECTOR_LEVELS[LOG_LEVELS.trace] = 'debug';
INSPECTOR_LEVELS[LOG_LEVELS.info] = 'log';
INSPECTOR_LEVELS[LOG_LEVELS.warn] = 'warning';
INSPECTOR_LEVELS[LOG_LEVELS.error] = 'error';
// Strip the inner function in getNativeLogFunction(), if in dev also
// strip method printing to originalConsole.
const INSPECTOR_FRAMES_TO_SKIP = __DEV__ ? 2 : 1;
function getNativeLogFunction(level) {
return function () {
let str;
if (arguments.length === 1 && typeof arguments[0] === 'string') {
str = arguments[0];
} else {
str = Array.prototype.map
.call(arguments, function (arg) {
return inspect(arg, {depth: 10});
})
.join(', ');
}
// TRICKY
// If more than one argument is provided, the code above collapses them all
// into a single formatted string. This transform wraps string arguments in
// single quotes (e.g. "foo" -> "'foo'") which then breaks the "Warning:"
// check below. So it's important that we look at the first argument, rather
// than the formatted argument string.
const firstArg = arguments[0];
let logLevel = level;
if (
typeof firstArg === 'string' &&
firstArg.slice(0, 9) === 'Warning: ' &&
logLevel >= LOG_LEVELS.error
) {
// React warnings use console.error so that a stack trace is shown,
// but we don't (currently) want these to show a redbox
// (Note: Logic duplicated in ExceptionsManager.js.)
logLevel = LOG_LEVELS.warn;
}
if (global.__inspectorLog) {
global.__inspectorLog(
INSPECTOR_LEVELS[logLevel],
str,
[].slice.call(arguments),
INSPECTOR_FRAMES_TO_SKIP,
);
}
if (groupStack.length) {
str = groupFormat('', str);
}
global.nativeLoggingHook(str, logLevel);
};
}
function repeat(element, n) {
return Array.apply(null, Array(n)).map(function () {
return element;
});
}
function consoleTablePolyfill(rows) {
// convert object -> array
if (!Array.isArray(rows)) {
var data = rows;
rows = [];
for (var key in data) {
if (data.hasOwnProperty(key)) {
var row = data[key];
row[OBJECT_COLUMN_NAME] = key;
rows.push(row);
}
}
}
if (rows.length === 0) {
global.nativeLoggingHook('', LOG_LEVELS.info);
return;
}
var columns = Object.keys(rows[0]).sort();
var stringRows = [];
var columnWidths = [];
// Convert each cell to a string. Also
// figure out max cell width for each column
columns.forEach(function (k, i) {
columnWidths[i] = k.length;
for (var j = 0; j < rows.length; j++) {
var cellStr = (rows[j][k] || '?').toString();
stringRows[j] = stringRows[j] || [];
stringRows[j][i] = cellStr;
columnWidths[i] = Math.max(columnWidths[i], cellStr.length);
}
});
// Join all elements in the row into a single string with | separators
// (appends extra spaces to each cell to make separators | aligned)
function joinRow(row, space) {
var cells = row.map(function (cell, i) {
var extraSpaces = repeat(' ', columnWidths[i] - cell.length).join('');
return cell + extraSpaces;
});
space = space || ' ';
return cells.join(space + '|' + space);
}
var separators = columnWidths.map(function (columnWidth) {
return repeat('-', columnWidth).join('');
});
var separatorRow = joinRow(separators, '-');
var header = joinRow(columns);
var table = [header, separatorRow];
for (var i = 0; i < rows.length; i++) {
table.push(joinRow(stringRows[i]));
}
// Notice extra empty line at the beginning.
// Native logging hook adds "RCTLog >" at the front of every
// logged string, which would shift the header and screw up
// the table
global.nativeLoggingHook('\n' + table.join('\n'), LOG_LEVELS.info);
}
const GROUP_PAD = '\u2502'; // Box light vertical
const GROUP_OPEN = '\u2510'; // Box light down+left
const GROUP_CLOSE = '\u2518'; // Box light up+left
const groupStack = [];
function groupFormat(prefix, msg) {
// Insert group formatting before the console message
return groupStack.join('') + prefix + ' ' + (msg || '');
}
function consoleGroupPolyfill(label) {
global.nativeLoggingHook(groupFormat(GROUP_OPEN, label), LOG_LEVELS.info);
groupStack.push(GROUP_PAD);
}
function consoleGroupCollapsedPolyfill(label) {
global.nativeLoggingHook(groupFormat(GROUP_CLOSE, label), LOG_LEVELS.info);
groupStack.push(GROUP_PAD);
}
function consoleGroupEndPolyfill() {
groupStack.pop();
global.nativeLoggingHook(groupFormat(GROUP_CLOSE), LOG_LEVELS.info);
}
function consoleAssertPolyfill(expression, label) {
if (!expression) {
global.nativeLoggingHook('Assertion failed: ' + label, LOG_LEVELS.error);
}
}
if (global.nativeLoggingHook) {
const originalConsole = global.console;
// Preserve the original `console` as `originalConsole`
if (__DEV__ && originalConsole) {
const descriptor = Object.getOwnPropertyDescriptor(global, 'console');
if (descriptor) {
Object.defineProperty(global, 'originalConsole', descriptor);
}
}
global.console = {
...(originalConsole ?? {}),
error: getNativeLogFunction(LOG_LEVELS.error),
info: getNativeLogFunction(LOG_LEVELS.info),
log: getNativeLogFunction(LOG_LEVELS.info),
warn: getNativeLogFunction(LOG_LEVELS.warn),
trace: getNativeLogFunction(LOG_LEVELS.trace),
debug: getNativeLogFunction(LOG_LEVELS.trace),
table: consoleTablePolyfill,
group: consoleGroupPolyfill,
groupEnd: consoleGroupEndPolyfill,
groupCollapsed: consoleGroupCollapsedPolyfill,
assert: consoleAssertPolyfill,
};
Object.defineProperty(console, '_isPolyfilled', {
value: true,
enumerable: false,
});
// If available, also call the original `console` method since that is
// sometimes useful. Ex: on OS X, this will let you see rich output in
// the Safari Web Inspector console.
if (__DEV__ && originalConsole) {
Object.keys(console).forEach(methodName => {
const reactNativeMethod = console[methodName];
if (
originalConsole[methodName] &&
reactNativeMethod !== originalConsole[methodName]
) {
console[methodName] = function () {
originalConsole[methodName](...arguments);
reactNativeMethod.apply(console, arguments);
};
}
});
// The following methods are not supported by this polyfill but
// we still should pass them to original console if they are
// supported by it.
['clear', 'dir', 'dirxml', 'profile', 'profileEnd'].forEach(methodName => {
if (typeof originalConsole[methodName] === 'function') {
console[methodName] = function () {
originalConsole[methodName](...arguments);
};
}
});
}
} else if (!global.console) {
function stub() {}
const log = global.print || stub;
global.console = {
debug: log,
error: log,
info: log,
log: log,
trace: log,
warn: log,
assert(expression, label) {
if (!expression) {
log('Assertion failed: ' + label);
}
},
clear: stub,
dir: stub,
dirxml: stub,
group: stub,
groupCollapsed: stub,
groupEnd: stub,
profile: stub,
profileEnd: stub,
table: stub,
};
Object.defineProperty(console, '_isPolyfilled', {
value: true,
enumerable: false,
});
}})(globalThis, true)
//# sourceURL=prelude.js
)___";

View File

@@ -0,0 +1,66 @@
/*
* 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 "InspectorFlagOverridesGuard.h"
#include <jsinspector-modern/InspectorFlags.h>
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <react/featureflags/ReactNativeFeatureFlagsDefaults.h>
#include <memory>
namespace facebook::react::jsinspector_modern {
/**
* Helper class that maps \c InspectorFlagOverrides to the shape of \c
* ReactNativeFeatureFlagsDefaults.
*/
class ReactNativeFeatureFlagsOverrides
: public ReactNativeFeatureFlagsDefaults {
public:
explicit ReactNativeFeatureFlagsOverrides(
const InspectorFlagOverrides& overrides)
: overrides_(overrides) {}
bool fuseboxEnabledRelease() override {
return overrides_.fuseboxEnabledRelease.value_or(
ReactNativeFeatureFlagsDefaults::fuseboxEnabledRelease());
}
bool fuseboxNetworkInspectionEnabled() override {
return overrides_.networkInspectionEnabled.value_or(
ReactNativeFeatureFlagsDefaults::fuseboxNetworkInspectionEnabled());
}
bool enableBridgelessArchitecture() override {
// NOTE: Network support is gated by (enableBridgelessArchitecture &&
// fuseboxNetworkInspectionEnabled).
return overrides_.networkInspectionEnabled.value_or(
ReactNativeFeatureFlagsDefaults::enableBridgelessArchitecture());
}
bool enableNetworkEventReporting() override {
return overrides_.enableNetworkEventReporting.value_or(
ReactNativeFeatureFlagsDefaults::enableNetworkEventReporting());
}
private:
InspectorFlagOverrides overrides_;
};
InspectorFlagOverridesGuard::InspectorFlagOverridesGuard(
const InspectorFlagOverrides& overrides) {
InspectorFlags::getInstance().dangerouslyResetFlags();
ReactNativeFeatureFlags::override(
std::make_unique<ReactNativeFeatureFlagsOverrides>(overrides));
}
InspectorFlagOverridesGuard::~InspectorFlagOverridesGuard() {
ReactNativeFeatureFlags::dangerouslyReset();
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,42 @@
/*
* 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 <react/featureflags/ReactNativeFeatureFlagsDefaults.h>
#include <optional>
namespace facebook::react::jsinspector_modern {
/**
* Overridden \c InspectorFlags values for use in tests.
*/
struct InspectorFlagOverrides {
// NOTE: Keep these entries in sync with ReactNativeFeatureFlagsOverrides in
// the implementation file.
std::optional<bool> fuseboxEnabledRelease;
std::optional<bool> networkInspectionEnabled;
std::optional<bool> enableNetworkEventReporting;
};
/**
* A RAII helper to set up and tear down \c InspectorFlags (via \c
* ReactNativeFeatureFlags) with overrides for the lifetime of a test object.
*/
class InspectorFlagOverridesGuard {
public:
explicit InspectorFlagOverridesGuard(const InspectorFlagOverrides &overrides);
InspectorFlagOverridesGuard(const InspectorFlagOverridesGuard &) = delete;
InspectorFlagOverridesGuard(InspectorFlagOverridesGuard &&) = default;
InspectorFlagOverridesGuard &operator=(const InspectorFlagOverridesGuard &) = delete;
InspectorFlagOverridesGuard &operator=(InspectorFlagOverridesGuard &&) = default;
~InspectorFlagOverridesGuard();
};
} // namespace facebook::react::jsinspector_modern