chromium/fuchsia_web/webengine/web_engine_integration_test.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.

#include <fuchsia/mediacodec/cpp/fidl.h>
#include <fuchsia/mem/cpp/fidl.h>
#include <lib/zx/vmo.h>
#include <zircon/rights.h>
#include <zircon/types.h>

#include <optional>
#include <string>
#include <vector>

#include "base/containers/contains.h"
#include "base/fuchsia/fuchsia_logging.h"
#include "base/fuchsia/mem_buffer_util.h"
#include "base/fuchsia/process_context.h"
#include "base/strings/stringprintf.h"
#include "base/test/test_future.h"
#include "build/build_config.h"
#include "components/version_info/version_info.h"
#include "fuchsia_web/common/test/fit_adapter.h"
#include "fuchsia_web/common/test/frame_test_util.h"
#include "fuchsia_web/common/test/test_devtools_list_fetcher.h"
#include "fuchsia_web/webengine/test/context_provider_for_test.h"
#include "fuchsia_web/webengine/web_engine_integration_test_base.h"
#include "media/base/media_switches.h"
#include "media/fuchsia/audio/fake_audio_consumer.h"
#include "media/fuchsia/audio/fake_audio_device_enumerator.h"
#include "media/fuchsia/camera/fake_fuchsia_camera.h"
#include "net/http/http_request_headers.h"
#include "testing/gmock/include/gmock/gmock.h"

namespace {

constexpr char kValidUserAgentProduct[] = "TestProduct";
constexpr char kValidUserAgentVersion[] = "dev.12345";
constexpr char kValidUserAgentProductAndVersion[] = "TestProduct/dev.12345";
constexpr char kInvalidUserAgentProduct[] = "Test/Product";
constexpr char kInvalidUserAgentVersion[] = "dev/12345";

constexpr char kAutoplayVp9OpusUrl[] =
    "fuchsia-dir://testdata/play_video.html?codecs=vp9,opus&autoplay=1";
constexpr char kAutoplayVp9OpusToEndUrl[] =
    "fuchsia-dir://testdata/"
    "play_video.html?codecs=vp9,opus&autoplay=1&reportended=1";
constexpr char kLoadVp9OpusUrl[] =
    "fuchsia-dir://testdata/play_video.html?codecs=vp9,opus";

}  // namespace

// Starts a WebEngine instance before running the test.
class WebEngineIntegrationTest : public WebEngineIntegrationTestBase {
 protected:
  WebEngineIntegrationTest() = default;

  ~WebEngineIntegrationTest() override {
    // We're about to shut down the realm; unbind to unhook the error handler.
    frame_.Unbind();
    context_.Unbind();
  }

  void StartWebEngine(base::CommandLine command_line) override {
    context_provider_.emplace(
        ContextProviderForTest::Create(std::move(command_line)));
    context_provider_->ptr().set_error_handler(
        [](zx_status_t status) { FAIL() << zx_status_get_string(status); });
  }

  fuchsia::web::ContextProvider* GetContextProvider() override {
    return context_provider_->get();
  }

  void RunPermissionTest(bool grant);

 private:
  std::optional<ContextProviderForTest> context_provider_;
};

class WebEngineIntegrationUserAgentTest : public WebEngineIntegrationTest {
 protected:
  GURL GetEchoUserAgentUrl() {
    static std::string echo_user_agent_header_path =
        std::string("/echoheader?") + net::HttpRequestHeaders::kUserAgent;
    return embedded_test_server_.GetURL(echo_user_agent_header_path);
  }

  // Returns the expected user agent string for the current Chrome version.
  static std::string GetExpectedUserAgentString() {
    // The default (base) user agent string without any client modifications.
    // Due to reduced user agent, only the major version is populated, the
    // version number is <majorVersion>.0.0.0.
    constexpr char kDefaultUserAgentStringWithVersionPlaceholder[] =
        "Mozilla/5.0 (Fuchsia) AppleWebKit/537.36 (KHTML, like Gecko) "
        "Chrome/%d.0.0.0 Safari/537.36";

    std::string expected_ua =
        base::StringPrintf(kDefaultUserAgentStringWithVersionPlaceholder,
                           version_info::GetMajorVersionNumberAsInt());

    // Ensure the field was actually populated.
    EXPECT_TRUE(
        base::Contains(expected_ua, version_info::GetMajorVersionNumber()));

    return expected_ua;
  }

  static std::string GetExpectedUserAgentStringWithProduct(
      const char* product) {
    return base::StringPrintf("%s %s", GetExpectedUserAgentString().c_str(),
                              product);
  }
};

