chromium/chromecast/bindings/bindings_manager_cast_browsertest.cc

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef CHROMECAST_BINDINGS_BINDINGS_MANAGER_CAST_BROWSERTEST_H_
#define CHROMECAST_BINDINGS_BINDINGS_MANAGER_CAST_BROWSERTEST_H_

#include "chromecast/bindings/bindings_manager_cast.h"

#include <string>
#include <string_view>
#include <vector>

#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/memory/weak_ptr.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "chromecast/base/chromecast_switches.h"
#include "chromecast/base/metrics/cast_metrics_helper.h"
#include "chromecast/bindings/public/mojom/api_bindings.mojom.h"
#include "chromecast/browser/cast_browser_context.h"
#include "chromecast/browser/cast_browser_process.h"
#include "chromecast/browser/cast_web_contents_impl.h"
#include "chromecast/browser/cast_web_contents_observer.h"
#include "components/cast/message_port/test_message_port_receiver.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_base.h"
#include "content/public/test/browser_test_utils.h"
#include "mojo/public/cpp/bindings/connector.h"
#include "mojo/public/cpp/bindings/message.h"
#include "mojo/public/cpp/system/message_pipe.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"

using ::testing::_;
using ::testing::Expectation;
using ::testing::InSequence;
using ::testing::Mock;
using ::testing::NiceMock;

namespace content {
class WebContents;
}  // namespace content

namespace chromecast {

namespace {

const base::FilePath::CharType kTestDataPath[] =
    FILE_PATH_LITERAL("chromecast/bindings/testdata");

base::FilePath GetTestDataPath() {
  return base::FilePath(kTestDataPath);
}

base::FilePath GetTestDataFilePath(const std::string& name) {
  base::FilePath file_path;
  CHECK(base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &file_path));
  return file_path.Append(GetTestDataPath()).AppendASCII(name);
}

class TitleChangeObserver : public CastWebContentsObserver {
 public:
  TitleChangeObserver() = default;

  TitleChangeObserver(const TitleChangeObserver&) = delete;
  TitleChangeObserver& operator=(const TitleChangeObserver&) = delete;

  ~TitleChangeObserver() override = default;

  // Spins a Runloop until the title of the page matches the |expected_title|
  // that have been set.
  void RunUntilTitleEquals(std::string_view expected_title) {
    expected_title_ = std::string(expected_title);
    // Spin the runloop until the expected conditions are met.
    if (current_title_ != expected_title_) {
      expected_title_ = std::string(expected_title);
      base::RunLoop run_loop;
      quit_closure_ = run_loop.QuitClosure();
      run_loop.Run();
    }
  }

  // CastWebContentsObserver implementation:
  void UpdateTitle(const std::string& title) override {
    // Resumes execution of RunUntilTitleEquals() if |title| matches
    // expectations.
    current_title_ = title;
    if (!quit_closure_.is_null() && current_title_ == expected_title_) {
      std::move(quit_closure_).Run();
    }
  }

 private:
  std::string current_title_;
  std::string expected_title_;

  base::OnceClosure quit_closure_;
};

// =============================================================================
// Mocks
// =============================================================================
class MockWebContentsDelegate : public content::WebContentsDelegate {
 public:
  MockWebContentsDelegate() = default;

  MockWebContentsDelegate(const MockWebContentsDelegate&) = delete;
  MockWebContentsDelegate& operator=(const MockWebContentsDelegate&) = delete;

  ~MockWebContentsDelegate() override = default;

  MOCK_METHOD1(CloseContents, void(content::WebContents* source));
};

}  // namespace

// =============================================================================
// Test class
// =============================================================================
class BindingsManagerCastBrowserTest : public content::BrowserTestBase {
 public:
  BindingsManagerCastBrowserTest(const BindingsManagerCastBrowserTest&) =
      delete;
  BindingsManagerCastBrowserTest& operator=(
      const BindingsManagerCastBrowserTest&) = delete;

 protected:
  BindingsManagerCastBrowserTest() = default;
  ~BindingsManagerCastBrowserTest() override = default;

  void SetUp() final {
    SetUpCommandLine(base::CommandLine::ForCurrentProcess());
    BrowserTestBase::SetUp();
  }
  void SetUpCommandLine(base::CommandLine* command_line) final {
    command_line->AppendSwitchASCII(switches::kTestType, "browser");
  }

  void PreRunTestOnMainThread() override {
    // Pump startup related events.
    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
    base::RunLoop().RunUntilIdle();

    metrics::CastMetricsHelper::GetInstance()->SetDummySessionIdForTesting();
    content::WebContents::CreateParams create_params(
        shell::CastBrowserProcess::GetInstance()->browser_context(), nullptr);
    web_contents_ = content::WebContents::Create(create_params);
    web_contents_->SetDelegate(&mock_wc_delegate_);

    // CastWebContents::Delegate must be set for receiving PageStateChanged
    // event.
    mojom::CastWebViewParamsPtr params = mojom::CastWebViewParams::New();
    params->is_root_window = true;

    cast_web_contents_ = std::make_unique<CastWebContentsImpl>(
        web_contents_.get(), std::move(params));
    title_change_observer_.Observe(cast_web_contents_.get());
    bindings_manager_ = std::make_unique<bindings::BindingsManagerCast>();
    cast_web_contents_->ConnectToBindingsService(
        bindings_manager_->CreateRemote());
  }

