chromium/chrome/browser/ui/ash/assistant/assistant_timers_browsertest.cc

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

#include <deque>
#include <string>
#include <vector>

#include "ash/public/cpp/test/app_list_test_api.h"
#include "ash/shelf/shelf.h"
#include "ash/shelf/shelf_widget.h"
#include "ash/shell.h"
#include "ash/system/notification_center/notification_center_tray.h"
#include "ash/system/notification_center/views/notification_center_view.h"
#include "ash/system/notification_center/views/notification_list_view.h"
#include "ash/system/status_area_widget.h"
#include "base/command_line.h"
#include "base/scoped_observation.h"
#include "base/strings/string_util.h"
#include "base/test/bind.h"
#include "base/test/icu_test_util.h"
#include "base/time/time.h"
#include "chrome/browser/ui/ash/assistant/assistant_test_mixin.h"
#include "chrome/browser/ui/ash/assistant/test_support/test_util.h"
#include "chrome/test/base/mixin_based_in_process_browser_test.h"
#include "chromeos/ash/services/assistant/public/cpp/features.h"
#include "chromeos/ash/services/assistant/public/cpp/switches.h"
#include "content/public/test/browser_test.h"
#include "sandbox/policy/switches.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "ui/aura/window.h"
#include "ui/events/test/event_generator.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/message_center_observer.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/views/notification_view.h"
#include "ui/views/controls/button/label_button.h"