// Although this does not need to be an integration test, all other user agent
// tests are here.
TEST_F(WebEngineIntegrationUserAgentTest, Default) {
  StartWebEngine(base::CommandLine(base::CommandLine::NO_PROGRAM));
  // Create a Context with no values specified.
  fuchsia::web::CreateContextParams create_params = TestContextParams();
  CreateContextAndFrameAndLoadUrl(std::move(create_params),
                                  GetEchoUserAgentUrl());

  // Query & verify that the header echoed into the document body is as
  // expected.
  std::string result =
      ExecuteJavaScriptWithStringResult("document.body.innerText;");
  EXPECT_EQ(result, GetExpectedUserAgentString());

  // Query & verify that the navigator.userAgent is as expected.
  result = ExecuteJavaScriptWithStringResult("navigator.userAgent;");
  EXPECT_EQ(result, GetExpectedUserAgentString());
}

TEST_F(WebEngineIntegrationUserAgentTest, ValidProductOnly) {
  StartWebEngine(base::CommandLine(base::CommandLine::NO_PROGRAM));
  // Create a Context with just an embedder product specified.
  fuchsia::web::CreateContextParams create_params = TestContextParams();
  create_params.set_user_agent_product(kValidUserAgentProduct);
  CreateContextAndFrameAndLoadUrl(std::move(create_params),
                                  GetEchoUserAgentUrl());

  std::string expected =
      GetExpectedUserAgentStringWithProduct(kValidUserAgentProduct);

  // Query & verify that the header echoed into the document body contains
  // the product tag.
  std::string result =
      ExecuteJavaScriptWithStringResult("document.body.innerText;");
  EXPECT_TRUE(base::Contains(result, kValidUserAgentProduct));
  EXPECT_EQ(result, expected);

  // Query & verify that the navigator.userAgent contains the product tag.
  result = ExecuteJavaScriptWithStringResult("navigator.userAgent;");
  EXPECT_TRUE(base::Contains(result, kValidUserAgentProduct));
  EXPECT_EQ(result, expected);
}

TEST_F(WebEngineIntegrationUserAgentTest, ValidProductAndVersion) {
  StartWebEngine(base::CommandLine(base::CommandLine::NO_PROGRAM));
  // Create a Context with both product and version specified.
  fuchsia::web::CreateContextParams create_params = TestContextParams();
  create_params.set_user_agent_product(kValidUserAgentProduct);
  create_params.set_user_agent_version(kValidUserAgentVersion);
  CreateContextAndFrameAndLoadUrl(std::move(create_params),
                                  GetEchoUserAgentUrl());

  std::string expected =
      GetExpectedUserAgentStringWithProduct(kValidUserAgentProductAndVersion);

  // Query & verify that the header echoed into the document body contains
  // both product & version.
  std::string result =
      ExecuteJavaScriptWithStringResult("document.body.innerText;");
  EXPECT_TRUE(base::Contains(result, kValidUserAgentProductAndVersion));
  EXPECT_EQ(result, expected);

  // Query & verify that the navigator.userAgent contains product & version.
  result = ExecuteJavaScriptWithStringResult("navigator.userAgent;");
  EXPECT_TRUE(base::Contains(result, kValidUserAgentProductAndVersion));
  EXPECT_EQ(result, expected);

  // Verify navigator.platform is empty, see crbug.com/1348646.
  EXPECT_EQ(ExecuteJavaScriptWithStringResult("navigator.platform;"), "");
}

TEST_F(WebEngineIntegrationUserAgentTest, InvalidProduct) {
  StartWebEngine(base::CommandLine(base::CommandLine::NO_PROGRAM));
  // Try to create a Context with an invalid embedder product tag.
  fuchsia::web::CreateContextParams create_params = TestContextParams();
  create_params.set_user_agent_product(kInvalidUserAgentProduct);
  CreateContextAndExpectError(std::move(create_params), ZX_ERR_INVALID_ARGS);
}

TEST_F(WebEngineIntegrationUserAgentTest, VersionOnly) {
  StartWebEngine(base::CommandLine(base::CommandLine::NO_PROGRAM));
  // Try to create a Context with an embedder version but no product.
  fuchsia::web::CreateContextParams create_params = TestContextParams();
  create_params.set_user_agent_version(kValidUserAgentVersion);
  CreateContextAndExpectError(std::move(create_params), ZX_ERR_INVALID_ARGS);
}

TEST_F(WebEngineIntegrationUserAgentTest, ValidProductAndInvalidVersion) {
  StartWebEngine(base::CommandLine(base::CommandLine::NO_PROGRAM));
  // Try to create a Context with valid product tag, but invalid version.
  fuchsia::web::CreateContextParams create_params = TestContextParams();
  create_params.set_user_agent_product(kValidUserAgentProduct);
  create_params.set_user_agent_version(kInvalidUserAgentVersion);
  CreateContextAndExpectError(std::move(create_params), ZX_ERR_INVALID_ARGS);
}

