chromium/chrome/browser/chromeos/extensions/telemetry/api/events/events_api_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/chromeos/extensions/telemetry/api/events/events_api.h"

#include <cstddef>
#include <memory>
#include <utility>
#include <vector>

#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/chromeos/extensions/telemetry/api/common/base_telemetry_extension_browser_test.h"
#include "chrome/browser/chromeos/extensions/telemetry/api/events/fake_events_service.h"
#include "chrome/browser/ui/browser.h"
#include "chromeos/crosapi/mojom/probe_service.mojom.h"
#include "chromeos/crosapi/mojom/telemetry_event_service.mojom.h"
#include "chromeos/crosapi/mojom/telemetry_extension_exception.mojom.h"
#include "chromeos/crosapi/mojom/telemetry_keyboard_event.mojom.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "extensions/common/extension_features.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"

#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "base/memory/weak_ptr.h"
#include "chrome/browser/chromeos/extensions/telemetry/api/events/fake_events_service_factory.h"
#include "chrome/browser/profiles/profile.h"         // nogncheck
#include "chrome/browser/ui/browser_list.h"          // nogncheck
#include "chrome/browser/ui/tabs/tab_strip_model.h"  // nogncheck
#include "chromeos/ash/components/telemetry_extension/events/telemetry_event_service_ash.h"
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)

#if BUILDFLAG(IS_CHROMEOS_LACROS)
#include "chromeos/lacros/lacros_service.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#endif  // BUILDFLAG(IS_CHROMEOS_LACROS)

namespace chromeos {

namespace {

namespace crosapi = ::crosapi::mojom;

#if BUILDFLAG(IS_CHROMEOS_ASH)
const char kKeyboardDiagnosticsUrl[] =
    "chrome://diagnostics?input&showDefaultKeyboardTester";
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)

}  // namespace

class TelemetryExtensionEventsApiBrowserTest
    : public BaseTelemetryExtensionBrowserTest {
 public:
  void SetUpOnMainThread() override {
    BaseTelemetryExtensionBrowserTest::SetUpOnMainThread();
#if BUILDFLAG(IS_CHROMEOS_ASH)
    fake_events_service_impl_ = new FakeEventsService();
    // SAFETY: We hand over ownership over the destruction of this pointer to
    // the first caller of `TelemetryEventsServiceAsh::Create`. The only
    // consumer of this is the `EventManager`, that lives as long as the profile
    // and therefore longer than this test, so we are safe to access
    // fake_events_service_impl_ in the test body.
    fake_events_service_factory_.SetCreateInstanceResponse(
        std::unique_ptr<FakeEventsService>(fake_events_service_impl_));
    ash::TelemetryEventServiceAsh::Factory::SetForTesting(
        &fake_events_service_factory_);
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)
#if BUILDFLAG(IS_CHROMEOS_LACROS)
    fake_events_service_impl_ = std::make_unique<FakeEventsService>();
    // Replace the production TelemetryEventsService with a fake for testing.
    chromeos::LacrosService::Get()->InjectRemoteForTesting(
        fake_events_service_impl_->BindNewPipeAndPassRemote());
#endif  // BUILDFLAG(IS_CHROMEOS_LACROS)
  }

  void TearDownOnMainThread() override {
#if BUILDFLAG(IS_CHROMEOS_ASH)
    fake_events_service_impl_ = nullptr;
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)
#if BUILDFLAG(IS_CHROMEOS_LACROS)
    // Since one of tests opens browser window UI in Ash, it should close the
    // UI so that it won't pollute other tests running against the shared Ash.
    CloseAllAshBrowserWindows();
#endif
    BaseTelemetryExtensionBrowserTest::TearDownOnMainThread();
  }

 protected:
  void CheckIsEventSupported(const std::vector<std::string>& events,
                             const std::string& status);

  FakeEventsService* GetFakeService() {
    return fake_events_service_impl_.get();
  }

 private:
#if BUILDFLAG(IS_CHROMEOS_ASH)
  // SAFETY: This pointer is owned in a unique_ptr by the EventManager. Since
  // the EventManager lives longer than this test, it is always safe to access
  // the fake in the test body.
  raw_ptr<FakeEventsService> fake_events_service_impl_;
  FakeEventsServiceFactory fake_events_service_factory_;
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)
#if BUILDFLAG(IS_CHROMEOS_LACROS)
  std::unique_ptr<FakeEventsService> fake_events_service_impl_;
#endif  // BUILDFLAG(IS_CHROMEOS_LACROS)
};

void TelemetryExtensionEventsApiBrowserTest::CheckIsEventSupported(
    const std::vector<std::string>& events,
    const std::string& status) {
  if (events.empty()) {
    return;
  }

  std::string event_str;
  for (const auto& event : events) {
    if (event_str.empty()) {
      event_str.append("[");
    } else {
      event_str.append(",");
    }
    event_str.append("'");
    event_str.append(event);
    event_str.append("'");
  }
  event_str.append("]");

  // Don't use array.forEach because it doesn't support await.
  CreateExtensionAndRunServiceWorker(base::StringPrintf(R"(
    chrome.test.runTests([
      async function isEventSupported() {
        const events = %s;
        for (let i = 0; i < events.length; i++) {
          const result = await chrome.os.events.isEventSupported(events[i]);
          chrome.test.assertEq(result, {
            status: '%s'
          });
        }
        chrome.test.succeed();
      }
    ]);
    )",
                                                        event_str.c_str(),
                                                        status.c_str()));
}

