chromium/chrome/browser/accessibility/live_caption/live_caption_surface_browsertest.cc

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

#include "chrome/browser/accessibility/live_caption/live_caption_surface.h"

#include <optional>

#include "base/path_service.h"
#include "base/test/bind.h"
#include "base/unguessable_token.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "content/public/browser/web_contents_delegate.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/common/content_paths.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/frame/fullscreen.mojom.h"
#include "ui/base/page_transition_types.h"
#include "ui/gfx/geometry/rect.h"
#include "url/gurl.h"
#include "url/url_constants.h"

namespace captions {
namespace {

// A WebContentsObserver that runs tasks until some media starts / stops playing
// fullscreen.
class ScopedFullscreenRunner : public content::WebContentsObserver {
 public:
  explicit ScopedFullscreenRunner(content::WebContents* web_contents)
      : WebContentsObserver(web_contents) {}

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

  ~ScopedFullscreenRunner() override {
    // On destruction, wait for any fullscreen hooks to have executed. Then,
    // allow any tasks scheduled by the hooks to complete too.
    run_loop_.Run();
    base::RunLoop().RunUntilIdle();
  }

  void MediaEffectivelyFullscreenChanged(bool /*is_fullscreen*/) override {
    run_loop_.Quit();
  }

 private:
  base::RunLoop run_loop_;
};

// A surface client whose methods can have expectations placed on them.
class MockSurfaceClient : public media::mojom::SpeechRecognitionSurfaceClient {
 public:
  MockSurfaceClient() = default;
  ~MockSurfaceClient() override = default;

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

  // media::mojom::SpeechRecognitionSurfaceClient:
  MOCK_METHOD(void, OnSessionEnded, (), (override));
  MOCK_METHOD(void, OnFullscreenToggled, (), (override));

  void BindToSurface(
      mojo::PendingReceiver<media::mojom::SpeechRecognitionSurfaceClient>
          receiver,
      mojo::PendingRemote<media::mojom::SpeechRecognitionSurface> surface) {
    receiver_.Bind(std::move(receiver));
    surface_.Bind(std::move(surface));
  }

 private:
  // The remote must be held somewhere even though we don't call methods on it.
  mojo::Remote<media::mojom::SpeechRecognitionSurface> surface_;

  mojo::Receiver<media::mojom::SpeechRecognitionSurfaceClient> receiver_{this};
};

class LiveCaptionSurfaceTest : public InProcessBrowserTest {
 public:
  LiveCaptionSurfaceTest() = default;
  ~LiveCaptionSurfaceTest() override = default;

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

  void SetUpOnMainThread() override {
    InProcessBrowserTest::SetUpOnMainThread();

    // Load premade test pages.
    base::FilePath test_data_dir;
    CHECK(base::PathService::Get(content::DIR_TEST_DATA, &test_data_dir));
    embedded_test_server()->ServeFilesFromDirectory(test_data_dir);
    CHECK(embedded_test_server()->Start());
  }