TEST_F(WebEngineIntegrationTest, CreateFrameWithUnclonableFrameParamsFails) {
  StartWebEngine(base::CommandLine(base::CommandLine::NO_PROGRAM));
  CreateContext(TestContextParams());

  zx_rights_t kReadRightsWithoutDuplicate =
      ZX_RIGHT_TRANSFER | ZX_RIGHT_READ | ZX_RIGHT_MAP | ZX_RIGHT_GET_PROPERTY;

  // Create a buffer and remove the ability clone it by changing its rights to
  // not include ZX_RIGHT_DUPLICATE.
  auto buffer = base::MemBufferFromString("some data", "some name");
  zx::vmo unclonable_readonly_vmo;
  EXPECT_EQ(ZX_OK, buffer.vmo.duplicate(kReadRightsWithoutDuplicate,
                                        &unclonable_readonly_vmo));
  buffer.vmo = std::move(unclonable_readonly_vmo);

  // Creation will fail, which will be reported in the error handler below.
  fuchsia::web::CreateFrameParams create_frame_params;
  create_frame_params.set_explicit_sites_filter_error_page(
      fuchsia::mem::Data::WithBuffer(std::move(buffer)));
  context_->CreateFrameWithParams(std::move(create_frame_params),
                                  frame_.NewRequest());

  base::RunLoop loop;
  frame_.set_error_handler(
      [quit_loop = loop.QuitClosure()](zx_status_t status) {
        EXPECT_EQ(status, ZX_ERR_INVALID_ARGS);
        quit_loop.Run();
      });
  loop.Run();

  EXPECT_FALSE(frame_);
}

// Check that if the CreateContextParams has |remote_debugging_port| set then:
// - DevTools becomes available when the first debuggable Frame is created.
// - DevTools closes when the last debuggable Frame is closed.
TEST_F(WebEngineIntegrationTest, RemoteDebuggingPort) {
  StartWebEngine(base::CommandLine(base::CommandLine::NO_PROGRAM));
  // Create a Context with remote debugging enabled via an ephemeral port.
  fuchsia::web::CreateContextParams create_params = TestContextParams();
  create_params.set_remote_debugging_port(0);

  // Create a Frame with remote debugging enabled.
  fuchsia::web::CreateFrameParams create_frame_params;
  create_frame_params.set_enable_remote_debugging(true);
  CreateContext(std::move(create_params));
  CreateFrameWithParams(std::move(create_frame_params));

  // Expect to receive a notification of the selected DevTools port.
  base::test::TestFuture<fuchsia::web::Context_GetRemoteDebuggingPort_Result>
      port_receiver;
  context_->GetRemoteDebuggingPort(
      CallbackToFitFunction(port_receiver.GetCallback()));
  ASSERT_TRUE(port_receiver.Wait());

  ASSERT_TRUE(port_receiver.Get().is_response());
  uint16_t remote_debugging_port = port_receiver.Get().response().port;
  ASSERT_TRUE(remote_debugging_port != 0);

  // Navigate to a URL.
  GURL url = embedded_test_server_.GetURL("/defaultresponse");
  ASSERT_NO_FATAL_FAILURE(LoadUrlAndExpectResponse(url.spec()));
  navigation_listener()->RunUntilUrlEquals(url);

  base::Value::List devtools_list =
      GetDevToolsListFromPort(remote_debugging_port);
  EXPECT_EQ(devtools_list.size(), 1u);

  {
    const auto* devtools_url = devtools_list[0].GetDict().FindString("url");
    ASSERT_TRUE(devtools_url);
    EXPECT_EQ(*devtools_url, url);
  }

  // Create a second frame, without remote debugging enabled. The remote
  // debugging service should still report a single Frame is present.
  fuchsia::web::FramePtr web_frame2;
  web_frame2.set_error_handler(
      [](zx_status_t status) { FAIL() << zx_status_get_string(status); });
  context()->CreateFrame(web_frame2.NewRequest());

  devtools_list = GetDevToolsListFromPort(remote_debugging_port);
  EXPECT_EQ(devtools_list.size(), 1u);

  {
    const auto* devtools_url = devtools_list[0].GetDict().FindString("url");
    ASSERT_TRUE(devtools_url);
    EXPECT_EQ(*devtools_url, url);
  }

  // Tear down the debuggable Frame. The remote debugging service should have
  // shut down.
  base::RunLoop controller_run_loop;
  auto navigation_controller = CreateNavigationController();
  navigation_controller.set_error_handler(
      [&controller_run_loop](zx_status_t) { controller_run_loop.Quit(); });
  frame_.Unbind();

  // Wait until the NavigationController shuts down to ensure WebEngine has
  // handled the Frame tear down.
  controller_run_loop.Run();

  devtools_list = GetDevToolsListFromPort(remote_debugging_port);
  EXPECT_TRUE(devtools_list.empty());
}