namespace ash::assistant {

namespace {

using ::message_center::MessageCenter;
using ::message_center::MessageCenterObserver;

// Please remember to set auth token when *not* running in |kReplay| mode.
constexpr auto kMode = FakeS3Mode::kReplay;

// Update this when you introduce breaking changes to existing tests.
constexpr int kVersion = 1;

// Macros ----------------------------------------------------------------------

#define EXPECT_VISIBLE_NOTIFICATIONS_BY_PREFIXED_ID(prefix_)                  \
  {                                                                           \
    if (!FindVisibleNotificationsByPrefixedId(prefix_).empty()) {             \
      return;                                                                 \
    }                                                                         \
    MockMessageCenterObserver mock_observer;                                  \
    base::ScopedObservation<MessageCenter, MessageCenterObserver>             \
        observation_{&mock_observer};                                         \
    observation_.Observe(MessageCenter::Get());                               \
                                                                              \
    base::RunLoop run_loop;                                                   \
    EXPECT_CALL(mock_observer, OnNotificationAdded)                           \
        .WillOnce(                                                            \
            testing::Invoke([&run_loop](const std::string& notification_id) { \
              if (!FindVisibleNotificationsByPrefixedId(prefix_).empty())     \
                run_loop.QuitClosure().Run();                                 \
            }));                                                              \
    run_loop.Run();                                                           \
  }

// Helpers ---------------------------------------------------------------------

// Returns the status area widget.
StatusAreaWidget* FindStatusAreaWidget() {
  return Shelf::ForWindow(Shell::GetRootWindowForNewWindows())
      ->shelf_widget()
      ->status_area_widget();
}

// Returns the set of Assistant notifications (as indicated by application id).
message_center::NotificationList::Notifications FindAssistantNotifications() {
  return MessageCenter::Get()->FindNotificationsByAppId("assistant");
}

// Returns the visible notification specified by |id|.
message_center::Notification* FindVisibleNotificationById(
    const std::string& id) {
  return MessageCenter::Get()->FindVisibleNotificationById(id);
}

// Returns visible notifications having id starting with |prefix|.
std::vector<message_center::Notification*> FindVisibleNotificationsByPrefixedId(
    const std::string& prefix) {
  std::vector<message_center::Notification*> notifications;
  for (message_center::Notification* notification :
       MessageCenter::Get()->GetVisibleNotifications()) {
    if (base::StartsWith(notification->id(), prefix,
                         base::CompareCase::SENSITIVE)) {
      notifications.push_back(notification);
    }
  }
  return notifications;
}

// Returns the view for the specified |notification|.
message_center::MessageView* FindViewForNotification(
    const message_center::Notification* notification) {
  NotificationListView* notification_list_view =
      FindStatusAreaWidget()
          ->notification_center_tray()
          ->GetNotificationListView();

  // TODO(crbug/1335196): `FindDescendentsOfClass` returning empty list for
  // `NotificationCenterView` even when `MessageView`s exist. Need to
  // investigate and resolve.
  return notification_list_view->GetMessageViewForNotificationId(
      notification->id());
}

// Returns the action buttons for the specified |notification|.
std::vector<views::LabelButton*> FindActionButtonsForNotification(
    const message_center::Notification* notification) {
  auto* notification_view = FindViewForNotification(notification);

  std::vector<views::LabelButton*> action_buttons;
  FindDescendentsOfClass(notification_view, &action_buttons);

  return action_buttons;
}

// Returns the label for the specified |notification| title.
// NOTE: This method assumes that the title string is unique from other strings
// displayed in the notification. This should be safe since we only use this API
// under controlled circumstances.
views::Label* FindTitleLabelForNotification(
    const message_center::Notification* notification) {
  std::vector<views::Label*> labels;
  FindDescendentsOfClass(FindViewForNotification(notification), &labels);
  for (auto* label : labels) {
    if (label->GetText() == notification->title())
      return label;
  }
  return nullptr;
}

// Performs a tap of the specified |view| and waits until the RunLoop idles.
void TapOnAndWait(const views::View* view) {
  auto* root_window = view->GetWidget()->GetNativeWindow()->GetRootWindow();
  ui::test::EventGenerator event_generator(root_window);
  event_generator.MoveTouch(view->GetBoundsInScreen().CenterPoint());
  event_generator.PressTouch();
  event_generator.ReleaseTouch();
  base::RunLoop().RunUntilIdle();
}

// Mocks -----------------------------------------------------------------------

class MockMessageCenterObserver
    : public testing::NiceMock<MessageCenterObserver> {
 public:
  // MessageCenterObserver:
  MOCK_METHOD(void,
              OnNotificationAdded,
              (const std::string& notification_id),
              (override));

  MOCK_METHOD(void,
              OnNotificationUpdated,
              (const std::string& notification_id),
              (override));
};

}  // namespace

// AssistantTimersBrowserTest
// --------------------------------------------------

// All tests are disabled because LibAssistant V2 binary does not run on Linux
// bot. To run the tests on gLinux, please add
// `--gtest_also_run_disabled_tests`.
class DISABLED_AssistantTimersBrowserTest
    : public MixinBasedInProcessBrowserTest,
      public testing::WithParamInterface<bool> {
 public:
  DISABLED_AssistantTimersBrowserTest() {
    // Do not log to file in test. Otherwise multiple tests may create/delete
    // the log file at the same time. See http://crbug.com/1307868.
    base::CommandLine::ForCurrentProcess()->AppendSwitch(
        switches::kDisableLibAssistantLogfile);

    // In browser tests, the fake_s3_server uses gRPC framework, which is not
    // allowed in the sandbox by default. Instead of enabling and setting up the
    // gRPC policy, we do not enable sandbox in the tests.
    base::CommandLine::ForCurrentProcess()->AppendSwitch(
        sandbox::policy::switches::kNoSandbox);
  }

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

  ~DISABLED_AssistantTimersBrowserTest() override = default;

  void ShowAssistantUi() {
    if (!tester()->IsVisible())
      tester()->PressAssistantKey();
    AppListTestApi().WaitForBubbleWindow(
        /*wait_for_opening_animation=*/true);
  }

  AssistantTestMixin* tester() { return &tester_; }

 private:
  base::test::ScopedRestoreICUDefaultLocale locale_{"en_US"};
  AssistantTestMixin tester_{&mixin_host_, this, embedded_test_server(), kMode,
                             kVersion};
};

// Tests -----------------------------------------------------------------------

// Timer notifications should be dismissed when disabling Assistant in settings.
// Flaky. See https://crbug.com/1196564.
IN_PROC_BROWSER_TEST_F(DISABLED_AssistantTimersBrowserTest,
                       ShouldDismissTimerNotificationsWhenDisablingAssistant) {
  tester()->StartAssistantAndWaitForReady();

  ShowAssistantUi();
  EXPECT_TRUE(tester()->IsVisible());

  // Confirm no Assistant notifications are currently being shown.
  EXPECT_TRUE(FindAssistantNotifications().empty());

  // Start a timer for one minute.
  tester()->SendTextQuery("Set a timer for 1 minute.");

  // Check for a stable substring of the expected answers.
  tester()->ExpectTextResponse("1 min.");

  // Expect that an Assistant timer notification is now showing.
  EXPECT_VISIBLE_NOTIFICATIONS_BY_PREFIXED_ID("assistant/timer");

  // Disable Assistant.
  tester()->SetAssistantEnabled(false);
  base::RunLoop().RunUntilIdle();

  // Confirm that our Assistant timer notification has been dismissed.
  EXPECT_TRUE(FindAssistantNotifications().empty());
}

// Pressing the "STOP" action button in a timer notification should result in
// the timer being removed.
// Flaky. See https://crbug.com/1196564.
IN_PROC_BROWSER_TEST_F(DISABLED_AssistantTimersBrowserTest,
                       ShouldRemoveTimerWhenStoppingViaNotification) {
  tester()->StartAssistantAndWaitForReady();

  ShowAssistantUi();
  EXPECT_TRUE(tester()->IsVisible());

  // Confirm no Assistant notifications are currently being shown.
  EXPECT_TRUE(FindAssistantNotifications().empty());

  // Start a timer for five minutes.
  tester()->SendTextQuery("Set a timer for 5 minutes");
  tester()->ExpectTextResponse("5 min.");

  // Confirm that an Assistant timer notification is now showing.
  EXPECT_VISIBLE_NOTIFICATIONS_BY_PREFIXED_ID("assistant/timer");
  auto notifications = FindVisibleNotificationsByPrefixedId("assistant/timer");
  ASSERT_EQ(1u, notifications.size());

  // Find the action buttons for our notification.
  // NOTE: We expect action buttons for "Pause" and "Cancel".
  auto action_buttons = FindActionButtonsForNotification(notifications.at(0));
  EXPECT_EQ(2u, action_buttons.size());

  // Tap the "Cancel" action button in the notification.
  EXPECT_EQ(u"Cancel", action_buttons.at(1)->GetText());
  TapOnAndWait(action_buttons.at(1));

  ShowAssistantUi();
  EXPECT_TRUE(tester()->IsVisible());

  // Confirm that no timers exist anymore.
  tester()->SendTextQuery("Show my timers");
  tester()->ExpectAnyOfTheseTextResponses({
      "It looks like you don't have any timers set at the moment.",
  });
}

// Verifies that timer notifications are ticked at regular intervals.
IN_PROC_BROWSER_TEST_F(DISABLED_AssistantTimersBrowserTest,
                       ShouldTickNotificationsAtRegularIntervals) {
  // Observe notifications.
  MockMessageCenterObserver mock;
  base::ScopedObservation<MessageCenter, MessageCenterObserver>
      scoped_observation{&mock};
  scoped_observation.Observe(MessageCenter::Get());

  // Show Assistant UI (once ready).
  tester()->StartAssistantAndWaitForReady();
  ShowAssistantUi();
  EXPECT_TRUE(tester()->IsVisible());

  // Start a timer for five seconds.
  tester()->SendTextQuery("Set a timer for 5 seconds");
  tester()->ExpectTextResponse("5 sec.");

  // We're going to cache the time of the last notification update so that we
  // can verify updates occur within an expected time frame.
  base::Time last_update;

  // Expect and wait for our five second timer notification to be created.
  EXPECT_VISIBLE_NOTIFICATIONS_BY_PREFIXED_ID("assistant/timer");
  last_update = base::Time::Now();

  auto* notification = FindVisibleNotificationById("assistant");
  auto* title_label = FindTitleLabelForNotification(notification);
  auto title = base::UTF16ToUTF8(title_label->GetText());
  EXPECT_EQ("0:05", title);

  // We are going to assert that updates to our notification occur within an
  // expected time frame, allowing a degree of tolerance to reduce flakiness.
  constexpr auto kExpectedMillisBetweenUpdates = 1000;
  constexpr auto kMillisBetweenUpdatesTolerance = 100;

  // We're going to watch notification updates until 5 seconds past fire time.
  std::deque<std::string> expected_titles = {"0:04",  "0:03",  "0:02",  "0:01",
                                             "0:00",  "-0:01", "-0:02", "-0:03",
                                             "-0:04", "-0:05"};
  bool is_first_update = true;

  // Watch |title_label| and await all expected notification updates.
  base::RunLoop notification_update_run_loop;
  auto notification_update_subscription =
      title_label->AddTextChangedCallback(base::BindLambdaForTesting([&]() {
        base::Time now = base::Time::Now();

        // Assert that the update was received within our expected time frame.
        if (is_first_update) {
          is_first_update = false;
          // Our updates are synced to the nearest full second, meaning our
          // first update can come anywhere from 1 ms to 1000 ms from the time
          // our notification was shown.
          EXPECT_LE((now - last_update).InMilliseconds(),
                    1000 + kMillisBetweenUpdatesTolerance);
        } else {
          // Consecutive updates must come regularly.
          EXPECT_NEAR((now - last_update).InMilliseconds(),
                      kExpectedMillisBetweenUpdates,
                      kMillisBetweenUpdatesTolerance);
        }

        // Assert that the notification has the expected title.
        auto title = base::UTF16ToUTF8(title_label->GetText());
        EXPECT_EQ(expected_titles.front(), title);

        // Update time of |last_update|.
        last_update = now;

        // When |expected_titles| is empty, our test is finished.
        expected_titles.pop_front();
        if (expected_titles.empty()) {
          notification_update_run_loop.QuitClosure().Run();
        }
      }));
  notification_update_run_loop.Run();
}

}  // namespace ash::assistant