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

1503 lines
52 KiB
C++
Raw Normal View History

2025-02-13 09:59:20 +08:00
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include <folly/dynamic.h>
#include <folly/executors/QueuedImmediateExecutor.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 "FollyDynamicMatchers.h"
#include "InspectorMocks.h"
#include "UniquePtrFactory.h"
using namespace ::testing;
namespace facebook::react::jsinspector_modern {
namespace {
class HostTargetTest : public Test {
folly::QueuedImmediateExecutor immediateExecutor_;
protected:
HostTargetTest() {
EXPECT_CALL(runtimeTargetDelegate_, createAgentDelegate(_, _, _, _, _))
.WillRepeatedly(runtimeAgentDelegates_.lazily_make_unique<
FrontendChannel,
SessionState&,
std::unique_ptr<RuntimeAgentDelegate::ExportedState>,
const ExecutionContextDescription&,
RuntimeExecutor>());
}
void connect() {
ASSERT_FALSE(toPage_) << "Can only connect once in a HostTargetTest.";
auto conn = makeConnection();
toPage_ = std::move(conn.first);
}
std::pair<std::unique_ptr<ILocalConnection>, MockRemoteConnection&>
makeConnection() {
size_t connectionIndex = remoteConnections_.objectsVended();
auto toPage = page_->connect(remoteConnections_.make_unique());
// We'll always get an onDisconnect call when we tear
// down the test. Expect it in order to satisfy the strict mock.
EXPECT_CALL(*remoteConnections_[connectionIndex], onDisconnect());
return {std::move(toPage), *remoteConnections_[connectionIndex]};
}
MockHostTargetDelegate hostTargetDelegate_;
MockRemoteConnection& fromPage() {
assert(toPage_);
return *remoteConnections_[0];
}
VoidExecutor inspectorExecutor_ = [this](auto callback) {
immediateExecutor_.add(callback);
};
std::shared_ptr<HostTarget> page_ =
HostTarget::create(hostTargetDelegate_, inspectorExecutor_);
MockInstanceTargetDelegate instanceTargetDelegate_;
MockRuntimeTargetDelegate runtimeTargetDelegate_;
// We don't have access to a jsi::Runtime in these tests, so just use an
// executor that never runs the scheduled callbacks.
RuntimeExecutor runtimeExecutor_ = [](auto) {};
UniquePtrFactory<StrictMock<MockRuntimeAgentDelegate>> runtimeAgentDelegates_;
private:
UniquePtrFactory<StrictMock<MockRemoteConnection>> remoteConnections_;
protected:
// NOTE: Needs to be destroyed before page_.
std::unique_ptr<ILocalConnection> toPage_;
};
/**
* Simplified test harness focused on sending messages to and from a HostTarget.
*/
class HostTargetProtocolTest : public HostTargetTest {
public:
HostTargetProtocolTest() {
connect();
}
private:
// Protocol tests shouldn't manually call connect()
using HostTargetTest::connect;
};
} // namespace
TEST_F(HostTargetProtocolTest, UnrecognizedMethod) {
EXPECT_CALL(
fromPage(),
onMessage(JsonParsed(AllOf(
AtJsonPtr("/error/code", Eq(-32601)), AtJsonPtr("/id", Eq(1))))))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": "SomeUnrecognizedMethod",
"params": [1, 2]
})");
}
TEST_F(HostTargetProtocolTest, TypeErrorInMethodName) {
EXPECT_CALL(
fromPage(),
onMessage(JsonParsed(AllOf(
AtJsonPtr("/error/code", Eq(-32600)),
AtJsonPtr("/id", Eq(nullptr))))))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": 42,
"params": [1, 2]
})");
}
TEST_F(HostTargetProtocolTest, MissingId) {
EXPECT_CALL(
fromPage(),
onMessage(JsonParsed(AllOf(
AtJsonPtr("/error/code", Eq(-32600)),
AtJsonPtr("/id", Eq(nullptr))))))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"method": "SomeUnrecognizedMethod",
"params": [1, 2]
})");
}
TEST_F(HostTargetProtocolTest, MalformedJson) {
EXPECT_CALL(
fromPage(),
onMessage(JsonParsed(AllOf(
AtJsonPtr("/error/code", Eq(-32700)),
AtJsonPtr("/id", Eq(nullptr))))))
.RetiresOnSaturation();
toPage_->sendMessage("{");
}
TEST_F(HostTargetProtocolTest, InjectLogsToIdentifyBackend) {
InSequence s;
EXPECT_CALL(
fromPage(),
onMessage(JsonParsed(AllOf(
AtJsonPtr("/method", "Log.entryAdded"),
AtJsonPtr("/params/entry", Not(IsEmpty()))))))
.Times(AtLeast(1));
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 1,
"result": {}
})")))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": "Log.enable"
})");
}
TEST_F(HostTargetProtocolTest, PageReloadMethod) {
InSequence s;
EXPECT_CALL(
hostTargetDelegate_,
onReload(Eq(HostTargetDelegate::PageReloadRequest{
.ignoreCache = std::nullopt,
.scriptToEvaluateOnLoad = std::nullopt})))
.RetiresOnSaturation();
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 1,
"result": {}
})")))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": "Page.reload"
})");
EXPECT_CALL(
hostTargetDelegate_,
onReload(Eq(HostTargetDelegate::PageReloadRequest{
.ignoreCache = true, .scriptToEvaluateOnLoad = "alert('hello');"})))
.RetiresOnSaturation();
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 2,
"result": {}
})")))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 2,
"method": "Page.reload",
"params": {
"ignoreCache": true,
"scriptToEvaluateOnLoad": "alert('hello');"
}
})");
}
TEST_F(HostTargetProtocolTest, OverlaySetPausedInDebuggerMessageMethod) {
InSequence s;
EXPECT_CALL(
hostTargetDelegate_,
onSetPausedInDebuggerMessage(
Eq(HostTargetDelegate::OverlaySetPausedInDebuggerMessageRequest{
.message = std::nullopt})))
.RetiresOnSaturation();
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 1,
"result": {}
})")))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": "Overlay.setPausedInDebuggerMessage"
})");
EXPECT_CALL(
hostTargetDelegate_,
onSetPausedInDebuggerMessage(
Eq(HostTargetDelegate::OverlaySetPausedInDebuggerMessageRequest{
.message = "Paused in debugger"})))
.RetiresOnSaturation();
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 2,
"result": {}
})")))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 2,
"method": "Overlay.setPausedInDebuggerMessage",
"params": {
"message": "Paused in debugger"
}
})");
// A cleanup message is sent automatically when we destroy the session.
EXPECT_CALL(
hostTargetDelegate_,
onSetPausedInDebuggerMessage(
Eq(HostTargetDelegate::OverlaySetPausedInDebuggerMessageRequest{
.message = std::nullopt})))
.RetiresOnSaturation();
}
TEST_F(HostTargetProtocolTest, OverlaySetPausedInDebuggerMultipleClients) {
auto [toPage2, fromPage2] = makeConnection();
InSequence s;
EXPECT_CALL(
hostTargetDelegate_,
onSetPausedInDebuggerMessage(
Eq(HostTargetDelegate::OverlaySetPausedInDebuggerMessageRequest{
.message = "Paused in debugger - client 1"})))
.RetiresOnSaturation();
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 1,
"result": {}
})")))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": "Overlay.setPausedInDebuggerMessage",
"params": {
"message": "Paused in debugger - client 1"
}
})");
EXPECT_CALL(
hostTargetDelegate_,
onSetPausedInDebuggerMessage(
Eq(HostTargetDelegate::OverlaySetPausedInDebuggerMessageRequest{
.message = "Paused in debugger - client 2"})))
.RetiresOnSaturation();
EXPECT_CALL(fromPage2, onMessage(JsonEq(R"({
"id": 1,
"result": {}
})")))
.RetiresOnSaturation();
toPage2->sendMessage(R"({
"id": 1,
"method": "Overlay.setPausedInDebuggerMessage",
"params": {
"message": "Paused in debugger - client 2"
}
})");
toPage2.reset();
// The cleanup message is sent exactly once.
EXPECT_CALL(
hostTargetDelegate_,
onSetPausedInDebuggerMessage(
Eq(HostTargetDelegate::OverlaySetPausedInDebuggerMessageRequest{
.message = std::nullopt})))
.Times(1)
.RetiresOnSaturation();
}
TEST_F(HostTargetProtocolTest, RegisterUnregisterInstanceWithoutEvents) {
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
page_->unregisterInstance(instanceTarget);
}
TEST_F(HostTargetTest, ConnectToAlreadyRegisteredInstanceWithoutEvents) {
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
connect();
page_->unregisterInstance(instanceTarget);
}
TEST_F(HostTargetProtocolTest, RegisterUnregisterInstanceWithEvents) {
InSequence s;
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 1,
"result": {}
})")));
toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.enable"
})");
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"method": "Runtime.executionContextsCleared"
})")));
page_->unregisterInstance(instanceTarget);
}
TEST_F(HostTargetTest, ConnectToAlreadyRegisteredInstanceWithEvents) {
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
connect();
InSequence s;
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 1,
"result": {}
})")));
toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.enable"
})");
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"method": "Runtime.executionContextsCleared"
})")));
page_->unregisterInstance(instanceTarget);
}
TEST_F(HostTargetTest, ConnectToAlreadyRegisteredRuntimeWithEvents) {
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
auto& runtimeTarget =
instanceTarget.registerRuntime(runtimeTargetDelegate_, runtimeExecutor_);
connect();
InSequence s;
ASSERT_TRUE(runtimeAgentDelegates_[0]);
EXPECT_CALL(*runtimeAgentDelegates_[0], handleRequest(_))
.WillOnce(Return(true))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": "CustomRuntimeDomain.Foo",
"params": {
"expression": "42"
}
})");
static constexpr auto kFooResponse = R"({
"id": 1,
"result": {
"fooValue": 42
}
})";
EXPECT_CALL(fromPage(), onMessage(JsonEq(kFooResponse)))
.RetiresOnSaturation();
runtimeAgentDelegates_[0]->frontendChannel(kFooResponse);
instanceTarget.unregisterRuntime(runtimeTarget);
page_->unregisterInstance(instanceTarget);
}
TEST_F(HostTargetProtocolTest, RuntimeAgentDelegateLifecycle) {
{
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
auto& runtimeTarget = instanceTarget.registerRuntime(
runtimeTargetDelegate_, runtimeExecutor_);
EXPECT_TRUE(runtimeAgentDelegates_[0]);
instanceTarget.unregisterRuntime(runtimeTarget);
page_->unregisterInstance(instanceTarget);
}
EXPECT_FALSE(runtimeAgentDelegates_[0]);
{
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
auto& runtimeTarget = instanceTarget.registerRuntime(
runtimeTargetDelegate_, runtimeExecutor_);
EXPECT_TRUE(runtimeAgentDelegates_[1]);
instanceTarget.unregisterRuntime(runtimeTarget);
page_->unregisterInstance(instanceTarget);
}
EXPECT_FALSE(runtimeAgentDelegates_[1]);
}
TEST_F(HostTargetProtocolTest, MethodNotHandledByRuntimeAgentDelegate) {
InSequence s;
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
auto& runtimeTarget =
instanceTarget.registerRuntime(runtimeTargetDelegate_, runtimeExecutor_);
ASSERT_TRUE(runtimeAgentDelegates_[0]);
EXPECT_CALL(*runtimeAgentDelegates_[0], handleRequest(_))
.WillOnce(Return(false))
.RetiresOnSaturation();
EXPECT_CALL(
fromPage(), onMessage(JsonParsed(AtJsonPtr("/error/code", Eq(-32601)))))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": "CustomRuntimeDomain.Foo",
"params": {
"expression": "42"
}
})");
instanceTarget.unregisterRuntime(runtimeTarget);
page_->unregisterInstance(instanceTarget);
}
TEST_F(HostTargetProtocolTest, MethodHandledByRuntimeAgentDelegate) {
InSequence s;
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
auto& runtimeTarget =
instanceTarget.registerRuntime(runtimeTargetDelegate_, runtimeExecutor_);
ASSERT_TRUE(runtimeAgentDelegates_[0]);
EXPECT_CALL(*runtimeAgentDelegates_[0], handleRequest(_))
.WillOnce(Return(true))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": "CustomRuntimeDomain.Foo",
"params": {
"expression": "42"
}
})");
static constexpr auto kFooResponse = R"({
"id": 1,
"result": {
"fooValue": 42
}
})";
EXPECT_CALL(fromPage(), onMessage(JsonEq(kFooResponse)))
.RetiresOnSaturation();
runtimeAgentDelegates_[0]->frontendChannel(kFooResponse);
instanceTarget.unregisterRuntime(runtimeTarget);
page_->unregisterInstance(instanceTarget);
}
TEST_F(HostTargetProtocolTest, MessageRoutingWhileNoRuntimeAgentDelegate) {
InSequence s;
EXPECT_CALL(
fromPage(), onMessage(JsonParsed(AtJsonPtr("/error/code", Eq(-32601)))))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": "CustomRuntimeDomain.Foo",
"params": {
"expression": "42"
}
})");
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
auto& runtimeTarget =
instanceTarget.registerRuntime(runtimeTargetDelegate_, runtimeExecutor_);
ASSERT_TRUE(runtimeAgentDelegates_[0]);
EXPECT_CALL(*runtimeAgentDelegates_[0], handleRequest(_))
.WillOnce(Return(true))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 2,
"method": "CustomRuntimeDomain.Foo",
"params": {
"expression": "42"
}
})");
static constexpr auto kFooResponse = R"({
"id": 2,
"result": {
"fooValue": 42
}
})";
EXPECT_CALL(fromPage(), onMessage(JsonEq(kFooResponse)))
.RetiresOnSaturation();
runtimeAgentDelegates_[0]->frontendChannel(kFooResponse);
instanceTarget.unregisterRuntime(runtimeTarget);
page_->unregisterInstance(instanceTarget);
EXPECT_FALSE(runtimeAgentDelegates_[0]);
EXPECT_CALL(
fromPage(), onMessage(JsonParsed(AtJsonPtr("/error/code", Eq(-32601)))))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 3,
"method": "CustomRuntimeDomain.Foo",
"params": {
"expression": "42"
}
})");
}
TEST_F(HostTargetProtocolTest, InstanceWithNullRuntimeAgentDelegate) {
InSequence s;
EXPECT_CALL(runtimeTargetDelegate_, createAgentDelegate(_, _, _, _, _))
.WillRepeatedly(ReturnNull());
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
auto& runtimeTarget =
instanceTarget.registerRuntime(runtimeTargetDelegate_, runtimeExecutor_);
EXPECT_FALSE(runtimeAgentDelegates_[0]);
EXPECT_CALL(
fromPage(), onMessage(JsonParsed(AtJsonPtr("/error/code", Eq(-32601)))))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": "CustomRuntimeDomain.Foo",
"params": {
"expression": "42"
}
})");
instanceTarget.unregisterRuntime(runtimeTarget);
page_->unregisterInstance(instanceTarget);
}
TEST_F(HostTargetProtocolTest, RuntimeAgentDelegateHasAccessToSessionState) {
// Ignore console messages originating inside the backend.
EXPECT_CALL(
fromPage(),
onMessage(JsonParsed(AllOf(
AtJsonPtr("/method", "Runtime.consoleAPICalled"),
AtJsonPtr("/params/context", "main#InstanceAgent")))))
.Times(AnyNumber());
InSequence s;
// Send Runtime.enable before registering the Instance (which in turns creates
// the RuntimeAgentDelegate).
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 1,
"result": {}
})")))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.enable"
})");
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
EXPECT_CALL(
fromPage(),
onMessage(
JsonParsed(AtJsonPtr("/method", "Runtime.executionContextCreated"))))
.RetiresOnSaturation();
instanceTarget.registerRuntime(runtimeTargetDelegate_, runtimeExecutor_);
ASSERT_TRUE(runtimeAgentDelegates_[0]);
EXPECT_TRUE(runtimeAgentDelegates_[0]->sessionState.isRuntimeDomainEnabled);
// Send Runtime.disable while the RuntimeAgentDelegate exists - it receives
// the message and can also observe the updated state.
EXPECT_CALL(*runtimeAgentDelegates_[0], handleRequest(Eq(cdp::preparse(R"({
"id": 2,
"method": "Runtime.disable"
})"))));
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 2,
"result": {}
})")));
toPage_->sendMessage(R"({
"id": 2,
"method": "Runtime.disable"
})");
EXPECT_FALSE(runtimeAgentDelegates_[0]->sessionState.isRuntimeDomainEnabled);
}
TEST_F(HostTargetTest, HostCommands) {
// Set up expectations for the RuntimeAgentDelegate that will be created
// as part of the private session inside HostCommandSender.
EXPECT_CALL(runtimeTargetDelegate_, createAgentDelegate(_, _, _, _, _))
.WillOnce([this](
FrontendChannel frontendChannel,
SessionState& sessionState,
std::unique_ptr<RuntimeAgentDelegate::ExportedState>
exportedState,
const ExecutionContextDescription& context,
RuntimeExecutor runtimeExecutor) {
auto delegate = runtimeAgentDelegates_.make_unique(
std::move(frontendChannel),
sessionState,
std::move(exportedState),
context,
std::move(runtimeExecutor));
InSequence s;
EXPECT_CALL(
*delegate,
handleRequest(
Field(&cdp::PreparsedRequest::method, "Debugger.resume")))
.WillOnce(Return(false))
.RetiresOnSaturation();
EXPECT_CALL(
*delegate,
handleRequest(
Field(&cdp::PreparsedRequest::method, "Debugger.stepOver")))
.WillOnce(Return(false))
.RetiresOnSaturation();
return delegate;
})
.RetiresOnSaturation();
// No RuntimeAgent yet; this command is simply ignored.
page_->sendCommand(HostCommand::DebuggerStepOver);
EXPECT_FALSE(runtimeAgentDelegates_[0]);
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
auto& runtimeTarget =
instanceTarget.registerRuntime(runtimeTargetDelegate_, runtimeExecutor_);
page_->sendCommand(HostCommand::DebuggerResume);
page_->sendCommand(HostCommand::DebuggerStepOver);
ASSERT_TRUE(runtimeAgentDelegates_[0]);
connect();
// This is part of the HostCommandSender session.
ASSERT_TRUE(runtimeAgentDelegates_[0]);
// This is part of the session we just connect()ed to above.
EXPECT_TRUE(runtimeAgentDelegates_[1]);
// We can still send commands.
EXPECT_CALL(
*runtimeAgentDelegates_[0],
handleRequest(Field(&cdp::PreparsedRequest::method, "Debugger.stepOver")))
.WillOnce(Return(false))
.RetiresOnSaturation();
page_->sendCommand(HostCommand::DebuggerStepOver);
// NOTE: Our use of StrictMock ensures that the session doesn't receive any
// noise resulting from the sendCommand call ( = no
// runtimeAgentDelegates_[1]->handleRequest, no fromPage()->onMessage, etc).
instanceTarget.unregisterRuntime(runtimeTarget);
page_->unregisterInstance(instanceTarget);
}
TEST_F(HostTargetTest, NetworkLoadNetworkResourceSuccess) {
connect();
InSequence s;
ScopedExecutor<NetworkRequestListener> executor;
EXPECT_CALL(
hostTargetDelegate_,
loadNetworkResource(
Field(&LoadNetworkResourceRequest::url, "http://example.com"), _))
.Times(1)
.WillOnce([&executor](
const LoadNetworkResourceRequest& /*params*/,
ScopedExecutor<NetworkRequestListener> executorArg) {
// Capture the ScopedExecutor<NetworkRequestListener> to use later.
executor = std::move(executorArg);
})
.RetiresOnSaturation();
// Load the resource, expect a CDP response as soon as headers are received.
toPage_->sendMessage(R"({
"id": 1,
"method": "Network.loadNetworkResource",
"params": {
"url": "http://example.com"
}
})");
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 1,
"result": {
"resource": {
"success": true,
"stream": "0",
"httpStatusCode": 200,
"headers": {
"x-test": "foo",
"Content-Type": "text/plain"
}
}
}
})")));
executor([](NetworkRequestListener& listener) {
listener.onHeaders(
200, Headers{{"x-test", "foo"}, {"Content-Type", "text/plain"}});
});
// Retrieve the first chunk of data.
toPage_->sendMessage(R"({
"id": 2,
"method": "IO.read",
"params": {
"handle": "0",
"size": 8
}
})");
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 2,
"result": {
"data": "Hello, W",
"eof": false,
"base64Encoded": false
}
})")));
executor([](NetworkRequestListener& listener) {
listener.onData("Hello, World!");
});
// Retrieve the remaining data.
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 3,
"result": {
"data": "orld!",
"eof": false,
"base64Encoded": false
}
})")));
toPage_->sendMessage(R"({
"id": 3,
"method": "IO.read",
"params": {
"handle": "0",
"size": 8
}
})");
// No more data - expect empty payload with eof: true.
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 4,
"result": {
"data": "",
"eof": true,
"base64Encoded": false
}
})")));
toPage_->sendMessage(R"({
"id": 4,
"method": "IO.read",
"params": {
"handle": "0",
"size": 8
}
})");
executor([](NetworkRequestListener& listener) { listener.onCompletion(); });
// Close the stream.
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 5,
"result": {}
})")));
toPage_->sendMessage(R"({
"id": 5,
"method": "IO.close",
"params": {
"handle": "0"
}
})");
}
TEST_F(HostTargetTest, NetworkLoadNetworkResourceBinaryData) {
connect();
InSequence s;
ScopedExecutor<NetworkRequestListener> executor;
EXPECT_CALL(
hostTargetDelegate_,
loadNetworkResource(
Field(&LoadNetworkResourceRequest::url, "http://example.com"), _))
.Times(1)
.WillOnce([&executor](
const LoadNetworkResourceRequest& /*params*/,
ScopedExecutor<NetworkRequestListener> executorArg) {
// Capture the ScopedExecutor<NetworkRequestListener> to use later.
executor = std::move(executorArg);
})
.RetiresOnSaturation();
// Load the resource, expect a CDP response as soon as headers are received.
toPage_->sendMessage(R"({
"id": 1,
"method": "Network.loadNetworkResource",
"params": {
"url": "http://example.com"
}
})");
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 1,
"result": {
"resource": {
"success": true,
"stream": "0",
"httpStatusCode": 200,
"headers": {
"Content-Type": "application/octet-stream"
}
}
}
})")));
executor([](NetworkRequestListener& listener) {
// Arbitrary binary data.
listener.onHeaders(
200, Headers{{"Content-Type", "application/octet-stream"}});
});
// Retrieve the first chunk of data.
toPage_->sendMessage(R"({
"id": 2,
"method": "IO.read",
"params": {
"handle": "0",
"size": 4
}
})");
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 2,
"result": {
"data": "3q2+7w==",
"eof": false,
"base64Encoded": true
}
})")));
executor([](NetworkRequestListener& listener) {
std::array<char, 8> binaryData = {
'\xDE',
'\xAD',
'\xBE',
'\xEF',
'\x00',
'\x11',
'\x22',
'\x33',
};
listener.onData(std::string_view(binaryData.data(), binaryData.size()));
});
// Retrieve the remaining data.
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 3,
"result": {
"data": "ABEiMw==",
"eof": false,
"base64Encoded": true
}
})")));
toPage_->sendMessage(R"({
"id": 3,
"method": "IO.read",
"params": {
"handle": "0",
"size": 4
}
})");
// No more data - expect empty payload with eof: true.
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 4,
"result": {
"data": "",
"eof": true,
"base64Encoded": true
}
})")));
toPage_->sendMessage(R"({
"id": 4,
"method": "IO.read",
"params": {
"handle": "0",
"size": 8
}
})");
executor([](NetworkRequestListener& listener) { listener.onCompletion(); });
// Close the stream.
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 5,
"result": {}
})")));
toPage_->sendMessage(R"({
"id": 5,
"method": "IO.close",
"params": {
"handle": "0"
}
})");
}
TEST_F(HostTargetTest, NetworkLoadNetworkResourceMimeIsTextContentIsNot) {
connect();
InSequence s;
ScopedExecutor<NetworkRequestListener> executor;
EXPECT_CALL(
hostTargetDelegate_,
loadNetworkResource(
Field(&LoadNetworkResourceRequest::url, "http://example.com"), _))
.Times(1)
.WillOnce([&executor](
const LoadNetworkResourceRequest& /*params*/,
ScopedExecutor<NetworkRequestListener> executorArg) {
// Capture the ScopedExecutor<NetworkRequestListener> to use later.
executor = std::move(executorArg);
})
.RetiresOnSaturation();
// Load the resource, expect a CDP response as soon as headers are received.
toPage_->sendMessage(R"({
"id": 1,
"method": "Network.loadNetworkResource",
"params": {
"url": "http://example.com"
}
})");
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 1,
"result": {
"resource": {
"success": true,
"stream": "0",
"httpStatusCode": 200,
"headers": {
"Content-Type": "text/plain"
}
}
}
})")));
executor([](NetworkRequestListener& listener) {
// Claim text/plain...
listener.onHeaders(200, Headers{{"Content-Type", "text/plain"}});
});
// Retrieve the first chunk of data.
toPage_->sendMessage(R"({
"id": 2,
"method": "IO.read",
"params": {
"handle": "0",
"size": 4
}
})");
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 2,
"error": {
"message": "Invalid UTF-8 sequence",
"code": -32603
}
})")));
executor([](NetworkRequestListener& listener) {
std::array<char, 4> binaryData = {
'\x80',
'\x80',
'\x80',
'\x80',
};
// Actually emit binary that cannot be represented as UTF-8.
listener.onData(std::string_view(binaryData.data(), binaryData.size()));
});
}
TEST_F(HostTargetTest, NetworkLoadNetworkResourceStreamInterrupted) {
connect();
InSequence s;
ScopedExecutor<NetworkRequestListener> executor;
EXPECT_CALL(
hostTargetDelegate_,
loadNetworkResource(
Field(&LoadNetworkResourceRequest::url, "http://example.com"), _))
.Times(1)
.WillOnce([&executor](
const LoadNetworkResourceRequest& /*params*/,
ScopedExecutor<NetworkRequestListener> executorArg) {
// Capture the ScopedExecutor<NetworkRequestListener> to use later.
executor = std::move(executorArg);
})
.RetiresOnSaturation();
// Load the resource, receiving headers succesfully.
toPage_->sendMessage(R"({
"id": 1,
"method": "Network.loadNetworkResource",
"params": {
"url": "http://example.com"
}
})");
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 1,
"result": {
"resource": {
"success": true,
"stream": "0",
"httpStatusCode": 200,
"headers": {
"x-test": "foo"
}
}
}
})")));
executor([](NetworkRequestListener& listener) {
listener.onHeaders(200, Headers{{"x-test", "foo"}});
});
// Retrieve the first chunk of data.
toPage_->sendMessage(R"({
"id": 2,
"method": "IO.read",
"params": {
"handle": "0",
"size": 20
}
})");
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 2,
"result": {
"data": "VGhlIG1lYW5pbmcgb2YgbGlmZSA=",
"eof": false,
"base64Encoded": true
}
})")));
executor([](NetworkRequestListener& listener) {
listener.onData("The meaning of life is...");
});
// Simulate an error mid-stream, expect in-flight IO.reads to return a CDP
// error.
toPage_->sendMessage(R"({
"id": 3,
"method": "IO.read",
"params": {
"handle": "0",
"size": 20
}
})");
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 3,
"error": {
"code": -32603,
"message": "Connection lost"
}
})")));
executor([](NetworkRequestListener& listener) {
listener.onError("Connection lost");
});
// IO.close should be a successful no-op after an error.
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 4,
"result": {}
})")));
toPage_->sendMessage(R"({
"id": 4,
"method": "IO.close",
"params": {
"handle": "0"
}
})");
}
TEST_F(HostTargetTest, NetworkLoadNetworkResource404) {
connect();
InSequence s;
ScopedExecutor<NetworkRequestListener> executor;
EXPECT_CALL(
hostTargetDelegate_,
loadNetworkResource(
Field(&LoadNetworkResourceRequest::url, "http://example.com/404"), _))
.Times(1)
.WillOnce([&executor](
const LoadNetworkResourceRequest& /*params*/,
ScopedExecutor<NetworkRequestListener> executorArg) {
// Capture the ScopedExecutor<NetworkRequestListener> to use later.
executor = std::move(executorArg);
})
.RetiresOnSaturation();
// A 404 response should trigger a CDP result with success: false, including
// the status code, headers, but *no* stream handle.
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 1,
"result": {
"resource": {
"success": false,
"httpStatusCode": 404,
"headers": {
"x-test": "foo"
}
}
}
})")));
toPage_->sendMessage(R"({
"id": 1,
"method": "Network.loadNetworkResource",
"params": {
"url": "http://example.com/404"
}
})");
executor([](NetworkRequestListener& listener) {
listener.onHeaders(404, Headers{{"x-test", "foo"}});
});
// Assuming a successful request would have assigned handle "0", verify that
// handle has *not* been assigned.
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 2,
"error": {
"code": -32603,
"message": "Stream not found with handle 0"
}
})")));
toPage_->sendMessage(R"({
"id": 2,
"method": "IO.read",
"params": {
"handle": "0",
"size": 20
}
})");
}
TEST_F(HostTargetTest, NetworkLoadNetworkResourceInitialNetError) {
connect();
InSequence s;
ScopedExecutor<NetworkRequestListener> executor;
EXPECT_CALL(
hostTargetDelegate_,
loadNetworkResource(
Field(&LoadNetworkResourceRequest::url, "http://baddomain.com"), _))
.Times(1)
.WillOnce([&executor](
const LoadNetworkResourceRequest& /*params*/,
ScopedExecutor<NetworkRequestListener> executorArg) {
// Capture the ScopedExecutor<NetworkRequestListener> to use later.
executor = std::move(executorArg);
})
.RetiresOnSaturation();
// Load the resource, expect a CDP resonse with no headers or status code,
// but with success: false and a netErrorName
toPage_->sendMessage(R"({
"id": 1,
"method": "Network.loadNetworkResource",
"params": {
"url": "http://baddomain.com"
}
})");
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 1,
"result": {
"resource": {
"success": false,
"netErrorName": "Arbitrary error string"
}
}
})")));
executor([](NetworkRequestListener& listener) {
listener.onError("Arbitrary error string");
});
}
TEST_F(HostTargetTest, NetworkLoadNetworkResourceStreamClosed) {
connect();
InSequence s;
ScopedExecutor<NetworkRequestListener> executor;
EXPECT_CALL(
hostTargetDelegate_,
loadNetworkResource(
Field(&LoadNetworkResourceRequest::url, "http://example.com"), _))
.Times(1)
.WillOnce([&executor](
const LoadNetworkResourceRequest& /*params*/,
ScopedExecutor<NetworkRequestListener> executorArg) {
// Capture the ScopedExecutor<NetworkRequestListener> to use later.
executor = std::move(executorArg);
})
.RetiresOnSaturation();
// Load the resource, receiving headers succesfully.
toPage_->sendMessage(R"({
"id": 1,
"method": "Network.loadNetworkResource",
"params": {
"url": "http://example.com"
}
})");
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 1,
"result": {
"resource": {
"success": true,
"stream": "0",
"httpStatusCode": 200,
"headers": {
"content-type": "text/plain"
}
}
}
})")));
bool cancelFunctionCalled = false;
executor([&cancelFunctionCalled](NetworkRequestListener& listener) {
listener.setCancelFunction(
[&cancelFunctionCalled]() { cancelFunctionCalled = true; });
listener.onHeaders(200, Headers{{"content-type", "text/plain"}});
});
// Retrieve the first chunk of data.
toPage_->sendMessage(R"({
"id": 2,
"method": "IO.read",
"params": {
"handle": "0",
"size": 22
}
})");
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 2,
"result": {
"data": "The meaning of life is",
"eof": false,
"base64Encoded": false
}
})")));
executor([](NetworkRequestListener& listener) {
listener.onData("The meaning of life is...");
});
EXPECT_FALSE(cancelFunctionCalled);
// Simulate the client closing the stream while data is still incoming.
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 3,
"result": {}
})")));
toPage_->sendMessage(R"({
"id": 3,
"method": "IO.close",
"params": {
"handle": "0"
}
})");
EXPECT_TRUE(cancelFunctionCalled);
}
TEST_F(HostTargetTest, NetworkLoadNetworkResourceAgentDisconnect) {
connect();
InSequence s;
ScopedExecutor<NetworkRequestListener> executor;
EXPECT_CALL(hostTargetDelegate_, loadNetworkResource(_, _))
.Times(1)
.WillOnce([&executor](
const LoadNetworkResourceRequest& /*params*/,
ScopedExecutor<NetworkRequestListener> executorArg) {
// Capture the ScopedExecutor<NetworkRequestListener> to use later.
executor = std::move(executorArg);
})
.RetiresOnSaturation();
// Load the resource, receiving headers succesfully.
toPage_->sendMessage(R"({
"id": 1,
"method": "Network.loadNetworkResource",
"params": {
"url": "http://example.com"
}
})");
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 1,
"result": {
"resource": {
"success": true,
"stream": "0",
"httpStatusCode": 200,
"headers": {
"x-test": "foo"
}
}
}
})")));
bool cancelFunctionCalled = false;
executor([&cancelFunctionCalled](NetworkRequestListener& listener) {
listener.setCancelFunction(
[&cancelFunctionCalled]() { cancelFunctionCalled = true; });
listener.onHeaders(200, Headers{{"x-test", "foo"}});
});
EXPECT_FALSE(cancelFunctionCalled);
// Simulate the frontend disconnecting while data is still incoming.
toPage_->disconnect();
// Expect the destruction of the agent to notify the platform implementation
// that it may cancel any download.
EXPECT_TRUE(cancelFunctionCalled);
// The host may still hold a scoped executor, but our listener has now been
// destroyed because it was owned by the (disconnected) agent, so we expect
// a late executor call to be a) safe and b) never execute.
bool callbackCalledAfterDisconnect = false;
executor(
[&callbackCalledAfterDisconnect](NetworkRequestListener& /*listener*/) {
callbackCalledAfterDisconnect = true;
});
EXPECT_FALSE(callbackCalledAfterDisconnect);
}
TEST_F(HostTargetTest, NetworkLoadNetworkResourceNotImplementedByDelegate) {
connect();
InSequence s;
EXPECT_CALL(
hostTargetDelegate_,
loadNetworkResource(
Field(&LoadNetworkResourceRequest::url, "http://example.com"), _))
.Times(1)
.WillOnce([](const LoadNetworkResourceRequest& /*params*/,
ScopedExecutor<NetworkRequestListener> /*executor*/) {
throw NotImplementedException(
"This delegate does not implement loadNetworkResource.");
})
.RetiresOnSaturation();
// The delegate's loadNetworkResource may throw immediately - verify this is
// handled and that we clean up.
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 1,
"error": {
"code": -32601,
"message": "This delegate does not implement loadNetworkResource."
}
})")));
toPage_->sendMessage(R"({
"id": 1,
"method": "Network.loadNetworkResource",
"params": {
"url": "http://example.com"
}
})");
// Check no stream is retained
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 2,
"error": {
"code": -32603,
"message": "Stream not found: 0"
}
})")));
toPage_->sendMessage(R"({
"id": 2,
"method": "IO.close",
"params": {
"handle": "0"
}
})");
}
TEST_F(HostTargetTest, NetworkLoadNetworkResource3xx) {
connect();
InSequence s;
ScopedExecutor<NetworkRequestListener> executor;
EXPECT_CALL(
hostTargetDelegate_,
loadNetworkResource(
Field(&LoadNetworkResourceRequest::url, "http://example.com/3xx"), _))
.Times(1)
.WillOnce([&executor](
const LoadNetworkResourceRequest& /*params*/,
ScopedExecutor<NetworkRequestListener> executorArg) {
// Capture the ScopedExecutor<NetworkRequestListener> to use later.
executor = std::move(executorArg);
})
.RetiresOnSaturation();
// We don't support 3xx responses, and treat them as a CDP error (as if not
// implemented so that the frontend may fall back.
toPage_->sendMessage(R"({
"id": 1,
"method": "Network.loadNetworkResource",
"params": {
"url": "http://example.com/3xx"
}
})");
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 1,
"error": {
"code": -32603,
"message": "Handling of status 301 not implemented."
}
})")));
executor([](NetworkRequestListener& listener) {
listener.onHeaders(301, Headers{{"Location", "/new"}});
});
}
} // namespace facebook::react::jsinspector_modern