// Check that remote debugging requests for Frames in non-debuggable Contexts
// cause an error to be reported.
TEST_F(WebEngineIntegrationTest, RequestDebuggableFrameInNonDebuggableContext) {
  StartWebEngine(base::CommandLine(base::CommandLine::NO_PROGRAM));
  fuchsia::web::CreateFrameParams create_frame_params;
  create_frame_params.set_enable_remote_debugging(true);
  CreateContext(TestContextParams());
  CreateFrameWithParams(std::move(create_frame_params));

  base::RunLoop loop;
  frame_.set_error_handler(
      [quit_loop = loop.QuitClosure()](zx_status_t status) {
        EXPECT_EQ(status, ZX_ERR_INVALID_ARGS);
        quit_loop.Run();
      });
  loop.Run();
}

// Navigates to a resource served under the "testdata" ContentDirectory.
TEST_F(WebEngineIntegrationTest, ContentDirectoryProvider) {
  const GURL kUrl("fuchsia-dir://testdata/title1.html");
  constexpr char kTitle[] = "title 1";

  StartWebEngine(base::CommandLine(base::CommandLine::NO_PROGRAM));
  CreateContextAndFrame(TestContextParamsWithTestData());

  // Navigate to test1.html and verify that the resource was correctly
  // downloaded and interpreted by inspecting the document title.
  ASSERT_NO_FATAL_FAILURE(LoadUrlAndExpectResponse(kUrl.spec()));
  navigation_listener()->RunUntilUrlAndTitleEquals(kUrl, kTitle);
}

class WebEngineIntegrationReduceAcceptLanguageTest
    : public WebEngineIntegrationTest {
 protected:
  GURL GetEchoAcceptLanguageUrl() {
    static std::string echo_accept_language_header_path =
        std::string("/echoheader?") + net::HttpRequestHeaders::kAcceptLanguage;
    return embedded_test_server_.GetURL(echo_accept_language_header_path);
  }
  std::vector<std::string> GetNavigatorLanguages() {
    std::optional<base::Value> value =
        ExecuteJavaScript(frame_.get(), "navigator.languages;");
    std::vector<std::string> accept_languages;
    if (!value) {
      return accept_languages;
    }
    for (const auto& language : value->GetList()) {
      accept_languages.push_back(language.GetString());
    }
    return accept_languages;
  }
};

TEST_F(WebEngineIntegrationReduceAcceptLanguageTest,
       DisableReduceAcceptLanguage) {
  base::CommandLine command_line(base::CommandLine::NO_PROGRAM);
  command_line.AppendSwitchASCII("disable-features", "ReduceAcceptLanguage");
  StartWebEngine(std::move(command_line));

  // Create a Context with no values specified.
  fuchsia::web::CreateContextParams create_params = TestContextParams();
  CreateContextAndFrameAndLoadUrl(std::move(create_params),
                                  GetEchoAcceptLanguageUrl());

  // Query & verify that the header echoed into the document body is as
  // expected.
  std::string result =
      ExecuteJavaScriptWithStringResult("document.body.innerText;");
  EXPECT_EQ(result, "en-US,en;q=0.9");

  // Query & verify that the navigator.languages is as expected.
  EXPECT_THAT(GetNavigatorLanguages(), testing::ElementsAre("en-US"));
}

TEST_F(WebEngineIntegrationReduceAcceptLanguageTest, ReduceAcceptLanguage) {
  base::CommandLine command_line(base::CommandLine::NO_PROGRAM);
  command_line.AppendSwitchASCII("enable-features", "ReduceAcceptLanguage");
  StartWebEngine(std::move(command_line));

  // Create a Context with no values specified.
  fuchsia::web::CreateContextParams create_params = TestContextParams();
  CreateContextAndFrameAndLoadUrl(std::move(create_params),
                                  GetEchoAcceptLanguageUrl());

  // Query & verify that the header echoed into the document body is as
  // expected.
  std::string result =
      ExecuteJavaScriptWithStringResult("document.body.innerText;");
  EXPECT_EQ(result, "en-US,en;q=0.9");

  // Query & verify that the navigator.languages is as expected.
  EXPECT_THAT(GetNavigatorLanguages(), testing::ElementsAre("en-US"));
}

class FakeAudioRenderer
    : public fuchsia::media::testing::AudioRenderer_TestBase {
 public:
  FakeAudioRenderer() = default;
  ~FakeAudioRenderer() override = default;

  void set_on_set_usage_callback(base::OnceClosure on_set_usage_callback) {
    on_set_usage_callback_ = std::move(on_set_usage_callback);
  }

  void Bind(fidl::InterfaceRequest<fuchsia::media::AudioRenderer> request) {
    binding_.AddBinding(this, std::move(request));
  }

  const std::optional<fuchsia::media::AudioRenderUsage>& usage() const {
    return usage_;
  }

  // AudioRenderer_TestBase overrides.
  void SetUsage(fuchsia::media::AudioRenderUsage usage) override {
    usage_ = usage;
    if (on_set_usage_callback_)
      std::move(on_set_usage_callback_).Run();
  }
  void NotImplemented_(const std::string& name) override {}

 private:
  fidl::BindingSet<fuchsia::media::AudioRenderer> binding_;
  base::OnceClosure on_set_usage_callback_;
  std::optional<fuchsia::media::AudioRenderUsage> usage_;
};