  // Opens a new tab and waits for it to load the given URL.
  content::WebContents* LoadNewTab(GURL url) {
    content::RenderFrameHost* rfh = ui_test_utils::NavigateToURLWithDisposition(
        browser(), url, WindowOpenDisposition::NEW_FOREGROUND_TAB,
        ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
    return content::WebContents::FromRenderFrameHost(rfh);
  }

  // Attaches a new surface to the given web contents and binds it to the given
  // client.
  LiveCaptionSurface* NewSurfaceForWebContents(
      content::WebContents* web_contents,
      MockSurfaceClient* mock_client) {
    mojo::PendingReceiver<media::mojom::SpeechRecognitionSurface>
        surface_receiver;
    mojo::PendingRemote<media::mojom::SpeechRecognitionSurfaceClient>
        client_remote;

    mock_client->BindToSurface(client_remote.InitWithNewPipeAndPassReceiver(),
                               surface_receiver.InitWithNewPipeAndPassRemote());

    auto* surface = LiveCaptionSurface::GetOrCreateForWebContents(web_contents);
    surface->BindToSurfaceClient(std::move(surface_receiver),
                                 std::move(client_remote));

    return surface;
  }

  const GURL kAboutBlankUrl = GURL(url::kAboutBlankURL);
};

// Test that the surface can be used to focus its tab.
IN_PROC_BROWSER_TEST_F(LiveCaptionSurfaceTest, Activate) {
  // Create an initial tab with a surface attached.
  MockSurfaceClient client;
  content::WebContents* wc_1 = LoadNewTab(kAboutBlankUrl);
  LiveCaptionSurface* surface = NewSurfaceForWebContents(wc_1, &client);

  // The initial tab should be focused.
  EXPECT_EQ(browser()->tab_strip_model()->GetActiveWebContents(), wc_1);

  // Create a new tab, which should start focused.
  content::WebContents* wc_2 = LoadNewTab(kAboutBlankUrl);
  EXPECT_EQ(browser()->tab_strip_model()->GetActiveWebContents(), wc_2);

  // Activating the first tab via its surface should bring it into focus.
  surface->Activate();
  EXPECT_EQ(browser()->tab_strip_model()->GetActiveWebContents(), wc_1);
}

// Test that the surface correctly reports tab bounds on screen.
IN_PROC_BROWSER_TEST_F(LiveCaptionSurfaceTest, Bounds) {
  // Create a tab with a surface attached.
  MockSurfaceClient client;
  LiveCaptionSurface* surface =
      NewSurfaceForWebContents(LoadNewTab(kAboutBlankUrl), &client);

  // Callback to assign bounds to local variables.
  const auto assign_bounds = [](gfx::Rect* d,
                                const std::optional<gfx::Rect>& b) {
    ASSERT_TRUE(b.has_value());
    *d = *b;
  };

  // Set known window bounds.
  const gfx::Rect window_bounds_1 = gfx::Rect(10, 10, 800, 600);
  browser()->window()->SetBounds(window_bounds_1);

  // Fetch bounds using the surface.
  gfx::Rect bounds_1;
  surface->GetBounds(base::BindOnce(assign_bounds, &bounds_1));
  base::RunLoop().RunUntilIdle();

  // The surface should have correctly reported the window bounds.
  EXPECT_EQ(window_bounds_1, bounds_1);

  // Set new window bounds.
  const gfx::Rect window_bounds_2 = gfx::Rect(50, 50, 800, 600);
  browser()->window()->SetBounds(window_bounds_2);

  // Fetch bounds using the surface.
  gfx::Rect bounds_2;
  surface->GetBounds(base::BindOnce(assign_bounds, &bounds_2));
  base::RunLoop().RunUntilIdle();

  // The surface should have correctly reported the window bounds.
  EXPECT_EQ(window_bounds_2, bounds_2);
}

// Test that the client is informed of fullscreen changes.
IN_PROC_BROWSER_TEST_F(LiveCaptionSurfaceTest, Fullscreen) {
  // Load a pre-prepared page that defines JS to (un/)fullscreen the tab.
  MockSurfaceClient client;
  content::WebContents* web_contents =
      LoadNewTab(embedded_test_server()->GetURL("/media/fullscreen.html"));
  NewSurfaceForWebContents(web_contents, &client);

  // We use a mock function to set up "checkpoints" by which each side effect
  // should have occurred.
  testing::MockFunction<void(int)> checkpointer;

  {
    testing::InSequence seq;

    // Fullscreen media on the page.
    EXPECT_CALL(client, OnFullscreenToggled());
    EXPECT_CALL(checkpointer, Call(1));
    {
      ScopedFullscreenRunner runner(web_contents);
      EXPECT_TRUE(
          content::ExecJs(web_contents, "makeFullscreen('small_video')"));
    }
    checkpointer.Call(1);

    // Un-fullscreen the media.
    EXPECT_CALL(client, OnFullscreenToggled());
    EXPECT_CALL(checkpointer, Call(2));
    {
      ScopedFullscreenRunner runner(web_contents);
      EXPECT_TRUE(content::ExecJs(web_contents, "exitFullscreen()"));
    }
    checkpointer.Call(2);
  }
}

// Test that the surface correctly reports session IDs.
IN_PROC_BROWSER_TEST_F(LiveCaptionSurfaceTest, SessionIds) {
  // Create two tabs with two surfaces.
  MockSurfaceClient client_1, client_2;
  LiveCaptionSurface* surface_1 =
      NewSurfaceForWebContents(LoadNewTab(kAboutBlankUrl), &client_1);
  LiveCaptionSurface* surface_2 =
      NewSurfaceForWebContents(LoadNewTab(kAboutBlankUrl), &client_2);

  // The session IDs of the two surfaces should be different because they
  // represent different web contents.
  const base::UnguessableToken init_id_1 = surface_1->session_id();
  EXPECT_NE(init_id_1, surface_2->session_id());

  // Navigating a tab shouldn't change its session ID.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), kAboutBlankUrl));
  EXPECT_EQ(init_id_1, surface_1->session_id());
}

// Test that a surface reports the end of live caption sessions.
IN_PROC_BROWSER_TEST_F(LiveCaptionSurfaceTest, Sessions) {
  // Create two tabs with surfaces attached.
  MockSurfaceClient client_1, client_2;
  content::WebContents* wc_1 = LoadNewTab(kAboutBlankUrl);
  content::WebContents* wc_2 = LoadNewTab(kAboutBlankUrl);
  LiveCaptionSurface* surface_1 = NewSurfaceForWebContents(wc_1, &client_1);
  NewSurfaceForWebContents(wc_2, &client_2);

  // We use a mock function to set up "checkpoints" by which each side effect
  // should have occurred.
  testing::MockFunction<void(int)> checkpointer;

  {
    testing::InSequence seq;

    // We will start by making a navigation in the focused tab. We expect this
    // should end its session, but not the other tab's.

    EXPECT_CALL(client_1, OnSessionEnded()).Times(0);
    EXPECT_CALL(client_2, OnSessionEnded());
    EXPECT_CALL(checkpointer, Call(1));

    ASSERT_EQ(wc_2, browser()->tab_strip_model()->GetActiveWebContents());
    ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), kAboutBlankUrl));
    base::RunLoop().RunUntilIdle();
    checkpointer.Call(1);

    // Next we will refresh the first tab. We expect this should end its
    // session.

    EXPECT_CALL(client_1, OnSessionEnded());
    EXPECT_CALL(checkpointer, Call(2));

    surface_1->Activate();
    ASSERT_EQ(wc_1, browser()->tab_strip_model()->GetActiveWebContents());

    chrome::Reload(browser(), WindowOpenDisposition::CURRENT_TAB);
    content::WaitForLoadStop(wc_1);
    base::RunLoop().RunUntilIdle();
    checkpointer.Call(2);

    // Lastly, we will navigate from the first tab. This should end the session
    // again.

    EXPECT_CALL(client_1, OnSessionEnded());
    EXPECT_CALL(checkpointer, Call(3));

    // Navigating from the first tab should end its session again.
    ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), kAboutBlankUrl));
    base::RunLoop().RunUntilIdle();
    checkpointer.Call(3);
  }
}

}  // namespace
}  // namespace captions