// Checks the event supportability.
IN_PROC_BROWSER_TEST_F(TelemetryExtensionEventsApiBrowserTest,
                       IsEventSupported) {
  auto supported = crosapi::TelemetryExtensionSupportStatus::NewSupported(
      crosapi::TelemetryExtensionSupported::New());
  GetFakeService()->SetIsEventSupportedResponse(std::move(supported));

  std::vector<std::string> unsupported_events;
  std::vector<std::string> supported_events;
  crosapi::TelemetryEventCategoryEnum category =
      crosapi::TelemetryEventCategoryEnum::kUnmappedEnumField;
  switch (category) {
    // Features behind a feature flag.
    case crosapi::TelemetryEventCategoryEnum::kUnmappedEnumField:
      [[fallthrough]];
    // Features without a feature flag.
    case crosapi::TelemetryEventCategoryEnum::kAudioJack:
      supported_events.push_back("audio_jack");
      [[fallthrough]];
    case crosapi::TelemetryEventCategoryEnum::kLid:
      supported_events.push_back("lid");
      [[fallthrough]];
    case crosapi::TelemetryEventCategoryEnum::kUsb:
      supported_events.push_back("usb");
      [[fallthrough]];
    case crosapi::TelemetryEventCategoryEnum::kExternalDisplay:
      supported_events.push_back("external_display");
      [[fallthrough]];
    case crosapi::TelemetryEventCategoryEnum::kSdCard:
      supported_events.push_back("sd_card");
      [[fallthrough]];
    case crosapi::TelemetryEventCategoryEnum::kPower:
      supported_events.push_back("power");
      [[fallthrough]];
    case crosapi::TelemetryEventCategoryEnum::kKeyboardDiagnostic:
      supported_events.push_back("keyboard_diagnostic");
      [[fallthrough]];
    case crosapi::TelemetryEventCategoryEnum::kStylusGarage:
      supported_events.push_back("stylus_garage");
      [[fallthrough]];
    case crosapi::TelemetryEventCategoryEnum::kTouchpadButton:
      supported_events.push_back("touchpad_button");
      [[fallthrough]];
    case crosapi::TelemetryEventCategoryEnum::kTouchpadTouch:
      supported_events.push_back("touchpad_touch");
      [[fallthrough]];
    case crosapi::TelemetryEventCategoryEnum::kTouchpadConnected:
      supported_events.push_back("touchpad_connected");
      [[fallthrough]];
    case crosapi::TelemetryEventCategoryEnum::kStylusTouch:
      supported_events.push_back("stylus_touch");
      [[fallthrough]];
    case crosapi::TelemetryEventCategoryEnum::kStylusConnected:
      supported_events.push_back("stylus_connected");
      [[fallthrough]];
    case crosapi::TelemetryEventCategoryEnum::kTouchscreenTouch:
      supported_events.push_back("touchscreen_touch");
      [[fallthrough]];
    case crosapi::TelemetryEventCategoryEnum::kTouchscreenConnected:
      supported_events.push_back("touchscreen_connected");
      break;
  }

  CheckIsEventSupported(unsupported_events, "unsupported");
  CheckIsEventSupported(supported_events, "supported");
}

IN_PROC_BROWSER_TEST_F(TelemetryExtensionEventsApiBrowserTest,
                       IsEventSupported_Error) {
  auto exception = crosapi::TelemetryExtensionException::New();
  exception->reason = crosapi::TelemetryExtensionException::Reason::kUnexpected;
  exception->debug_message = "My test message";

  auto input = crosapi::TelemetryExtensionSupportStatus::NewException(
      std::move(exception));

  GetFakeService()->SetIsEventSupportedResponse(std::move(input));

  CreateExtensionAndRunServiceWorker(R"(
    chrome.test.runTests([
      async function isEventSupported() {
        await chrome.test.assertPromiseRejects(
            chrome.os.events.isEventSupported("audio_jack"),
            'Error: My test message'
        );

        chrome.test.succeed();
      }
    ]);
    )");

  auto unmapped =
      crosapi::TelemetryExtensionSupportStatus::NewUnmappedUnionField(0);
  GetFakeService()->SetIsEventSupportedResponse(std::move(unmapped));

  CreateExtensionAndRunServiceWorker(R"(
    chrome.test.runTests([
      async function isEventSupported() {
        await chrome.test.assertPromiseRejects(
            chrome.os.events.isEventSupported("audio_jack"),
            'Error: API internal error.'
        );

        chrome.test.succeed();
      }
    ]);
    )");
}

IN_PROC_BROWSER_TEST_F(TelemetryExtensionEventsApiBrowserTest,
                       IsEventSupported_Success) {
  auto supported = crosapi::TelemetryExtensionSupportStatus::NewSupported(
      crosapi::TelemetryExtensionSupported::New());

  GetFakeService()->SetIsEventSupportedResponse(std::move(supported));

  CreateExtensionAndRunServiceWorker(R"(
    chrome.test.runTests([
      async function isEventSupported() {
        const result = await chrome.os.events.isEventSupported("audio_jack");
        chrome.test.assertEq(result, {
          status: 'supported'
        });

        chrome.test.succeed();
      }
    ]);
    )");

  auto unsupported = crosapi::TelemetryExtensionSupportStatus::NewUnsupported(
      crosapi::TelemetryExtensionUnsupported::New());

  GetFakeService()->SetIsEventSupportedResponse(std::move(unsupported));

  CreateExtensionAndRunServiceWorker(R"(
    chrome.test.runTests([
      async function isEventSupported() {
        const result = await chrome.os.events.isEventSupported("audio_jack");
        chrome.test.assertEq(result, {
          status: 'unsupported'
        });

        chrome.test.succeed();
      }
    ]);
    )");
}