class FakeAudio : public fuchsia::media::testing::Audio_TestBase {
 public:
  FakeAudio() = default;
  ~FakeAudio() override = default;

  void Bind(fidl::InterfaceRequest<fuchsia::media::Audio> request) {
    binding_.AddBinding(this, std::move(request));
  }

  FakeAudioRenderer& renderer() { return renderer_; }

  // Audio_TestBase overrides.
  void CreateAudioRenderer(
      fidl::InterfaceRequest<fuchsia::media::AudioRenderer> request) override {
    renderer_.Bind(std::move(request));
  }
  void NotImplemented_(const std::string& name) override {
    FAIL() << "Not implemented: " << name;
  }

 private:
  fidl::BindingSet<fuchsia::media::Audio> binding_;
  FakeAudioRenderer renderer_;
};

// Configures the default filtered service directory with a fake AudioConsumer
// service for testing.
class WebEngineIntegrationMediaTest : public WebEngineIntegrationTest {
 protected:
  WebEngineIntegrationMediaTest()
      : fake_audio_consumer_service_(filtered_service_directory()
                                         .outgoing_directory()
                                         ->GetOrCreateDirectory("svc")) {
    auto* outgoing_directory =
        filtered_service_directory().outgoing_directory();

    // Publish fake AudioDeviceEnumerator.
    outgoing_directory
        ->RemovePublicService<fuchsia::media::AudioDeviceEnumerator>();
    fake_audio_device_enumerator_.emplace(
        outgoing_directory->GetOrCreateDirectory("svc"));

    // Intercept `fuchsia::media::Audio` connections in order to count them.
    outgoing_directory->RemovePublicService<fuchsia::media::Audio>();
    zx_status_t status = outgoing_directory->AddPublicService(
        fidl::InterfaceRequestHandler<fuchsia::media::Audio>(
            [this](auto request) {
              ++num_audio_connections_;
              if (fake_audio_) {
                fake_audio_->Bind(std::move(request));
              } else {
                base::ComponentContextForProcess()->svc()->Connect(
                    std::move(request));
              }
            }));
    ZX_CHECK(status == ZX_OK, status) << "AddPublicService";
  }

  // Returns a CreateContextParams that has AUDIO feature, and the "testdata"
  // content directory provider configured.
  fuchsia::web::CreateContextParams ContextParamsWithAudioAndTestData() {
    fuchsia::web::CreateContextParams create_params =
        TestContextParamsWithTestData();
    *create_params.mutable_features() |=
        fuchsia::web::ContextFeatureFlags::AUDIO;
    return create_params;
  }

  media::FakeAudioConsumerService fake_audio_consumer_service_;
  std::optional<media::FakeAudioDeviceEnumerator> fake_audio_device_enumerator_;
  std::optional<FakeAudio> fake_audio_;

  size_t num_audio_connections_ = 0;
};

TEST_F(WebEngineIntegrationMediaTest, PlayAudioToAudioRenderer) {
  StartWebEngine(base::CommandLine(base::CommandLine::NO_PROGRAM));
  CreateContextAndFrame(ContextParamsWithAudioAndTestData());

  ASSERT_NO_FATAL_FAILURE(
      LoadUrlAndExpectResponse("fuchsia-dir://testdata/play_audio.html",
                               CreateLoadUrlParamsWithUserActivation()));

  navigation_listener()->RunUntilTitleEquals("ended");

  EXPECT_EQ(num_audio_connections_, 1U);
  EXPECT_EQ(fake_audio_consumer_service_.num_instances(), 0U);
}

TEST_F(WebEngineIntegrationMediaTest, PlayAudioToAudioRendererWithUsage) {
  StartWebEngine(base::CommandLine(base::CommandLine::NO_PROGRAM));
  CreateContextAndFrame(ContextParamsWithAudioAndTestData());

  base::RunLoop run_loop;
  fake_audio_.emplace();
  fake_audio_->renderer().set_on_set_usage_callback(run_loop.QuitClosure());

  static const fuchsia::media::AudioRenderUsage kTestRenderUsage =
      fuchsia::media::AudioRenderUsage::SYSTEM_AGENT;
  fuchsia::web::FrameMediaSettings media_settings;
  media_settings.set_renderer_usage(kTestRenderUsage);
  frame_->SetMediaSettings(std::move(media_settings));

  ASSERT_NO_FATAL_FAILURE(
      LoadUrlAndExpectResponse("fuchsia-dir://testdata/play_audio.html",
                               CreateLoadUrlParamsWithUserActivation()));

  run_loop.Run();

  EXPECT_EQ(num_audio_connections_, 1U);
  EXPECT_EQ(fake_audio_->renderer().usage(), kTestRenderUsage);
}

