// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/app_list/views/app_list_bubble_apps_page.h"
#include <utility>
#include "ash/app_list/app_list_controller_impl.h"
#include "ash/app_list/app_list_metrics.h"
#include "ash/app_list/test/app_list_test_helper.h"
#include "ash/app_list/test/test_focus_change_listener.h"
#include "ash/app_list/views/app_list_a11y_announcer.h"
#include "ash/app_list/views/app_list_bubble_search_page.h"
#include "ash/app_list/views/app_list_bubble_view.h"
#include "ash/app_list/views/app_list_toast_container_view.h"
#include "ash/app_list/views/app_list_toast_view.h"
#include "ash/app_list/views/apps_grid_view_test_api.h"
#include "ash/app_list/views/continue_section_view.h"
#include "ash/app_list/views/recent_apps_view.h"
#include "ash/app_list/views/scrollable_apps_grid_view.h"
#include "ash/app_list/views/search_box_view.h"
#include "ash/public/cpp/app_list/app_list_controller.h"
#include "ash/shell.h"
#include "ash/style/icon_button.h"
#include "ash/test/ash_test_base.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/time/time.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animator.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"
#include "ui/compositor/test/layer_animation_stopped_waiter.h"
#include "ui/compositor/test/test_utils.h"
#include "ui/events/keycodes/keyboard_codes_posix.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/textfield/textfield.h"
namespace ash {
namespace {
class AppListBubbleAppsPageTest : public AshTestBase {
public:
AppListBubbleAppsPageTest() = default;
void OnReorderAnimationDone(base::OnceClosure closure,
bool expect_abort,
bool aborted,
AppListGridAnimationStatus status) {
EXPECT_EQ(aborted, expect_abort);
EXPECT_EQ(AppListGridAnimationStatus::kReorderFadeIn, status);
std::move(closure).Run();
}
// Sorts app list with the specified order. If `wait` is true, wait for the
// reorder animation to complete. The animation is expected to be aborted if
// `expect_abort` is set to true.
void SortAppList(const std::optional<AppListSortOrder>& order,
bool wait,
bool expect_abort = false) {
AppListController::Get()->UpdateAppListWithNewTemporarySortOrder(
order,
/*animate=*/true, /*update_position_closure=*/base::DoNothing());
if (!wait)
return;
base::RunLoop run_loop;
GetAppListTestHelper()
->GetBubbleAppsPage()
->scrollable_apps_grid_view()
->AddReorderCallbackForTest(base::BindRepeating(
&AppListBubbleAppsPageTest::OnReorderAnimationDone,
base::Unretained(this), run_loop.QuitClosure(), expect_abort));
run_loop.Run();
}
};
TEST_F(AppListBubbleAppsPageTest, SlideViewIntoPositionCleansUpLayers) {
// Open the app list without animation.
ASSERT_EQ(ui::ScopedAnimationDurationScaleMode::duration_multiplier(),
ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);
auto* helper = GetAppListTestHelper();
helper->AddRecentApps(5);
helper->AddAppItems(5);
helper->ShowAppList();
// Recent apps view starts without a layer.
auto* recent_apps = helper->GetBubbleRecentAppsView();
ASSERT_FALSE(recent_apps->layer());
// Trigger a slide animation.
ui::ScopedAnimationDurationScaleMode duration(
ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
constexpr int kVerticalOffset = 20;
constexpr base::TimeDelta kSlideDuration = base::Milliseconds(100);
constexpr gfx::Tween::Type kTweenType = gfx::Tween::LINEAR;
helper->StartSlideAnimationOnBubbleAppsPage(recent_apps, kVerticalOffset,
kSlideDuration, kTweenType);
ASSERT_TRUE(recent_apps->layer());
EXPECT_TRUE(recent_apps->layer()->GetAnimator()->is_animating());
// While that animation is running, run another animation.
helper->StartSlideAnimationOnBubbleAppsPage(recent_apps, kVerticalOffset,
kSlideDuration, kTweenType);
auto* compositor = recent_apps->layer()->GetCompositor();
while (recent_apps->layer() &&
recent_apps->layer()->GetAnimator()->is_animating()) {
EXPECT_TRUE(ui::WaitForNextFrameToBePresented(compositor));
}
// At the end of the animation, the recent apps layer is still destroyed,
// even though the layer existed at the start of the second animation.
EXPECT_FALSE(recent_apps->layer());
}
// Regression test for https://crbug.com/1295794
TEST_F(AppListBubbleAppsPageTest, AppsPageVisibleAfterQuicklyClearingSearch) {
// Open the app list without animation.
ASSERT_EQ(ui::ScopedAnimationDurationScaleMode::duration_multiplier(),
ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);
auto* helper = GetAppListTestHelper();
helper->AddAppItems(5);
helper->ShowAppList();
auto* apps_page = helper->GetBubbleAppsPage();
ASSERT_TRUE(apps_page->GetVisible());
// Enable animations.
ui::ScopedAnimationDurationScaleMode duration(
ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
// Type a key to trigger the animation to transition to the search page.
PressAndReleaseKey(ui::VKEY_A);
ASSERT_TRUE(
apps_page->GetPageAnimationLayerForTest()->GetAnimator()->is_animating());
// Before the animation completes, delete the search. This should abort
// animations, animate back to the apps page, and leave the apps page visible.
PressAndReleaseKey(ui::VKEY_BACK);
ui::LayerAnimationStoppedWaiter().Wait(
apps_page->GetPageAnimationLayerForTest());
EXPECT_TRUE(apps_page->GetVisible());
EXPECT_EQ(1.0f, apps_page->scroll_view()->contents()->layer()->opacity());
}
// Regression test for https://crbug.com/1349833
TEST_F(AppListBubbleAppsPageTest,
AppsPageVisibleAfterQuicklyHidingAndShowingLauncherFromSearchPage) {
// Open the app list without animation.
ASSERT_EQ(ui::ScopedAnimationDurationScaleMode::duration_multiplier(),
ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);
auto* helper = GetAppListTestHelper();
helper->AddAppItems(5);
helper->ShowAppList();
auto* apps_page = helper->GetBubbleAppsPage();
ASSERT_TRUE(apps_page->GetVisible());
// Type a key to trigger the animation to transition to the search page.
PressAndReleaseKey(ui::VKEY_A);
EXPECT_FALSE(apps_page->GetVisible());
// Enable animations.
ui::ScopedAnimationDurationScaleMode duration(
ui::ScopedAnimationDurationScaleMode::NORMAL_DURATION);
helper->GetBubbleView()->StartHideAnimation(/*is_side_shelf=*/false,
base::DoNothing());
helper->GetBubbleView()->StartShowAnimation(/*is_side_shelf=*/false);
apps_page->AbortAllAnimations();
ui::LayerAnimationStoppedWaiter().Wait(
apps_page->GetPageAnimationLayerForTest());
EXPECT_TRUE(apps_page->GetVisible());
EXPECT_EQ(1.0f, apps_page->scroll_view()->contents()->layer()->opacity());
EXPECT_EQ(gfx::Transform(),
apps_page->scroll_view()->contents()->layer()->transform());
}
TEST_F(AppListBubbleAppsPageTest, AnimateHidePage) {
// Open the app list without animation.
ASSERT_EQ(ui::ScopedAnimationDurationScaleMode::duration_multiplier(),
ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);
auto* helper = GetAppListTestHelper();
helper->AddAppItems(5);
helper->ShowAppList();
auto* apps_page = helper->GetBubbleAppsPage();
ASSERT_TRUE(apps_page->GetVisible());
// Enable animations.
ui::ScopedAnimationDurationScaleMode duration(
ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
base::HistogramTester histograms;
// Type a key to trigger the animation to transition to the search page.
PressAndReleaseKey(ui::VKEY_A);
ui::Layer* layer = apps_page->GetPageAnimationLayerForTest();
ui::LayerAnimationStoppedWaiter().Wait(layer);
// Ensure there is one more frame presented after animation finishes to allow
// animation throughput data to be passed from cc to ui.
layer->GetCompositor()->ScheduleFullRedraw();
EXPECT_TRUE(ui::WaitForNextFrameToBePresented(layer->GetCompositor()));
// Apps page is not visible.
EXPECT_FALSE(apps_page->GetVisible());
// Smoothness was recorded.
histograms.ExpectTotalCount(
"Apps.ClamshellLauncher.AnimationSmoothness.HideAppsPage", 1);
}
TEST_F(AppListBubbleAppsPageTest, AnimateShowPage) {
// Open the app list without animation.
ASSERT_EQ(ui::ScopedAnimationDurationScaleMode::duration_multiplier(),
ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);
auto* helper = GetAppListTestHelper();
helper->AddAppItems(5);
helper->ShowAppList();
// Type a key switch to the search page.
PressAndReleaseKey(ui::VKEY_A);
auto* apps_page = helper->GetBubbleAppsPage();
ASSERT_FALSE(apps_page->GetVisible());
// Enable animations. NON_ZERO_DURATION does not work here. The animation
// end callback is not called, for reasons I don't understand. It works fine
// in production, and in tests with NORMAL_DURATION.
ui::ScopedAnimationDurationScaleMode duration(
ui::ScopedAnimationDurationScaleMode::NORMAL_DURATION);
base::HistogramTester histograms;
// Press escape to trigger animation back to the apps page.
PressAndReleaseKey(ui::VKEY_ESCAPE);
ui::Layer* layer = apps_page->GetPageAnimationLayerForTest();
ui::LayerAnimationStoppedWaiter().Wait(layer);
// Ensure there is one more frame presented after animation finishes to allow
// animation throughput data to be passed from cc to ui.
layer->GetCompositor()->ScheduleFullRedraw();
EXPECT_TRUE(ui::WaitForNextFrameToBePresented(layer->GetCompositor()));
// Apps page is visible.
EXPECT_TRUE(apps_page->GetVisible());
// Smoothness was recorded.
histograms.ExpectTotalCount(
"Apps.ClamshellLauncher.AnimationSmoothness.ShowAppsPage", 1);
}
TEST_F(AppListBubbleAppsPageTest, ScrollPositionResetOnShow) {
// Show an app list with enough apps to allow scrolling.
auto* helper = GetAppListTestHelper();
helper->AddAppItems(50);
helper->ShowAppList();
// Press the up arrow, which will scroll the view to select an app in the
// last row.
PressAndReleaseKey(ui::VKEY_UP);
auto* apps_page = helper->GetBubbleAppsPage();
ASSERT_GT(apps_page->scroll_view()->vertical_scroll_bar()->GetPosition(), 0);
// Hide the launcher, then show it again.
helper->Dismiss();
helper->ShowAppList();
// Scroll position is reset to top.
EXPECT_EQ(apps_page->scroll_view()->vertical_scroll_bar()->GetPosition(), 0);
}
TEST_F(AppListBubbleAppsPageTest, ContinueSectionVisibleByDefault) {
// Show the app list with enough items to make the continue section and
// recent apps visible.
auto* helper = GetAppListTestHelper();
helper->AddContinueSuggestionResults(4);
helper->AddRecentApps(5);
helper->AddAppItems(5);
helper->ShowAppList();
// The continue section and recent apps are visible.
auto* apps_page = helper->GetBubbleAppsPage();
EXPECT_TRUE(helper->GetBubbleContinueSectionView()->GetVisible());
EXPECT_TRUE(helper->GetBubbleRecentAppsView()->GetVisible());
EXPECT_TRUE(apps_page->separator_for_test()->GetVisible());
}
TEST_F(AppListBubbleAppsPageTest, ContinueLabelHiddenWhenNoTasksAndNoRecents) {
// Show the app list with no continue suggestions and no recent apps.
auto* helper = GetAppListTestHelper();
helper->AddAppItems(5);
helper->ShowAppList();
auto* apps_page = helper->GetBubbleAppsPage();
ASSERT_TRUE(apps_page->continue_label_container_for_test());
EXPECT_FALSE(apps_page->continue_label_container_for_test()->GetVisible());
}
TEST_F(AppListBubbleAppsPageTest, CanHideContinueSectionByClickingButton) {
// Show the app list with enough items to make the continue section and
// recent apps visible.
auto* helper = GetAppListTestHelper();
helper->AddContinueSuggestionResults(4);
helper->AddRecentApps(5);
helper->AddAppItems(5);
helper->ShowAppList();
// The toggle continue section button has the "hide" tooltip.
auto* apps_page = helper->GetBubbleAppsPage();
IconButton* toggle_continue_section_button =
apps_page->toggle_continue_section_button();
ASSERT_TRUE(toggle_continue_section_button);
EXPECT_EQ(toggle_continue_section_button->GetTooltipText(),
u"Hide all suggestions");
// Hide the continue section.
LeftClickOn(toggle_continue_section_button);
// Continue section and recent apps are hidden.
EXPECT_FALSE(helper->GetBubbleContinueSectionView()->GetVisible());
EXPECT_FALSE(helper->GetBubbleRecentAppsView()->GetVisible());
// Label container and separator stay visible.
EXPECT_TRUE(apps_page->continue_label_container_for_test()->GetVisible());
EXPECT_TRUE(apps_page->separator_for_test()->GetVisible());
}
TEST_F(AppListBubbleAppsPageTest, CanHideContinueSectionByClickingHeader) {
// Show the app list with enough items to make the continue section and
// recent apps visible.
auto* helper = GetAppListTestHelper();
helper->AddContinueSuggestionResults(4);
helper->AddRecentApps(5);
helper->AddAppItems(5);
helper->ShowAppList();
// The toggle continue section button has the "hide" tooltip.
auto* apps_page = helper->GetBubbleAppsPage();
views::View* continue_label_container =
apps_page->continue_label_container_for_test();
ASSERT_TRUE(continue_label_container);
// Click on the container to hide the continue section.
LeftClickOn(continue_label_container);
// Continue section and recent apps are hidden.
EXPECT_FALSE(helper->GetBubbleContinueSectionView()->GetVisible());
EXPECT_FALSE(helper->GetBubbleRecentAppsView()->GetVisible());
// Tap on the container to show the continue section.
GestureTapOn(continue_label_container);
// Continue section and recent apps are shown.
EXPECT_TRUE(helper->GetBubbleContinueSectionView()->GetVisible());
EXPECT_TRUE(helper->GetBubbleRecentAppsView()->GetVisible());
}
TEST_F(AppListBubbleAppsPageTest, HideContinueSectionPlaysAnimation) {
// Open the app list without animation.
ASSERT_EQ(ui::ScopedAnimationDurationScaleMode::duration_multiplier(),
ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);
auto* helper = GetAppListTestHelper();
helper->AddRecentApps(5);
helper->AddAppItems(5);
helper->ShowAppList();
// Enable animations.
ui::ScopedAnimationDurationScaleMode duration(
ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
// Hide the continue section.
auto* apps_page = helper->GetBubbleAppsPage();
LeftClickOn(apps_page->toggle_continue_section_button());
// Separator and apps grid are animating.
auto* separator = apps_page->separator_for_test();
ASSERT_TRUE(separator->layer());
EXPECT_TRUE(separator->layer()->GetAnimator()->is_animating());
auto* apps_grid = apps_page->scrollable_apps_grid_view();
ASSERT_TRUE(apps_grid->layer());
EXPECT_TRUE(apps_grid->layer()->GetAnimator()->is_animating());
}
TEST_F(AppListBubbleAppsPageTest, CanShowContinueSectionByClickingButton) {
// Simulate a user with the continue section hidden on startup.
Shell::Get()->app_list_controller()->SetHideContinueSection(true);
// Show the app list with enough items to make the continue section and
// recent apps visible.
auto* helper = GetAppListTestHelper();
helper->AddContinueSuggestionResults(4);
helper->AddRecentApps(5);
helper->AddAppItems(5);
helper->ShowAppList();
// The toggle continue section button has the "show" tooltip.
auto* apps_page = helper->GetBubbleAppsPage();
IconButton* toggle_continue_section_button =
apps_page->toggle_continue_section_button();
ASSERT_TRUE(toggle_continue_section_button);
EXPECT_EQ(toggle_continue_section_button->GetTooltipText(),
u"Show all suggestions");
// Continue section and recent apps are hidden.
EXPECT_FALSE(helper->GetBubbleContinueSectionView()->GetVisible());
EXPECT_FALSE(helper->GetBubbleRecentAppsView()->GetVisible());
EXPECT_TRUE(apps_page->separator_for_test()->GetVisible());
// Click the show continue section button.
LeftClickOn(toggle_continue_section_button);
// The continue section and recent apps are visible.
EXPECT_TRUE(helper->GetBubbleContinueSectionView()->GetVisible());
EXPECT_TRUE(helper->GetBubbleRecentAppsView()->GetVisible());
EXPECT_TRUE(apps_page->separator_for_test()->GetVisible());
}
TEST_F(AppListBubbleAppsPageTest, ShowContinueSectionPlaysAnimation) {
// Simulate a user with the continue section hidden on startup.
Shell::Get()->app_list_controller()->SetHideContinueSection(true);
// Show the app list with enough items to make the continue section and
// recent apps visible.
auto* helper = GetAppListTestHelper();
helper->AddContinueSuggestionResults(4);
helper->AddRecentApps(5);
helper->AddAppItems(5);
helper->ShowAppList();
// Enable animations.
ui::ScopedAnimationDurationScaleMode duration(
ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
// Click the show continue section button.
auto* apps_page = helper->GetBubbleAppsPage();
LeftClickOn(apps_page->toggle_continue_section_button());
// Animations play for continue section, recent apps, separator and apps grid.
auto* continue_section = helper->GetBubbleContinueSectionView();
ASSERT_TRUE(continue_section->layer());
EXPECT_TRUE(continue_section->layer()->GetAnimator()->is_animating());
EXPECT_EQ(continue_section->layer()->opacity(), 0.0f);
EXPECT_EQ(continue_section->layer()->GetTargetOpacity(), 1.0f);
auto* recent_apps = helper->GetBubbleRecentAppsView();
ASSERT_TRUE(recent_apps->layer());
EXPECT_TRUE(recent_apps->layer()->GetAnimator()->is_animating());
EXPECT_EQ(recent_apps->layer()->opacity(), 0.0f);
EXPECT_EQ(recent_apps->layer()->GetTargetOpacity(), 1.0f);
auto* separator = apps_page->separator_for_test();
ASSERT_TRUE(separator->layer());
EXPECT_TRUE(separator->layer()->GetAnimator()->is_animating());
auto* apps_grid = apps_page->scrollable_apps_grid_view();
ASSERT_TRUE(apps_grid->layer());
EXPECT_TRUE(apps_grid->layer()->GetAnimator()->is_animating());
}
// Regression test for https://crbug.com/1329227
TEST_F(AppListBubbleAppsPageTest, HiddenContinueSectionDoesNotAnimateOnShow) {
// Simulate a user with the continue section hidden on startup.
Shell::Get()->app_list_controller()->SetHideContinueSection(true);
// Enable animations.
ui::ScopedAnimationDurationScaleMode duration(
ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
// Show the app list with enough items that the continue section and
// recent apps would normally be visible.
auto* helper = GetAppListTestHelper();
helper->AddContinueSuggestionResults(4);
helper->AddRecentApps(5);
helper->AddAppItems(5);
helper->ShowAppList();
// Continue section view is not visible and does not have a layer animation.
auto* continue_section = helper->GetBubbleContinueSectionView();
EXPECT_FALSE(continue_section->GetVisible());
EXPECT_FALSE(continue_section->layer());
// Recent apps view is not visible and does not have a layer animation.
auto* recent_apps = helper->GetBubbleRecentAppsView();
EXPECT_FALSE(recent_apps->GetVisible());
EXPECT_FALSE(recent_apps->layer());
}
TEST_F(AppListBubbleAppsPageTest, SortAppsMakesA11yAnnouncement) {
auto* helper = GetAppListTestHelper();
helper->AddAppItems(5);
helper->ShowAppList();
auto* apps_page = helper->GetBubbleAppsPage();
views::View* announcement_view = helper->GetAccessibilityAnnounceView();
ASSERT_TRUE(announcement_view);
// Add a callback to wait for an accessibility event.
ax::mojom::Event event = ax::mojom::Event::kNone;
base::RunLoop run_loop;
announcement_view->GetViewAccessibility().set_accessibility_events_callback(
base::BindLambdaForTesting([&](const ui::AXPlatformNodeDelegate* unused,
const ax::mojom::Event event_in) {
event = event_in;
run_loop.Quit();
}));
// Simulate sorting the apps. Because `run_loop` waits for the a11y event,
// it is unnecessary to wait for app list sort.
SortAppList(AppListSortOrder::kNameAlphabetical, /*wait=*/false);
run_loop.Run();
// An alert fired with a message.
EXPECT_EQ(event, ax::mojom::Event::kAlert);
ui::AXNodeData sort_data, button_data;
announcement_view->GetViewAccessibility().GetAccessibleNodeData(&sort_data);
EXPECT_EQ(sort_data.GetStringAttribute(ax::mojom::StringAttribute::kName),
"Apps are sorted by name");
views::LabelButton* undo_button =
apps_page->toast_container_for_test()->GetToastButton();
undo_button->GetViewAccessibility().GetAccessibleNodeData(&button_data);
EXPECT_EQ(button_data.GetStringAttribute(ax::mojom::StringAttribute::kName),
"Undo sort order by name");
// Test the announcement that is announced after reverting the sort.
base::RunLoop undo_run_loop;
announcement_view->GetViewAccessibility().set_accessibility_events_callback(
base::BindLambdaForTesting([&](const ui::AXPlatformNodeDelegate* unused,
const ax::mojom::Event event_in) {
event = event_in;
undo_run_loop.Quit();
}));
// Simulate the sort undo by setting the new order to nullopt.
SortAppList(std::nullopt, /*wait=*/false);
undo_run_loop.Run();
EXPECT_EQ(event, ax::mojom::Event::kAlert);
ui::AXNodeData undo_data;
announcement_view->GetViewAccessibility().GetAccessibleNodeData(&undo_data);
EXPECT_EQ(undo_data.GetStringAttribute(ax::mojom::StringAttribute::kName),
"Sort undo successful");
}
// Verify that after sorting app list with animation, the undo sort toast in app
// list should have focus. In addition, the focus should move to the search box
// after reverting the sort.
TEST_F(AppListBubbleAppsPageTest, SortAppsWithItemFocused) {
ui::ScopedAnimationDurationScaleMode scope_duration(
ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
auto* helper = GetAppListTestHelper();
helper->AddAppItems(5);
helper->ShowAppList();
views::View* first_item =
test::AppsGridViewTestApi(helper->GetScrollableAppsGridView())
.GetViewAtVisualIndex(/*page=*/0, /*slot=*/0);
first_item->RequestFocus();
TestFocusChangeListener listener(
helper->GetScrollableAppsGridView()->GetFocusManager());
SortAppList(AppListSortOrder::kNameAlphabetical, /*wait=*/true);
// Verify that the focus moves twice. It first goes to the search box during
// the animation and then the undo button on the undo toast after the end of
// animation.
EXPECT_EQ(2, listener.focus_change_count());
EXPECT_FALSE(first_item->HasFocus());
EXPECT_TRUE(helper->GetBubbleAppsPage()
->toast_container_for_test()
->GetToastButton()
->HasFocus());
// Simulate the sort undo by setting the new order to nullopt. The focus
// should be on the search box after undoing the sort.
SortAppList(std::nullopt, /*wait=*/true);
EXPECT_TRUE(helper->GetBubbleSearchBoxView()->search_box()->HasFocus());
}
class AppListBubbleAppsReorderTest
: public AppListBubbleAppsPageTest,
public testing::WithParamInterface</*is_separator_visible=*/bool> {
public:
AppListBubbleAppsReorderTest() = default;
AppListBubbleAppsReorderTest(AppListBubbleAppsReorderTest&) = delete;
AppListBubbleAppsReorderTest& operator=(const AppListBubbleAppsReorderTest&) =
delete;
~AppListBubbleAppsReorderTest() override = default;
};
INSTANTIATE_TEST_SUITE_P(All, AppListBubbleAppsReorderTest, testing::Bool());
// Verifies that after sorting the undo toast should fully show.
TEST_P(AppListBubbleAppsReorderTest, ScrollToShowUndoToastWhenSorting) {
ui::ScopedAnimationDurationScaleMode scope_duration(
ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
// Show an app list with enough apps to allow scrolling.
auto* helper = GetAppListTestHelper();
helper->AddAppItems(50);
const bool is_separator_visible = GetParam();
if (is_separator_visible)
GetAppListTestHelper()->AddRecentApps(5);
helper->ShowAppList();
auto* apps_page = helper->GetBubbleAppsPage();
AppListToastContainerView* reorder_undo_toast_container =
apps_page->toast_container_for_test();
// Verify the separator's visibility.
EXPECT_EQ(is_separator_visible,
apps_page->separator_for_test()->GetVisible());
// Before sorting, the undo toast should be invisible.
EXPECT_FALSE(reorder_undo_toast_container->IsToastVisible());
SortAppList(AppListSortOrder::kNameAlphabetical, /*wait=*/true);
// After sorting, the undo toast should be visible.
EXPECT_TRUE(reorder_undo_toast_container->IsToastVisible());
// Scroll the apps page to the end.
views::ScrollView* scroll_view = apps_page->scroll_view();
scroll_view->ScrollToPosition(scroll_view->vertical_scroll_bar(), INT_MAX);
// Verify that after scrolling the undo toast is not fully visible through
// the view port.
gfx::Point origin;
views::View::ConvertPointToTarget(reorder_undo_toast_container, scroll_view,
&origin);
gfx::Rect toast_container_bounds_in_scroll_view(
origin, reorder_undo_toast_container->size());
EXPECT_FALSE(scroll_view->GetVisibleRect().Contains(
toast_container_bounds_in_scroll_view));
SortAppList(AppListSortOrder::kColor, /*wait=*/true);
// Verify that after sorting again the undo toast is fully shown.
origin = gfx::Point();
views::View::ConvertPointToTarget(reorder_undo_toast_container,
scroll_view->contents(), &origin);
toast_container_bounds_in_scroll_view =
gfx::Rect(origin, reorder_undo_toast_container->size());
EXPECT_TRUE(reorder_undo_toast_container->IsToastVisible());
EXPECT_TRUE(scroll_view->GetVisibleRect().Contains(
toast_container_bounds_in_scroll_view));
}
// Test clicking on close button to dismiss the reorder toast.
TEST_F(AppListBubbleAppsPageTest, CloseReorderToast) {
ui::ScopedAnimationDurationScaleMode scope_duration(
ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
auto* helper = GetAppListTestHelper();
helper->AddAppItems(5);
helper->ShowAppList();
AppListToastContainerView* toast_container =
helper->GetBubbleAppsPage()->toast_container_for_test();
EXPECT_FALSE(toast_container->IsToastVisible());
// Sort app list and expect undo toast to be shown with close button.
SortAppList(AppListSortOrder::kNameAlphabetical, /*wait=*/true);
EXPECT_TRUE(toast_container->IsToastVisible());
EXPECT_TRUE(toast_container->GetCloseButton());
// Click on close button to dismiss the toast.
LeftClickOn(toast_container->GetCloseButton());
// Wait for the toast to finish fade out animation.
EXPECT_EQ(toast_container->toast_view()->layer()->GetTargetOpacity(), 0.0f);
ui::LayerAnimationStoppedWaiter().Wait(
toast_container->toast_view()->layer());
EXPECT_FALSE(toast_container->IsToastVisible());
}
// Verifies that sorting the app list with no app is properly handled.
TEST_F(AppListBubbleAppsPageTest, SortingAppListWithNoApp) {
ui::ScopedAnimationDurationScaleMode scope_duration(
ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
auto* helper = GetAppListTestHelper();
helper->ShowAppList();
// Sort app list that contains no app. The animation should be aborted in this
// case.
SortAppList(AppListSortOrder::kNameAlphabetical, /*wait=*/true,
/*expect_abort=*/true);
// Make sure the toast container shows up even if the animation is aborted.
auto* toast_container =
helper->GetBubbleAppsPage()->toast_container_for_test();
EXPECT_TRUE(toast_container->IsToastVisible());
}
// Verifies that a UserAction is recorded for scrolling to the bottom of the
// Apps Grid.
TEST_F(AppListBubbleAppsPageTest, ScrollToBottomLogsAction) {
ui::ScopedAnimationDurationScaleMode scope_duration(
ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
// Show an app list with enough apps to allow scrolling.
auto* helper = GetAppListTestHelper();
helper->AddAppItems(50);
helper->ShowAppList();
auto* apps_page = helper->GetBubbleAppsPage();
base::HistogramTester histograms;
// Scroll the apps page but do not hit the end.
views::ScrollView* scroll_view = apps_page->scroll_view();
scroll_view->ScrollToPosition(scroll_view->vertical_scroll_bar(), 10);
histograms.ExpectUniqueSample("Apps.AppList.UserAction.ClamshellMode",
AppListUserAction::kNavigatedToBottomOfAppList,
0);
// Scroll the apps page to the end.
scroll_view->ScrollToPosition(scroll_view->vertical_scroll_bar(), INT_MAX);
histograms.ExpectUniqueSample("Apps.AppList.UserAction.ClamshellMode",
AppListUserAction::kNavigatedToBottomOfAppList,
1);
// Scroll upwards and check that the bucket count keeps the same.
scroll_view->ScrollToPosition(scroll_view->vertical_scroll_bar(), 10);
histograms.ExpectUniqueSample("Apps.AppList.UserAction.ClamshellMode",
AppListUserAction::kNavigatedToBottomOfAppList,
1);
// Scroll the apps page to the end one more time.
scroll_view->ScrollToPosition(scroll_view->vertical_scroll_bar(), INT_MAX);
histograms.ExpectUniqueSample("Apps.AppList.UserAction.ClamshellMode",
AppListUserAction::kNavigatedToBottomOfAppList,
2);
}
// Verifies that a UserAction is recorded for keyboard navigating to the bottom
// of the Apps Grid.
TEST_F(AppListBubbleAppsPageTest, KeyboardSelectToBottomLogsAction) {
ui::ScopedAnimationDurationScaleMode scope_duration(
ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
// Show an app list with enough apps to allow scrolling.
auto* helper = GetAppListTestHelper();
helper->AddAppItems(50);
helper->ShowAppList();
base::HistogramTester histograms;
// Verify histogram initial state
histograms.ExpectUniqueSample("Apps.AppList.UserAction.ClamshellMode",
AppListUserAction::kNavigatedToBottomOfAppList,
0);
// Select the last app on the grid with the up arrow.
GetEventGenerator()->PressAndReleaseKey(ui::VKEY_UP);
histograms.ExpectUniqueSample("Apps.AppList.UserAction.ClamshellMode",
AppListUserAction::kNavigatedToBottomOfAppList,
1);
// Move down twice to return to the top of the grid.
GetEventGenerator()->PressAndReleaseKey(ui::VKEY_DOWN);
GetEventGenerator()->PressAndReleaseKey(ui::VKEY_DOWN);
histograms.ExpectUniqueSample("Apps.AppList.UserAction.ClamshellMode",
AppListUserAction::kNavigatedToBottomOfAppList,
1);
// Move to the bottom again and verify that the metric is recorded again.
GetEventGenerator()->PressAndReleaseKey(ui::VKEY_UP);
GetEventGenerator()->PressAndReleaseKey(ui::VKEY_UP);
histograms.ExpectUniqueSample("Apps.AppList.UserAction.ClamshellMode",
AppListUserAction::kNavigatedToBottomOfAppList,
2);
}
} // namespace
} // namespace ash