// 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 <memory>
#include <optional>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include "ash/constants/ash_features.h"
#include "ash/root_window_controller.h"
#include "ash/shelf/shelf.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/notification_center/ash_message_popup_collection.h"
#include "ash/system/notification_center/message_popup_animation_waiter.h"
#include "ash/system/notification_center/notification_center_test_api.h"
#include "ash/system/notification_center/notification_center_tray.h"
#include "ash/system/notification_center/views/ash_notification_view.h"
#include "ash/system/status_area_widget.h"
#include "ash/test/view_drawn_waiter.h"
#include "ash/webui/media_app_ui/url_constants.h"
#include "base/ranges/algorithm.h"
#include "base/run_loop.h"
#include "base/scoped_observation.h"
#include "base/test/bind.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/metrics/user_action_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "chrome/browser/ash/crosapi/crosapi_ash.h"
#include "chrome/browser/ash/crosapi/crosapi_manager.h"
#include "chrome/browser/ash/crosapi/mock_download_status_updater_client.h"
#include "chrome/browser/ash/system_web_apps/test_support/system_web_app_browsertest_base.h"
#include "chrome/browser/notifications/notification_display_service.h"
#include "chrome/browser/notifications/notification_display_service_factory.h"
#include "chrome/browser/notifications/profile_notification.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/ash/download_status/display_metadata.h"
#include "chrome/browser/ui/ash/download_status/display_test_util.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/test/base/ash/util/ash_test_util.h"
#include "chromeos/crosapi/mojom/download_status_updater.mojom.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/test_navigation_observer.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "ui/aura/env.h"
#include "ui/aura/env_observer.h"
#include "ui/aura/test/find_window.h"
#include "ui/aura/window.h"
#include "ui/base/clipboard/clipboard.h"
#include "ui/base/clipboard/clipboard_buffer.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/image/image_unittest_util.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/views/large_image_view.h"
#include "ui/message_center/views/message_popup_view.h"
#include "ui/message_center/views/notification_control_buttons_view.h"
#include "ui/message_center/views/notification_view_base.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/progress_bar.h"
#include "ui/views/view_utils.h"
namespace ash::download_status {
namespace {
// Alias -----------------------------------------------------------------------
using ::testing::_;
using ::testing::AllOf;
using ::testing::Contains;
using ::testing::Each;
using ::testing::ElementsAre;
using ::testing::Eq;
using ::testing::Field;
using ::testing::Mock;
using ::testing::NiceMock;
using ::testing::Not;
using ::testing::Pointee;
using ::testing::Property;
using ::testing::WithArg;
// MockEnvObserver -------------------------------------------------------------
class MockEnvObserver : public aura::EnvObserver {
public:
MOCK_METHOD(void, OnWindowInitialized, (aura::Window*), (override));
};
// MockNotificationDisplayServiceObserver --------------------------------------
// NOTE: When a download notification is closed, `OnNotificationClosed()` is not
// called because the notification's handler type is `TRANSIENT`.
class MockNotificationDisplayServiceObserver
: public NotificationDisplayService::Observer {
public:
MOCK_METHOD(void,
OnNotificationDisplayed,
(const message_center::Notification&,
const NotificationCommon::Metadata*),
(override));
MOCK_METHOD(void, OnNotificationClosed, (const std::string&), (override));
MOCK_METHOD(void,
OnNotificationDisplayServiceDestroyed,
(NotificationDisplayService * service),
(override));
};
// MockTabStripModelObserver ---------------------------------------------------
class MockTabStripModelObserver : public TabStripModelObserver {
public:
MOCK_METHOD(void, OnTabWillBeAdded, (), (override));
};
// Helpers ---------------------------------------------------------------------
// Returns the text ID for the given `command_type`.
int GetCommandTextId(CommandType command_type) {
switch (command_type) {
case CommandType::kCancel:
return IDS_ASH_DOWNLOAD_COMMAND_TEXT_CANCEL;
case CommandType::kCopyToClipboard:
return IDS_ASH_DOWNLOAD_COMMAND_TEXT_COPY_TO_CLIPBOARD;
case CommandType::kEditWithMediaApp:
return IDS_DOWNLOAD_NOTIFICATION_LABEL_OPEN_AND_EDIT;
case CommandType::kOpenFile:
NOTREACHED();
case CommandType::kOpenWithMediaApp:
return IDS_DOWNLOAD_NOTIFICATION_LABEL_OPEN;
case CommandType::kPause:
return IDS_ASH_DOWNLOAD_COMMAND_TEXT_PAUSE;
case CommandType::kResume:
return IDS_ASH_DOWNLOAD_COMMAND_TEXT_RESUME;
case CommandType::kShowInBrowser:
NOTREACHED();
case CommandType::kShowInFolder:
return IDS_ASH_DOWNLOAD_COMMAND_TEXT_SHOW_IN_FOLDER;
case CommandType::kViewDetailsInBrowser:
return IDS_ASH_DOWNLOAD_COMMAND_TEXT_VIEW_DETAILS_IN_BROWSER;
}
}
NotificationDisplayService* GetNotificationDisplayService() {
return NotificationDisplayServiceFactory::GetInstance()->GetForProfile(
ProfileManager::GetActiveUserProfile());
}
// Returns the IDs of the displayed notifications.
std::set<std::string> GetDisplayedNotificationIds() {
base::test::TestFuture<std::set<std::string>> future;
GetNotificationDisplayService()->GetDisplayed(
base::BindLambdaForTesting([&future](std::set<std::string> ids, bool) {
future.SetValue(std::move(ids));
}));
return future.Get();
}
// Returns the notification popup view specified by 'notification_id'. Returns
// `nullptr` if such a view cannot be found.
AshNotificationView* GetPopupView(Profile* profile,
const std::string& notification_id) {
// Wait until `popup_collection` becomes idle.
AshMessagePopupCollection* const popup_collection =
Shell::GetPrimaryRootWindowController()
->shelf()
->GetStatusAreaWidget()
->notification_center_tray()
->popup_collection();
if (!popup_collection) {
return nullptr;
}
MessagePopupAnimationWaiter(popup_collection).Wait();
// NOTE: The notification ID associated with the view differs from
// `notification_id` as it incorporates the profile ID.
auto* const popup_view = NotificationCenterTestApi().GetPopupViewForId(
ProfileNotification::GetProfileNotificationId(
notification_id, ProfileNotification::GetProfileID(profile)));
return popup_view ? views::AsViewClass<AshNotificationView>(
popup_view->message_view())
: nullptr;
}
} // namespace
class NotificationDisplayClientBrowserTest
: public SystemWebAppBrowserTestBase {
public:
// Updates download through the download status updater.
void Update(crosapi::mojom::DownloadStatusPtr status) {
download_status_updater_remote_->Update(std::move(status));
download_status_updater_remote_.FlushForTesting();
}
crosapi::MockDownloadStatusUpdaterClient& download_status_updater_client() {
return download_status_updater_client_;
}
MockNotificationDisplayServiceObserver& service_observer() {
return service_observer_;
}
private:
// SystemWebAppBrowserTestBase:
void SetUpOnMainThread() override {
SystemWebAppBrowserTestBase::SetUpOnMainThread();
crosapi::CrosapiManager::Get()->crosapi_ash()->BindDownloadStatusUpdater(
download_status_updater_remote_.BindNewPipeAndPassReceiver());
download_status_updater_remote_->BindClient(
download_status_updater_client_receiver_
.BindNewPipeAndPassRemoteWithVersion());
download_status_updater_remote_.FlushForTesting();
service_observation_.Observe(GetNotificationDisplayService());
}
void TearDownOnMainThread() override {
service_observation_.Reset();
SystemWebAppBrowserTestBase::TearDownOnMainThread();
}
base::test::ScopedFeatureList scoped_feature_list_;
mojo::Remote<crosapi::mojom::DownloadStatusUpdater>
download_status_updater_remote_;
MockNotificationDisplayServiceObserver service_observer_;
base::ScopedObservation<NotificationDisplayService,
NotificationDisplayService::Observer>
service_observation_{&service_observer_};
// The client bound to the download status updater under test.
crosapi::MockDownloadStatusUpdaterClient download_status_updater_client_;
mojo::Receiver<crosapi::mojom::DownloadStatusUpdaterClient>
download_status_updater_client_receiver_{
&download_status_updater_client_};
};
// Verifies that when an in-progress download is cancelled, its notification
// should be removed.
IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest, CancelDownload) {
// Add a download that is not cancellable. Cache the notification ID.
Profile* const profile = ProfileManager::GetActiveUserProfile();
std::string notification_id;
EXPECT_CALL(service_observer(), OnNotificationDisplayed)
.WillOnce(WithArg<0>(
[¬ification_id](const message_center::Notification& notification) {
notification_id = notification.id();
}));
crosapi::mojom::DownloadStatusPtr uncancellable_download =
CreateInProgressDownloadStatus(profile,
/*extension=*/"txt",
/*received_bytes=*/0,
/*total_bytes=*/1024);
uncancellable_download->cancellable = false;
Update(uncancellable_download->Clone());
Mock::VerifyAndClearExpectations(&service_observer());
// Check that the notification view of `uncancellable_download` does not have
// a cancel button.
AshNotificationView* const uncancellable_download_notification =
GetPopupView(profile, notification_id);
ASSERT_TRUE(uncancellable_download_notification);
std::vector<raw_ptr<views::LabelButton, VectorExperimental>> action_buttons =
uncancellable_download_notification->GetActionButtonsForTest();
const std::u16string cancel_button_text =
l10n_util::GetStringUTF16(GetCommandTextId(CommandType::kCancel));
auto cancel_button_iter = base::ranges::find(
action_buttons, cancel_button_text, &views::LabelButton::GetText);
EXPECT_EQ(cancel_button_iter, action_buttons.end());
// Add a cancellable download. Cache the notification ID.
EXPECT_CALL(service_observer(), OnNotificationDisplayed)
.WillOnce(WithArg<0>(
[¬ification_id](const message_center::Notification& notification) {
notification_id = notification.id();
}));
crosapi::mojom::DownloadStatusPtr cancellable_download =
CreateInProgressDownloadStatus(profile,
/*extension=*/"txt",
/*received_bytes=*/0,
/*total_bytes=*/1024);
cancellable_download->cancellable = true;
Update(cancellable_download->Clone());
Mock::VerifyAndClearExpectations(&service_observer());
// Implement download cancellation for the mock client.
base::RunLoop run_loop;
ON_CALL(download_status_updater_client(),
Cancel(cancellable_download->guid, _))
.WillByDefault(
[&](const std::string& guid,
crosapi::MockDownloadStatusUpdaterClient::CancelCallback
callback) {
cancellable_download->cancellable = false;
cancellable_download->state =
crosapi::mojom::DownloadState::kCancelled;
Update(cancellable_download->Clone());
std::move(callback).Run(/*handled=*/true);
run_loop.Quit();
});
// Get the cancel button.
AshNotificationView* const cancellable_download_notification =
GetPopupView(profile, notification_id);
ASSERT_TRUE(cancellable_download_notification);
action_buttons = cancellable_download_notification->GetActionButtonsForTest();
cancel_button_iter = base::ranges::find(action_buttons, cancel_button_text,
&views::LabelButton::GetText);
ASSERT_NE(cancel_button_iter, action_buttons.end());
// Click on the cancel button and wait until download is cancelled.
base::UserActionTester tester;
test::Click(*cancel_button_iter, ui::EF_NONE);
run_loop.Run();
// After download cancellation, the associated notification should be removed.
// The button click should be recorded.
EXPECT_THAT(GetDisplayedNotificationIds(), Not(Contains(notification_id)));
EXPECT_EQ(tester.GetActionCount("DownloadNotificationV2.Button_Cancel"), 1);
}
// Verifies clicking a completed download's notification.
IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest,
ClickCompletedDownload) {
// Wait until test system apps are installed so that opening a download file
// is handled.
WaitForTestSystemAppInstall();
// Add a completed download and cache its notification ID.
Profile* const profile = ProfileManager::GetActiveUserProfile();
std::string notification_id;
EXPECT_CALL(service_observer(), OnNotificationDisplayed)
.WillOnce(WithArg<0>(
[¬ification_id](const message_center::Notification& notification) {
notification_id = notification.id();
}));
crosapi::mojom::DownloadStatusPtr download = CreateDownloadStatus(
profile, /*extension=*/"txt", crosapi::mojom::DownloadState::kComplete,
crosapi::mojom::DownloadProgress::New(
/*loop=*/false,
/*received_bytes=*/1024,
/*total_bytes=*/1024,
/*visible=*/false));
Update(download->Clone());
Mock::VerifyAndClearExpectations(&service_observer());
// The command that shows downloads in browser should not be performed.
EXPECT_CALL(download_status_updater_client(), ShowInBrowser).Times(0);
// Set up an observer to wait until a tab is added.
NiceMock<MockTabStripModelObserver> tab_strip_model_observer;
base::ScopedObservation<TabStripModel, MockTabStripModelObserver>
tab_strip_model_observation{&tab_strip_model_observer};
base::test::TestFuture<void> future;
ON_CALL(tab_strip_model_observer, OnTabWillBeAdded)
.WillByDefault(base::test::RunClosure(future.GetRepeatingCallback()));
AshNotificationView* const notification_view =
GetPopupView(profile, notification_id);
ASSERT_TRUE(notification_view);
// Click `notification_view` and wait until a tab is added. Then verify that
// the click is recorded. It assumes the download file is opened by browser.
base::UserActionTester tester;
tab_strip_model_observation.Observe(browser()->tab_strip_model());
test::Click(notification_view, ui::EF_NONE);
EXPECT_TRUE(future.Wait());
Mock::VerifyAndClearExpectations(&tab_strip_model_observer);
Mock::VerifyAndClearExpectations(&download_status_updater_client());
EXPECT_EQ(tester.GetActionCount("DownloadNotificationV2.Click_Completed"), 1);
}
// Verifies clicking an in-progress download's notification.
IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest,
ClickInProgressDownload) {
// Add an in-progress download and cache its notification ID.
Profile* const profile = ProfileManager::GetActiveUserProfile();
std::string notification_id;
EXPECT_CALL(service_observer(), OnNotificationDisplayed)
.WillOnce(WithArg<0>(
[¬ification_id](const message_center::Notification& notification) {
notification_id = notification.id();
}));
crosapi::mojom::DownloadStatusPtr download =
CreateInProgressDownloadStatus(profile,
/*extension=*/"txt",
/*received_bytes=*/0,
/*total_bytes=*/1024);
Update(download->Clone());
Mock::VerifyAndClearExpectations(&service_observer());
AshNotificationView* const notification_view =
GetPopupView(profile, notification_id);
ASSERT_TRUE(notification_view);
// Click `notification_view` and wait until showing downloads in browser. Then
// verify that the click is recorded.
base::UserActionTester tester;
base::RunLoop run_loop;
EXPECT_CALL(download_status_updater_client(),
ShowInBrowser(download->guid, _))
.WillOnce(
[&run_loop](
const std::string& guid,
crosapi::MockDownloadStatusUpdaterClient::ShowInBrowserCallback
callback) {
std::move(callback).Run(/*handled=*/true);
run_loop.Quit();
});
test::Click(notification_view, ui::EF_NONE);
run_loop.Run();
Mock::VerifyAndClearExpectations(&download_status_updater_client());
EXPECT_EQ(tester.GetActionCount("DownloadNotificationV2.Click_InProgress"),
1);
}
// Verifies that when an in-progress download completes, its notification should
// still show.
IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest, CompleteDownload) {
Profile* const profile = ProfileManager::GetActiveUserProfile();
crosapi::mojom::DownloadStatusPtr download = CreateDownloadStatus(
profile,
/*extension=*/"txt", crosapi::mojom::DownloadState::kInProgress,
/*progress=*/nullptr);
EXPECT_FALSE(download->target_file_path);
std::string notification_id;
// When the notification for `download` displays:
// 1. Check the notification's properties. Since the download target file path
// is unavailable, the primary text should be the display name of the file
// referenced by the full path.
// 2. Cache the notification ID.
EXPECT_CALL(
service_observer(),
OnNotificationDisplayed(
AllOf(
Property(&message_center::Notification::progress_status,
Eq(std::u16string())),
Property(&message_center::Notification::title,
Eq(download->full_path->BaseName().LossyDisplayName()))),
_))
.WillOnce(WithArg<0>(
[¬ification_id](const message_center::Notification& notification) {
notification_id = notification.id();
}));
Update(download->Clone());
Mock::VerifyAndClearExpectations(&service_observer());
// The progress of `download` is not set so the progress bar should not show.
AshNotificationView* popup_view = GetPopupView(profile, notification_id);
ASSERT_TRUE(popup_view);
EXPECT_FALSE(popup_view->progress_bar_view_for_testing());
// Update the download's received bytes and total bytes. Then check the
// notification's progress.
download->progress = crosapi::mojom::DownloadProgress::New();
crosapi::mojom::DownloadProgressPtr& progress = download->progress;
progress->received_bytes = 0;
progress->total_bytes = 1024;
progress->visible = true;
EXPECT_CALL(
service_observer(),
OnNotificationDisplayed(
AllOf(
Property(&message_center::Notification::id, Eq(notification_id)),
Property(&message_center::Notification::progress, Eq(0))),
_));
Update(download->Clone());
Mock::VerifyAndClearExpectations(&service_observer());
// The notification view should have a visible progress bar because `progress`
// specifies the visibility to be true.
popup_view = GetPopupView(profile, notification_id);
ASSERT_TRUE(popup_view);
const views::ProgressBar* progress_bar =
popup_view->progress_bar_view_for_testing();
ASSERT_TRUE(progress_bar);
EXPECT_TRUE(progress_bar->GetVisible());
// Update the download's:
// 1. Received bytes
// 2. Status text
// 3. Target file path
// Then check the notification's properties.
progress->received_bytes = 512;
download->status_text = u"Random text";
download->target_file_path = test::CreateFile(profile);
EXPECT_NE(download->target_file_path, download->full_path);
EXPECT_CALL(
service_observer(),
OnNotificationDisplayed(
AllOf(
Property(&message_center::Notification::id, Eq(notification_id)),
Property(&message_center::Notification::progress, Eq(50)),
Property(&message_center::Notification::title,
Eq(download->target_file_path->BaseName()
.LossyDisplayName()))),
_));
Update(download->Clone());
Mock::VerifyAndClearExpectations(&service_observer());
// Verify that the notification view of an in-progress download has a visible
// progress bar with the expected status text.
popup_view = GetPopupView(profile, notification_id);
ASSERT_TRUE(popup_view);
progress_bar = popup_view->progress_bar_view_for_testing();
ASSERT_TRUE(progress_bar);
EXPECT_TRUE(progress_bar->GetVisible());
const views::Label* const status_view = popup_view->status_view_for_testing();
ASSERT_TRUE(status_view);
EXPECT_EQ(status_view->GetText(), u"Random text");
// Complete download. Then check the notification.
MarkDownloadStatusCompleted(*download);
EXPECT_CALL(
service_observer(),
OnNotificationDisplayed(
Property(&message_center::Notification::id, Eq(notification_id)), _));
Update(download->Clone());
EXPECT_THAT(GetDisplayedNotificationIds(), Contains(notification_id));
// Verify that the notification view of a completed download does not have
// a progress bar.
popup_view = GetPopupView(profile, notification_id);
ASSERT_TRUE(popup_view);
EXPECT_FALSE(popup_view->progress_bar_view_for_testing());
// Check the notification view's message label.
const views::Label* const message_label =
popup_view->message_label_for_testing();
ASSERT_TRUE(message_label);
EXPECT_EQ(message_label->GetText(), u"Random text");
}
// Verifies that a download notification should not show again if it has been
// closed by user.
IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest,
DoNotShowAfterCloseByUser) {
std::string notification_id;
EXPECT_CALL(service_observer(), OnNotificationDisplayed)
.WillOnce(WithArg<0>(
[¬ification_id](const message_center::Notification& notification) {
notification_id = notification.id();
}));
Profile* const profile = ProfileManager::GetActiveUserProfile();
crosapi::mojom::DownloadStatusPtr download =
CreateInProgressDownloadStatus(profile,
/*extension=*/"txt",
/*received_bytes=*/0,
/*total_bytes=*/1024);
Update(download->Clone());
Mock::VerifyAndClearExpectations(&service_observer());
AshNotificationView* const notification_view =
GetPopupView(profile, notification_id);
ASSERT_TRUE(notification_view);
// Move mouse to `notification_view` until `close_button` shows and then click
// `close_button` to remove the notification associated with
// `notification_id`.
test::MoveMouseTo(notification_view);
views::View* const close_button =
notification_view->control_buttons_view_for_test()->close_button();
ViewDrawnWaiter().Wait(close_button);
test::Click(close_button, ui::EF_NONE);
// The notification associated with `notification_id` should not display.
EXPECT_THAT(GetDisplayedNotificationIds(), Not(Contains(notification_id)));
// Update the same notification after closing. The closed notification should
// not show again.
Update(download->Clone());
EXPECT_THAT(GetDisplayedNotificationIds(), Not(Contains(notification_id)));
}
// Verifies that the image download notification works as expected.
IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest, ImageDownload) {
std::string notification_id;
EXPECT_CALL(service_observer(), OnNotificationDisplayed)
.WillOnce(WithArg<0>(
[¬ification_id](const message_center::Notification& notification) {
notification_id = notification.id();
}));
// Create an image download.
Profile* const profile = ProfileManager::GetActiveUserProfile();
crosapi::mojom::DownloadStatusPtr download = CreateDownloadStatus(
profile,
/*extension=*/"png", crosapi::mojom::DownloadState::kInProgress,
crosapi::mojom::DownloadProgress::New(
/*loop=*/false, /*received_bytes=*/0,
/*total_bytes=*/1024, /*visible=*/true));
Update(download->Clone());
Mock::VerifyAndClearExpectations(&service_observer());
// Because `download` does not have an image, the notification image is null.
AshNotificationView* popup_view = GetPopupView(profile, notification_id);
ASSERT_TRUE(popup_view);
EXPECT_FALSE(popup_view->GetViewByID(
message_center::NotificationViewBase::kLargeImageView));
// Update `download` with `image`.
constexpr SkColor image_color = SK_ColorRED;
const gfx::ImageSkia image =
gfx::test::CreateImageSkia(/*size=*/100, image_color);
download->image = image;
Update(download->Clone());
popup_view = GetPopupView(profile, notification_id);
ASSERT_TRUE(popup_view);
auto* const large_image_view =
static_cast<message_center::LargeImageView*>(popup_view->GetViewByID(
message_center::NotificationViewBase::kLargeImageView));
ASSERT_TRUE(large_image_view);
// Verify that the notification image is as expected.
EXPECT_TRUE(gfx::test::AreBitmapsEqual(
*large_image_view->original_image().bitmap(),
gfx::test::CreateBitmap(/*width=*/360,
/*height=*/240, image_color)));
// An in-progress image download's notification should not have a 'Copy to
// clipboard' button.
const std::u16string copy_to_clipboard_button_text =
l10n_util::GetStringUTF16(
GetCommandTextId(CommandType::kCopyToClipboard));
EXPECT_THAT(
popup_view->GetActionButtonsForTest(),
Not(Contains(Pointee(Property(&views::LabelButton::GetText,
Eq(copy_to_clipboard_button_text))))));
// Complete `download`. Then check action buttons.
MarkDownloadStatusCompleted(*download);
Update(download->Clone());
const std::vector<raw_ptr<views::LabelButton, VectorExperimental>>
action_buttons = popup_view->GetActionButtonsForTest();
EXPECT_THAT(
action_buttons,
ElementsAre(
Pointee(Property(&views::LabelButton::GetText,
Eq(l10n_util::GetStringUTF16(
GetCommandTextId(CommandType::kShowInFolder))))),
Pointee(Property(&views::LabelButton::GetText,
Eq(copy_to_clipboard_button_text)))));
// Click the 'Copy to clipboard' button. Then verify the click is recorded.
base::UserActionTester tester;
auto copy_to_clipboard_button_iter =
base::ranges::find(action_buttons, copy_to_clipboard_button_text,
&views::LabelButton::GetText);
ASSERT_NE(copy_to_clipboard_button_iter, action_buttons.cend());
test::Click(*copy_to_clipboard_button_iter, ui::EF_NONE);
EXPECT_EQ(
tester.GetActionCount("DownloadNotificationV2.Button_CopyToClipboard"),
1);
// Verify the filename in the clipboard as expected.
base::test::TestFuture<std::vector<ui::FileInfo>> test_future;
ui::Clipboard::GetForCurrentThread()->ReadFilenames(
ui::ClipboardBuffer::kCopyPaste,
/*data_dst=*/nullptr, test_future.GetCallback());
EXPECT_THAT(test_future.Get(),
ElementsAre(Field(&ui::FileInfo::path, *download->full_path)));
}
// Verifies that the PDF download notification works as expected.
IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest, PdfDownload) {
WaitForTestSystemAppInstall();
std::string notification_id;
EXPECT_CALL(service_observer(), OnNotificationDisplayed)
.WillOnce(WithArg<0>(
[¬ification_id](const message_center::Notification& notification) {
notification_id = notification.id();
}));
// Create an pdf download.
Profile* const profile = ProfileManager::GetActiveUserProfile();
crosapi::mojom::DownloadStatusPtr download = CreateDownloadStatus(
profile,
/*extension=*/"pdf", crosapi::mojom::DownloadState::kInProgress,
crosapi::mojom::DownloadProgress::New(
/*loop=*/false, /*received_bytes=*/0,
/*total_bytes=*/1024, /*visible=*/true));
Update(download->Clone());
Mock::VerifyAndClearExpectations(&service_observer());
AshNotificationView* popup_view = GetPopupView(profile, notification_id);
ASSERT_TRUE(popup_view);
// An in-progress PDF download's notification should not have a 'Open and
// Edit' button.
const std::u16string edit_text = l10n_util::GetStringUTF16(
GetCommandTextId(CommandType::kEditWithMediaApp));
EXPECT_THAT(popup_view->GetActionButtonsForTest(),
Not(Contains(Pointee(
Property(&views::LabelButton::GetText, Eq(edit_text))))));
// Complete `download`. Then check action buttons.
MarkDownloadStatusCompleted(*download);
Update(download->Clone());
const std::vector<raw_ptr<views::LabelButton, VectorExperimental>>
action_buttons = popup_view->GetActionButtonsForTest();
EXPECT_THAT(
action_buttons,
ElementsAre(
Pointee(Property(&views::LabelButton::GetText, Eq(edit_text))),
Pointee(Property(&views::LabelButton::GetText,
Eq(l10n_util::GetStringUTF16(GetCommandTextId(
CommandType::kShowInFolder)))))));
// Click the 'Open and edit' button. Then verify the click is recorded and
// the Media App is launched.
content::TestNavigationObserver observer =
content::TestNavigationObserver(GURL(kChromeUIMediaAppURL));
observer.StartWatchingNewWebContents();
base::UserActionTester tester;
auto edit_button_iter = base::ranges::find(action_buttons, edit_text,
&views::LabelButton::GetText);
ASSERT_NE(edit_button_iter, action_buttons.cend());
test::Click(*edit_button_iter, ui::EF_NONE);
EXPECT_EQ(
tester.GetActionCount("DownloadNotificationV2.Button_EditWithMediaApp"),
1);
observer.Wait();
}
// Verifies that the audio download notification works as expected.
IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest, AudioDownload) {
WaitForTestSystemAppInstall();
std::string notification_id;
EXPECT_CALL(service_observer(), OnNotificationDisplayed)
.WillOnce(WithArg<0>(
[¬ification_id](const message_center::Notification& notification) {
notification_id = notification.id();
}));
// Create an mp3 download.
Profile* const profile = ProfileManager::GetActiveUserProfile();
crosapi::mojom::DownloadStatusPtr download = CreateDownloadStatus(
profile,
/*extension=*/"mp3", crosapi::mojom::DownloadState::kInProgress,
crosapi::mojom::DownloadProgress::New(
/*loop=*/false, /*received_bytes=*/0,
/*total_bytes=*/1024, /*visible=*/true));
Update(download->Clone());
Mock::VerifyAndClearExpectations(&service_observer());
AshNotificationView* popup_view = GetPopupView(profile, notification_id);
ASSERT_TRUE(popup_view);
// An in-progress audio file download's notification should not have a 'Open'
// button.
const std::u16string open_text = l10n_util::GetStringUTF16(
GetCommandTextId(CommandType::kOpenWithMediaApp));
EXPECT_THAT(popup_view->GetActionButtonsForTest(),
Not(Contains(Pointee(
Property(&views::LabelButton::GetText, Eq(open_text))))));
// Complete `download`. Then check action buttons.
MarkDownloadStatusCompleted(*download);
Update(download->Clone());
const std::vector<raw_ptr<views::LabelButton, VectorExperimental>>
action_buttons = popup_view->GetActionButtonsForTest();
EXPECT_THAT(
action_buttons,
ElementsAre(
Pointee(Property(&views::LabelButton::GetText, Eq(open_text))),
Pointee(Property(&views::LabelButton::GetText,
Eq(l10n_util::GetStringUTF16(GetCommandTextId(
CommandType::kShowInFolder)))))));
// Click the 'Open' button. Then verify the click is recorded and the Media
// App is launched.
content::TestNavigationObserver observer =
content::TestNavigationObserver(GURL(kChromeUIMediaAppURL));
observer.StartWatchingNewWebContents();
base::UserActionTester tester;
auto open_button_iter = base::ranges::find(action_buttons, open_text,
&views::LabelButton::GetText);
ASSERT_NE(open_button_iter, action_buttons.cend());
test::Click(*open_button_iter, ui::EF_NONE);
EXPECT_EQ(
tester.GetActionCount("DownloadNotificationV2.Button_OpenWithMediaApp"),
1);
observer.Wait();
}
// Verifies that when an in-progress download is interrupted, its notification
// should be removed.
IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest,
InterruptDownload) {
std::string notification_id;
EXPECT_CALL(service_observer(), OnNotificationDisplayed)
.WillOnce(WithArg<0>(
[¬ification_id](const message_center::Notification& notification) {
notification_id = notification.id();
}));
crosapi::mojom::DownloadStatusPtr download =
CreateInProgressDownloadStatus(ProfileManager::GetActiveUserProfile(),
/*extension=*/"txt",
/*received_bytes=*/0,
/*total_bytes=*/1024);
Update(download->Clone());
Mock::VerifyAndClearExpectations(&service_observer());
download->state = crosapi::mojom::DownloadState::kInterrupted;
Update(download->Clone());
EXPECT_THAT(GetDisplayedNotificationIds(), Not(Contains(notification_id)));
}
// Verifies pausing and resuming download from a notification.
IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest,
PauseAndResumeDownload) {
// Add a pausable download. Cache the notification ID.
Profile* const profile = ProfileManager::GetActiveUserProfile();
std::string notification_id;
EXPECT_CALL(service_observer(), OnNotificationDisplayed)
.WillOnce(WithArg<0>(
[¬ification_id](const message_center::Notification& notification) {
notification_id = notification.id();
}));
crosapi::mojom::DownloadStatusPtr download =
CreateInProgressDownloadStatus(profile,
/*extension=*/"txt",
/*received_bytes=*/0,
/*total_bytes=*/1024);
download->pausable = true;
download->resumable = false;
Update(download->Clone());
Mock::VerifyAndClearExpectations(&service_observer());
// The notification of a pausable download should not have a resume button.
AshNotificationView* const popup_view =
GetPopupView(profile, notification_id);
ASSERT_TRUE(popup_view);
std::vector<raw_ptr<views::LabelButton, VectorExperimental>> action_buttons =
popup_view->GetActionButtonsForTest();
const std::u16string resume_button_text =
l10n_util::GetStringUTF16(GetCommandTextId(CommandType::kResume));
auto resume_button_iter = base::ranges::find(
action_buttons, resume_button_text, &views::LabelButton::GetText);
EXPECT_EQ(resume_button_iter, action_buttons.end());
// Implement download pause for the mock client.
ON_CALL(download_status_updater_client(), Pause(download->guid, _))
.WillByDefault([&](const std::string& guid,
crosapi::MockDownloadStatusUpdaterClient::PauseCallback
callback) {
download->pausable = false;
download->resumable = true;
Update(download->Clone());
std::move(callback).Run(/*handled=*/true);
});
// Get the pause button.
const std::u16string pause_button_text =
l10n_util::GetStringUTF16(GetCommandTextId(CommandType::kPause));
auto pause_button_iter = base::ranges::find(action_buttons, pause_button_text,
&views::LabelButton::GetText);
ASSERT_NE(pause_button_iter, action_buttons.end());
// Click on the pause button and wait until download is paused. Then verify
// that the click is recorded.
base::UserActionTester tester;
auto run_loop = std::make_unique<base::RunLoop>();
EXPECT_CALL(service_observer(), OnNotificationDisplayed)
.WillOnce(WithArg<0>(
[&run_loop](const message_center::Notification& notification) {
run_loop->Quit();
}));
test::Click(*pause_button_iter, ui::EF_NONE);
run_loop->Run();
Mock::VerifyAndClearExpectations(&service_observer());
EXPECT_EQ(tester.GetActionCount("DownloadNotificationV2.Button_Pause"), 1);
// After pausing, `popup_view` is hidden. Therefore, get the notification view
// from the notification center bubble.
NotificationCenterTestApi().ToggleBubble();
auto* const notification_view = views::AsViewClass<AshNotificationView>(
NotificationCenterTestApi().GetNotificationViewForId(
ProfileNotification::GetProfileNotificationId(
notification_id, ProfileNotification::GetProfileID(profile))));
// The pause button should not show because the download is already paused.
action_buttons = notification_view->GetActionButtonsForTest();
pause_button_iter = base::ranges::find(action_buttons, pause_button_text,
&views::LabelButton::GetText);
EXPECT_EQ(pause_button_iter, action_buttons.end());
// The resume button should show.
resume_button_iter = base::ranges::find(action_buttons, resume_button_text,
&views::LabelButton::GetText);
ASSERT_NE(resume_button_iter, action_buttons.end());
// Implement download resume for the mock client.
ON_CALL(download_status_updater_client(), Resume(download->guid, _))
.WillByDefault(
[&](const std::string& guid,
crosapi::MockDownloadStatusUpdaterClient::ResumeCallback
callback) {
download->pausable = true;
download->resumable = false;
Update(download->Clone());
std::move(callback).Run(/*handled=*/true);
});
// Click on the resume button and wait until download is resumed. Then verify
// that the click is recorded.
EXPECT_EQ(tester.GetActionCount("DownloadNotificationV2.Button_Resume"), 0);
run_loop = std::make_unique<base::RunLoop>();
EXPECT_CALL(service_observer(), OnNotificationDisplayed)
.WillOnce(WithArg<0>(
[&run_loop](const message_center::Notification& notification) {
run_loop->Quit();
}));
test::Click(*resume_button_iter, ui::EF_NONE);
run_loop->Run();
Mock::VerifyAndClearExpectations(&service_observer());
EXPECT_EQ(tester.GetActionCount("DownloadNotificationV2.Button_Pause"), 1);
EXPECT_EQ(tester.GetActionCount("DownloadNotificationV2.Button_Resume"), 1);
}
// Verifies that the show-in-folder button works as expected.
IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest, ShowInFolder) {
WaitForTestSystemAppInstall();
// Add a pausable download. Cache the notification ID.
Profile* const profile = ProfileManager::GetActiveUserProfile();
std::string notification_id;
EXPECT_CALL(service_observer(), OnNotificationDisplayed)
.WillOnce(WithArg<0>(
[¬ification_id](const message_center::Notification& notification) {
notification_id = notification.id();
}));
crosapi::mojom::DownloadStatusPtr download =
CreateInProgressDownloadStatus(profile,
/*extension=*/"txt",
/*received_bytes=*/0,
/*total_bytes=*/1024);
Update(download->Clone());
Mock::VerifyAndClearExpectations(&service_observer());
// An in-progress notification should not have a show-in-folder button.
AshNotificationView* popup_view = GetPopupView(profile, notification_id);
ASSERT_TRUE(popup_view);
const std::u16string button_text =
l10n_util::GetStringUTF16(GetCommandTextId(CommandType::kShowInFolder));
EXPECT_THAT(popup_view->GetActionButtonsForTest(),
Each(Pointee(Property(&views::LabelButton::GetText,
Not(Eq(button_text))))));
// Complete the download. Check the existence of the associated notification.
download->state = crosapi::mojom::DownloadState::kComplete;
Update(download->Clone());
EXPECT_THAT(GetDisplayedNotificationIds(), Contains(notification_id));
// Set up an observer to wait until the Files app opens.
NiceMock<MockEnvObserver> mock_env_observer;
base::ScopedObservation<aura::Env, MockEnvObserver> env_observation{
&mock_env_observer};
base::RunLoop run_loop;
ON_CALL(mock_env_observer, OnWindowInitialized)
.WillByDefault([&run_loop](aura::Window* window) {
if (aura::test::FindWindowWithTitle(aura::Env::GetInstance(),
u"Files")) {
run_loop.Quit();
}
});
// Find the show-in-folder button.
popup_view = GetPopupView(profile, notification_id);
ASSERT_TRUE(popup_view);
const auto action_buttons = popup_view->GetActionButtonsForTest();
auto show_in_folder_button_iter = base::ranges::find(
action_buttons, button_text, &views::LabelButton::GetText);
ASSERT_NE(show_in_folder_button_iter, action_buttons.end());
// Click the show-in-folder button and wait until the Files app opens. Then
// verify that the click is recorded.
env_observation.Observe(aura::Env::GetInstance());
base::UserActionTester tester;
test::Click(*show_in_folder_button_iter, ui::EF_NONE);
run_loop.Run();
EXPECT_EQ(tester.GetActionCount("DownloadNotificationV2.Button_ShowInFolder"),
1);
}
// Checks the button that enables users to view a download's details in browser.
IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest,
ViewDownloadDetailsInBrowser) {
// Create an in-progress download that can be canceled and paused.
Profile* const profile = ProfileManager::GetActiveUserProfile();
crosapi::mojom::DownloadStatusPtr download =
CreateInProgressDownloadStatus(profile,
/*extension=*/"txt",
/*received_bytes=*/0,
/*total_bytes=*/1024);
download->cancellable = true;
download->pausable = true;
download->resumable = false;
std::string notification_id;
EXPECT_CALL(service_observer(), OnNotificationDisplayed)
.WillOnce(WithArg<0>(
[¬ification_id](const message_center::Notification& notification) {
notification_id = notification.id();
}));
Update(download->Clone());
Mock::VerifyAndClearExpectations(&service_observer());
AshNotificationView* popup_view = GetPopupView(profile, notification_id);
ASSERT_TRUE(popup_view);
const std::u16string button_text = l10n_util::GetStringUTF16(
GetCommandTextId(CommandType::kViewDetailsInBrowser));
// Verify that the notification view does not have a "View details in browser"
// button because `download` is cancelable and pausable.
EXPECT_THAT(popup_view->GetActionButtonsForTest(),
Each(Pointee(Property(&views::LabelButton::GetText,
Not(Eq(button_text))))));
// Update `download` to disable canceling, pausing or resuming. In reality,
// this could happen when a dangerous download is blocked.
download->cancellable = false;
download->pausable = false;
Update(download->Clone());
// Find the "View details in browser" button.
popup_view = GetPopupView(profile, notification_id);
ASSERT_TRUE(popup_view);
const auto action_buttons = popup_view->GetActionButtonsForTest();
auto button_iter = base::ranges::find(action_buttons, button_text,
&views::LabelButton::GetText);
ASSERT_NE(button_iter, action_buttons.end());
// Click the "View details in browser" button and wait until showing downloads
// in browser. Verify that click is recorded.
base::UserActionTester tester;
base::RunLoop run_loop;
EXPECT_CALL(download_status_updater_client(),
ShowInBrowser(download->guid, _))
.WillOnce(
[&run_loop](
const std::string& guid,
crosapi::MockDownloadStatusUpdaterClient::ShowInBrowserCallback
callback) {
std::move(callback).Run(/*handled=*/true);
run_loop.Quit();
});
test::Click(*button_iter, ui::EF_NONE);
run_loop.Run();
EXPECT_EQ(tester.GetActionCount(
"DownloadNotificationV2.Button_ViewDetailsInBrowser"),
1);
}
// NotificationDisplayClientIndeterminateDownloadTest --------------------------
enum class IndeterminateDownloadType {
kNullTotalByteSize,
kUnknownTotalByteSize,
};
// Verifies the notification of an indeterminate download works as expected.
class NotificationDisplayClientIndeterminateDownloadTest
: public NotificationDisplayClientBrowserTest,
public testing::WithParamInterface<IndeterminateDownloadType> {
protected:
std::optional<int64_t> GetTotalByteSize() const {
switch (GetParam()) {
case IndeterminateDownloadType::kNullTotalByteSize:
return std::nullopt;
case IndeterminateDownloadType::kUnknownTotalByteSize:
return 0;
}
}
};
INSTANTIATE_TEST_SUITE_P(
All,
NotificationDisplayClientIndeterminateDownloadTest,
testing::Values(IndeterminateDownloadType::kNullTotalByteSize,
IndeterminateDownloadType::kUnknownTotalByteSize));
IN_PROC_BROWSER_TEST_P(NotificationDisplayClientIndeterminateDownloadTest,
Basics) {
std::string notification_id;
EXPECT_CALL(service_observer(), OnNotificationDisplayed)
.WillOnce(WithArg<0>(
[¬ification_id](const message_center::Notification& notification) {
notification_id = notification.id();
}));
Profile* const profile = ProfileManager::GetActiveUserProfile();
crosapi::mojom::DownloadStatusPtr download =
CreateInProgressDownloadStatus(profile,
/*extension=*/"txt",
/*received_bytes=*/0, GetTotalByteSize());
Update(download->Clone());
Mock::VerifyAndClearExpectations(&service_observer());
// Verify that the notification view of an in-progress download has a visible
// indeterminate progress bar.
AshNotificationView* popup_view = GetPopupView(profile, notification_id);
ASSERT_TRUE(popup_view);
const views::ProgressBar* const progress_bar =
popup_view->progress_bar_view_for_testing();
ASSERT_TRUE(progress_bar);
EXPECT_EQ(progress_bar->GetValue(), -1);
EXPECT_TRUE(progress_bar->GetVisible());
// Complete the download. Check the existence of the associated notification.
MarkDownloadStatusCompleted(*download);
Update(download->Clone());
EXPECT_THAT(GetDisplayedNotificationIds(), Contains(notification_id));
// Verify that the notification view of a completed download does not have
// a progress bar.
popup_view = GetPopupView(profile, notification_id);
ASSERT_TRUE(popup_view);
EXPECT_FALSE(popup_view->progress_bar_view_for_testing());
}
} // namespace ash::download_status