TEST_F(WebEngineIntegrationMediaTest, PlayAudioToAudioConsumer) {
  StartWebEngine(base::CommandLine(base::CommandLine::NO_PROGRAM));
  CreateContextAndFrame(ContextParamsWithAudioAndTestData());

  // Send `FrameMediaSettings` with `audio_consumer_session_id`. This enables
  // `AudioConsumer`.
  static const uint16_t kTestMediaSessionId = 43;
  fuchsia::web::FrameMediaSettings media_settings;
  media_settings.set_audio_consumer_session_id(kTestMediaSessionId);
  frame_->SetMediaSettings(std::move(media_settings));

  ASSERT_NO_FATAL_FAILURE(
      LoadUrlAndExpectResponse("fuchsia-dir://testdata/play_audio.html",
                               CreateLoadUrlParamsWithUserActivation()));

  navigation_listener()->RunUntilTitleEquals("ended");

  EXPECT_EQ(num_audio_connections_, 0U);
  ASSERT_EQ(fake_audio_consumer_service_.num_instances(), 1U);

  auto pos = fake_audio_consumer_service_.instance(0)->GetMediaPosition();
  EXPECT_GT(pos, base::Seconds(2.0));
  EXPECT_LT(pos, base::Seconds(2.5));

  EXPECT_EQ(fake_audio_consumer_service_.instance(0)->session_id(),
            kTestMediaSessionId);
  EXPECT_EQ(fake_audio_consumer_service_.instance(0)->volume(), 1.0);
  EXPECT_FALSE(fake_audio_consumer_service_.instance(0)->is_muted());
}

// Check that audio cannot play when the AUDIO ContextFeatureFlag is not
// provided.
TEST_F(WebEngineIntegrationMediaTest, PlayAudio_NoFlag) {
  StartWebEngine(base::CommandLine(base::CommandLine::NO_PROGRAM));
  // Both FilteredServiceDirectory and test data are needed.
  fuchsia::web::CreateContextParams create_params =
      TestContextParamsWithTestData();
  CreateContextAndFrame(std::move(create_params));

  ASSERT_NO_FATAL_FAILURE(
      LoadUrlAndExpectResponse("fuchsia-dir://testdata/play_audio.html",
                               CreateLoadUrlParamsWithUserActivation()));

  // The file is still expected to play to the end.
  navigation_listener()->RunUntilTitleEquals("ended");

  EXPECT_EQ(fake_audio_consumer_service_.num_instances(), 0U);
  EXPECT_EQ(num_audio_connections_, 0U);
}

TEST_F(WebEngineIntegrationMediaTest, PlayVideo) {
  StartWebEngine(base::CommandLine(base::CommandLine::NO_PROGRAM));
  CreateContextAndFrame(ContextParamsWithAudioAndTestData());

  ASSERT_NO_FATAL_FAILURE(LoadUrlAndExpectResponse(
      kAutoplayVp9OpusToEndUrl, CreateLoadUrlParamsWithUserActivation()));

  navigation_listener()->RunUntilTitleEquals("ended");

  // Audio should be sent to AudioRenderer (created though `fuchsia.web.Audio`).
  EXPECT_EQ(num_audio_connections_, 1U);
  EXPECT_EQ(fake_audio_consumer_service_.num_instances(), 0U);
}

void WebEngineIntegrationTest::RunPermissionTest(bool grant) {
  CreateContextAndFrame(TestContextParamsWithTestData());

  if (grant) {
    GrantPermission(fuchsia::web::PermissionType::MICROPHONE,
                    "fuchsia-dir://testdata/");
  }

  ASSERT_NO_FATAL_FAILURE(LoadUrlAndExpectResponse(
      "fuchsia-dir://testdata/check_mic_permission.html"));

  navigation_listener()->RunUntilTitleEquals(grant ? "granted" : "denied");
}

TEST_F(WebEngineIntegrationTest, PermissionDenied) {
  StartWebEngine(base::CommandLine(base::CommandLine::NO_PROGRAM));
  RunPermissionTest(false);
}

TEST_F(WebEngineIntegrationTest, PermissionGranted) {
  StartWebEngine(base::CommandLine(base::CommandLine::NO_PROGRAM));
  RunPermissionTest(true);
}

// TODO(crbug.com/40823475): Flaky.
TEST_F(WebEngineIntegrationMediaTest,
       DISABLED_MicrophoneAccess_WithPermission) {
  StartWebEngine(base::CommandLine(base::CommandLine::NO_PROGRAM));
  CreateContextAndFrame(ContextParamsWithAudioAndTestData());

  GrantPermission(
      fuchsia::web::PermissionType::MICROPHONE,
      embedded_test_server_.GetURL("/").DeprecatedGetOriginAsURL().spec());

  ASSERT_NO_FATAL_FAILURE(LoadUrlAndExpectResponse(
      embedded_test_server_.GetURL("/mic.html").spec()));

  navigation_listener()->RunUntilTitleEquals("ended");
}