  void PostRunTestOnMainThread() override {
    cast_web_contents_.reset();
    web_contents_.reset();
    bindings_manager_.reset();
  }

  void StartTestServer() {
    ASSERT_TRUE(embedded_test_server()->InitializeAndListen());
    embedded_test_server()->StartAcceptingConnections();
  }

  NiceMock<MockWebContentsDelegate> mock_wc_delegate_;
  TitleChangeObserver title_change_observer_;
  std::unique_ptr<content::WebContents> web_contents_;
  std::unique_ptr<CastWebContentsImpl> cast_web_contents_;

  std::unique_ptr<bindings::BindingsManagerCast> bindings_manager_;
};

// Handles connected ports from the NamedMessagePortConnector and
// provides a convenience methods for waiting for and then returning the port
// synchronously.
class MessagePortConnectionHandler {
 public:
  MessagePortConnectionHandler() {}
  ~MessagePortConnectionHandler() {}

  cast_api_bindings::Manager::MessagePortConnectedHandler GetConnectCallback() {
    return base::BindRepeating(&MessagePortConnectionHandler::OnConnect,
                               base::Unretained(this));
  }

  std::unique_ptr<cast_api_bindings::MessagePort> RunUntilPortConnected() {
    base::RunLoop run_loop;
    on_port_connected_ = run_loop.QuitClosure();
    run_loop.Run();
    return std::move(port_);
  }

 private:
  void OnConnect(std::unique_ptr<cast_api_bindings::MessagePort> port) {
    DCHECK(on_port_connected_);

    port_ = std::move(port);
    std::move(on_port_connected_).Run();
  }

  base::OnceClosure on_port_connected_;
  std::unique_ptr<cast_api_bindings::MessagePort> port_;
};

// =============================================================================
// Test cases
// =============================================================================
IN_PROC_BROWSER_TEST_F(BindingsManagerCastBrowserTest, EndToEnd) {
  // ===========================================================================
  // Test: Load BindingsManagerCast, ensure binding backend can receive a port
  // via BindingsManagerCast and the port is good to use.
  // Step 1: Create a TestBindingBackend object. TestBindingBackend will
  // register a PortHandler to BindingsManagerCast.
  // Step 2: Attach |bindings_manager_cast_| to |cast_web_contents_|, port
  // connector binding should be injected into |cast_web_contents_|.
  // Step 3: Load the test page, expected behaviours include:
  //  - BindingsManagerCast posts one end of MessagePort to the page.
  //    NamedMessagePort binding should be able to forward ports to native.
  //  - BindingManagerCast should successfully route a connected MessagePort to
  //    TestBindingBackend. This port is created by test page "connector.html".
  // Step 4: Verify that messages that are sent through the port are cached
  // before the port is not routed to native. And make sure TestBindingBackend
  // could use the |bindings_manager_cast_| provided port to send & receive
  // messages. Note: Messages should arrive in order.
  // ===========================================================================
  GURL test_url =
      content::GetFileUrlWithQuery(GetTestDataFilePath("connector.html"), "");

  MessagePortConnectionHandler connect_handler;
  bindings_manager_->RegisterPortHandler("hello",
                                         connect_handler.GetConnectCallback());

  // Load test page.
  constexpr char kTestPageTitle[] = "bindings";
  cast_web_contents_->LoadUrl(test_url);
  title_change_observer_.RunUntilTitleEquals(kTestPageTitle);

  auto message_port = connect_handler.RunUntilPortConnected();
  cast_api_bindings::TestMessagePortReceiver receiver;
  message_port->SetReceiver(&receiver);

  message_port->PostMessage("ping");

  // Test that message are received in order.
  receiver.RunUntilMessageCountEqual(3);
  EXPECT_EQ(receiver.buffer()[0].first, "early 1");
  EXPECT_EQ(receiver.buffer()[1].first, "early 2");
  EXPECT_EQ(receiver.buffer()[2].first, "ack ping");

  // Ensure that the MessagePort is dropped when navigating away.
  cast_web_contents_->LoadUrl(GURL(url::kAboutBlankURL));
  receiver.RunUntilDisconnected();

  bindings_manager_->UnregisterPortHandler("hello");
}

IN_PROC_BROWSER_TEST_F(BindingsManagerCastBrowserTest, OrderedBindings) {
  bindings_manager_->AddBinding("foo", "bar");
  bindings_manager_->AddBinding("hello", "world");

  // A repeated binding should have its order preserved
  bindings_manager_->AddBinding("foo", "BAR");

  std::vector<std::string> received_bindings;
  static_cast<chromecast::mojom::ApiBindings*>(bindings_manager_.get())
      ->GetAll(base::BindLambdaForTesting(
          [&](std::vector<chromecast::mojom::ApiBindingPtr> bindings) {
            for (auto& entry : bindings) {
              received_bindings.push_back(entry->script);
            }
          }));

  EXPECT_EQ(2UL, received_bindings.size());
  EXPECT_EQ("BAR", received_bindings[0]);
  EXPECT_EQ("world", received_bindings[1]);
}
}  // namespace chromecast

#endif  // CHROMECAST_BINDINGS_BINDINGS_MANAGER_CAST_BROWSERTEST_H_