IN_PROC_BROWSER_TEST_F(TelemetryExtensionEventsApiBrowserTest,
                       StartListeningToEvents_Success) {
  OpenAppUiAndMakeItSecure();

  // Emit an event as soon as the subscription is registered with the fake.
  GetFakeService()->SetOnSubscriptionChange(
      base::BindLambdaForTesting([this]() {
        auto audio_jack_info = crosapi::TelemetryAudioJackEventInfo::New();
        audio_jack_info->state =
            crosapi::TelemetryAudioJackEventInfo::State::kAdd;
        audio_jack_info->device_type =
            crosapi::TelemetryAudioJackEventInfo::DeviceType::kHeadphone;

        GetFakeService()->EmitEventForCategory(
            crosapi::TelemetryEventCategoryEnum::kAudioJack,
            crosapi::TelemetryEventInfo::NewAudioJackEventInfo(
                std::move(audio_jack_info)));
      }));

  CreateExtensionAndRunServiceWorker(R"(
    chrome.test.runTests([
      async function startCapturingEvents() {
        chrome.os.events.onAudioJackEvent.addListener((event) => {
          chrome.test.assertEq(event, {
            event: 'connected',
            deviceType: 'headphone'
          });

          chrome.test.succeed();
        });

        await chrome.os.events.startCapturingEvents("audio_jack");
      }
    ]);
  )");
}

IN_PROC_BROWSER_TEST_F(TelemetryExtensionEventsApiBrowserTest,
                       StartListeningToEvents_ErrorPwaClosed) {
  CreateExtensionAndRunServiceWorker(R"(
    chrome.test.runTests([
      async function startCapturingEvents() {
        await chrome.test.assertPromiseRejects(
            chrome.os.events.startCapturingEvents("audio_jack"),
            'Error: Companion app UI is not open.'
        );
        chrome.test.succeed();
      }
    ]);
  )");
}

IN_PROC_BROWSER_TEST_F(TelemetryExtensionEventsApiBrowserTest,
                       StartListeningToRegularEvents_SuccessPwaUnfocused) {
  OpenAppUiAndMakeItSecure();
  AddBlankTabAndShow(browser());

  // Emit an event as soon as the subscription is registered with the fake.
  GetFakeService()->SetOnSubscriptionChange(
      base::BindLambdaForTesting([this]() {
        auto audio_jack_info = crosapi::TelemetryAudioJackEventInfo::New();
        audio_jack_info->state =
            crosapi::TelemetryAudioJackEventInfo::State::kAdd;
        audio_jack_info->device_type =
            crosapi::TelemetryAudioJackEventInfo::DeviceType::kHeadphone;

        GetFakeService()->EmitEventForCategory(
            crosapi::TelemetryEventCategoryEnum::kAudioJack,
            crosapi::TelemetryEventInfo::NewAudioJackEventInfo(
                std::move(audio_jack_info)));
      }));

  CreateExtensionAndRunServiceWorker(R"(
    chrome.test.runTests([
      async function startCapturingEvents() {
        chrome.os.events.onAudioJackEvent.addListener((event) => {
          chrome.test.assertEq(event, {
            event: 'connected',
            deviceType: 'headphone'
          });

          chrome.test.succeed();
        });

        await chrome.os.events.startCapturingEvents("audio_jack");
      }
    ]);
  )");

  base::test::TestFuture<size_t> remote_set_size;
  GetFakeService()->SetOnSubscriptionChange(
      base::BindLambdaForTesting([this, &remote_set_size]() {
        auto* remote_set = GetFakeService()->GetObserversByCategory(
            crosapi::TelemetryEventCategoryEnum::kAudioJack);
        ASSERT_TRUE(remote_set);

        remote_set->FlushForTesting();
        remote_set_size.SetValue(remote_set->size());
      }));

  // Calling `stopCapturingEvents` will result in the connection being cut.
  CreateExtensionAndRunServiceWorker(R"(
    chrome.test.runTests([
      async function stopCapturingEvents() {
        await chrome.os.events.stopCapturingEvents("audio_jack");
        chrome.test.succeed();
      }
    ]);
  )");

  EXPECT_EQ(remote_set_size.Get(), 0UL);
}

IN_PROC_BROWSER_TEST_F(
    TelemetryExtensionEventsApiBrowserTest,
    StartListeningToFocusRestrictedEvents_ErrorPwaUnfocused) {
  OpenAppUiAndMakeItSecure();
  AddBlankTabAndShow(browser());
  CreateExtensionAndRunServiceWorker(R"(
    chrome.test.runTests([
      async function startCapturingEvents() {
        await chrome.test.assertPromiseRejects(
            chrome.os.events.startCapturingEvents("touchpad_connected"),
            'Error: Companion app UI is not focused.'
        );
        chrome.test.succeed();
      }
    ]);
  )");
}