TEST_F(WebEngineIntegrationMediaTest, MicrophoneAccess_WithoutPermission) {
  StartWebEngine(base::CommandLine(base::CommandLine::NO_PROGRAM));
  CreateContextAndFrame(ContextParamsWithAudioAndTestData());

  ASSERT_NO_FATAL_FAILURE(LoadUrlAndExpectResponse(
      embedded_test_server_.GetURL("/mic.html?NoPermission").spec()));

  navigation_listener()->RunUntilTitleEquals("ended-NotFoundError");
}

TEST_F(WebEngineIntegrationMediaTest, SetBlockMediaLoading_Blocked) {
  StartWebEngine(base::CommandLine(base::CommandLine::NO_PROGRAM));
  CreateContextAndFrame(ContextParamsWithAudioAndTestData());

  frame_->SetBlockMediaLoading(true);

  ASSERT_NO_FATAL_FAILURE(LoadUrlAndExpectResponse(
      kAutoplayVp9OpusUrl, CreateLoadUrlParamsWithUserActivation()));

  // Check different indicators that media has not loaded and is not playing.
  navigation_listener()->RunUntilTitleEquals("stalled");
  EXPECT_EQ(0 /*HAVE_NOTHING*/,
            ExecuteJavaScriptWithDoubleResult("bear.readyState"));
  EXPECT_EQ(0.0, ExecuteJavaScriptWithDoubleResult("bear.currentTime"));
  EXPECT_FALSE(ExecuteJavaScriptWithBoolResult("isMetadataLoaded"));
}

// Initially, set media blocking to be true. When media is unblocked, check that
// it begins playing, since autoplay=true.
TEST_F(WebEngineIntegrationMediaTest, SetBlockMediaLoading_AfterUnblock) {
  StartWebEngine(base::CommandLine(base::CommandLine::NO_PROGRAM));
  CreateContextAndFrame(ContextParamsWithAudioAndTestData());

  frame_->SetBlockMediaLoading(true);

  ASSERT_NO_FATAL_FAILURE(LoadUrlAndExpectResponse(
      kAutoplayVp9OpusUrl, CreateLoadUrlParamsWithUserActivation()));

  // Check that media loading has been blocked.
  navigation_listener()->RunUntilTitleEquals("stalled");

  // Unblock media from loading and see if media loads and plays, since
  // autoplay=true.
  frame_->SetBlockMediaLoading(false);
  navigation_listener()->RunUntilTitleEquals("playing");
  EXPECT_TRUE(ExecuteJavaScriptWithBoolResult("isMetadataLoaded"));
}

// Check that when autoplay=false and media loading was blocked after the
// element has started loading that media will play when play() is called.
TEST_F(WebEngineIntegrationMediaTest,
       SetBlockMediaLoading_SetBlockedAfterLoading) {
  StartWebEngine(base::CommandLine(base::CommandLine::NO_PROGRAM));
  CreateContextAndFrame(ContextParamsWithAudioAndTestData());

  ASSERT_NO_FATAL_FAILURE(LoadUrlAndExpectResponse(
      kLoadVp9OpusUrl, CreateLoadUrlParamsWithUserActivation()));

  navigation_listener()->RunUntilTitleEquals("loaded");
  frame_->SetBlockMediaLoading(true);
  ExecuteJavaScript(frame_.get(), "bear.play()");
  navigation_listener()->RunUntilTitleEquals("playing");
}

TEST_F(WebEngineIntegrationTest, WebGLContextAbsentWithoutVulkanFeature) {
  StartWebEngine(base::CommandLine(base::CommandLine::NO_PROGRAM));
  CreateContextAndFrame(TestContextParams());

  ASSERT_NO_FATAL_FAILURE(LoadUrlAndExpectResponse(
      embedded_test_server_.GetURL("/webgl_presence.html").spec()));

  navigation_listener()->RunUntilLoaded();

  EXPECT_EQ(navigation_listener()->title(), "absent");
}

#if defined(ARCH_CPU_ARM_FAMILY)
// TODO(crbug.com/42050537): Enable on ARM64 when bots support Vulkan.
#define MAYBE_VulkanWebEngineIntegrationTest \
  DISABLED_VulkanWebEngineIntegrationTest
#else
#define MAYBE_VulkanWebEngineIntegrationTest VulkanWebEngineIntegrationTest
#endif
class MAYBE_VulkanWebEngineIntegrationTest
    : public WebEngineIntegrationMediaTest {};

TEST_F(MAYBE_VulkanWebEngineIntegrationTest,
       WebGLContextPresentWithVulkanFeature) {
  StartWebEngine(base::CommandLine(base::CommandLine::NO_PROGRAM));
  fuchsia::web::CreateContextParams create_params = TestContextParams();
  *create_params.mutable_features() |=
      fuchsia::web::ContextFeatureFlags::VULKAN;
  CreateContextAndFrame(std::move(create_params));

  ASSERT_NO_FATAL_FAILURE(LoadUrlAndExpectResponse(
      embedded_test_server_.GetURL("/webgl_presence.html").spec()));

  navigation_listener()->RunUntilLoaded();

  EXPECT_EQ(navigation_listener()->title(), "present");
}

