// 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