// TODO(b/284428237): Add more browser tests regarding focus changes.
IN_PROC_BROWSER_TEST_F(
    TelemetryExtensionEventsApiBrowserTest,
    StartListeningToRegularAndFocusRestrictedEvents_Success) {
  OpenAppUiAndMakeItSecure();

  // Emit an event as soon as the subscription is registered with the fake.
  GetFakeService()->SetOnSubscriptionChange(
      base::BindLambdaForTesting([this]() {
        auto audio_jack_info = crosapi::TelemetryAudioJackEventInfo::New();
        audio_jack_info->state =
            crosapi::TelemetryAudioJackEventInfo::State::kAdd;
        audio_jack_info->device_type =
            crosapi::TelemetryAudioJackEventInfo::DeviceType::kHeadphone;

        GetFakeService()->EmitEventForCategory(
            crosapi::TelemetryEventCategoryEnum::kAudioJack,
            crosapi::TelemetryEventInfo::NewAudioJackEventInfo(
                std::move(audio_jack_info)));
      }));

  GetFakeService()->SetOnSubscriptionChange(
      base::BindLambdaForTesting([this]() {
        std::vector<crosapi::TelemetryInputTouchButton> buttons{
            crosapi::TelemetryInputTouchButton::kLeft,
            crosapi::TelemetryInputTouchButton::kMiddle,
            crosapi::TelemetryInputTouchButton::kRight};

        auto connected_event =
            crosapi::TelemetryTouchpadConnectedEventInfo::New(
                1, 2, 3, std::move(buttons));

        GetFakeService()->EmitEventForCategory(
            crosapi::TelemetryEventCategoryEnum::kTouchpadConnected,
            crosapi::TelemetryEventInfo::NewTouchpadConnectedEventInfo(
                std::move(connected_event)));
      }));

  CreateExtensionAndRunServiceWorker(R"(
    chrome.test.runTests([
      async function startCapturingEvents() {
        chrome.os.events.onAudioJackEvent.addListener((event) => {
          chrome.test.assertEq(event, {
            event: 'connected',
            deviceType: 'headphone'
          });
        });

        chrome.os.events.onTouchpadConnectedEvent.addListener((event) => {
          chrome.test.assertEq(event, {
            maxX: 1,
            maxY: 2,
            maxPressure: 3,
            buttons: [
              'left',
              'middle',
              'right'
            ]
          });
        });

        await chrome.os.events.startCapturingEvents("audio_jack");
        await chrome.os.events.startCapturingEvents("touchpad_connected");

        chrome.test.succeed();
      }
    ]);
  )");
}

IN_PROC_BROWSER_TEST_F(TelemetryExtensionEventsApiBrowserTest,
                       StopListeningToEvents) {
  OpenAppUiAndMakeItSecure();

  // Emit an event as soon as the subscription is registered with the fake.
  GetFakeService()->SetOnSubscriptionChange(
      base::BindLambdaForTesting([this]() {
        auto audio_jack_info = crosapi::TelemetryAudioJackEventInfo::New();
        audio_jack_info->state =
            crosapi::TelemetryAudioJackEventInfo::State::kAdd;
        audio_jack_info->device_type =
            crosapi::TelemetryAudioJackEventInfo::DeviceType::kHeadphone;

        GetFakeService()->EmitEventForCategory(
            crosapi::TelemetryEventCategoryEnum::kAudioJack,
            crosapi::TelemetryEventInfo::NewAudioJackEventInfo(
                std::move(audio_jack_info)));
      }));

  CreateExtensionAndRunServiceWorker(R"(
    chrome.test.runTests([
      async function startCapturingEvents() {
        chrome.os.events.onAudioJackEvent.addListener((event) => {
          chrome.test.assertEq(event, {
            event: 'connected',
            deviceType: 'headphone'
          });

          chrome.test.succeed();
        });

        await chrome.os.events.startCapturingEvents("audio_jack");
      }
    ]);
  )");

  base::test::TestFuture<size_t> remote_set_size;
  GetFakeService()->SetOnSubscriptionChange(
      base::BindLambdaForTesting([this, &remote_set_size]() {
        auto* remote_set = GetFakeService()->GetObserversByCategory(
            crosapi::TelemetryEventCategoryEnum::kAudioJack);
        ASSERT_TRUE(remote_set);

        remote_set->FlushForTesting();
        remote_set_size.SetValue(remote_set->size());
      }));

  // Calling `stopCapturingEvents` will result in the connection being cut.
  CreateExtensionAndRunServiceWorker(R"(
    chrome.test.runTests([
      async function stopCapturingEvents() {
        await chrome.os.events.stopCapturingEvents("audio_jack");
        chrome.test.succeed();
      }
    ]);
  )");

  EXPECT_EQ(remote_set_size.Get(), 0UL);
}

IN_PROC_BROWSER_TEST_F(TelemetryExtensionEventsApiBrowserTest,
                       ClosePwaConnection) {
  OpenAppUiAndMakeItSecure();

  // Emit an event as soon as the subscription is registered with the fake.
  GetFakeService()->SetOnSubscriptionChange(
      base::BindLambdaForTesting([this]() {
        auto audio_jack_info = crosapi::TelemetryAudioJackEventInfo::New();
        audio_jack_info->state =
            crosapi::TelemetryAudioJackEventInfo::State::kAdd;
        audio_jack_info->device_type =
            crosapi::TelemetryAudioJackEventInfo::DeviceType::kHeadphone;

        GetFakeService()->EmitEventForCategory(
            crosapi::TelemetryEventCategoryEnum::kAudioJack,
            crosapi::TelemetryEventInfo::NewAudioJackEventInfo(
                std::move(audio_jack_info)));
      }));

  CreateExtensionAndRunServiceWorker(R"(
    chrome.test.runTests([
      async function startCapturingEvents() {
        chrome.os.events.onAudioJackEvent.addListener((event) => {
          chrome.test.assertEq(event, {
            event: 'connected',
            deviceType: 'headphone'
          });

          chrome.test.succeed();
        });

        await chrome.os.events.startCapturingEvents("audio_jack");
      }
    ]);
  )");

  base::test::TestFuture<size_t> remote_set_size;
  GetFakeService()->SetOnSubscriptionChange(
      base::BindLambdaForTesting([this, &remote_set_size]() {
        auto* remote_set = GetFakeService()->GetObserversByCategory(
            crosapi::TelemetryEventCategoryEnum::kAudioJack);
        ASSERT_TRUE(remote_set);

        remote_set->FlushForTesting();
        remote_set_size.SetValue(remote_set->size());
      }));

  // Closing the PWA will result in the connection being cut.
  browser()->tab_strip_model()->CloseSelectedTabs();

  EXPECT_EQ(remote_set_size.Get(), 0UL);
}