class WebEngineIntegrationCameraTest : public WebEngineIntegrationTest {
 protected:
  WebEngineIntegrationCameraTest() = default;

  void RunCameraTest(bool grant_permission);
};

void WebEngineIntegrationCameraTest::RunCameraTest(bool grant_permission) {
  fuchsia::web::CreateContextParams create_params = TestContextParams();

  media::FakeCameraDeviceWatcher fake_camera_device_watcher(
      filtered_service_directory().outgoing_directory());

  CreateContextAndFrame(std::move(create_params));

  if (grant_permission) {
    GrantPermission(
        fuchsia::web::PermissionType::CAMERA,
        embedded_test_server_.GetURL("/").DeprecatedGetOriginAsURL().spec());
  }

  const char* url =
      grant_permission ? "/camera.html" : "/camera.html?NoPermission";
  ASSERT_NO_FATAL_FAILURE(
      LoadUrlAndExpectResponse(embedded_test_server_.GetURL(url).spec()));

  navigation_listener()->RunUntilTitleEquals("ended");
}

TEST_F(WebEngineIntegrationCameraTest, CameraAccess_WithPermission) {
  StartWebEngine(base::CommandLine(base::CommandLine::NO_PROGRAM));
  RunCameraTest(/*grant_permission=*/true);
}

TEST_F(WebEngineIntegrationCameraTest, CameraAccess_WithoutPermission) {
  StartWebEngine(base::CommandLine(base::CommandLine::NO_PROGRAM));
  RunCameraTest(/*grant_permission=*/false);
}

TEST_F(WebEngineIntegrationCameraTest, CameraNoVideoCaptureProcess) {
  base::CommandLine command_line(base::CommandLine::NO_PROGRAM);
  command_line.AppendSwitchASCII("disable-features", "MojoVideoCapture");
  StartWebEngine(std::move(command_line));
  RunCameraTest(/*grant_permission=*/true);
}

// Check that when the ContextFeatureFlag HARDWARE_VIDEO_DECODER is
// provided that the CodecFactory service is connected to.
TEST_F(MAYBE_VulkanWebEngineIntegrationTest,
       HardwareVideoDecoderFlag_Provided) {
  StartWebEngine(base::CommandLine(base::CommandLine::NO_PROGRAM));
  // Check that the CodecFactory service is requested.
  base::RunLoop codec_connected_run_loop;
  auto* outgoing_directory = filtered_service_directory().outgoing_directory();
  outgoing_directory->RemovePublicService<fuchsia::mediacodec::CodecFactory>();
  zx_status_t status = outgoing_directory->AddPublicService(
      fidl::InterfaceRequestHandler<fuchsia::mediacodec::CodecFactory>(
          [&codec_connected_run_loop](auto request) {
            codec_connected_run_loop.Quit();
          }));
  ZX_CHECK(status == ZX_OK, status) << "AddPublicService";

  // The VULKAN flag is required for hardware video decoders to be available.
  fuchsia::web::CreateContextParams create_params =
      ContextParamsWithAudioAndTestData();
  *create_params.mutable_features() |=
      fuchsia::web::ContextFeatureFlags::VULKAN |
      fuchsia::web::ContextFeatureFlags::HARDWARE_VIDEO_DECODER;
  CreateContextAndFrame(std::move(create_params));

  ASSERT_NO_FATAL_FAILURE(LoadUrlAndExpectResponse(
      kAutoplayVp9OpusToEndUrl, CreateLoadUrlParamsWithUserActivation()));
  codec_connected_run_loop.Run();
}

// Check that the CodecFactory service is not requested when
// HARDWARE_VIDEO_DECODER is not provided.
// The video should use software decoders and still play.
TEST_F(WebEngineIntegrationMediaTest, HardwareVideoDecoderFlag_NotProvided) {
  StartWebEngine(base::CommandLine(base::CommandLine::NO_PROGRAM));
  bool is_requested = false;
  auto* outgoing_directory = filtered_service_directory().outgoing_directory();
  outgoing_directory->RemovePublicService<fuchsia::mediacodec::CodecFactory>();
  zx_status_t status = outgoing_directory->AddPublicService(
      fidl::InterfaceRequestHandler<fuchsia::mediacodec::CodecFactory>(
          [&is_requested](auto request) { is_requested = true; }));
  ZX_CHECK(status == ZX_OK, status) << "AddPublicService";

  fuchsia::web::CreateContextParams create_params =
      ContextParamsWithAudioAndTestData();
  CreateContextAndFrame(std::move(create_params));

  ASSERT_NO_FATAL_FAILURE(LoadUrlAndExpectResponse(
      kAutoplayVp9OpusToEndUrl, CreateLoadUrlParamsWithUserActivation()));

  navigation_listener()->RunUntilTitleEquals("ended");

  EXPECT_FALSE(is_requested);
}