1503 lines
52 KiB
C++
1503 lines
52 KiB
C++
|
/*
|
||
|
* 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
|