IN_PROC_BROWSER_TEST_F(TelemetryExtensionEventsApiBrowserTest,
                       OnKeyboardDiagnosticEvent_Success) {
  OpenAppUiAndMakeItSecure();

  GetFakeService()->SetOnSubscriptionChange(
      base::BindLambdaForTesting([this]() {
        auto keyboard_info = crosapi::TelemetryKeyboardInfo::New();
        keyboard_info->id = crosapi::UInt32Value::New(1);
        keyboard_info->connection_type =
            crosapi::TelemetryKeyboardConnectionType::kBluetooth;
        keyboard_info->name = "TestName";
        keyboard_info->physical_layout =
            crosapi::TelemetryKeyboardPhysicalLayout::kChromeOS;
        keyboard_info->mechanical_layout =
            crosapi::TelemetryKeyboardMechanicalLayout::kAnsi;
        keyboard_info->region_code = "de";
        keyboard_info->number_pad_present =
            crosapi::TelemetryKeyboardNumberPadPresence::kPresent;

        auto info = crosapi::TelemetryKeyboardDiagnosticEventInfo::New();
        info->keyboard_info = std::move(keyboard_info);
        info->tested_keys = {1, 2, 3};
        info->tested_top_row_keys = {4, 5, 6};

        GetFakeService()->EmitEventForCategory(
            crosapi::TelemetryEventCategoryEnum::kKeyboardDiagnostic,
            crosapi::TelemetryEventInfo::NewKeyboardDiagnosticEventInfo(
                std::move(info)));
      }));

  CreateExtensionAndRunServiceWorker(R"(
    chrome.test.runTests([
      async function startCapturingEvents() {
        chrome.os.events.onKeyboardDiagnosticEvent.addListener((event) => {
          chrome.test.assertEq(event, {
            "keyboardInfo": {
              "connectionType":"bluetooth",
              "id":1,
              "mechanicalLayout":"ansi",
              "name":"TestName",
              "numberPadPresent":"present",
              "physicalLayout":"chrome_os",
              "regionCode":"de",
              "topRowKeys":[]
            },
            "testedKeys":[1,2,3],
            "testedTopRowKeys":[4,5,6]
            }
          );

          chrome.test.succeed();
        });

        await chrome.os.events.startCapturingEvents("keyboard_diagnostic");
      }
    ]);
  )");

#if BUILDFLAG(IS_CHROMEOS_LACROS)
  // This test opens a browser window UI in Ash.
  WaitUntilAtLeastOneAshBrowserWindowOpen();
#endif
}

#if BUILDFLAG(IS_CHROMEOS)
#define MAYBE_KeyboardDiagnosticEventOpensDiagnosticApp \
  DISABLED_KeyboardDiagnosticEventOpensDiagnosticApp
#else
#define MAYBE_KeyboardDiagnosticEventOpensDiagnosticApp \
  KeyboardDiagnosticEventOpensDiagnosticApp
#endif
IN_PROC_BROWSER_TEST_F(TelemetryExtensionEventsApiBrowserTest,
                       MAYBE_KeyboardDiagnosticEventOpensDiagnosticApp) {
  OpenAppUiAndMakeItSecure();

  GetFakeService()->SetOnSubscriptionChange(
      base::BindLambdaForTesting([this]() {
        auto keyboard_info = crosapi::TelemetryKeyboardInfo::New();
        keyboard_info->id = crosapi::UInt32Value::New(1);
        keyboard_info->connection_type =
            crosapi::TelemetryKeyboardConnectionType::kBluetooth;
        keyboard_info->name = "TestName";
        keyboard_info->physical_layout =
            crosapi::TelemetryKeyboardPhysicalLayout::kChromeOS;
        keyboard_info->mechanical_layout =
            crosapi::TelemetryKeyboardMechanicalLayout::kAnsi;
        keyboard_info->region_code = "de";
        keyboard_info->number_pad_present =
            crosapi::TelemetryKeyboardNumberPadPresence::kPresent;

        auto info = crosapi::TelemetryKeyboardDiagnosticEventInfo::New();
        info->keyboard_info = std::move(keyboard_info);
        info->tested_keys = {1, 2, 3};
        info->tested_top_row_keys = {4, 5, 6};

        GetFakeService()->EmitEventForCategory(
            crosapi::TelemetryEventCategoryEnum::kKeyboardDiagnostic,
            crosapi::TelemetryEventInfo::NewKeyboardDiagnosticEventInfo(
                std::move(info)));
      }));

  CreateExtensionAndRunServiceWorker(R"(
    chrome.test.runTests([
      async function startCapturingEvents() {
        chrome.os.events.onKeyboardDiagnosticEvent.addListener((event) => {
          chrome.test.assertEq(event, {
            "keyboardInfo": {
              "connectionType":"bluetooth",
              "id":1,
              "mechanicalLayout":"ansi",
              "name":"TestName",
              "numberPadPresent":"present",
              "physicalLayout":"chrome_os",
              "regionCode":"de",
              "topRowKeys":[]
            },
            "testedKeys":[1,2,3],
            "testedTopRowKeys":[4,5,6]
            }
          );

          chrome.test.succeed();
        });

        await chrome.os.events.startCapturingEvents("keyboard_diagnostic");
      }
    ]);
  )");

// If this is executed in Lacros we can stop the test here. If the above
// call succeeded, a request for opening the diagnostics application was
// sent to Ash. Since we only test Lacros, we stop the test here instead
// of checking if Ash opened the UI correctly.
// If we run in Ash however, we can check that the UI was correctly open.
#if BUILDFLAG(IS_CHROMEOS_ASH)
  bool is_diagnostic_app_open = false;
  for (Browser* target_browser : *BrowserList::GetInstance()) {
    TabStripModel* target_tab_strip = target_browser->tab_strip_model();
    for (int i = 0; i < target_tab_strip->count(); ++i) {
      content::WebContents* target_contents =
          target_tab_strip->GetWebContentsAt(i);

      if (target_contents->GetLastCommittedURL() ==
          GURL(kKeyboardDiagnosticsUrl)) {
        is_diagnostic_app_open = true;
      }
    }
  }

  EXPECT_TRUE(is_diagnostic_app_open);
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)
}

