// 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 <set>
#include <tuple>
#include <unordered_map>
#include <vector>
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/capture_mode/capture_mode_test_api.h"
#include "ash/public/cpp/holding_space/holding_space_client.h"
#include "ash/public/cpp/holding_space/holding_space_constants.h"
#include "ash/public/cpp/holding_space/holding_space_controller.h"
#include "ash/public/cpp/holding_space/holding_space_item.h"
#include "ash/public/cpp/holding_space/holding_space_item_updated_fields.h"
#include "ash/public/cpp/holding_space/holding_space_metrics.h"
#include "ash/public/cpp/holding_space/holding_space_model.h"
#include "ash/public/cpp/holding_space/holding_space_model_observer.h"
#include "ash/public/cpp/holding_space/holding_space_prefs.h"
#include "ash/public/cpp/holding_space/holding_space_progress.h"
#include "ash/public/cpp/holding_space/holding_space_test_api.h"
#include "ash/public/cpp/holding_space/holding_space_util.h"
#include "ash/public/cpp/holding_space/mock_holding_space_client.h"
#include "ash/public/cpp/holding_space/mock_holding_space_model_observer.h"
#include "ash/root_window_controller.h"
#include "ash/shelf/shelf.h"
#include "ash/shell.h"
#include "ash/style/ash_color_id.h"
#include "ash/style/dark_light_mode_controller_impl.h"
#include "ash/system/holding_space/holding_space_animation_registry.h"
#include "ash/system/notification_center/message_popup_animation_waiter.h"
#include "ash/system/notification_center/notification_center_tray.h"
#include "ash/system/progress_indicator/progress_ring_animation.h"
#include "ash/system/status_area_widget.h"
#include "ash/test/view_drawn_waiter.h"
#include "base/containers/contains.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/scoped_observation.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/scoped_locale.h"
#include "base/uuid.h"
#include "build/build_config.h"
#include "chrome/browser/ash/crosapi/crosapi_ash.h"
#include "chrome/browser/ash/crosapi/crosapi_manager.h"
#include "chrome/browser/ash/file_manager/file_tasks_observer.h"
#include "chrome/browser/ash/file_suggest/file_suggest_keyed_service.h"
#include "chrome/browser/ash/file_suggest/file_suggest_keyed_service_factory.h"
#include "chrome/browser/ash/file_suggest/local_file_suggestion_provider.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/download/chrome_download_manager_delegate.h"
#include "chrome/browser/download/download_core_service.h"
#include "chrome/browser/download/download_core_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/ash/holding_space/holding_space_browsertest_base.h"
#include "chrome/browser/ui/ash/holding_space/holding_space_downloads_delegate.h"
#include "chrome/browser/ui/ash/holding_space/holding_space_keyed_service_factory.h"
#include "chrome/browser/ui/ash/holding_space/holding_space_test_util.h"
#include "chrome/browser/ui/ash/holding_space/holding_space_util.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/test/base/ash/util/ash_test_util.h"
#include "chromeos/constants/chromeos_features.h"
#include "components/download/public/common/mock_download_item.h"
#include "components/user_manager/user.h"
#include "content/public/browser/download_item_utils.h"
#include "content/public/browser/download_manager_delegate.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/mock_download_manager.h"
#include "storage/browser/file_system/external_mount_points.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "ui/aura/window.h"
#include "ui/base/clipboard/custom_data_helper.h"
#include "ui/base/dragdrop/drag_drop_types.h"
#include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/compositor/compositor.h"
#include "ui/compositor/compositor_observer.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_tree_owner.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/event_constants.h"
#include "ui/events/keycodes/keyboard_codes_posix.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/image/image_unittest_util.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/menu/menu_controller.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/drag_controller.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
#include "ui/wm/public/activation_client.h"
#include "ui/wm/public/mock_activation_change_observer.h"
namespace ash {
namespace {
// Aliases.
using ::testing::Conditional;
using ::testing::Eq;
using ::testing::Matches;
using ::testing::Optional;
using ::testing::Property;
// Matchers --------------------------------------------------------------------
MATCHER_P(EnabledColorId, matcher, "") {
return Matches(matcher)(arg->GetEnabledColorId());
}
// Helpers ---------------------------------------------------------------------
// Returns the accessible name of the specified `view`.
std::string GetAccessibleName(const views::View* view) {
ui::AXNodeData a11y_data;
view->GetViewAccessibility().GetAccessibleNodeData(&a11y_data);
std::string a11y_name;
a11y_data.GetStringAttribute(ax::mojom::StringAttribute::kName, &a11y_name);
return a11y_name;
}
// Flushes the message loop by posting a task and waiting for it to run.
void FlushMessageLoop() {
base::RunLoop run_loop;
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, run_loop.QuitClosure());
run_loop.Run();
}
// Performs a double click on `view`.
void DoubleClick(const views::View* view) {
auto* root_window = HoldingSpaceBrowserTestBase::GetRootWindowForNewWindows();
ui::test::EventGenerator event_generator(root_window);
event_generator.MoveMouseTo(view->GetBoundsInScreen().CenterPoint());
event_generator.DoubleClickLeftButton();
}
using DragUpdateCallback =
base::RepeatingCallback<void(const gfx::Point& screen_location)>;
// Performs a gesture drag between `from` and `to`.
void GestureDrag(const views::View* from,
const views::View* to,
DragUpdateCallback drag_update_callback = base::DoNothing(),
base::OnceClosure before_release_callback = base::DoNothing(),
base::OnceClosure after_release_callback = base::DoNothing()) {
auto* root_window = HoldingSpaceBrowserTestBase::GetRootWindowForNewWindows();
ui::test::EventGenerator event_generator(root_window);
event_generator.PressTouch(from->GetBoundsInScreen().CenterPoint());
// Gesture drag is initiated only after an `ui::EventType::kGestureLongPress`
// event.
ui::GestureEvent long_press(
event_generator.current_screen_location().x(),
event_generator.current_screen_location().y(), ui::EF_NONE,
ui::EventTimeForNow(),
ui::GestureEventDetails(ui::EventType::kGestureLongPress));
event_generator.Dispatch(&long_press);
// Generate multiple interpolated touch move events.
// NOTE: The `ash::DragDropController` applies a vertical offset when
// determining the target view for touch-initiated dragging, so that needs to
// be compensated for here.
constexpr int kNumberOfTouchMoveEvents = 25;
constexpr gfx::Vector2d offset(0, 25);
const gfx::Point endpoint = to->GetBoundsInScreen().CenterPoint() + offset;
const gfx::Point origin = event_generator.current_screen_location();
const gfx::Vector2dF diff(endpoint - origin);
for (int i = 1; i <= kNumberOfTouchMoveEvents; ++i) {
gfx::Vector2dF step(diff);
step.Scale(i / static_cast<float>(kNumberOfTouchMoveEvents));
event_generator.MoveTouch(origin + gfx::ToRoundedVector2d(step));
drag_update_callback.Run(event_generator.current_screen_location());
}
std::move(before_release_callback).Run();
event_generator.ReleaseTouch();
std::move(after_release_callback).Run();
}
// Performs a gesture tap on `view`.
void GestureTap(const views::View* view) {
auto* root_window = HoldingSpaceBrowserTestBase::GetRootWindowForNewWindows();
ui::test::EventGenerator event_generator(root_window);
event_generator.GestureTapAt(view->GetBoundsInScreen().CenterPoint());
}
// Performs a mouse drag between `from` and `to`.
void MouseDrag(const views::View* from,
const views::View* to,
DragUpdateCallback drag_update_callback = base::DoNothing(),
base::OnceClosure before_release_callback = base::DoNothing(),
base::OnceClosure after_release_callback = base::DoNothing()) {
auto* root_window = HoldingSpaceBrowserTestBase::GetRootWindowForNewWindows();
ui::test::EventGenerator event_generator(root_window);
event_generator.MoveMouseTo(from->GetBoundsInScreen().CenterPoint());
event_generator.PressLeftButton();
// Generate multiple interpolated mouse move events so that views are notified
// of mouse enter/exit as they would be in production.
constexpr int kNumberOfMouseMoveEvents = 25;
const gfx::Point origin = event_generator.current_screen_location();
const gfx::Vector2dF diff(to->GetBoundsInScreen().CenterPoint() - origin);
for (int i = 1; i <= kNumberOfMouseMoveEvents; ++i) {
gfx::Vector2dF step(diff);
step.Scale(i / static_cast<float>(kNumberOfMouseMoveEvents));
event_generator.MoveMouseTo(origin + gfx::ToRoundedVector2d(step));
drag_update_callback.Run(event_generator.current_screen_location());
}
std::move(before_release_callback).Run();
event_generator.ReleaseLeftButton();
std::move(after_release_callback).Run();
}
// Waits for the specified `label` to have the desired `text`.
void WaitForText(views::Label* label, const std::u16string& text) {
if (label->GetText() == text)
return;
base::RunLoop run_loop;
auto subscription =
label->AddTextChangedCallback(base::BindLambdaForTesting([&]() {
if (label->GetText() == text)
run_loop.Quit();
}));
run_loop.Run();
}
// DropSenderView --------------------------------------------------------------
class DropSenderView : public views::WidgetDelegateView,
public views::DragController {
public:
DropSenderView(const DropSenderView&) = delete;
DropSenderView& operator=(const DropSenderView&) = delete;
~DropSenderView() override = default;
static DropSenderView* Create(aura::Window* context) {
return new DropSenderView(context);
}
void ClearFilenamesData() { filenames_data_.reset(); }
void SetFilenamesData(const std::vector<base::FilePath> file_paths) {
std::vector<ui::FileInfo> filenames;
for (const base::FilePath& file_path : file_paths)
filenames.emplace_back(file_path, /*display_name=*/base::FilePath());
filenames_data_.emplace(std::move(filenames));
}
void ClearFileSystemSourcesData() { file_system_sources_data_.reset(); }
void SetFileSystemSourcesData(const std::vector<GURL>& file_system_urls) {
constexpr char16_t kFileSystemSourcesType[] = u"fs/sources";
std::stringstream file_system_sources;
for (const GURL& file_system_url : file_system_urls)
file_system_sources << file_system_url.spec() << "\n";
base::Pickle pickle;
ui::WriteCustomDataToPickle(
std::unordered_map<std::u16string, std::u16string>(
{{kFileSystemSourcesType,
base::UTF8ToUTF16(file_system_sources.str())}}),
&pickle);
file_system_sources_data_.emplace(std::move(pickle));
}
private:
explicit DropSenderView(aura::Window* context) {
InitWidget(context);
set_drag_controller(this);
}
// views::DragController:
bool CanStartDragForView(views::View* sender,
const gfx::Point& press_pt,
const gfx::Point& current_pt) override {
DCHECK_EQ(sender, this);
return true;
}
int GetDragOperationsForView(views::View* sender,
const gfx::Point& press_pt) override {
DCHECK_EQ(sender, this);
return ui::DragDropTypes::DRAG_COPY;
}
void WriteDragDataForView(views::View* sender,
const gfx::Point& press_pt,
ui::OSExchangeData* data) override {
// Drag image.
// NOTE: Gesture drag is only enabled if a drag image is specified.
data->provider().SetDragImage(
/*image=*/gfx::test::CreateImageSkia(/*width=*/10, /*height=*/10),
/*cursor_offset=*/gfx::Vector2d());
// Payload.
if (filenames_data_)
data->provider().SetFilenames(filenames_data_.value());
if (file_system_sources_data_) {
data->provider().SetPickledData(
ui::ClipboardFormatType::DataTransferCustomType(),
file_system_sources_data_.value());
}
}
void InitWidget(aura::Window* context) {
views::Widget::InitParams params(
views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET,
views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
params.accept_events = true;
params.activatable = views::Widget::InitParams::Activatable::kNo;
params.context = context;
params.delegate = this;
params.wants_mouse_events_when_inactive = true;
views::Widget* widget = new views::Widget();
widget->Init(std::move(params));
}
std::optional<std::vector<ui::FileInfo>> filenames_data_;
std::optional<base::Pickle> file_system_sources_data_;
};
// DropTargetView --------------------------------------------------------------
class DropTargetView : public views::WidgetDelegateView {
public:
DropTargetView(const DropTargetView&) = delete;
DropTargetView& operator=(const DropTargetView&) = delete;
~DropTargetView() override = default;
static DropTargetView* Create(aura::Window* context) {
return new DropTargetView(context);
}
const base::FilePath& copied_file_path() const { return copied_file_path_; }
private:
explicit DropTargetView(aura::Window* context) { InitWidget(context); }
// views::WidgetDelegateView:
bool GetDropFormats(
int* formats,
std::set<ui::ClipboardFormatType>* format_types) override {
*formats = ui::OSExchangeData::FILE_NAME;
return true;
}
bool CanDrop(const ui::OSExchangeData& data) override { return true; }
int OnDragUpdated(const ui::DropTargetEvent& event) override {
return ui::DragDropTypes::DRAG_COPY;
}
DropCallback GetDropCallback(const ui::DropTargetEvent& event) override {
return base::BindOnce(&DropTargetView::PerformDrop, base::Unretained(this));
}
void PerformDrop(const ui::DropTargetEvent& event,
ui::mojom::DragOperation& output_drag_op,
std::unique_ptr<ui::LayerTreeOwner> drag_image_layer_owner) {
std::optional<std::vector<ui::FileInfo>> files =
event.data().GetFilenames();
ASSERT_TRUE(files.has_value());
ASSERT_EQ(1u, files.value().size());
copied_file_path_ = files.value()[0].path;
output_drag_op = ui::mojom::DragOperation::kCopy;
}
void InitWidget(aura::Window* context) {
views::Widget::InitParams params(
views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET,
views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
params.accept_events = true;
params.activatable = views::Widget::InitParams::Activatable::kNo;
params.context = context;
params.delegate = this;
params.wants_mouse_events_when_inactive = true;
views::Widget* widget = new views::Widget();
widget->Init(std::move(params));
}
base::FilePath copied_file_path_;
};
// NextMainFrameWaiter ---------------------------------------------------------
// A helper class that waits until the next main frame is processed.
class NextMainFrameWaiter : public ui::CompositorObserver {
public:
explicit NextMainFrameWaiter(ui::Compositor* compositor) {
observation_.Observe(compositor);
}
void Wait() {
CHECK(!run_loop_.running());
run_loop_.Run();
}
private:
// ui::CompositorObserver:
void OnDidBeginMainFrame(ui::Compositor* compositor) override {
if (run_loop_.running()) {
run_loop_.Quit();
}
}
base::RunLoop run_loop_;
base::ScopedObservation<ui::Compositor, ui::CompositorObserver> observation_{
this};
};
// HoldingSpaceUiBrowserTest ---------------------------------------------------
using HoldingSpaceUiBrowserTest = HoldingSpaceUiBrowserTestBase;
} // namespace
// Tests -----------------------------------------------------------------------
using PerformDragAndDropCallback =
base::RepeatingCallback<void(const views::View* from,
const views::View* to,
DragUpdateCallback drag_update_callback,
base::OnceClosure before_release_callback,
base::OnceClosure after_release_callback)>;
enum StorageLocationFlag : uint32_t {
kFilenames = 1 << 0,
kFileSystemSources = 1 << 1,
};
using StorageLocationFlags = uint32_t;
// Base class for holding space UI browser tests that test drag-and-drop.
// Parameterized by:
// [0] - callback to invoke to perform a drag-and-drop.
// [1] - storage location(s) on `ui::OSExchangeData` at which to store files.
class HoldingSpaceUiDragAndDropBrowserTest
: public HoldingSpaceUiBrowserTest,
public testing::WithParamInterface<
std::tuple<PerformDragAndDropCallback, StorageLocationFlags>> {
public:
HoldingSpaceUiDragAndDropBrowserTest() {
// Drag-and-drop tests will close the browser because browser events
// sometimes get in the way of drag-and-drop events, causing test flakiness.
set_exit_when_last_browser_closes(false);
}
// Asserts expectations that the holding space tray is or isn't a drop target.
void ExpectTrayIsDropTarget(bool is_drop_target) {
EXPECT_EQ(
test_api().GetTrayDropTargetOverlay()->layer()->GetTargetOpacity(),
is_drop_target ? 1.f : 0.f);
EXPECT_EQ(test_api().GetDefaultTrayIcon()->layer()->GetTargetOpacity(),
is_drop_target ? 0.f : 1.f);
EXPECT_EQ(test_api().GetPreviewsTrayIcon()->layer()->GetTargetOpacity(),
is_drop_target ? 0.f : 1.f);
// Cache a reference to preview layers.
const ui::Layer* previews_container_layer =
test_api().GetPreviewsTrayIcon()->layer()->children()[0];
const std::vector<raw_ptr<ui::Layer, VectorExperimental>>& preview_layers =
previews_container_layer->children();
// Iterate over the layers for each preview.
for (size_t i = 0; i < preview_layers.size(); ++i) {
const ui::Layer* preview_layer = preview_layers[i];
const float preview_width = preview_layer->size().width();
// Previews layers are expected to be translated w/ incremental offset.
gfx::Vector2dF expected_translation(i * preview_width / 2.f, 0.f);
// When the holding space tray is a drop target, preview layers are
// expected to be translated by a fixed amount in addition to the standard
// incremental offset.
if (is_drop_target) {
constexpr int kPreviewIndexOffsetForDropTarget = 3;
expected_translation += gfx::Vector2dF(
kPreviewIndexOffsetForDropTarget * preview_width / 2.f, 0.f);
}
EXPECT_EQ(preview_layer->transform().To2dTranslation(),
expected_translation);
}
}
// Returns true if `screen_location` is within sufficient range of the holding
// space tray so as to make it present itself as a drop target.
bool IsWithinTrayDropTargetRange(const gfx::Point& screen_location) {
constexpr int kProximityThreshold = 20;
gfx::Rect screen_bounds(test_api().GetTray()->GetBoundsInScreen());
screen_bounds.Inset(gfx::Insets(-kProximityThreshold));
return screen_bounds.Contains(screen_location);
}
// Performs a drag-and-drop between `from` and `to`.
void PerformDragAndDrop(
const views::View* from,
const views::View* to,
DragUpdateCallback drag_update_callback = base::DoNothing(),
base::OnceClosure before_release_callback = base::DoNothing(),
base::OnceClosure after_release_callback = base::DoNothing()) {
GetPerformDragAndDropCallback().Run(
from, to, std::move(drag_update_callback),
std::move(before_release_callback), std::move(after_release_callback));
}
// Sets data on `sender()` at the storage location specified by test params.
void SetSenderData(const std::vector<base::FilePath>& file_paths) {
if (ShouldStoreDataIn(StorageLocationFlag::kFilenames))
sender()->SetFilenamesData(file_paths);
else
sender()->ClearFilenamesData();
if (!ShouldStoreDataIn(StorageLocationFlag::kFileSystemSources)) {
sender()->ClearFileSystemSourcesData();
return;
}
std::vector<GURL> file_system_urls;
for (const base::FilePath& file_path : file_paths) {
file_system_urls.push_back(
holding_space_util::ResolveFileSystemUrl(GetProfile(), file_path));
}
sender()->SetFileSystemSourcesData(file_system_urls);
}
// Returns the view serving as the drop sender for tests.
DropSenderView* sender() { return drop_sender_view_; }
// Returns the view serving as the drop target for tests.
const DropTargetView* target() const { return drop_target_view_; }
private:
// HoldingSpaceUiBrowserTest:
void SetUpOnMainThread() override {
HoldingSpaceUiBrowserTest::SetUpOnMainThread();
// Close the browser because browser events sometimes get in the way of
// drag-and-drop events, causing test flakiness.
CloseBrowserSynchronously(browser());
content::RunAllTasksUntilIdle();
// Initialize `drop_sender_view_`.
drop_sender_view_ = DropSenderView::Create(GetRootWindowForNewWindows());
drop_sender_view_->GetWidget()->SetBounds(gfx::Rect(0, 0, 100, 100));
drop_sender_view_->GetWidget()->ShowInactive();
// Initialize `drop_target_view_`.
drop_target_view_ = DropTargetView::Create(GetRootWindowForNewWindows());
drop_target_view_->GetWidget()->SetBounds(gfx::Rect(100, 100, 100, 100));
drop_target_view_->GetWidget()->ShowInactive();
}
void TearDownOnMainThread() override {
drop_sender_view_->GetWidget()->Close();
drop_target_view_->GetWidget()->Close();
HoldingSpaceUiBrowserTest::TearDownOnMainThread();
}
PerformDragAndDropCallback GetPerformDragAndDropCallback() {
return std::get<0>(GetParam());
}
StorageLocationFlags GetStorageLocationFlags() const {
return std::get<1>(GetParam());
}
bool ShouldStoreDataIn(StorageLocationFlag flag) const {
return GetStorageLocationFlags() & flag;
}
raw_ptr<DropSenderView, DanglingUntriaged> drop_sender_view_ = nullptr;
raw_ptr<DropTargetView, DanglingUntriaged> drop_target_view_ = nullptr;
};
// Verifies that drag-and-drop of holding space items works.
IN_PROC_BROWSER_TEST_P(HoldingSpaceUiDragAndDropBrowserTest, DragAndDrop) {
ui::ScopedAnimationDurationScaleMode scoped_animation_duration_scale_mode(
ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);
// Verify drag-and-drop of download items.
HoldingSpaceItem* const download_file = AddDownloadFile();
test_api().Show();
ASSERT_TRUE(test_api().IsShowing());
std::vector<views::View*> download_chips = test_api().GetDownloadChips();
ASSERT_EQ(1u, download_chips.size());
PerformDragAndDrop(/*from=*/download_chips[0], /*to=*/target());
EXPECT_EQ(download_file->file().file_path, target()->copied_file_path());
// Drag-and-drop should close holding space UI.
FlushMessageLoop();
ASSERT_FALSE(test_api().IsShowing());
// Verify drag-and-drop of pinned file items.
// NOTE: Dragging a pinned file from a non-top row of the pinned files
// container grid previously resulted in a crash (crbug.com/1143426). To
// explicitly test against this case we will add and drag a second row item.
HoldingSpaceItem* const pinned_file = AddPinnedFile();
AddPinnedFile();
AddPinnedFile();
test_api().Show();
ASSERT_TRUE(test_api().IsShowing());
std::vector<views::View*> pinned_file_chips = test_api().GetPinnedFileChips();
ASSERT_EQ(3u, pinned_file_chips.size());
PerformDragAndDrop(/*from=*/pinned_file_chips.back(), /*to=*/target());
EXPECT_EQ(pinned_file->file().file_path, target()->copied_file_path());
// Drag-and-drop should close holding space UI.
FlushMessageLoop();
ASSERT_FALSE(test_api().IsShowing());
// Verify drag-and-drop of screenshot items.
HoldingSpaceItem* const screenshot_file = AddScreenshotFile();
test_api().Show();
ASSERT_TRUE(test_api().IsShowing());
std::vector<views::View*> screen_capture_views =
test_api().GetScreenCaptureViews();
ASSERT_EQ(1u, screen_capture_views.size());
PerformDragAndDrop(/*from=*/screen_capture_views[0], /*to=*/target());
EXPECT_EQ(screenshot_file->file().file_path, target()->copied_file_path());
// Drag-and-drop should close holding space UI.
FlushMessageLoop();
ASSERT_FALSE(test_api().IsShowing());
}
// Verifies that drag-and-drop to pin holding space items works.
IN_PROC_BROWSER_TEST_P(HoldingSpaceUiDragAndDropBrowserTest, DragAndDropToPin) {
ui::ScopedAnimationDurationScaleMode scoped_animation_duration_scale_mode(
ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);
// Add an item to holding space to cause the holding space tray to appear.
AddDownloadFile();
ASSERT_TRUE(test_api().IsShowingInShelf());
// Bind an observer to watch for updates to the holding space model.
testing::NiceMock<MockHoldingSpaceModelObserver> mock;
base::ScopedObservation<HoldingSpaceModel, HoldingSpaceModelObserver>
observer{&mock};
observer.Observe(HoldingSpaceController::Get()->model());
// Create a file to be dragged into the holding space.
std::vector<base::FilePath> file_paths;
file_paths.push_back(CreateFile());
SetSenderData(file_paths);
// Expect no events have been recorded to histograms.
base::HistogramTester histogram_tester;
histogram_tester.ExpectBucketCount(
"HoldingSpace.Pod.Action.All",
holding_space_metrics::PodAction::kDragAndDropToPin, 0);
{
base::RunLoop run_loop;
EXPECT_CALL(mock, OnHoldingSpaceItemsAdded)
.WillOnce([&](const std::vector<const HoldingSpaceItem*>& items) {
ASSERT_EQ(items.size(), 1u);
ASSERT_EQ(items[0]->type(), HoldingSpaceItem::Type::kPinnedFile);
ASSERT_EQ(items[0]->file().file_path, file_paths[0]);
run_loop.Quit();
});
// Perform and verify the ability to pin a file via drag-and-drop.
ExpectTrayIsDropTarget(false);
PerformDragAndDrop(
/*from=*/sender(), /*to=*/test_api().GetTray(),
/*drag_update_callback=*/
base::BindRepeating(
&HoldingSpaceUiDragAndDropBrowserTest::IsWithinTrayDropTargetRange,
base::Unretained(this))
.Then(base::BindRepeating(
&HoldingSpaceUiDragAndDropBrowserTest::ExpectTrayIsDropTarget,
base::Unretained(this))),
/*before_release_callback=*/
base::BindOnce(
&HoldingSpaceUiDragAndDropBrowserTest::ExpectTrayIsDropTarget,
base::Unretained(this), true),
/*after_release_callback=*/
base::BindOnce(
&HoldingSpaceUiDragAndDropBrowserTest::ExpectTrayIsDropTarget,
base::Unretained(this), false));
run_loop.Run();
// Expect the event has been recorded to histograms.
histogram_tester.ExpectBucketCount(
"HoldingSpace.Pod.Action.All",
holding_space_metrics::PodAction::kDragAndDropToPin, 1);
}
// Create a few more files to be dragged into the holding space.
file_paths.push_back(CreateFile());
file_paths.push_back(CreateFile());
SetSenderData(file_paths);
{
base::RunLoop run_loop;
EXPECT_CALL(mock, OnHoldingSpaceItemsAdded)
.WillOnce([&](const std::vector<const HoldingSpaceItem*>& items) {
ASSERT_EQ(items.size(), 2u);
ASSERT_EQ(items[0]->type(), HoldingSpaceItem::Type::kPinnedFile);
ASSERT_EQ(items[0]->file().file_path, file_paths[1]);
ASSERT_EQ(items[1]->type(), HoldingSpaceItem::Type::kPinnedFile);
ASSERT_EQ(items[1]->file().file_path, file_paths[2]);
run_loop.Quit();
});
// Perform and verify the ability to pin multiple files via drag-and-drop.
// Note that any already pinned files in the drop payload are ignored.
ExpectTrayIsDropTarget(false);
PerformDragAndDrop(
/*from=*/sender(), /*to=*/test_api().GetTray(),
/*drag_update_callback=*/
base::BindRepeating(
&HoldingSpaceUiDragAndDropBrowserTest::IsWithinTrayDropTargetRange,
base::Unretained(this))
.Then(base::BindRepeating(
&HoldingSpaceUiDragAndDropBrowserTest::ExpectTrayIsDropTarget,
base::Unretained(this))),
/*before_release_callback=*/
base::BindOnce(
&HoldingSpaceUiDragAndDropBrowserTest::ExpectTrayIsDropTarget,
base::Unretained(this), true),
/*after_release_callback=*/
base::BindOnce(
&HoldingSpaceUiDragAndDropBrowserTest::ExpectTrayIsDropTarget,
base::Unretained(this), false));
run_loop.Run();
// Expect the event has been recorded to histograms.
histogram_tester.ExpectBucketCount(
"HoldingSpace.Pod.Action.All",
holding_space_metrics::PodAction::kDragAndDropToPin, 2);
}
// Swap out the registered holding space client with a mock.
testing::NiceMock<MockHoldingSpaceClient> client;
HoldingSpaceController::Get()->RegisterClientAndModelForUser(
ProfileHelper::Get()->GetUserByProfile(GetProfile())->GetAccountId(),
&client, HoldingSpaceController::Get()->model());
ASSERT_EQ(&client, HoldingSpaceController::Get()->client());
{
// Verify that attempting to drag-and-drop a payload which contains only
// files that are already pinned will not result in a client interaction.
EXPECT_CALL(client, PinFiles).Times(0);
ExpectTrayIsDropTarget(false);
PerformDragAndDrop(
/*from=*/sender(), /*to=*/test_api().GetTray(),
/*drag_update_callback=*/
base::BindRepeating(
[](HoldingSpaceUiDragAndDropBrowserTest* test,
const gfx::Point& screen_location) {
// The drag payload cannot be handled by holding space so the tray
// should never indicate it is a drop target regardless of drag
// update `screen_location`.
test->ExpectTrayIsDropTarget(false);
},
base::Unretained(this)),
/*before_release_callback=*/
base::BindOnce(
&HoldingSpaceUiDragAndDropBrowserTest::ExpectTrayIsDropTarget,
base::Unretained(this), false),
/*after_release_callback=*/
base::BindOnce(
&HoldingSpaceUiDragAndDropBrowserTest::ExpectTrayIsDropTarget,
base::Unretained(this), false));
testing::Mock::VerifyAndClearExpectations(&client);
// Expect no event has been recorded to histograms.
histogram_tester.ExpectBucketCount(
"HoldingSpace.Pod.Action.All",
holding_space_metrics::PodAction::kDragAndDropToPin, 2);
}
}
INSTANTIATE_TEST_SUITE_P(
All,
HoldingSpaceUiDragAndDropBrowserTest,
testing::Combine(testing::ValuesIn({
base::BindRepeating(&MouseDrag),
base::BindRepeating(&GestureDrag),
}),
testing::ValuesIn(std::vector<StorageLocationFlags>({
StorageLocationFlag::kFilenames,
StorageLocationFlag::kFileSystemSources,
StorageLocationFlag::kFilenames |
StorageLocationFlag::kFileSystemSources,
}))));
// Verifies that the holding space tray does not appear on the lock screen.
IN_PROC_BROWSER_TEST_F(HoldingSpaceUiBrowserTest, LockScreen) {
ui::ScopedAnimationDurationScaleMode scoped_animation_duration_scale_mode(
ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);
ASSERT_TRUE(test_api().IsShowingInShelf());
RequestAndAwaitLockScreen();
ASSERT_FALSE(test_api().IsShowingInShelf());
}
// Verifies that pinning and unpinning holding space items works as intended.
IN_PROC_BROWSER_TEST_F(HoldingSpaceUiBrowserTest, PinAndUnpinItems) {
ui::ScopedAnimationDurationScaleMode scoped_animation_duration_scale_mode(
ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);
// Add an item of every type. For downloads, also add an in-progress item.
for (const auto type : holding_space_util::GetAllItemTypes()) {
AddItem(GetProfile(), type, CreateFile());
}
AddItem(GetProfile(), HoldingSpaceItem::Type::kDownload, CreateFile(),
HoldingSpaceProgress(/*current_bytes=*/0, /*total_bytes=*/100));
// Show holding space UI.
test_api().Show();
ASSERT_TRUE(test_api().IsShowing());
// Verify existence of views for pinned files, screen captures, and downloads.
using ViewList = std::vector<views::View*>;
ViewList pinned_file_chips = test_api().GetPinnedFileChips();
ASSERT_EQ(pinned_file_chips.size(), 1u);
ViewList screen_capture_views = test_api().GetScreenCaptureViews();
ASSERT_GE(screen_capture_views.size(), 1u);
ViewList download_chips = test_api().GetDownloadChips();
ASSERT_GE(download_chips.size(), 2u);
// Attempt to pin a screen capture via context menu.
RightClick(screen_capture_views.front());
ASSERT_TRUE(SelectMenuItemWithCommandId(HoldingSpaceCommandId::kPinItem));
PressAndReleaseKey(ui::KeyboardCode::VKEY_RETURN);
pinned_file_chips = test_api().GetPinnedFileChips();
ASSERT_EQ(pinned_file_chips.size(), 2u);
ASSERT_EQ(
test_api().GetHoldingSpaceItemFilePath(pinned_file_chips.front()),
test_api().GetHoldingSpaceItemFilePath(screen_capture_views.front()));
// Attempt to pin a completed download via context menu. Note that the first
// download is the in-progress download, so don't select that one.
RightClick(download_chips.at(1));
ASSERT_TRUE(SelectMenuItemWithCommandId(HoldingSpaceCommandId::kPinItem));
PressAndReleaseKey(ui::KeyboardCode::VKEY_RETURN);
pinned_file_chips = test_api().GetPinnedFileChips();
ASSERT_EQ(pinned_file_chips.size(), 3u);
ASSERT_EQ(test_api().GetHoldingSpaceItemFilePath(pinned_file_chips.front()),
test_api().GetHoldingSpaceItemFilePath(download_chips.at(1)));
// Attempt to pin an in-progress download via context menu. Because the
// download is in-progress, it should neither be pin- or unpin-able.
RightClick(download_chips.front());
ASSERT_TRUE(views::MenuController::GetActiveInstance());
ASSERT_FALSE(SelectMenuItemWithCommandId(HoldingSpaceCommandId::kPinItem));
ASSERT_FALSE(SelectMenuItemWithCommandId(HoldingSpaceCommandId::kUnpinItem));
PressAndReleaseKey(ui::KeyboardCode::VKEY_ESCAPE);
// Attempt to unpin the pinned download via context menu without de-selecting
// the in-progress download. Because the selection contains items which are
// not in-progress and all of those items are pinned, the selection should be
// unpin-able.
RightClick(download_chips.at(1), ui::EF_CONTROL_DOWN);
ASSERT_TRUE(SelectMenuItemWithCommandId(HoldingSpaceCommandId::kUnpinItem));
PressAndReleaseKey(ui::KeyboardCode::VKEY_RETURN);
pinned_file_chips = test_api().GetPinnedFileChips();
ASSERT_EQ(pinned_file_chips.size(), 2u);
ASSERT_EQ(
test_api().GetHoldingSpaceItemFilePath(pinned_file_chips.front()),
test_api().GetHoldingSpaceItemFilePath(screen_capture_views.front()));
// Select the pinned file and again attempt to pin the completed download via
// context menu, still without de-selecting the in-progress download. Because
// the selection contains items which are not in-progress and at least one of
// those items are unpinned, the selection should be pin-able.
test::Click(pinned_file_chips.front(), ui::EF_CONTROL_DOWN);
RightClick(download_chips.front());
ASSERT_TRUE(SelectMenuItemWithCommandId(HoldingSpaceCommandId::kPinItem));
PressAndReleaseKey(ui::KeyboardCode::VKEY_RETURN);
pinned_file_chips = test_api().GetPinnedFileChips();
ASSERT_EQ(pinned_file_chips.size(), 3u);
ASSERT_EQ(test_api().GetHoldingSpaceItemFilePath(pinned_file_chips.front()),
test_api().GetHoldingSpaceItemFilePath(download_chips.at(1)));
}
// Verifies that opening holding space items works.
IN_PROC_BROWSER_TEST_F(HoldingSpaceUiBrowserTest, OpenItem) {
// Install the Media App, which we expect to open holding space items.
WaitForTestSystemAppInstall();
ui::ScopedAnimationDurationScaleMode scoped_animation_duration_scale_mode(
ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);
auto* const activation_client = wm::GetActivationClient(
HoldingSpaceBrowserTestBase::GetRootWindowForNewWindows());
// Observe the `activation_client` so we can detect windows becoming active as
// a result of opening holding space items.
testing::NiceMock<MockActivationChangeObserver> mock;
base::ScopedObservation<wm::ActivationClient, wm::ActivationChangeObserver>
obs{&mock};
obs.Observe(activation_client);
// Create a holding space item.
AddScreenshotFile();
// We're going to verify we can open holding space items by interacting with
// the view in a few ways as we expect a user to.
std::vector<base::OnceCallback<void(const views::View*)>> user_interactions;
user_interactions.push_back(base::BindOnce(&DoubleClick));
user_interactions.push_back(base::BindOnce(&GestureTap));
user_interactions.push_back(base::BindOnce([](const views::View* view) {
while (!view->HasFocus())
PressAndReleaseKey(ui::KeyboardCode::VKEY_TAB);
PressAndReleaseKey(ui::KeyboardCode::VKEY_RETURN);
}));
for (auto& user_interaction : user_interactions) {
// Show holding space UI and verify a holding space item view exists.
test_api().Show();
ASSERT_TRUE(test_api().IsShowing());
std::vector<views::View*> screen_capture_views =
test_api().GetScreenCaptureViews();
ASSERT_EQ(1u, screen_capture_views.size());
// Attempt to open the holding space item via user interaction on its view.
std::move(user_interaction).Run(screen_capture_views[0]);
// Expect and wait for a `Gallery` window to be activated since the holding
// space item that we attempted to open was a screenshot.
base::RunLoop run_loop;
EXPECT_CALL(mock, OnWindowActivated)
.WillRepeatedly(
[&](wm::ActivationChangeObserver::ActivationReason reason,
aura::Window* gained_active, aura::Window* lost_active) {
if (gained_active->GetTitle() == u"Gallery")
run_loop.Quit();
});
run_loop.Run();
// Reset.
testing::Mock::VerifyAndClearExpectations(&mock);
activation_client->DeactivateWindow(activation_client->GetActiveWindow());
}
}
// Verifies that removing holding space items works as intended.
IN_PROC_BROWSER_TEST_F(HoldingSpaceUiBrowserTest, RemoveItem) {
ui::ScopedAnimationDurationScaleMode scoped_animation_duration_scale_mode(
ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);
// Populate holding space with items of all types.
for (const auto type : holding_space_util::GetAllItemTypes()) {
AddItem(GetProfile(), type, CreateFile());
}
test_api().Show();
ASSERT_TRUE(test_api().IsShowing());
std::vector<views::View*> pinned_file_chips = test_api().GetPinnedFileChips();
ASSERT_EQ(1u, pinned_file_chips.size());
// Right clicking a pinned item should cause a context menu to show.
ASSERT_FALSE(views::MenuController::GetActiveInstance());
ViewDrawnWaiter().Wait(pinned_file_chips.front());
RightClick(pinned_file_chips.front());
ASSERT_TRUE(views::MenuController::GetActiveInstance());
// There should be no `kRemoveItem` command for pinned items.
ASSERT_FALSE(SelectMenuItemWithCommandId(HoldingSpaceCommandId::kRemoveItem));
// Close the context menu.
PressAndReleaseKey(ui::KeyboardCode::VKEY_ESCAPE);
ASSERT_FALSE(views::MenuController::GetActiveInstance());
std::vector<views::View*> download_chips = test_api().GetDownloadChips();
ASSERT_GT(download_chips.size(), 1u);
// Add a download item to the selection and show the context menu.
ViewDrawnWaiter().Wait(download_chips.front());
test::Click(download_chips.front(), ui::EF_CONTROL_DOWN);
RightClick(download_chips.front());
ASSERT_TRUE(views::MenuController::GetActiveInstance());
// There should be no `kRemoveItem` command since a pinned item is selected.
ASSERT_FALSE(SelectMenuItemWithCommandId(HoldingSpaceCommandId::kRemoveItem));
// Close the context menu.
PressAndReleaseKey(ui::KeyboardCode::VKEY_ESCAPE);
ASSERT_FALSE(views::MenuController::GetActiveInstance());
// Unselect the pinned item and right click show the context menu.
test::Click(pinned_file_chips.front(), ui::EF_CONTROL_DOWN);
RightClick(download_chips.front());
ASSERT_TRUE(views::MenuController::GetActiveInstance());
// There should be a `kRemoveItem` command in the context menu.
ASSERT_TRUE(SelectMenuItemWithCommandId(HoldingSpaceCommandId::kRemoveItem));
// Bind an observer to watch for updates to the holding space model.
testing::NiceMock<MockHoldingSpaceModelObserver> mock;
base::ScopedObservation<HoldingSpaceModel, HoldingSpaceModelObserver>
observer{&mock};
observer.Observe(HoldingSpaceController::Get()->model());
{
// Cache `item_id` of the download item to be removed.
const std::string item_id =
test_api().GetHoldingSpaceItemId(download_chips.front());
EXPECT_EQ(test_api().GetHoldingSpaceItemView(download_chips, item_id),
download_chips.front());
base::RunLoop run_loop;
EXPECT_CALL(mock, OnHoldingSpaceItemsRemoved)
.WillOnce([&](const std::vector<const HoldingSpaceItem*>& items) {
ASSERT_EQ(items.size(), 1u);
EXPECT_EQ(items[0]->id(), item_id);
run_loop.Quit();
});
// Press `ENTER` to remove the selected download item.
PressAndReleaseKey(ui::KeyboardCode::VKEY_RETURN);
run_loop.Run();
// Verify the download chip has been removed.
download_chips = test_api().GetDownloadChips();
EXPECT_FALSE(test_api().GetHoldingSpaceItemView(download_chips, item_id));
}
std::vector<views::View*> screen_capture_views =
test_api().GetScreenCaptureViews();
ASSERT_GT(screen_capture_views.size(), 1u);
// Select a screen capture item and show the context menu.
ViewDrawnWaiter().Wait(screen_capture_views.front());
test::Click(screen_capture_views.front());
RightClick(screen_capture_views.front());
ASSERT_TRUE(views::MenuController::GetActiveInstance());
// There should be a `kRemoveItem` command in the context menu.
ASSERT_TRUE(SelectMenuItemWithCommandId(HoldingSpaceCommandId::kRemoveItem));
{
// Cache `item_id` of the screen capture item to be removed.
const std::string item_id =
test_api().GetHoldingSpaceItemId(screen_capture_views.front());
EXPECT_EQ(test_api().GetHoldingSpaceItemView(screen_capture_views, item_id),
screen_capture_views.front());
base::RunLoop run_loop;
EXPECT_CALL(mock, OnHoldingSpaceItemsRemoved)
.WillOnce([&](const std::vector<const HoldingSpaceItem*>& items) {
ASSERT_EQ(items.size(), 1u);
EXPECT_EQ(items[0]->id(), item_id);
run_loop.Quit();
});
// Press `ENTER` to remove the selected screen capture item.
PressAndReleaseKey(ui::KeyboardCode::VKEY_RETURN);
run_loop.Run();
// Verify the screen capture view has been removed.
screen_capture_views = test_api().GetScreenCaptureViews();
EXPECT_FALSE(
test_api().GetHoldingSpaceItemView(screen_capture_views, item_id));
}
// Remove all items in the recent files bubble. Note that not all download
// items or screen capture items that exist may be visible at the same time
// due to max visibility count restrictions.
while (!download_chips.empty() || !screen_capture_views.empty()) {
// Select all visible download items.
for (views::View* download_chip : download_chips) {
ViewDrawnWaiter().Wait(download_chip);
test::Click(download_chip, ui::EF_CONTROL_DOWN);
}
// Select all visible screen capture items.
for (views::View* screen_capture_view : screen_capture_views) {
ViewDrawnWaiter().Wait(screen_capture_view);
test::Click(screen_capture_view, ui::EF_CONTROL_DOWN);
}
// Show the context menu. There should be a `kRemoveItem` command.
RightClick(download_chips.size() ? download_chips.front()
: screen_capture_views.front());
ASSERT_TRUE(views::MenuController::GetActiveInstance());
ASSERT_TRUE(
SelectMenuItemWithCommandId(HoldingSpaceCommandId::kRemoveItem));
{
// Cache `item_ids` of download and screen capture items to be removed.
std::set<std::string> item_ids;
for (const views::View* download_chip : download_chips) {
auto it =
item_ids.insert(test_api().GetHoldingSpaceItemId(download_chip));
EXPECT_EQ(test_api().GetHoldingSpaceItemView(download_chips, *it.first),
download_chip);
}
for (const views::View* screen_capture_view : screen_capture_views) {
auto it = item_ids.insert(
test_api().GetHoldingSpaceItemId(screen_capture_view));
EXPECT_EQ(
test_api().GetHoldingSpaceItemView(screen_capture_views, *it.first),
screen_capture_view);
}
base::RunLoop run_loop;
EXPECT_CALL(mock, OnHoldingSpaceItemsRemoved)
.WillOnce([&](const std::vector<const HoldingSpaceItem*>& items) {
ASSERT_EQ(items.size(), item_ids.size());
for (const HoldingSpaceItem* item : items) {
ASSERT_TRUE(base::Contains(item_ids, item->id()));
}
run_loop.Quit();
});
// Press `ENTER` to remove the selected items.
PressAndReleaseKey(ui::KeyboardCode::VKEY_RETURN);
run_loop.Run();
// Verify all previously visible download chips and screen capture views
// have been removed.
download_chips = test_api().GetDownloadChips();
screen_capture_views = test_api().GetScreenCaptureViews();
for (const std::string& item_id : item_ids) {
EXPECT_FALSE(
test_api().GetHoldingSpaceItemView(download_chips, item_id));
EXPECT_FALSE(
test_api().GetHoldingSpaceItemView(screen_capture_views, item_id));
}
}
}
// The recent files bubble should be empty and therefore hidden.
ASSERT_FALSE(test_api().RecentFilesBubbleShown());
}
// Verifies that unpinning a pinned holding space item works as intended.
IN_PROC_BROWSER_TEST_F(HoldingSpaceUiBrowserTest, UnpinItem) {
ui::ScopedAnimationDurationScaleMode scoped_animation_duration_scale_mode(
ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);
// Add enough pinned items for there to be multiple rows in the section.
constexpr size_t kNumPinnedItems = 3u;
for (size_t i = 0; i < kNumPinnedItems; ++i)
AddPinnedFile();
test_api().Show();
ASSERT_TRUE(test_api().IsShowing());
std::vector<views::View*> pinned_file_chips = test_api().GetPinnedFileChips();
ASSERT_EQ(kNumPinnedItems, pinned_file_chips.size());
// Operate on the last `pinned_file_chip` as there was an easy to reproduce
// bug in which unpinning a chip *not* in the top row resulted in a crash on
// destruction due to its ink drop layer attempting to be reordered.
views::View* pinned_file_chip = pinned_file_chips.back();
// The pin button is only visible after mousing over the `pinned_file_chip`,
// so move the mouse and wait for the pin button to be drawn. Note that the
// mouse is moved over multiple events to ensure that the appropriate mouse
// enter event is also generated.
test::MoveMouseTo(pinned_file_chip, /*count=*/10);
auto* pin_btn = pinned_file_chip->GetViewByID(kHoldingSpaceItemPinButtonId);
ViewDrawnWaiter().Wait(pin_btn);
test::Click(pin_btn);
pinned_file_chips = test_api().GetPinnedFileChips();
ASSERT_EQ(kNumPinnedItems - 1, pinned_file_chips.size());
}
// Verifies that previews can be toggled via context menu.
IN_PROC_BROWSER_TEST_F(HoldingSpaceUiBrowserTest, TogglePreviews) {
ui::ScopedAnimationDurationScaleMode scoped_animation_duration_scale_mode(
ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);
ASSERT_TRUE(test_api().IsShowingInShelf());
// Initially, the default icon should be shown.
auto* default_tray_icon = test_api().GetDefaultTrayIcon();
ASSERT_TRUE(default_tray_icon);
EXPECT_TRUE(default_tray_icon->GetVisible());
auto* previews_tray_icon = test_api().GetPreviewsTrayIcon();
ASSERT_TRUE(previews_tray_icon);
ASSERT_TRUE(previews_tray_icon->layer());
ASSERT_EQ(1u, previews_tray_icon->layer()->children().size());
auto* previews_container_layer =
previews_tray_icon->layer()->children()[0].get();
EXPECT_FALSE(previews_tray_icon->GetVisible());
// After pinning a file, we should have a single preview in the tray icon.
AddPinnedFile();
FlushMessageLoop();
EXPECT_FALSE(default_tray_icon->GetVisible());
EXPECT_TRUE(previews_tray_icon->GetVisible());
EXPECT_EQ(1u, previews_container_layer->children().size());
EXPECT_EQ(gfx::Size(32, 32), previews_tray_icon->size());
// After downloading a file, we should have two previews in the tray icon.
AddDownloadFile();
FlushMessageLoop();
EXPECT_FALSE(default_tray_icon->GetVisible());
EXPECT_TRUE(previews_tray_icon->GetVisible());
EXPECT_EQ(2u, previews_container_layer->children().size());
EXPECT_EQ(gfx::Size(48, 32), previews_tray_icon->size());
// After taking a screenshot, we should have three previews in the tray icon.
AddScreenshotFile();
FlushMessageLoop();
EXPECT_FALSE(default_tray_icon->GetVisible());
EXPECT_TRUE(previews_tray_icon->GetVisible());
EXPECT_EQ(3u, previews_container_layer->children().size());
EXPECT_EQ(gfx::Size(64, 32), previews_tray_icon->size());
// Right click the tray icon, and expect a context menu to be shown which will
// allow the user to hide previews.
ViewDrawnWaiter().Wait(previews_tray_icon);
RightClick(previews_tray_icon);
ASSERT_TRUE(views::MenuController::GetActiveInstance());
// Use the keyboard to select the context menu item to hide previews. Doing so
// should dismiss the context menu.
PressAndReleaseKey(ui::KeyboardCode::VKEY_DOWN);
PressAndReleaseKey(ui::KeyboardCode::VKEY_RETURN);
EXPECT_FALSE(views::MenuController::GetActiveInstance());
FlushMessageLoop();
// The tray icon should now contain no previews, but have a single child which
// contains the static image to show when previews are disabled.
EXPECT_TRUE(default_tray_icon->GetVisible());
EXPECT_FALSE(previews_tray_icon->GetVisible());
EXPECT_EQ(gfx::Size(32, 32), default_tray_icon->size());
// Right click the tray icon, and expect a context menu to be shown which will
// allow the user to show previews.
ViewDrawnWaiter().Wait(default_tray_icon);
RightClick(default_tray_icon);
ASSERT_TRUE(views::MenuController::GetActiveInstance());
// Use the keyboard to select the context menu item to show previews. Doing so
// should dismiss the context menu.
PressAndReleaseKey(ui::KeyboardCode::VKEY_DOWN);
PressAndReleaseKey(ui::KeyboardCode::VKEY_RETURN);
EXPECT_FALSE(views::MenuController::GetActiveInstance());
FlushMessageLoop();
// The tray icon should once again show three previews.
EXPECT_FALSE(default_tray_icon->GetVisible());
EXPECT_TRUE(previews_tray_icon->GetVisible());
EXPECT_EQ(3u, previews_container_layer->children().size());
EXPECT_EQ(gfx::Size(64, 32), previews_tray_icon->size());
}
// Base class for holding space UI browser tests that require in-progress
// downloads integration. NOTE: This test suite will swap out the production
// download manager with a mock instance.
class HoldingSpaceUiInProgressDownloadsBrowserTest
: public HoldingSpaceUiBrowserTest {
public:
HoldingSpaceUiInProgressDownloadsBrowserTest() {
// Use a testing factory to give us a chance to swap out the production
// download manager for a given browser `context` with a mock prior to
// holding space keyed service creation.
HoldingSpaceKeyedServiceFactory::SetTestingFactory(
base::BindLambdaForTesting([&](content::BrowserContext* context) {
DCHECK(!download_manager_);
// Create a mock download manager.
download_manager_ =
new testing::NiceMock<content::MockDownloadManager>();
// Mock `content::DownloadManager::Shutdown()`.
ON_CALL(*download_manager_, Shutdown)
.WillByDefault(testing::Invoke([&]() {
if (download_manager_->GetDelegate()) {
download_manager_->GetDelegate()->Shutdown();
download_manager_->SetDelegate(nullptr);
}
}));
// Mock `content::DownloadManager::IsManagerInitialized()`.
ON_CALL(*download_manager_, IsManagerInitialized())
.WillByDefault(testing::Return(true));
// Mock `content::DownloadManager::AddObserver()`.
ON_CALL(*download_manager_, AddObserver)
.WillByDefault(testing::Invoke(
&download_manager_observers_,
&base::ObserverList<content::DownloadManager::Observer>::
Unchecked::AddObserver));
// Mock `content::DownloadManager::RemoveObserver()`.
ON_CALL(*download_manager_, RemoveObserver)
.WillByDefault(testing::Invoke(
&download_manager_observers_,
&base::ObserverList<content::DownloadManager::Observer>::
Unchecked::RemoveObserver));
// Mock `content::DownloadManager::GetBrowserContext()`.
ON_CALL(*download_manager_, GetBrowserContext)
.WillByDefault(testing::Return(context));
// Mock `content::DownloadManager::SetDelegate()`.
ON_CALL(*download_manager_, SetDelegate)
.WillByDefault(testing::Invoke(
[&](content::DownloadManagerDelegate* delegate) {
download_manager_delegate_ = delegate;
}));
// Mock `content::DownloadManager::GetDelegate()`.
ON_CALL(*download_manager_, GetDelegate)
.WillByDefault(testing::Invoke(
[&]() { return download_manager_delegate_; }));
// Swap out the production download manager for the mock.
context->SetDownloadManagerForTesting(
base::WrapUnique(download_manager_.get()));
// Install a new download manager delegate after swapping out the
// production download manager so it will properly register itself
// with the mock.
DownloadCoreServiceFactory::GetForBrowserContext(context)
->SetDownloadManagerDelegateForTesting(
std::make_unique<ChromeDownloadManagerDelegate>(
Profile::FromBrowserContext(context)));
// Resume default construction sequence.
return HoldingSpaceKeyedServiceFactory::GetDefaultTestingFactory()
.Run(context);
}));
}
~HoldingSpaceUiInProgressDownloadsBrowserTest() override {
HoldingSpaceKeyedServiceFactory::SetTestingFactory(base::NullCallback());
}
// HoldingSpaceUiBrowserTest:
void TearDownOnMainThread() override {
HoldingSpaceUiBrowserTest::TearDownOnMainThread();
for (auto& observer : download_manager_observers_)
observer.ManagerGoingDown(download_manager_);
}
using AshDownload = testing::NiceMock<download::MockDownloadItem>;
// Creates an in-progress download. If `paused` is `true`, the in-progress
// download will be paused.
std::unique_ptr<AshDownload> CreateInProgressDownload(bool paused = false) {
std::unique_ptr<AshDownload> in_progress_download = CreateAshDownloadItem(
download::DownloadItem::IN_PROGRESS, /*file_path=*/CreateFile(),
/*target_file_path=*/CreateFile(), /*received_bytes=*/0,
/*total_bytes=*/100);
if (paused) {
in_progress_download->Pause();
}
NotifyObserversAshDownloadUpdated(in_progress_download.get());
return in_progress_download;
}
// Creates a completed download.
std::unique_ptr<AshDownload> CreateCompletedDownload() {
// NOTE: In production, the download manager will create completed download
// items from previous sessions during initialization, so we ignore them.
// To match production behavior, create an in-progress download item and
// only then update it to complete state.
std::unique_ptr<AshDownload> completed_download = CreateAshDownloadItem(
download::DownloadItem::IN_PROGRESS, /*file_path=*/CreateFile(),
/*target_file_path=*/CreateFile(), /*received_bytes=*/0,
/*total_bytes=*/100);
ON_CALL(*completed_download, GetState())
.WillByDefault(testing::Return(download::DownloadItem::COMPLETE));
ON_CALL(*completed_download, GetReceivedBytes())
.WillByDefault(testing::Return(100));
NotifyObserversAshDownloadUpdated(completed_download.get());
return completed_download;
}
// Completes the specified `in_progress_download`.
void CompleteInProgressDownload(AshDownload* in_progress_download) {
ON_CALL(*in_progress_download, GetState())
.WillByDefault(testing::Return(download::DownloadItem::COMPLETE));
ON_CALL(*in_progress_download, GetReceivedBytes())
.WillByDefault(testing::Return(in_progress_download->GetTotalBytes()));
NotifyObserversAshDownloadUpdated(in_progress_download);
}
// Pauses the specified `in_progress_download`.
void PauseInProgressDownload(AshDownload* in_progress_download) {
in_progress_download->Pause();
}
// Updates the byte counts for the specified `in_progress_download`.
void UpdateInProgressDownloadByteCounts(AshDownload* in_progress_download,
int32_t received_bytes,
int32_t total_bytes) {
ON_CALL(*in_progress_download, GetReceivedBytes())
.WillByDefault(testing::Return(received_bytes));
ON_CALL(*in_progress_download, GetTotalBytes())
.WillByDefault(testing::Return(total_bytes));
NotifyObserversAshDownloadUpdated(in_progress_download);
}
// Updates whether the specified `in_progress_download` is dangerous,
// insecure, or might be malicious.
void UpdateInProgressDownloadIsDangerousInsecureOrMightBeMalicious(
AshDownload* in_progress_download,
bool is_dangerous,
bool is_insecure,
bool might_be_malicious) {
ASSERT_TRUE(is_dangerous || !might_be_malicious);
ON_CALL(*in_progress_download, GetDangerType())
.WillByDefault(testing::Return(
is_dangerous
? might_be_malicious
? download::DownloadDangerType::
DOWNLOAD_DANGER_TYPE_MAYBE_DANGEROUS_CONTENT
: download::DownloadDangerType::
DOWNLOAD_DANGER_TYPE_DANGEROUS_FILE
: download::DownloadDangerType::
DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS));
ON_CALL(*in_progress_download, IsDangerous())
.WillByDefault(testing::Return(is_dangerous));
ON_CALL(*in_progress_download, IsInsecure())
.WillByDefault(testing::Return(is_insecure));
NotifyObserversAshDownloadUpdated(in_progress_download);
}
// Updates whether the specified `in_progress_download` is scanning.
void UpdateInProgressDownloadIsScanning(AshDownload* in_progress_download,
bool is_scanning) {
const bool was_scanning =
in_progress_download->GetDangerType() ==
download::DownloadDangerType::DOWNLOAD_DANGER_TYPE_ASYNC_SCANNING;
if (is_scanning != was_scanning) {
ON_CALL(*in_progress_download, GetDangerType())
.WillByDefault(testing::Return(
is_scanning ? download::DownloadDangerType::
DOWNLOAD_DANGER_TYPE_ASYNC_SCANNING
: download::DownloadDangerType::
DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS));
ON_CALL(*in_progress_download, IsDangerous())
.WillByDefault(testing::Return(false));
NotifyObserversAshDownloadUpdated(in_progress_download);
}
}
// Returns the target file path for the specified `download`.
base::FilePath GetTargetFilePath(const AshDownload* download) const {
return download->GetTargetFilePath();
}
private:
// Creates and returns an Ash download item with the specified `state`,
// `file_path`, `target_file_path`, `received_bytes`, and `total_bytes`.
std::unique_ptr<AshDownload> CreateAshDownloadItem(
download::DownloadItem::DownloadState state,
const base::FilePath& file_path,
const base::FilePath& target_file_path,
int64_t received_bytes,
int64_t total_bytes) {
auto ash_download_item = std::make_unique<AshDownload>();
content::DownloadItemUtils::AttachInfo(
ash_download_item.get(), GetProfile(),
/*web_contents=*/nullptr, content::GlobalRenderFrameHostId());
// Mock `download::DownloadItem::Cancel()`.
ON_CALL(*ash_download_item, Cancel(/*from_user=*/testing::Eq(true)))
.WillByDefault(testing::InvokeWithoutArgs(
[ash_download_item = ash_download_item.get()]() {
// When a download is cancelled, the underlying file is deleted.
const auto& file_path = ash_download_item->GetFullPath();
if (!file_path.empty()) {
base::ScopedAllowBlockingForTesting allow_blocking;
ASSERT_TRUE(base::DeleteFile(file_path));
}
// Any subsequent calls to `download::DownloadItem::GetState()`
// should indicate that the `mock_download_item` is cancelled.
ON_CALL(*ash_download_item, GetState)
.WillByDefault(
testing::Return(download::DownloadItem::CANCELLED));
// Calling `download::DownloadItem::Cancel()` results in updates.
ash_download_item->NotifyObserversDownloadUpdated();
}));
// Mock `download::DownloadItem::GetETag()`.
ON_CALL(*ash_download_item, GetETag)
.WillByDefault(testing::ReturnRefOfCopy(std::string()));
// Mock `download::DownloadItem::GetFullPath()`.
ON_CALL(*ash_download_item, GetFullPath)
.WillByDefault(testing::Invoke(
[ash_download_item = ash_download_item.get(),
file_path = base::FilePath(file_path)]() -> const base::FilePath& {
return ash_download_item->GetState() ==
download::DownloadItem::COMPLETE
? ash_download_item->GetTargetFilePath()
: file_path;
}));
// Mock `download::DownloadItem::GetGuid()`.
ON_CALL(*ash_download_item, GetGuid)
.WillByDefault(testing::ReturnRefOfCopy(
base::Uuid::GenerateRandomV4().AsLowercaseString()));
// Mock `download::DownloadItem::GetId()`.
ON_CALL(*ash_download_item, GetId).WillByDefault(testing::Invoke([]() {
static uint32_t kNextId = 1u;
return kNextId++;
}));
// Mock `download::DownloadItem::GetLastModifiedTime()`.
ON_CALL(*ash_download_item, GetLastModifiedTime)
.WillByDefault(testing::ReturnRefOfCopy(std::string()));
// Mock `download::DownloadItem::GetLastReason()`.
ON_CALL(*ash_download_item, GetLastReason)
.WillByDefault(
testing::Invoke([ash_download_item = ash_download_item.get()]() {
return ash_download_item->GetState() ==
download::DownloadItem::CANCELLED
? download::DownloadInterruptReason::
DOWNLOAD_INTERRUPT_REASON_USER_CANCELED
: download::DownloadInterruptReason::
DOWNLOAD_INTERRUPT_REASON_NONE;
}));
// Mock `download::DownloadItem::GetOpenWhenComplete()`.
auto open_when_complete = std::make_unique<bool>(false);
ON_CALL(*ash_download_item, GetOpenWhenComplete)
.WillByDefault(testing::ReturnPointee(open_when_complete.get()));
// Mock `download::DownloadItem::GetReceivedBytes()`.
ON_CALL(*ash_download_item, GetReceivedBytes)
.WillByDefault(testing::Return(received_bytes));
// Mock `download::DownloadItem::GetReceivedSlices()`.
ON_CALL(*ash_download_item, GetReceivedSlices)
.WillByDefault(testing::ReturnRefOfCopy(
std::vector<download::DownloadItem::ReceivedSlice>()));
// Mock `download::DownloadItem::GetSerializedEmbedderDownloadData()`.
ON_CALL(*ash_download_item, GetSerializedEmbedderDownloadData)
.WillByDefault(testing::ReturnRefOfCopy(std::string()));
// Mock `download::DownloadItem::GetReferrerUrl()`.
ON_CALL(*ash_download_item, GetReferrerUrl)
.WillByDefault(testing::ReturnRefOfCopy(GURL()));
// Mock `download::DownloadItem::GetTabUrl()`.
ON_CALL(*ash_download_item, GetTabUrl)
.WillByDefault(testing::ReturnRefOfCopy(GURL()));
// Mock `download::DownloadItem::GetTabReferrerUrl()`.
ON_CALL(*ash_download_item, GetTabReferrerUrl)
.WillByDefault(testing::ReturnRefOfCopy(GURL()));
// Mock `download::DownloadItem::GetState()`.
ON_CALL(*ash_download_item, GetState).WillByDefault(testing::Return(state));
// Mock `download::DownloadItem::GetTargetFilePath()`.
ON_CALL(*ash_download_item, GetTargetFilePath)
.WillByDefault(testing::ReturnRefOfCopy(target_file_path));
// Mock `download::DownloadItem::GetTotalBytes()`.
ON_CALL(*ash_download_item, GetTotalBytes)
.WillByDefault(testing::Return(total_bytes));
// Mock `download::DownloadItem::GetURL()`.
ON_CALL(*ash_download_item, GetURL)
.WillByDefault(testing::ReturnRefOfCopy(GURL()));
// Mock `download::DownloadItem::GetUrlChain()`.
ON_CALL(*ash_download_item, GetUrlChain)
.WillByDefault(testing::ReturnRefOfCopy(std::vector<GURL>()));
// Mock `download::DownloadItem::IsDone()`.
ON_CALL(*ash_download_item, IsDone)
.WillByDefault(
testing::Invoke([ash_download_item = ash_download_item.get()]() {
return ash_download_item->GetState() ==
download::DownloadItem::COMPLETE;
}));
// Mock `download::DownloadItem::IsPaused()`.
auto paused = std::make_unique<bool>(false);
ON_CALL(*ash_download_item, IsPaused)
.WillByDefault(testing::ReturnPointee(paused.get()));
// Create a callback which can be run to set `paused` state and which
// mirrors production behavior by notifying observers on change.
auto set_paused = base::BindRepeating(
[](download::MockDownloadItem* ash_download_item, bool* paused,
bool new_paused) {
if (*paused != new_paused) {
*paused = new_paused;
ash_download_item->NotifyObserversDownloadUpdated();
}
},
base::Unretained(ash_download_item.get()),
base::Owned(std::move(paused)));
// Mock `download::DownloadItem::Pause()`.
ON_CALL(*ash_download_item, Pause).WillByDefault([set_paused]() {
set_paused.Run(true);
});
// Mock `download::DownloadItem::Resume()`.
ON_CALL(*ash_download_item, Resume(/*from_user=*/testing::Eq(true)))
.WillByDefault([set_paused]() { set_paused.Run(false); });
// Mock `download::DownloadItem::SetOpenWhenComplete()`.
ON_CALL(*ash_download_item, SetOpenWhenComplete)
.WillByDefault(
[callback = base::BindRepeating(
[](download::MockDownloadItem* ash_download_item,
bool* open_when_complete, bool new_open_when_complete) {
if (*open_when_complete != new_open_when_complete) {
*open_when_complete = new_open_when_complete;
ash_download_item->NotifyObserversDownloadUpdated();
}
},
base::Unretained(ash_download_item.get()),
base::Owned(std::move(open_when_complete)))](
bool new_open_when_complete) {
callback.Run(new_open_when_complete);
});
// Notify observers of the created download.
for (auto& observer : download_manager_observers_)
observer.OnDownloadCreated(download_manager_, ash_download_item.get());
return ash_download_item;
}
// Notifies observers that the specified `ash_download` has been updated.
void NotifyObserversAshDownloadUpdated(
download::MockDownloadItem* ash_download) {
ash_download->NotifyObserversDownloadUpdated();
}
base::test::ScopedFeatureList scoped_feature_list_;
raw_ptr<testing::NiceMock<content::MockDownloadManager>, DanglingUntriaged>
download_manager_ = nullptr;
raw_ptr<content::DownloadManagerDelegate> download_manager_delegate_ =
nullptr;
base::ObserverList<content::DownloadManager::Observer>::Unchecked
download_manager_observers_;
};
// Verifies that primary, secondary, and accessible text work as intended.
IN_PROC_BROWSER_TEST_F(HoldingSpaceUiInProgressDownloadsBrowserTest,
PrimarySecondaryAndAccessibleText) {
ui::ScopedAnimationDurationScaleMode scoped_animation_duration_scale_mode(
ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);
// Force locale since strings are being verified.
base::ScopedLocale scoped_locale("en_US.UTF-8");
// Create an in-progress download.
auto in_progress_download = CreateInProgressDownload();
// Update byte counts.
int32_t received_bytes = 0;
int32_t total_bytes = -1;
UpdateInProgressDownloadByteCounts(in_progress_download.get(), received_bytes,
total_bytes);
// Show holding space UI.
test_api().Show();
ASSERT_TRUE(test_api().IsShowing());
// Verify the existence of a single download chip.
std::vector<views::View*> download_chips = test_api().GetDownloadChips();
ASSERT_EQ(download_chips.size(), 1u);
// Wait for the download chip to be drawn with an indeterminate progress ring
// animation.
NextMainFrameWaiter(Shell::GetPrimaryRootWindow()->GetHost()->compositor())
.Wait();
EXPECT_THAT(
HoldingSpaceAnimationRegistry::GetInstance()
->GetProgressRingAnimationForKey(
ProgressIndicatorAnimationRegistry::AsAnimationKey(
HoldingSpaceController::Get()->model()->GetItem(
test_api().GetHoldingSpaceItemId(download_chips[0])))),
Property(&ProgressRingAnimation::type,
Eq(ProgressRingAnimation::Type::kIndeterminate)));
// Cache pointers to the `primary_label` and `secondary_label`.
auto* primary_label = static_cast<views::Label*>(
download_chips[0]->GetViewByID(kHoldingSpaceItemPrimaryChipLabelId));
auto* secondary_label = static_cast<views::Label*>(
download_chips[0]->GetViewByID(kHoldingSpaceItemSecondaryChipLabelId));
// The `primary_label` should always be visible and should always show the
// lossy display name of the download's target file path.
const auto target_file_path = GetTargetFilePath(in_progress_download.get());
const auto target_file_name = target_file_path.BaseName().LossyDisplayName();
EXPECT_TRUE(primary_label->GetVisible());
EXPECT_EQ(primary_label->GetText(), target_file_name);
// Initially, no bytes have been received so `secondary_label` should display
// `0 B` as there is no knowledge of the total number of bytes expected.
EXPECT_TRUE(secondary_label->GetVisible());
EXPECT_EQ(secondary_label->GetText(), u"0 B");
EXPECT_THAT(secondary_label,
EnabledColorId(Optional(kColorAshTextColorSecondary)));
// The accessible name should indicate that the download is in progress.
EXPECT_EQ(GetAccessibleName(download_chips.at(0)),
base::UTF16ToUTF8(u"Downloading " + target_file_name));
// Pause the download.
RightClick(download_chips.at(0));
EXPECT_TRUE(SelectMenuItemWithCommandId(HoldingSpaceCommandId::kPauseItem));
PressAndReleaseKey(ui::VKEY_RETURN);
// When paused with no bytes received, the `secondary_label` should display
// "Paused, 0 B" as there is still no knowledge of the total number of
// bytes expected.
EXPECT_TRUE(primary_label->GetVisible());
EXPECT_EQ(primary_label->GetText(), target_file_name);
EXPECT_TRUE(secondary_label->GetVisible());
WaitForText(secondary_label, u"Paused, 0 B");
EXPECT_THAT(secondary_label,
EnabledColorId(Optional(kColorAshTextColorSecondary)));
// The accessible name should indicate that the download is in progress and
// that progress is paused.
EXPECT_EQ(GetAccessibleName(download_chips.at(0)),
base::UTF16ToUTF8(u"Download paused " + target_file_name));
// Update received bytes.
received_bytes = 1024 * 1024;
UpdateInProgressDownloadByteCounts(in_progress_download.get(), received_bytes,
total_bytes);
// When paused with bytes received, the `secondary_label` should display both
// the paused state and the number of bytes received with appropriate units.
EXPECT_TRUE(primary_label->GetVisible());
EXPECT_EQ(primary_label->GetText(), target_file_name);
EXPECT_TRUE(secondary_label->GetVisible());
WaitForText(secondary_label, u"Paused, 1,024 KB");
EXPECT_THAT(secondary_label,
EnabledColorId(Optional(kColorAshTextColorSecondary)));
// The accessible name should indicate that the download is in progress and
// that progress is paused.
EXPECT_EQ(GetAccessibleName(download_chips.at(0)),
base::UTF16ToUTF8(u"Download paused " + target_file_name));
// Resume the download.
RightClick(download_chips.at(0));
EXPECT_TRUE(SelectMenuItemWithCommandId(HoldingSpaceCommandId::kResumeItem));
PressAndReleaseKey(ui::VKEY_RETURN);
// If resumed with bytes received, the `secondary_label` should display only
// the number of bytes received with appropriate units.
EXPECT_TRUE(primary_label->GetVisible());
EXPECT_EQ(primary_label->GetText(), target_file_name);
EXPECT_TRUE(secondary_label->GetVisible());
WaitForText(secondary_label, u"1,024 KB");
EXPECT_THAT(secondary_label,
EnabledColorId(Optional(kColorAshTextColorSecondary)));
// The accessible name should indicate that the download is in progress.
EXPECT_EQ(GetAccessibleName(download_chips.at(0)),
base::UTF16ToUTF8(u"Downloading " + target_file_name));
// Update total bytes.
total_bytes = 2 * received_bytes;
UpdateInProgressDownloadByteCounts(in_progress_download.get(), received_bytes,
total_bytes);
// If both the number of bytes received and the total number of bytes expected
// are known, the `secondary_label` should display both with appropriate
// units.
EXPECT_TRUE(primary_label->GetVisible());
EXPECT_EQ(primary_label->GetText(), target_file_name);
EXPECT_TRUE(secondary_label->GetVisible());
WaitForText(secondary_label, u"1.0/2.0 MB");
EXPECT_THAT(secondary_label,
EnabledColorId(Optional(kColorAshTextColorSecondary)));
// The accessible name should indicate that the download is in progress.
EXPECT_EQ(GetAccessibleName(download_chips.at(0)),
base::UTF16ToUTF8(u"Downloading " + target_file_name));
// Pause the download.
RightClick(download_chips.at(0));
EXPECT_TRUE(SelectMenuItemWithCommandId(HoldingSpaceCommandId::kPauseItem));
PressAndReleaseKey(ui::VKEY_RETURN);
// If paused with both the number of bytes received and the total number of
// bytes expected known, the `secondary_label` should display the paused state
// and both received and total bytes with appropriate units.
EXPECT_TRUE(primary_label->GetVisible());
EXPECT_EQ(primary_label->GetText(), target_file_name);
EXPECT_TRUE(secondary_label->GetVisible());
WaitForText(secondary_label, u"Paused, 1.0/2.0 MB");
EXPECT_THAT(secondary_label,
EnabledColorId(Optional(kColorAshTextColorSecondary)));
// The accessible name should indicate that the download is in progress and
// that progress is paused.
EXPECT_EQ(GetAccessibleName(download_chips.at(0)),
base::UTF16ToUTF8(u"Download paused " + target_file_name));
// Update received bytes to indicate that all bytes have been received.
received_bytes = total_bytes;
UpdateInProgressDownloadByteCounts(in_progress_download.get(), received_bytes,
total_bytes);
// Because the download has not yet been marked complete, the number of bytes
// received will not equal the total number of expected bytes but in most
// cases that will be imperceptible to the user due to rounding. This is to
// prevent giving the impression of completion before download progress is
// truly complete (which does not occur until after renaming, etc).
EXPECT_TRUE(primary_label->GetVisible());
EXPECT_EQ(primary_label->GetText(), target_file_name);
EXPECT_TRUE(secondary_label->GetVisible());
WaitForText(secondary_label, u"Paused, 2.0/2.0 MB");
EXPECT_THAT(secondary_label,
EnabledColorId(Optional(kColorAshTextColorSecondary)));
// The accessible name should indicate that the download is in progress and
// that progress is paused.
EXPECT_EQ(GetAccessibleName(download_chips.at(0)),
base::UTF16ToUTF8(u"Download paused " + target_file_name));
// Mark the download as dangerous.
UpdateInProgressDownloadIsDangerousInsecureOrMightBeMalicious(
in_progress_download.get(),
/*is_dangerous=*/true,
/*is_insecure=*/false,
/*might_be_malicious=*/true);
// Because the download is marked as dangerous, that should be indicated in
// the `secondary_label` of the holding space item chip view.
EXPECT_TRUE(primary_label->GetVisible());
EXPECT_EQ(primary_label->GetText(), target_file_name);
EXPECT_TRUE(secondary_label->GetVisible());
WaitForText(secondary_label, u"Dangerous file");
EXPECT_THAT(secondary_label,
EnabledColorId(Optional(cros_tokens::kTextColorAlert)));
// The accessible name should indicate that the download is dangerous.
EXPECT_EQ(GetAccessibleName(download_chips.at(0)),
base::UTF16ToUTF8(u"Download dangerous " + target_file_name));
// Mark the download as being scanned.
UpdateInProgressDownloadIsScanning(in_progress_download.get(), true);
// Because the download is marked as being scanned, that should be indicated
// in the `secondary_label` of the holding space item chip view.
EXPECT_TRUE(primary_label->GetVisible());
EXPECT_EQ(primary_label->GetText(), target_file_name);
EXPECT_TRUE(secondary_label->GetVisible());
WaitForText(secondary_label, u"Scanning");
EXPECT_THAT(secondary_label,
EnabledColorId(Optional(cros_tokens::kTextColorProminent)));
// The accessible name should indicate that the download is being scanning.
EXPECT_EQ(GetAccessibleName(download_chips.at(0)),
base::UTF16ToUTF8(u"Download scanning " + target_file_name));
// Stop scanning and mark that the download is *not* malicious.
UpdateInProgressDownloadIsDangerousInsecureOrMightBeMalicious(
in_progress_download.get(), /*is_dangerous=*/true,
/*is_insecure=*/false, /*might_be_malicious=*/false);
// Because the download is *not* malicious, the user will be able to keep/
// discard the download via notification. That should be indicated in the
// `secondary_label` of the holding space item chip view.
EXPECT_TRUE(primary_label->GetVisible());
EXPECT_EQ(primary_label->GetText(), target_file_name);
EXPECT_TRUE(secondary_label->GetVisible());
WaitForText(secondary_label, u"Confirm download");
EXPECT_THAT(secondary_label,
EnabledColorId(Optional(cros_tokens::kTextColorWarning)));
// The accessible name should indicate that the download must be confirmed.
EXPECT_EQ(GetAccessibleName(download_chips.at(0)),
base::UTF16ToUTF8(u"Confirm download " + target_file_name));
// Mark the download as safe.
UpdateInProgressDownloadIsDangerousInsecureOrMightBeMalicious(
in_progress_download.get(),
/*is_dangerous=*/false,
/*is_insecure=*/false,
/*might_be_malicious=*/false);
// Because the download is no longer marked as dangerous, that should be
// indicated in the `secondary_label` of the holding space item chip view.
EXPECT_TRUE(primary_label->GetVisible());
EXPECT_EQ(primary_label->GetText(), target_file_name);
EXPECT_TRUE(secondary_label->GetVisible());
WaitForText(secondary_label, u"Paused, 2.0/2.0 MB");
EXPECT_THAT(secondary_label,
EnabledColorId(Optional(kColorAshTextColorSecondary)));
// The accessible name should indicate that the download is in progress and
// that progress is paused.
EXPECT_EQ(GetAccessibleName(download_chips.at(0)),
base::UTF16ToUTF8(u"Download paused " + target_file_name));
// Mark the download as insecure.
UpdateInProgressDownloadIsDangerousInsecureOrMightBeMalicious(
in_progress_download.get(),
/*is_dangerous=*/false,
/*is_insecure=*/true,
/*might_be_malicious=*/false);
// Because the download is marked as insecure, that should be indicated
// in the `secondary_label` of the holding space item chip view.
EXPECT_TRUE(primary_label->GetVisible());
EXPECT_EQ(primary_label->GetText(), target_file_name);
EXPECT_TRUE(secondary_label->GetVisible());
WaitForText(secondary_label, u"Dangerous file");
EXPECT_THAT(secondary_label,
EnabledColorId(Optional(cros_tokens::kTextColorAlert)));
// The accessible name should indicate that the download is dangerous.
EXPECT_EQ(GetAccessibleName(download_chips.at(0)),
base::UTF16ToUTF8(u"Download dangerous " + target_file_name));
// Mark the download as *not* insecure.
UpdateInProgressDownloadIsDangerousInsecureOrMightBeMalicious(
in_progress_download.get(),
/*is_dangerous=*/false,
/*is_insecure=*/false,
/*might_be_malicious=*/false);
// Because the download is no longer marked as insecure, that should be
// indicated in the `secondary_label` of the holding space item chip view.
EXPECT_TRUE(primary_label->GetVisible());
EXPECT_EQ(primary_label->GetText(), target_file_name);
EXPECT_TRUE(secondary_label->GetVisible());
WaitForText(secondary_label, u"Paused, 2.0/2.0 MB");
EXPECT_THAT(secondary_label,
EnabledColorId(Optional(kColorAshTextColorSecondary)));
// The accessible name should indicate that the download is in progress and
// that progress is paused.
EXPECT_EQ(GetAccessibleName(download_chips.at(0)),
base::UTF16ToUTF8(u"Download paused " + target_file_name));
// Complete the download.
CompleteInProgressDownload(in_progress_download.get());
// When no longer in progress, the `secondary_label` should be hidden.
EXPECT_TRUE(primary_label->GetVisible());
EXPECT_EQ(primary_label->GetText(), target_file_name);
EXPECT_FALSE(secondary_label->GetVisible());
EXPECT_THAT(secondary_label,
EnabledColorId(Optional(kColorAshTextColorSecondary)));
// The accessible name should indicate the target file name.
EXPECT_EQ(GetAccessibleName(download_chips.at(0)),
base::UTF16ToUTF8(target_file_name));
}
// Verifies that canceling holding space items works as intended.
IN_PROC_BROWSER_TEST_F(HoldingSpaceUiInProgressDownloadsBrowserTest,
CancelItem) {
ui::ScopedAnimationDurationScaleMode scoped_animation_duration_scale_mode(
ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);
// Create an in-progress download and a completed download.
auto in_progress_download = CreateInProgressDownload();
auto completed_download = CreateCompletedDownload();
// Show holding space UI.
test_api().Show();
ASSERT_TRUE(test_api().IsShowing());
// Expect two download chips, one for each created download item.
std::vector<views::View*> download_chips = test_api().GetDownloadChips();
ASSERT_EQ(download_chips.size(), 2u);
// Cache download chips. NOTE: Chips are displayed in reverse order of their
// underlying holding space item creation.
views::View* const completed_download_chip = download_chips.at(0);
views::View* const in_progress_download_chip = download_chips.at(1);
// Right click the `completed_download_chip`. Because the underlying download
// is completed, the context menu should *not* contain a "Cancel" command.
RightClick(completed_download_chip);
ASSERT_FALSE(SelectMenuItemWithCommandId(HoldingSpaceCommandId::kCancelItem));
// Close the context menu and control-right click the
// `in_progress_download_chip`. Because the `completed_download_chip` is still
// selected and its underlying download is completed, the context menu should
// *not* contain a "Cancel" command.
PressAndReleaseKey(ui::KeyboardCode::VKEY_ESCAPE);
RightClick(in_progress_download_chip, ui::EF_CONTROL_DOWN);
ASSERT_FALSE(SelectMenuItemWithCommandId(HoldingSpaceCommandId::kCancelItem));
// Close the context menu, press the `in_progress_download_chip` and then
// right click it. Because the `in_progress_download_chip` is the only chip
// selected and its underlying download is in-progress, the context menu
// should contain a "Cancel" command.
PressAndReleaseKey(ui::KeyboardCode::VKEY_ESCAPE);
test::Click(in_progress_download_chip);
RightClick(in_progress_download_chip);
ASSERT_TRUE(SelectMenuItemWithCommandId(HoldingSpaceCommandId::kCancelItem));
// Cache the holding space item IDs associated with the two download chips.
const std::string completed_download_id =
test_api().GetHoldingSpaceItemId(completed_download_chip);
const std::string in_progress_download_id =
test_api().GetHoldingSpaceItemId(in_progress_download_chip);
// Bind an observer to watch for updates to the holding space model.
testing::NiceMock<MockHoldingSpaceModelObserver> mock;
base::ScopedObservation<HoldingSpaceModel, HoldingSpaceModelObserver>
observer{&mock};
observer.Observe(HoldingSpaceController::Get()->model());
// Press ENTER to execute the "Cancel" command, expecting and waiting for
// the in-progress download item to be removed from the holding space model.
base::RunLoop run_loop;
EXPECT_CALL(mock, OnHoldingSpaceItemsRemoved)
.WillOnce([&](const std::vector<const HoldingSpaceItem*>& items) {
ASSERT_EQ(items.size(), 1u);
ASSERT_EQ(items[0]->id(), in_progress_download_id);
run_loop.Quit();
});
PressAndReleaseKey(ui::KeyboardCode::VKEY_RETURN);
run_loop.Run();
// Verify that there is now only a single download chip.
download_chips = test_api().GetDownloadChips();
EXPECT_EQ(download_chips.size(), 1u);
// Because the in-progress download was canceled, only the completed download
// chip should still be present in the UI.
EXPECT_TRUE(test_api().GetHoldingSpaceItemView(download_chips,
completed_download_id));
EXPECT_FALSE(test_api().GetHoldingSpaceItemView(download_chips,
in_progress_download_id));
}
// Verifies that canceling holding space items via primary action is WAI.
IN_PROC_BROWSER_TEST_F(HoldingSpaceUiInProgressDownloadsBrowserTest,
CancelItemViaPrimaryAction) {
ui::ScopedAnimationDurationScaleMode scoped_animation_duration_scale_mode(
ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);
// Create an in-progress download and a completed download.
auto in_progress_download = CreateInProgressDownload();
auto completed_download = CreateCompletedDownload();
// Show holding space UI.
test_api().Show();
ASSERT_TRUE(test_api().IsShowing());
// Expect two download chips, one for each created download item.
std::vector<views::View*> download_chips = test_api().GetDownloadChips();
ASSERT_EQ(download_chips.size(), 2u);
// Cache download chips. NOTE: Chips are displayed in reverse order of their
// underlying holding space item creation.
views::View* const completed_download_chip = download_chips.at(0);
views::View* const in_progress_download_chip = download_chips.at(1);
// Hover over the `completed_download_chip`. Because the underlying download
// is completed, the chip should contain a visible primary action for "Pin".
test::MoveMouseTo(completed_download_chip, /*count=*/10);
auto* primary_action_container = completed_download_chip->GetViewByID(
kHoldingSpaceItemPrimaryActionContainerId);
auto* primary_action_cancel =
primary_action_container->GetViewByID(kHoldingSpaceItemCancelButtonId);
auto* primary_action_pin =
primary_action_container->GetViewByID(kHoldingSpaceItemPinButtonId);
ViewDrawnWaiter().Wait(primary_action_container);
EXPECT_FALSE(primary_action_cancel->GetVisible());
EXPECT_TRUE(primary_action_pin->GetVisible());
// Hover over the `in_progress_download_chip`. Because the underlying download
// is in-progress, the chip should contain a visible primary action for
// "Cancel".
test::MoveMouseTo(in_progress_download_chip, /*count=*/10);
primary_action_container = in_progress_download_chip->GetViewByID(
kHoldingSpaceItemPrimaryActionContainerId);
primary_action_cancel =
primary_action_container->GetViewByID(kHoldingSpaceItemCancelButtonId);
primary_action_pin =
primary_action_container->GetViewByID(kHoldingSpaceItemPinButtonId);
ViewDrawnWaiter().Wait(primary_action_container);
EXPECT_TRUE(primary_action_cancel->GetVisible());
EXPECT_FALSE(primary_action_pin->GetVisible());
// Cache the holding space item IDs associated with the two download chips.
const std::string completed_download_id =
test_api().GetHoldingSpaceItemId(completed_download_chip);
const std::string in_progress_download_id =
test_api().GetHoldingSpaceItemId(in_progress_download_chip);
// Bind an observer to watch for updates to the holding space model.
testing::NiceMock<MockHoldingSpaceModelObserver> mock;
base::ScopedObservation<HoldingSpaceModel, HoldingSpaceModelObserver>
observer{&mock};
observer.Observe(HoldingSpaceController::Get()->model());
// Press the `primary_action_container` to execute "Cancel", expecting and
// waiting for the in-progress download item to be removed from the holding
// space model.
base::RunLoop run_loop;
EXPECT_CALL(mock, OnHoldingSpaceItemsRemoved)
.WillOnce([&](const std::vector<const HoldingSpaceItem*>& items) {
ASSERT_EQ(items.size(), 1u);
ASSERT_EQ(items[0]->id(), in_progress_download_id);
run_loop.Quit();
});
test::Click(primary_action_container);
run_loop.Run();
// Verify that there is now only a single download chip.
download_chips = test_api().GetDownloadChips();
EXPECT_EQ(download_chips.size(), 1u);
// Because the in-progress download was canceled, only the completed download
// chip should still be present in the UI.
EXPECT_TRUE(test_api().GetHoldingSpaceItemView(download_chips,
completed_download_id));
EXPECT_FALSE(test_api().GetHoldingSpaceItemView(download_chips,
in_progress_download_id));
}
// Verifies that opening in-progress download items works as intended.
IN_PROC_BROWSER_TEST_F(HoldingSpaceUiInProgressDownloadsBrowserTest,
OpenItemWhenComplete) {
ui::ScopedAnimationDurationScaleMode scoped_animation_duration_scale_mode(
ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);
// Force locale since strings are being verified.
base::ScopedLocale scoped_locale("en_US.UTF-8");
// Create an in-progress download.
auto in_progress_download = CreateInProgressDownload();
// Show holding space UI.
test_api().Show();
ASSERT_TRUE(test_api().IsShowing());
// Expect a single download chip.
std::vector<views::View*> download_chips = test_api().GetDownloadChips();
ASSERT_EQ(download_chips.size(), 1u);
// Cache pointers to the `primary_label` and `secondary_label`.
auto* const primary_label = static_cast<views::Label*>(
download_chips.front()->GetViewByID(kHoldingSpaceItemPrimaryChipLabelId));
auto* const secondary_label =
static_cast<views::Label*>(download_chips.front()->GetViewByID(
kHoldingSpaceItemSecondaryChipLabelId));
// The `primary_label` should be visible and should show the lossy display
// name of the download's target file path.
const auto target_file_path = GetTargetFilePath(in_progress_download.get());
const auto target_file_name = target_file_path.BaseName().LossyDisplayName();
EXPECT_TRUE(primary_label->GetVisible());
EXPECT_EQ(primary_label->GetText(), target_file_name);
// The `secondary_label` should also be visible and should show `0/100 B` as
// no bytes have been received but the total number of bytes is known.
EXPECT_TRUE(secondary_label->GetVisible());
EXPECT_EQ(secondary_label->GetText(), u"0/100 B");
// Double click the download chip to open the item. Because the underlying
// item is in-progress, opening should not occur immediately but should
// instead be queued up until download completion.
DoubleClick(download_chips.front());
// The `primary_label` should still be visible and should still show the
// lossy display name of the download's target file path.
EXPECT_TRUE(primary_label->GetVisible());
EXPECT_EQ(primary_label->GetText(), target_file_name);
// The `secondary_label` should still be visible but should have been updated
// to reflect that the underlying download will be opened when complete.
EXPECT_TRUE(secondary_label->GetVisible());
WaitForText(secondary_label, u"Open when complete");
// Pause the download.
PauseInProgressDownload(in_progress_download.get());
// The `secondary_label` should still be visible but should have been updated
// to reflect that the underlying download is paused.
EXPECT_TRUE(secondary_label->GetVisible());
WaitForText(secondary_label, u"Paused, 0/100 B");
// Complete the download.
CompleteInProgressDownload(in_progress_download.get());
// When no longer in progress, the `secondary_label` should be hidden.
EXPECT_TRUE(primary_label->GetVisible());
EXPECT_EQ(primary_label->GetText(), target_file_name);
EXPECT_FALSE(secondary_label->GetVisible());
}
// Verifies that removing holding space items works as intended.
IN_PROC_BROWSER_TEST_F(HoldingSpaceUiInProgressDownloadsBrowserTest,
RemoveItem) {
ui::ScopedAnimationDurationScaleMode scoped_animation_duration_scale_mode(
ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);
// Create an in-progress download and a completed download.
auto in_progress_download = CreateInProgressDownload();
auto completed_download = CreateCompletedDownload();
// Show holding space UI.
test_api().Show();
ASSERT_TRUE(test_api().IsShowing());
// Expect two download chips, one for each created download item.
std::vector<views::View*> download_chips = test_api().GetDownloadChips();
ASSERT_EQ(download_chips.size(), 2u);
// Cache download chips. NOTE: Chips are displayed in reverse order of their
// underlying holding space item creation.
views::View* const completed_download_chip = download_chips.at(0);
views::View* const in_progress_download_chip = download_chips.at(1);
// Right click the `in_progress_download_chip`. Because the underlying
// download is in-progress, the context menu should *not* contain a "Remove"
// command.
RightClick(in_progress_download_chip);
ASSERT_FALSE(SelectMenuItemWithCommandId(HoldingSpaceCommandId::kRemoveItem));
// Close the context menu and control-right click the
// `completed_download_chip`. Because the `in_progress_download_chip` is still
// selected and its underlying download is in-progress, the context menu
// should *not* contain a "Remove" command.
PressAndReleaseKey(ui::KeyboardCode::VKEY_ESCAPE);
RightClick(completed_download_chip, ui::EF_CONTROL_DOWN);
ASSERT_FALSE(SelectMenuItemWithCommandId(HoldingSpaceCommandId::kRemoveItem));
// Close the context menu, press the `completed_download_chip` and then
// right click it. Because the `completed_download_chip` is the only chip
// selected and its underlying download is completed, the context menu should
// contain a "Remove" command.
PressAndReleaseKey(ui::KeyboardCode::VKEY_ESCAPE);
test::Click(completed_download_chip);
RightClick(completed_download_chip);
ASSERT_TRUE(SelectMenuItemWithCommandId(HoldingSpaceCommandId::kRemoveItem));
// Cache the holding space item IDs associated with the two download chips.
const std::string completed_download_id =
test_api().GetHoldingSpaceItemId(completed_download_chip);
const std::string in_progress_download_id =
test_api().GetHoldingSpaceItemId(in_progress_download_chip);
// Bind an observer to watch for updates to the holding space model.
testing::NiceMock<MockHoldingSpaceModelObserver> mock;
base::ScopedObservation<HoldingSpaceModel, HoldingSpaceModelObserver>
observer{&mock};
observer.Observe(HoldingSpaceController::Get()->model());
// Press ENTER to execute the "Remove" command, expecting and waiting for
// the completed download item to be removed from the holding space model.
base::RunLoop run_loop;
EXPECT_CALL(mock, OnHoldingSpaceItemsRemoved)
.WillOnce([&](const std::vector<const HoldingSpaceItem*>& items) {
ASSERT_EQ(items.size(), 1u);
ASSERT_EQ(items[0]->id(), completed_download_id);
run_loop.Quit();
});
PressAndReleaseKey(ui::KeyboardCode::VKEY_RETURN);
run_loop.Run();
// Verify that there is now only a single download chip.
download_chips = test_api().GetDownloadChips();
EXPECT_EQ(download_chips.size(), 1u);
// Because the completed download was canceled, only the in-progress download
// chip should still be present in the UI.
EXPECT_FALSE(test_api().GetHoldingSpaceItemView(download_chips,
completed_download_id));
EXPECT_TRUE(test_api().GetHoldingSpaceItemView(download_chips,
in_progress_download_id));
// Complete the in-progress download.
CompleteInProgressDownload(in_progress_download.get());
// Because the in-progress download has been completed, right clicking it
// should now surface the "Remove" command.
RightClick(download_chips.front());
ASSERT_TRUE(SelectMenuItemWithCommandId(HoldingSpaceCommandId::kRemoveItem));
}
// Base class for tests of the pause or resume commands, parameterized by the
// command to use. This will either be `kPauseItem` or `kResumeItem`.
class HoldingSpaceUiPauseOrResumeBrowserTest
: public HoldingSpaceUiInProgressDownloadsBrowserTest,
public testing::WithParamInterface<HoldingSpaceCommandId> {
public:
HoldingSpaceUiPauseOrResumeBrowserTest()
: HoldingSpaceUiInProgressDownloadsBrowserTest() {
const HoldingSpaceCommandId command_id(GetPauseOrResumeCommandId());
EXPECT_TRUE(command_id == HoldingSpaceCommandId::kPauseItem ||
command_id == HoldingSpaceCommandId::kResumeItem);
}
// Returns either `kPauseItem` or `kResumeItem` depending on parameterization.
HoldingSpaceCommandId GetPauseOrResumeCommandId() const { return GetParam(); }
};
INSTANTIATE_TEST_SUITE_P(
All,
HoldingSpaceUiPauseOrResumeBrowserTest,
testing::ValuesIn({HoldingSpaceCommandId::kPauseItem,
HoldingSpaceCommandId::kResumeItem}));
// Verifies that pausing or resuming holding space items works as intended.
IN_PROC_BROWSER_TEST_P(HoldingSpaceUiPauseOrResumeBrowserTest,
PauseOrResumeItem) {
// Use zero animation duration so that UI updates are immediate.
ui::ScopedAnimationDurationScaleMode scoped_animation_duration_scale_mode(
ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);
// Create an in-progress download which may or may not be paused depending
// on parameterization.
auto in_progress_download =
CreateInProgressDownload(/*paused=*/GetPauseOrResumeCommandId() ==
HoldingSpaceCommandId::kResumeItem);
// Create a completed download.
auto completed_download = CreateCompletedDownload();
// Show holding space UI.
test_api().Show();
ASSERT_TRUE(test_api().IsShowing());
// Expect two download chips, one for each created download item.
std::vector<views::View*> download_chips = test_api().GetDownloadChips();
ASSERT_EQ(download_chips.size(), 2u);
// Cache download chips. NOTE: Chips are displayed in reverse order of their
// underlying holding space item creation.
views::View* const completed_download_chip = download_chips.at(0);
views::View* const in_progress_download_chip = download_chips.at(1);
// Right click the `completed_download_chip`. Because the underlying download
// is completed, the context menu should *not* contain a "Pause" or "Resume"
// command.
RightClick(completed_download_chip);
ASSERT_FALSE(SelectMenuItemWithCommandId(GetPauseOrResumeCommandId()));
// Close the context menu and control-right click the
// `in_progress_download_chip`. Because the `completed_download_chip` is still
// selected and its underlying download is completed, the context menu should
// *not* contain a "Pause" or "Resume" command.
PressAndReleaseKey(ui::KeyboardCode::VKEY_ESCAPE);
RightClick(in_progress_download_chip, ui::EF_CONTROL_DOWN);
ASSERT_FALSE(SelectMenuItemWithCommandId(GetPauseOrResumeCommandId()));
// Close the context menu, press the `in_progress_download_chip` and then
// right click it. Because the `in_progress_download_chip` is the only chip
// selected and its underlying download is in-progress, the context menu
// should contain a "Pause" or "Resume" command.
PressAndReleaseKey(ui::KeyboardCode::VKEY_ESCAPE);
test::Click(in_progress_download_chip);
RightClick(in_progress_download_chip);
ASSERT_TRUE(SelectMenuItemWithCommandId(GetPauseOrResumeCommandId()));
// Bind an observer to watch for updates to the holding space model.
testing::NiceMock<MockHoldingSpaceModelObserver> mock;
base::ScopedObservation<HoldingSpaceModel, HoldingSpaceModelObserver>
observer{&mock};
observer.Observe(HoldingSpaceController::Get()->model());
// Press ENTER to execute the "Pause" or "Resume" command, expecting and
// waiting for the in-progress download item to be updated in the holding
// space model.
base::RunLoop run_loop;
EXPECT_CALL(mock, OnHoldingSpaceItemUpdated)
.WillOnce([&](const HoldingSpaceItem* item,
const HoldingSpaceItemUpdatedFields& updated_fields) {
EXPECT_EQ(item->id(),
test_api().GetHoldingSpaceItemId(in_progress_download_chip));
EXPECT_TRUE(updated_fields.previous_in_progress_commands);
run_loop.Quit();
});
PressAndReleaseKey(ui::KeyboardCode::VKEY_RETURN);
run_loop.Run();
// Verify that there are still two download chips.
download_chips = test_api().GetDownloadChips();
ASSERT_EQ(download_chips.size(), 2u);
// The two download chips present should still be the original chips for the
// completed download and the (now paused or resumed) in-progress download.
EXPECT_EQ(download_chips.at(0), completed_download_chip);
EXPECT_EQ(download_chips.at(1), in_progress_download_chip);
}
// Verifies that pausing or resuming holding space items via secondary action is
// working as intended.
IN_PROC_BROWSER_TEST_P(HoldingSpaceUiPauseOrResumeBrowserTest,
PauseOrResumeItemViaSecondaryAction) {
// Use zero animation duration so that UI updates are immediate.
ui::ScopedAnimationDurationScaleMode scoped_animation_duration_scale_mode(
ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);
// Create an in-progress download which may or may not be paused depending
// on parameterization.
auto in_progress_download =
CreateInProgressDownload(/*paused=*/GetPauseOrResumeCommandId() ==
HoldingSpaceCommandId::kResumeItem);
// Create a completed download.
auto completed_download = CreateCompletedDownload();
// Show holding space UI.
test_api().Show();
ASSERT_TRUE(test_api().IsShowing());
// Expect two download chips, one for each created download item.
std::vector<views::View*> download_chips = test_api().GetDownloadChips();
ASSERT_EQ(download_chips.size(), 2u);
// Cache download chips. NOTE: Chips are displayed in reverse order of their
// underlying holding space item creation.
views::View* const completed_download_chip = download_chips.at(0);
views::View* const in_progress_download_chip = download_chips.at(1);
// Hover over the `completed_download_chip`. Because the underlying download
// is completed, the chip should not contain a visible secondary action.
test::MoveMouseTo(completed_download_chip, /*count=*/10);
ASSERT_FALSE(completed_download_chip
->GetViewByID(kHoldingSpaceItemSecondaryActionContainerId)
->GetVisible());
// Hover over the `in_progress_download_chip`. Because the underlying download
// is in-progress, the chip should contain a visible secondary action for
// either "Pause" or "Resume", depending on test parameterization.
test::MoveMouseTo(in_progress_download_chip, /*count=*/10);
auto* secondary_action_container = in_progress_download_chip->GetViewByID(
kHoldingSpaceItemSecondaryActionContainerId);
auto* secondary_action_pause =
secondary_action_container->GetViewByID(kHoldingSpaceItemPauseButtonId);
auto* secondary_action_resume =
secondary_action_container->GetViewByID(kHoldingSpaceItemResumeButtonId);
ViewDrawnWaiter().Wait(secondary_action_container);
EXPECT_EQ(secondary_action_pause->GetVisible(),
GetPauseOrResumeCommandId() == HoldingSpaceCommandId::kPauseItem);
EXPECT_EQ(secondary_action_resume->GetVisible(),
GetPauseOrResumeCommandId() == HoldingSpaceCommandId::kResumeItem);
// Bind an observer to watch for updates to the holding space model.
testing::NiceMock<MockHoldingSpaceModelObserver> mock;
base::ScopedObservation<HoldingSpaceModel, HoldingSpaceModelObserver>
observer{&mock};
observer.Observe(HoldingSpaceController::Get()->model());
// Press the `secondary_action_container` to execute the "Pause" or "Resume"
// command, expecting and waiting for the in-progress download item to be
// updated in the holding space model.
base::RunLoop run_loop;
EXPECT_CALL(mock, OnHoldingSpaceItemUpdated)
.WillOnce([&](const HoldingSpaceItem* item,
const HoldingSpaceItemUpdatedFields& updated_fields) {
EXPECT_EQ(item->id(),
test_api().GetHoldingSpaceItemId(in_progress_download_chip));
EXPECT_TRUE(updated_fields.previous_in_progress_commands);
run_loop.Quit();
});
test::Click(secondary_action_container);
run_loop.Run();
// Verify that there are still two download chips.
download_chips = test_api().GetDownloadChips();
ASSERT_EQ(download_chips.size(), 2u);
// The two download chips present should still be the original chips for the
// completed download and the (now paused or resumed) in-progress download.
EXPECT_EQ(download_chips.at(0), completed_download_chip);
EXPECT_EQ(download_chips.at(1), in_progress_download_chip);
}
// Verifies that taking a screenshot adds a screenshot holding space item.
IN_PROC_BROWSER_TEST_F(HoldingSpaceUiBrowserTest, AddScreenshot) {
// Verify that no screenshots exist in holding space UI.
test_api().Show();
ASSERT_TRUE(test_api().IsShowing());
EXPECT_TRUE(test_api().GetScreenCaptureViews().empty());
test_api().Close();
ASSERT_FALSE(test_api().IsShowing());
// Take a screenshot using the keyboard. The screenshot will be taken using
// the `CaptureModeController`.
PressAndReleaseKey(ui::VKEY_MEDIA_LAUNCH_APP1,
ui::EF_ALT_DOWN | ui::EF_CONTROL_DOWN);
// Move the mouse over to the browser window. The reason for that is the
// capture mode implementation will not automatically capture the topmost
// window unless the mouse is hovered above it.
aura::Window* browser_window = browser()->window()->GetNativeWindow();
ui::test::EventGenerator event_generator(browser_window->GetRootWindow());
event_generator.MoveMouseTo(
browser_window->GetBoundsInScreen().CenterPoint());
PressAndReleaseKey(ui::VKEY_RETURN);
// Bind an observer to watch for updates to the holding space model.
testing::NiceMock<MockHoldingSpaceModelObserver> mock;
base::ScopedObservation<HoldingSpaceModel, HoldingSpaceModelObserver>
observer{&mock};
observer.Observe(HoldingSpaceController::Get()->model());
// Expect and wait for a screenshot item to be added to holding space.
base::RunLoop run_loop;
EXPECT_CALL(mock, OnHoldingSpaceItemsAdded)
.WillOnce([&](const std::vector<const HoldingSpaceItem*>& items) {
ASSERT_EQ(items.size(), 1u);
ASSERT_EQ(items[0]->type(), HoldingSpaceItem::Type::kScreenshot);
run_loop.Quit();
});
run_loop.Run();
// Verify that the screenshot appears in holding space UI.
test_api().Show();
ASSERT_TRUE(test_api().IsShowing());
EXPECT_EQ(1u, test_api().GetScreenCaptureViews().size());
}
// Base class for tests of holding space's integration with the capture mode
// screen recording feature, parameterized by the type of recording.
class HoldingSpaceScreenRecordingUiBrowserTest
: public HoldingSpaceUiBrowserTest,
public testing::WithParamInterface<RecordingType> {
public:
RecordingType recording_type() const { return GetParam(); }
};
INSTANTIATE_TEST_SUITE_P(All,
HoldingSpaceScreenRecordingUiBrowserTest,
testing::Values(RecordingType::kGif,
RecordingType::kWebM));
// Verifies that taking a screen recording adds a screen recording holding space
// item. The type of holding space item depends on the type of screen recording.
IN_PROC_BROWSER_TEST_P(HoldingSpaceScreenRecordingUiBrowserTest,
AddScreenRecording) {
// Verify that no screen recordings exist in holding space UI.
test_api().Show();
ASSERT_TRUE(test_api().IsShowing());
EXPECT_TRUE(test_api().GetScreenCaptureViews().empty());
test_api().Close();
ASSERT_FALSE(test_api().IsShowing());
ash::CaptureModeTestApi capture_mode_test_api;
capture_mode_test_api.SetRecordingType(recording_type());
capture_mode_test_api.StartForRegion(/*for_video=*/true);
capture_mode_test_api.SetUserSelectedRegion(gfx::Rect(200, 200));
capture_mode_test_api.PerformCapture();
// Record a 100 ms long video.
base::RunLoop video_recording_time;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, video_recording_time.QuitClosure(), base::Milliseconds(100));
video_recording_time.Run();
capture_mode_test_api.StopVideoRecording();
// Bind an observer to watch for updates to the holding space model.
testing::NiceMock<MockHoldingSpaceModelObserver> mock;
base::ScopedObservation<HoldingSpaceModel, HoldingSpaceModelObserver>
observer{&mock};
observer.Observe(HoldingSpaceController::Get()->model());
base::RunLoop wait_for_item;
// Expect and wait for a screen recording item to be added to holding space.
EXPECT_CALL(mock, OnHoldingSpaceItemsAdded)
.WillOnce([&](const std::vector<const HoldingSpaceItem*>& items) {
ASSERT_EQ(items.size(), 1u);
ASSERT_EQ(items[0]->type(),
recording_type() == RecordingType::kGif
? HoldingSpaceItem::Type::kScreenRecordingGif
: HoldingSpaceItem::Type::kScreenRecording);
wait_for_item.Quit();
});
wait_for_item.Run();
// The video recording and / or the GIF recording progress notifications can
// get in the way while tapping on the holding space tray button. Therefore,
// we must wait until the notification animation completes before attempting
// to tap on it.
// TODO(b/275558519): This should not be needed, since the notification should
// not overlap the shelf.
MessagePopupAnimationWaiter(ash::Shell::GetPrimaryRootWindowController()
->shelf()
->GetStatusAreaWidget()
->notification_center_tray()
->popup_collection())
.Wait();
// Verify that the screen recording appears in holding space UI.
test_api().Show();
ASSERT_TRUE(test_api().IsShowing());
EXPECT_EQ(1u, test_api().GetScreenCaptureViews().size());
}
// Used to check the holding space suggestion feature.
class HoldingSpaceSuggestionUiBrowserTest : public HoldingSpaceUiBrowserTest {
public:
HoldingSpaceSuggestionUiBrowserTest() {
scoped_feature_list_.InitAndEnableFeature(
features::kHoldingSpaceSuggestions);
}
// HoldingSpaceUiBrowserTest:
void SetUpOnMainThread() override {
HoldingSpaceUiBrowserTest::SetUpOnMainThread();
// Initialize `local_file_directory_`.
EXPECT_TRUE(local_file_directory_.CreateUniqueTempDirUnderPath(
browser()->profile()->GetPath()));
EXPECT_TRUE(browser()->profile()->GetMountPoints()->RegisterFileSystem(
/*mount_name=*/"archive", storage::kFileSystemTypeLocal,
storage::FileSystemMountOption(), GetLocalFileMountPath()));
}
// Creates multiple files and suggests them through service.
std::vector<base::FilePath> CreateFileSuggestions(size_t count) {
using FileOpenEvent =
file_manager::file_tasks::FileTasksObserver::FileOpenEvent;
using FileOpenType = file_manager::file_tasks::FileTasksObserver::OpenType;
base::ScopedAllowBlockingForTesting allow_blocking;
std::vector<base::FilePath> paths(count);
std::vector<FileOpenEvent> open_events;
for (auto& path : paths) {
EXPECT_TRUE(
base::CreateTemporaryFileInDir(GetLocalFileMountPath(), &path));
FileOpenEvent e;
e.path = path;
e.open_type = FileOpenType::kOpen;
open_events.push_back(std::move(e));
}
FileSuggestKeyedServiceFactory::GetInstance()
->GetService(GetProfile())
->local_file_suggestion_provider_for_test()
->OnFilesOpened(open_events);
return paths;
}
base::FilePath GetLocalFileMountPath() {
return local_file_directory_.GetPath();
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
// The directory that hosts local files.
base::ScopedTempDir local_file_directory_;
};
// Verifies suggestion removal through holding space item context menu.
IN_PROC_BROWSER_TEST_F(HoldingSpaceSuggestionUiBrowserTest, RemoveSuggestion) {
// Use zero animation duration so that UI updates are immediate.
ui::ScopedAnimationDurationScaleMode scoped_animation_duration_scale_mode(
ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);
// Create file suggestions and wait until the suggested files exist in the
// holding space model.
constexpr size_t kSuggestionFileCount = 3;
std::vector<base::FilePath> file_paths =
CreateFileSuggestions(kSuggestionFileCount);
HoldingSpaceModel* model = HoldingSpaceController::Get()->model();
WaitForSuggestionsInModel(
model,
/*expected_suggestions=*/
{{HoldingSpaceItem::Type::kLocalSuggestion, file_paths[0]},
{HoldingSpaceItem::Type::kLocalSuggestion, file_paths[1]},
{HoldingSpaceItem::Type::kLocalSuggestion, file_paths[2]}});
test_api().Show();
// The count of suggestion chips should be equal to that of suggested files.
std::vector<views::View*> suggestion_chips = test_api().GetSuggestionChips();
ASSERT_EQ(suggestion_chips.size(), kSuggestionFileCount);
// Select two suggestion chips and open context menu.
ASSERT_FALSE(views::MenuController::GetActiveInstance());
test::Click(suggestion_chips.front(), ui::EF_CONTROL_DOWN);
RightClick(suggestion_chips[1], ui::EF_CONTROL_DOWN);
ASSERT_TRUE(views::MenuController::GetActiveInstance());
// Remove the selected suggestion chips through context menu.
auto* menu_item =
SelectMenuItemWithCommandId(HoldingSpaceCommandId::kRemoveItem);
ASSERT_TRUE(menu_item);
test::Click(menu_item);
WaitForSuggestionsInModel(
model, /*expected_suggestions=*/{
{HoldingSpaceItem::Type::kLocalSuggestion, file_paths[0]}});
// Remove the remaining suggestion item view through context menu.
suggestion_chips = test_api().GetSuggestionChips();
ASSERT_EQ(suggestion_chips.size(), 1u);
ASSERT_FALSE(views::MenuController::GetActiveInstance());
RightClick(suggestion_chips.front());
ASSERT_TRUE(views::MenuController::GetActiveInstance());
menu_item = SelectMenuItemWithCommandId(HoldingSpaceCommandId::kRemoveItem);
ASSERT_TRUE(menu_item);
test::Click(menu_item);
// There should not be any suggestion item view left.
EXPECT_EQ(test_api().GetSuggestionChips().size(), 0u);
}
} // namespace ash