mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Console API tests
Summary: TSIA Changelog: [Internal] bypass-github-export-checks Reviewed By: huntie Differential Revision: D54846940 fbshipit-source-id: f322ed9b60f35f0b01b64a74a329572a16b1d485
This commit is contained in:
committed by
Facebook GitHub Bot
parent
3cf6c64a80
commit
04de21abb9
@@ -0,0 +1,571 @@
|
||||
/*
|
||||
* 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/JsiIntegrationTestHermesWithCDPAgentEngineAdapter.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 JsiIntegrationPortableTest<
|
||||
JsiIntegrationTestHermesWithCDPAgentEngineAdapter>,
|
||||
public WithParamInterface<Params> {
|
||||
struct ExpectedConsoleApiCall {
|
||||
std::string type;
|
||||
std::string argsJson;
|
||||
};
|
||||
|
||||
protected:
|
||||
void SetUp() override {
|
||||
JsiIntegrationPortableTest::SetUp();
|
||||
connect();
|
||||
if (GetParam().runtimeEnabledAtStart) {
|
||||
enableRuntimeDomain();
|
||||
}
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!GetParam().runtimeEnabledAtStart) {
|
||||
enableRuntimeDomain();
|
||||
}
|
||||
JsiIntegrationPortableTest::TearDown();
|
||||
}
|
||||
|
||||
void expectConsoleApiCall(std::string type, std::string argsJson) {
|
||||
ExpectedConsoleApiCall call{
|
||||
.type = std::move(type), .argsJson = std::move(argsJson)};
|
||||
if (runtimeEnabled_) {
|
||||
expectConsoleApiCallImpl(std::move(call));
|
||||
} else {
|
||||
expectedConsoleApiCalls_.emplace_back(call);
|
||||
}
|
||||
}
|
||||
|
||||
bool isRuntimeDomainEnabled() const {
|
||||
return runtimeEnabled_;
|
||||
}
|
||||
|
||||
void clearExpectedConsoleApiCalls() {
|
||||
expectedConsoleApiCalls_.clear();
|
||||
}
|
||||
|
||||
private:
|
||||
void expectConsoleApiCallImpl(ExpectedConsoleApiCall call) {
|
||||
this->expectMessageFromPage(JsonParsed(AllOf(
|
||||
AtJsonPtr("/method", "Runtime.consoleAPICalled"),
|
||||
AtJsonPtr("/params/type", call.type),
|
||||
AtJsonPtr("/params/args", Eq(folly::parseJson(call.argsJson))))));
|
||||
}
|
||||
|
||||
void enableRuntimeDomain() {
|
||||
InSequence s;
|
||||
auto executionContextInfo = this->expectMessageFromPage(JsonParsed(
|
||||
AllOf(AtJsonPtr("/method", "Runtime.executionContextCreated"))));
|
||||
if (!runtimeEnabled_) {
|
||||
for (auto& call : expectedConsoleApiCalls_) {
|
||||
expectConsoleApiCallImpl(call);
|
||||
}
|
||||
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<ExpectedConsoleApiCall> expectedConsoleApiCalls_;
|
||||
bool runtimeEnabled_{false};
|
||||
};
|
||||
|
||||
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("log", R"([{
|
||||
"type": "string",
|
||||
"value": "hello"
|
||||
}, {
|
||||
"type": "string",
|
||||
"value": "world"
|
||||
}])");
|
||||
eval("console.log('hello', 'world');");
|
||||
}
|
||||
|
||||
TEST_P(ConsoleApiTest, testConsoleDebug) {
|
||||
InSequence s;
|
||||
expectConsoleApiCall("debug", R"([{
|
||||
"type": "string",
|
||||
"value": "hello fusebox"
|
||||
}])");
|
||||
eval("console.debug('hello fusebox');");
|
||||
}
|
||||
|
||||
TEST_P(ConsoleApiTest, testConsoleInfo) {
|
||||
InSequence s;
|
||||
expectConsoleApiCall("info", R"([{
|
||||
"type": "string",
|
||||
"value": "you should know this"
|
||||
}])");
|
||||
eval("console.info('you should know this');");
|
||||
}
|
||||
|
||||
TEST_P(ConsoleApiTest, testConsoleError) {
|
||||
InSequence s;
|
||||
expectConsoleApiCall("error", R"([{
|
||||
"type": "string",
|
||||
"value": "uh oh"
|
||||
}])");
|
||||
eval("console.error('uh oh');");
|
||||
}
|
||||
|
||||
TEST_P(ConsoleApiTest, testConsoleWarn) {
|
||||
InSequence s;
|
||||
expectConsoleApiCall("warning", R"([{
|
||||
"type": "string",
|
||||
"value": "careful"
|
||||
}])");
|
||||
eval("console.warn('careful');");
|
||||
}
|
||||
|
||||
TEST_P(ConsoleApiTest, testConsoleDir) {
|
||||
InSequence s;
|
||||
expectConsoleApiCall("dir", R"([{
|
||||
"type": "string",
|
||||
"value": "something"
|
||||
}])");
|
||||
eval("console.dir('something');");
|
||||
}
|
||||
|
||||
TEST_P(ConsoleApiTest, testConsoleDirxml) {
|
||||
InSequence s;
|
||||
expectConsoleApiCall("dirxml", R"([{
|
||||
"type": "string",
|
||||
"value": "pretend this is a DOM element"
|
||||
}])");
|
||||
eval("console.dirxml('pretend this is a DOM element');");
|
||||
}
|
||||
|
||||
TEST_P(ConsoleApiTest, testConsoleTable) {
|
||||
InSequence s;
|
||||
expectConsoleApiCall("table", R"([{
|
||||
"type": "string",
|
||||
"value": "pretend this is a complex object"
|
||||
}])");
|
||||
eval("console.table('pretend this is a complex object');");
|
||||
}
|
||||
|
||||
TEST_P(ConsoleApiTest, testConsoleTrace) {
|
||||
InSequence s;
|
||||
expectConsoleApiCall("trace", R"([{
|
||||
"type": "string",
|
||||
"value": "trace trace"
|
||||
}])");
|
||||
eval("console.trace('trace trace');");
|
||||
}
|
||||
|
||||
TEST_P(ConsoleApiTest, testConsoleClear) {
|
||||
InSequence s;
|
||||
expectConsoleApiCall("clear", "[]");
|
||||
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("log", R"([{
|
||||
"type": "string",
|
||||
"value": "hello"
|
||||
}])");
|
||||
}
|
||||
expectConsoleApiCall("clear", "[]");
|
||||
eval("console.log('hello');");
|
||||
eval("console.clear();");
|
||||
}
|
||||
|
||||
TEST_P(ConsoleApiTest, testConsoleGroup) {
|
||||
InSequence s;
|
||||
expectConsoleApiCall("startGroup", R"([{
|
||||
"type": "string",
|
||||
"value": "group title"
|
||||
}])");
|
||||
expectConsoleApiCall("log", R"([{
|
||||
"type": "string",
|
||||
"value": "in group"
|
||||
}])");
|
||||
expectConsoleApiCall("endGroup", "[]");
|
||||
eval("console.group('group title');");
|
||||
eval("console.log('in group');");
|
||||
eval("console.groupEnd();");
|
||||
}
|
||||
|
||||
TEST_P(ConsoleApiTest, testConsoleGroupCollapsed) {
|
||||
InSequence s;
|
||||
expectConsoleApiCall("startGroupCollapsed", R"([{
|
||||
"type": "string",
|
||||
"value": "group collapsed title"
|
||||
}])");
|
||||
expectConsoleApiCall("log", R"([{
|
||||
"type": "string",
|
||||
"value": "in group collapsed"
|
||||
}])");
|
||||
expectConsoleApiCall("endGroup", "[]");
|
||||
eval("console.groupCollapsed('group collapsed title');");
|
||||
eval("console.log('in group collapsed');");
|
||||
eval("console.groupEnd();");
|
||||
}
|
||||
|
||||
TEST_P(ConsoleApiTest, testConsoleAssert) {
|
||||
InSequence s;
|
||||
expectConsoleApiCall("assert", R"([{
|
||||
"type": "string",
|
||||
"value": "Assertion failed: something is bad"
|
||||
}])");
|
||||
eval("console.assert(true, 'everything is good');");
|
||||
eval("console.assert(false, 'something is bad');");
|
||||
|
||||
expectConsoleApiCall("assert", R"([{
|
||||
"type": "string",
|
||||
"value": "Assertion failed"
|
||||
}])");
|
||||
eval("console.assert();");
|
||||
}
|
||||
|
||||
TEST_P(ConsoleApiTest, testConsoleCount) {
|
||||
InSequence s;
|
||||
expectConsoleApiCall("count", R"([{
|
||||
"type": "string",
|
||||
"value": "default: 1"
|
||||
}])");
|
||||
expectConsoleApiCall("count", R"([{
|
||||
"type": "string",
|
||||
"value": "default: 2"
|
||||
}])");
|
||||
expectConsoleApiCall("count", R"([{
|
||||
"type": "string",
|
||||
"value": "default: 3"
|
||||
}])");
|
||||
eval("console.count();");
|
||||
eval("console.count('default');");
|
||||
eval("console.count();");
|
||||
eval("console.countReset();");
|
||||
|
||||
expectConsoleApiCall("count", R"([{
|
||||
"type": "string",
|
||||
"value": "default: 1"
|
||||
}])");
|
||||
eval("console.count();");
|
||||
eval("console.countReset('default');");
|
||||
|
||||
expectConsoleApiCall("count", R"([{
|
||||
"type": "string",
|
||||
"value": "default: 1"
|
||||
}])");
|
||||
eval("console.count();");
|
||||
}
|
||||
|
||||
TEST_P(ConsoleApiTest, testConsoleCountLabel) {
|
||||
InSequence s;
|
||||
expectConsoleApiCall("count", R"([{
|
||||
"type": "string",
|
||||
"value": "foo: 1"
|
||||
}])");
|
||||
expectConsoleApiCall("count", R"([{
|
||||
"type": "string",
|
||||
"value": "foo: 2"
|
||||
}])");
|
||||
expectConsoleApiCall("count", R"([{
|
||||
"type": "string",
|
||||
"value": "foo: 3"
|
||||
}])");
|
||||
eval("console.count('foo');");
|
||||
eval("console.count('foo');");
|
||||
eval("console.count('foo');");
|
||||
eval("console.countReset('foo');");
|
||||
|
||||
expectConsoleApiCall("count", R"([{
|
||||
"type": "string",
|
||||
"value": "foo: 1"
|
||||
}])");
|
||||
eval("console.count('foo');");
|
||||
}
|
||||
|
||||
TEST_P(ConsoleApiTest, testConsoleCountResetInvalidLabel) {
|
||||
InSequence s;
|
||||
expectConsoleApiCall("warning", R"([{
|
||||
"type": "string",
|
||||
"value": "Count for 'default' does not exist"
|
||||
}])");
|
||||
expectConsoleApiCall("warning", R"([{
|
||||
"type": "string",
|
||||
"value": "Count for 'default' does not exist"
|
||||
}])");
|
||||
expectConsoleApiCall("warning", R"([{
|
||||
"type": "string",
|
||||
"value": "Count for 'foo' does not exist"
|
||||
}])");
|
||||
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("warning", R"([{
|
||||
"type": "string",
|
||||
"value": "Timer 'default' already exists"
|
||||
}])");
|
||||
eval("console.time('default');");
|
||||
|
||||
eval("console.time('foo');");
|
||||
expectConsoleApiCall("warning", R"([{
|
||||
"type": "string",
|
||||
"value": "Timer 'foo' already exists"
|
||||
}])");
|
||||
eval("console.time('foo');");
|
||||
}
|
||||
|
||||
TEST_P(ConsoleApiTest, testConsoleTimeInvalidLabel) {
|
||||
InSequence s;
|
||||
expectConsoleApiCall("warning", R"([{
|
||||
"type": "string",
|
||||
"value": "Timer 'default' does not exist"
|
||||
}])");
|
||||
expectConsoleApiCall("warning", R"([{
|
||||
"type": "string",
|
||||
"value": "Timer 'default' does not exist"
|
||||
}])");
|
||||
expectConsoleApiCall("warning", R"([{
|
||||
"type": "string",
|
||||
"value": "Timer 'foo' does not exist"
|
||||
}])");
|
||||
eval("console.timeEnd();");
|
||||
eval("console.timeEnd('default');");
|
||||
eval("console.timeEnd('foo');");
|
||||
|
||||
expectConsoleApiCall("warning", R"([{
|
||||
"type": "string",
|
||||
"value": "Timer 'default' does not exist"
|
||||
}])");
|
||||
expectConsoleApiCall("warning", R"([{
|
||||
"type": "string",
|
||||
"value": "Timer 'default' does not exist"
|
||||
}])");
|
||||
expectConsoleApiCall("warning", R"([{
|
||||
"type": "string",
|
||||
"value": "Timer 'foo' does not exist"
|
||||
}])");
|
||||
eval("console.timeLog();");
|
||||
eval("console.timeLog('default');");
|
||||
eval("console.timeLog('foo');");
|
||||
}
|
||||
|
||||
TEST_P(ConsoleApiTest, testConsoleSilentlyClearedOnReload) {
|
||||
InSequence s;
|
||||
expectConsoleApiCall("log", R"([{
|
||||
"type": "string",
|
||||
"value": "hello"
|
||||
}])");
|
||||
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("log", R"([{
|
||||
"type": "string",
|
||||
"value": "world"
|
||||
}])");
|
||||
eval("console.log('world');");
|
||||
}
|
||||
|
||||
TEST_P(ConsoleApiTestWithPreExistingConsole, testPreExistingConsoleObject) {
|
||||
InSequence s;
|
||||
expectConsoleApiCall("log", R"([{
|
||||
"type": "string",
|
||||
"value": "hello"
|
||||
}])");
|
||||
expectConsoleApiCall("warning", R"([{
|
||||
"type": "string",
|
||||
"value": "world"
|
||||
}])");
|
||||
expectConsoleApiCall("table", R"([{
|
||||
"type": "number",
|
||||
"value": 42
|
||||
}])");
|
||||
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"
|
||||
]
|
||||
}])"));
|
||||
}
|
||||
|
||||
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
|
||||
@@ -50,11 +50,17 @@ class JsiIntegrationPortableTest : public ::testing::Test,
|
||||
protected:
|
||||
JsiIntegrationPortableTest()
|
||||
: inspectorFlagsGuard_{EngineAdapter::getInspectorFlagOverrides()},
|
||||
engineAdapter_{immediateExecutor_} {
|
||||
engineAdapter_{immediateExecutor_} {}
|
||||
|
||||
void SetUp() override {
|
||||
// NOTE: Using SetUp() so we can call virtual methods like
|
||||
// setupRuntimeBeforeRegistration().
|
||||
instance_ = &page_->registerInstance(instanceTargetDelegate_);
|
||||
setupRuntimeBeforeRegistration(engineAdapter_->getRuntime());
|
||||
runtimeTarget_ = &instance_->registerRuntime(
|
||||
engineAdapter_->getRuntimeTargetDelegate(),
|
||||
engineAdapter_->getRuntimeExecutor());
|
||||
loadMainBundle();
|
||||
}
|
||||
|
||||
~JsiIntegrationPortableTest() override {
|
||||
@@ -70,6 +76,19 @@ class JsiIntegrationPortableTest : public ::testing::Test,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
ASSERT_FALSE(toPage_) << "Can only connect once in a JSI integration test.";
|
||||
toPage_ = page_->connect(
|
||||
@@ -94,9 +113,11 @@ class JsiIntegrationPortableTest : public ::testing::Test,
|
||||
// Recreate the engine (e.g. to wipe any state in the inner jsi::Runtime)
|
||||
engineAdapter_.emplace(immediateExecutor_);
|
||||
instance_ = &page_->registerInstance(instanceTargetDelegate_);
|
||||
setupRuntimeBeforeRegistration(engineAdapter_->getRuntime());
|
||||
runtimeTarget_ = &instance_->registerRuntime(
|
||||
engineAdapter_->getRuntimeTargetDelegate(),
|
||||
engineAdapter_->getRuntimeExecutor());
|
||||
loadMainBundle();
|
||||
}
|
||||
|
||||
MockRemoteConnection& fromPage() {
|
||||
|
||||
Reference in New Issue
Block a user