IN_PROC_BROWSER_TEST_F(TelemetryExtensionEventsApiBrowserTest,
                       OnSdCardEvent_Success) {
  OpenAppUiAndMakeItSecure();

  GetFakeService()->SetOnSubscriptionChange(
      base::BindLambdaForTesting([this]() {
        auto sd_card_info = crosapi::TelemetrySdCardEventInfo::New();
        sd_card_info->state = crosapi::TelemetrySdCardEventInfo::State::kAdd;

        GetFakeService()->EmitEventForCategory(
            crosapi::TelemetryEventCategoryEnum::kSdCard,
            crosapi::TelemetryEventInfo::NewSdCardEventInfo(
                std::move(sd_card_info)));
      }));

  CreateExtensionAndRunServiceWorker(R"(
    chrome.test.runTests([
      async function startCapturingEvents() {
        chrome.os.events.onSdCardEvent.addListener((event) => {
          chrome.test.assertEq(event, {
            event: 'connected'
          });

          chrome.test.succeed();
        });

        await chrome.os.events.startCapturingEvents("sd_card");
      }
    ]);
  )");
}

IN_PROC_BROWSER_TEST_F(TelemetryExtensionEventsApiBrowserTest,
                       OnPowerEvent_Success) {
  OpenAppUiAndMakeItSecure();

  GetFakeService()->SetOnSubscriptionChange(
      base::BindLambdaForTesting([this]() {
        auto power_info = crosapi::TelemetryPowerEventInfo::New();
        power_info->state =
            crosapi::TelemetryPowerEventInfo::State::kAcInserted;

        GetFakeService()->EmitEventForCategory(
            crosapi::TelemetryEventCategoryEnum::kPower,
            crosapi::TelemetryEventInfo::NewPowerEventInfo(
                std::move(power_info)));
      }));

  CreateExtensionAndRunServiceWorker(R"(
    chrome.test.runTests([
      async function startCapturingEvents() {
        chrome.os.events.onPowerEvent.addListener((event) => {
          chrome.test.assertEq(event, {
            event: 'ac_inserted'
          });

          chrome.test.succeed();
        });

        await chrome.os.events.startCapturingEvents("power");
      }
    ]);
  )");
}

IN_PROC_BROWSER_TEST_F(TelemetryExtensionEventsApiBrowserTest,
                       OnStylusGarageEvent_Success) {
  OpenAppUiAndMakeItSecure();

  GetFakeService()->SetOnSubscriptionChange(
      base::BindLambdaForTesting([this]() {
        auto stylus_garage_info =
            crosapi::TelemetryStylusGarageEventInfo::New();
        stylus_garage_info->state =
            crosapi::TelemetryStylusGarageEventInfo::State::kInserted;

        GetFakeService()->EmitEventForCategory(
            crosapi::TelemetryEventCategoryEnum::kStylusGarage,
            crosapi::TelemetryEventInfo::NewStylusGarageEventInfo(
                std::move(stylus_garage_info)));
      }));

  CreateExtensionAndRunServiceWorker(R"(
    chrome.test.runTests([
      async function startCapturingEvents() {
        chrome.os.events.onStylusGarageEvent.addListener((event) => {
          chrome.test.assertEq(event, {
            event: 'inserted'
          });

          chrome.test.succeed();
        });

        await chrome.os.events.startCapturingEvents("stylus_garage");
      }
    ]);
  )");
}

IN_PROC_BROWSER_TEST_F(TelemetryExtensionEventsApiBrowserTest,
                       OnTouchpadButtonEvent_Success) {
  OpenAppUiAndMakeItSecure();

  GetFakeService()->SetOnSubscriptionChange(
      base::BindLambdaForTesting([this]() {
        auto button_event = crosapi::TelemetryTouchpadButtonEventInfo::New();
        button_event->state =
            crosapi::TelemetryTouchpadButtonEventInfo_State::kPressed;
        button_event->button = crosapi::TelemetryInputTouchButton::kLeft;

        GetFakeService()->EmitEventForCategory(
            crosapi::TelemetryEventCategoryEnum::kTouchpadButton,
            crosapi::TelemetryEventInfo::NewTouchpadButtonEventInfo(
                std::move(button_event)));
      }));

  CreateExtensionAndRunServiceWorker(R"(
    chrome.test.runTests([
      async function startCapturingEvents() {
        chrome.os.events.onTouchpadButtonEvent.addListener((event) => {
          chrome.test.assertEq(event, {
            button: 'left',
            state: 'pressed'
          });

          chrome.test.succeed();
        });

        await chrome.os.events.startCapturingEvents("touchpad_button");
      }
    ]);
  )");
}

IN_PROC_BROWSER_TEST_F(TelemetryExtensionEventsApiBrowserTest,
                       OnTouchpadTouchEvent_Success) {
  OpenAppUiAndMakeItSecure();

  GetFakeService()->SetOnSubscriptionChange(
      base::BindLambdaForTesting([this]() {
        std::vector<crosapi::TelemetryTouchPointInfoPtr> touch_points;
        touch_points.push_back(crosapi::TelemetryTouchPointInfo::New(
            1, 2, 3, crosapi::UInt32Value::New(4), crosapi::UInt32Value::New(5),
            crosapi::UInt32Value::New(6)));
        touch_points.push_back(crosapi::TelemetryTouchPointInfo::New(
            7, 8, 9, nullptr, nullptr, nullptr));

        auto touch_event = crosapi::TelemetryTouchpadTouchEventInfo::New(
            std::move(touch_points));

        GetFakeService()->EmitEventForCategory(
            crosapi::TelemetryEventCategoryEnum::kTouchpadTouch,
            crosapi::TelemetryEventInfo::NewTouchpadTouchEventInfo(
                std::move(touch_event)));
      }));

  CreateExtensionAndRunServiceWorker(R"(
    chrome.test.runTests([
      async function startCapturingEvents() {
        chrome.os.events.onTouchpadTouchEvent.addListener((event) => {
          chrome.test.assertEq(event, {
            touchPoints: [{
              trackingId: 1,
              x: 2,
              y: 3,
              pressure: 4,
              touchMajor: 5,
              touchMinor: 6
            },{
              trackingId: 7,
              x: 8,
              y: 9,
            }]
          });

          chrome.test.succeed();
        });

        await chrome.os.events.startCapturingEvents("touchpad_touch");
      }
    ]);
  )");
}

IN_PROC_BROWSER_TEST_F(TelemetryExtensionEventsApiBrowserTest,
                       OnTouchpadConnectedEvent_Success) {
  OpenAppUiAndMakeItSecure();

  GetFakeService()->SetOnSubscriptionChange(
      base::BindLambdaForTesting([this]() {
        std::vector<crosapi::TelemetryInputTouchButton> buttons{
            crosapi::TelemetryInputTouchButton::kLeft,
            crosapi::TelemetryInputTouchButton::kMiddle,
            crosapi::TelemetryInputTouchButton::kRight};

        auto connected_event =
            crosapi::TelemetryTouchpadConnectedEventInfo::New(
                1, 2, 3, std::move(buttons));

        GetFakeService()->EmitEventForCategory(
            crosapi::TelemetryEventCategoryEnum::kTouchpadConnected,
            crosapi::TelemetryEventInfo::NewTouchpadConnectedEventInfo(
                std::move(connected_event)));
      }));

  CreateExtensionAndRunServiceWorker(R"(
    chrome.test.runTests([
      async function startCapturingEvents() {
        chrome.os.events.onTouchpadConnectedEvent.addListener((event) => {
          chrome.test.assertEq(event, {
            maxX: 1,
            maxY: 2,
            maxPressure: 3,
            buttons: [
              'left',
              'middle',
              'right'
            ]
          });

          chrome.test.succeed();
        });

        await chrome.os.events.startCapturingEvents("touchpad_connected");
      }
    ]);
  )");
}

IN_PROC_BROWSER_TEST_F(TelemetryExtensionEventsApiBrowserTest,
                       OnTouchscreenTouchEvent_Success) {
  OpenAppUiAndMakeItSecure();

  GetFakeService()->SetOnSubscriptionChange(
      base::BindLambdaForTesting([this]() {
        std::vector<crosapi::TelemetryTouchPointInfoPtr> touch_points;
        touch_points.push_back(crosapi::TelemetryTouchPointInfo::New(
            1, 2, 3, crosapi::UInt32Value::New(4), crosapi::UInt32Value::New(5),
            crosapi::UInt32Value::New(6)));
        touch_points.push_back(crosapi::TelemetryTouchPointInfo::New(
            7, 8, 9, nullptr, nullptr, nullptr));

        auto touch_event = crosapi::TelemetryTouchscreenTouchEventInfo::New(
            std::move(touch_points));

        GetFakeService()->EmitEventForCategory(
            crosapi::TelemetryEventCategoryEnum::kTouchscreenTouch,
            crosapi::TelemetryEventInfo::NewTouchscreenTouchEventInfo(
                std::move(touch_event)));
      }));

  CreateExtensionAndRunServiceWorker(R"(
    chrome.test.runTests([
      async function startCapturingEvents() {
        chrome.os.events.onTouchscreenTouchEvent.addListener((event) => {
          chrome.test.assertEq(event, {
            touchPoints: [{
              trackingId: 1,
              x: 2,
              y: 3,
              pressure: 4,
              touchMajor: 5,
              touchMinor: 6
            },{
              trackingId: 7,
              x: 8,
              y: 9,
            }]
          });

          chrome.test.succeed();
        });

        await chrome.os.events.startCapturingEvents("touchscreen_touch");
      }
    ]);
  )");
}

IN_PROC_BROWSER_TEST_F(TelemetryExtensionEventsApiBrowserTest,
                       OnTouchscreenConnectedEvent_Success) {
  OpenAppUiAndMakeItSecure();

  GetFakeService()->SetOnSubscriptionChange(
      base::BindLambdaForTesting([this]() {
        auto connected_event =
            crosapi::TelemetryTouchscreenConnectedEventInfo::New(1, 2, 3);

        GetFakeService()->EmitEventForCategory(
            crosapi::TelemetryEventCategoryEnum::kTouchscreenConnected,
            crosapi::TelemetryEventInfo::NewTouchscreenConnectedEventInfo(
                std::move(connected_event)));
      }));

  CreateExtensionAndRunServiceWorker(R"(
    chrome.test.runTests([
      async function startCapturingEvents() {
        chrome.os.events.onTouchscreenConnectedEvent.addListener((event) => {
          chrome.test.assertEq(event, {
            maxX: 1,
            maxY: 2,
            maxPressure: 3
          });

          chrome.test.succeed();
        });

        await chrome.os.events.startCapturingEvents("touchscreen_connected");
      }
    ]);
  )");
}

IN_PROC_BROWSER_TEST_F(TelemetryExtensionEventsApiBrowserTest,
                       OnExternalDisplayEvent_Success) {
  OpenAppUiAndMakeItSecure();

  GetFakeService()->SetOnSubscriptionChange(
      base::BindLambdaForTesting([this]() {
        auto external_display_info =
            crosapi::TelemetryExternalDisplayEventInfo::New();
        external_display_info->state =
            crosapi::TelemetryExternalDisplayEventInfo::State::kAdd;

        auto display_info = crosapi::ProbeExternalDisplayInfo::New();
        display_info->display_width = 1;
        display_info->display_height = 2;
        display_info->resolution_horizontal = 3;
        display_info->resolution_vertical = 4;
        display_info->refresh_rate = 5;
        display_info->manufacturer = "manufacturer";
        display_info->model_id = 6;
        display_info->serial_number = 7;
        display_info->manufacture_week = 8;
        display_info->manufacture_year = 9;
        display_info->edid_version = "1.4";
        display_info->input_type = crosapi::ProbeDisplayInputType::kAnalog;
        display_info->display_name = "display";

        external_display_info->display_info = std::move(display_info);

        GetFakeService()->EmitEventForCategory(
            crosapi::TelemetryEventCategoryEnum::kExternalDisplay,
            crosapi::TelemetryEventInfo::NewExternalDisplayEventInfo(
                std::move(external_display_info)));
      }));

  CreateExtensionAndRunServiceWorker(R"(
    chrome.test.runTests([
      async function startCapturingEvents() {
        chrome.os.events.onExternalDisplayEvent.addListener((event) => {
          chrome.test.assertEq(event, {
            event: 'connected',
            displayInfo: {
                "displayHeight": 2,
                "displayName": "display",
                "displayWidth": 1,
                "edidVersion": "1.4",
                "inputType": "analog",
                "manufactureWeek": 8,
                "manufactureYear": 9,
                "manufacturer": "manufacturer",
                "modelId": 6,
                "refreshRate": 5,
                "resolutionHorizontal": 3,
                "resolutionVertical": 4
              }
          });

          chrome.test.succeed();
        });

        await chrome.os.events.startCapturingEvents("external_display");
      }
    ]);
  )");
}

IN_PROC_BROWSER_TEST_F(TelemetryExtensionEventsApiBrowserTest,
                       OnStylusConnectedEvent_Success) {
  OpenAppUiAndMakeItSecure();

  GetFakeService()->SetOnSubscriptionChange(
      base::BindLambdaForTesting([this]() {
        auto connected_event =
            crosapi::TelemetryStylusConnectedEventInfo::New(1, 2, 3);

        GetFakeService()->EmitEventForCategory(
            crosapi::TelemetryEventCategoryEnum::kStylusConnected,
            crosapi::TelemetryEventInfo::NewStylusConnectedEventInfo(
                std::move(connected_event)));
      }));

  CreateExtensionAndRunServiceWorker(R"(
    chrome.test.runTests([
      async function startCapturingEvents() {
        chrome.os.events.onStylusConnectedEvent.addListener((event) => {
          chrome.test.assertEq(event, {
            max_x: 1,
            max_y: 2,
            max_pressure: 3
          });

          chrome.test.succeed();
        });

        await chrome.os.events.startCapturingEvents("stylus_connected");
      }
    ]);
  )");
}

IN_PROC_BROWSER_TEST_F(TelemetryExtensionEventsApiBrowserTest,
                       OnStylusTouchEvent_Success) {
  OpenAppUiAndMakeItSecure();

  GetFakeService()->SetOnSubscriptionChange(
      base::BindLambdaForTesting([this]() {
        crosapi::TelemetryStylusTouchPointInfoPtr touch_point =
            crosapi::TelemetryStylusTouchPointInfo::New(1, 2, 3);

        auto touch_event =
            crosapi::TelemetryStylusTouchEventInfo::New(std::move(touch_point));

        GetFakeService()->EmitEventForCategory(
            crosapi::TelemetryEventCategoryEnum::kStylusTouch,
            crosapi::TelemetryEventInfo::NewStylusTouchEventInfo(
                std::move(touch_event)));
      }));

  CreateExtensionAndRunServiceWorker(R"(
    chrome.test.runTests([
      async function startCapturingEvents() {
        chrome.os.events.onStylusTouchEvent.addListener((event) => {
          chrome.test.assertEq(event, {
            "touch_point": {
              x: 1,
              y: 2,
              pressure: 3
            }
          });

          chrome.test.succeed();
        });
        await chrome.os.events.startCapturingEvents("stylus_touch");
      }
    ]);
  )");
}

}  // namespace chromeos