// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/glanceables/tasks/glanceables_tasks_view.h"
#include <memory>
#include "ash/api/tasks/fake_tasks_client.h"
#include "ash/constants/ash_features.h"
#include "ash/glanceables/common/glanceables_list_footer_view.h"
#include "ash/glanceables/common/glanceables_util.h"
#include "ash/glanceables/common/glanceables_view_id.h"
#include "ash/glanceables/common/test/glanceables_test_new_window_delegate.h"
#include "ash/glanceables/glanceables_controller.h"
#include "ash/glanceables/tasks/glanceables_task_view.h"
#include "ash/glanceables/tasks/test/glanceables_tasks_test_util.h"
#include "ash/shell.h"
#include "ash/style/combobox.h"
#include "ash/style/counter_expand_button.h"
#include "ash/style/icon_button.h"
#include "ash/test/ash_test_base.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/strcat.h"
#include "base/test/gtest_tags.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/metrics/user_action_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/types/cxx23_to_underlying.h"
#include "components/account_id/account_id.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/events/event_constants.h"
#include "ui/events/keycodes/keyboard_codes_posix.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/progress_bar.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/mouse_constants.h"
#include "ui/views/view.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget.h"
namespace ash {
class GlanceablesTasksViewTest : public AshTestBase {
public:
GlanceablesTasksViewTest()
: AshTestBase(std::make_unique<base::test::TaskEnvironment>(
base::test::TaskEnvironment::MainThreadType::UI,
base::test::TaskEnvironment::TimeSource::MOCK_TIME)) {
feature_list_.InitWithFeatures(
/*enabled_features=*/{features::kGlanceablesTimeManagementTasksView},
/*disabled_features=*/{});
}
void SetUp() override {
AshTestBase::SetUp();
SimulateUserLogin(account_id_);
fake_glanceables_tasks_client_ =
glanceables_tasks_test_util::InitializeFakeTasksClient(
base::Time::Now());
fake_glanceables_tasks_client_->set_http_error(
google_apis::ApiErrorCode::HTTP_SUCCESS);
Shell::Get()->glanceables_controller()->UpdateClientsRegistration(
account_id_, GlanceablesController::ClientsRegistration{
.tasks_client = fake_glanceables_tasks_client_.get()});
ASSERT_TRUE(Shell::Get()->glanceables_controller()->GetTasksClient());
widget_ = CreateFramelessTestWidget();
widget_->SetFullscreen(true);
view_ = widget_->SetContentsView(std::make_unique<GlanceablesTasksView>(
fake_glanceables_tasks_client_->task_lists()));
glanceables_util::SetIsNetworkConnectedForTest(true);
}
void TearDown() override {
// Destroy `widget_` first, before destroying `LayoutProvider` (needed in
// the `views::Combobox`'s destruction chain).
CloseWidget();
AshTestBase::TearDown();
}
// Populates `num` of tasks to the default task list.
void PopulateTasks(size_t num, std::string task_list_id = "TaskListID1") {
for (size_t i = 0; i < num; ++i) {
auto num_string = base::NumberToString(i);
fake_glanceables_tasks_client_->AddTask(
task_list_id, base::StrCat({"title_", num_string}),
base::DoNothing());
}
// Simulate closing the glanceables bubble to cache the tasks.
fake_glanceables_tasks_client_->OnGlanceablesBubbleClosed(
base::DoNothing());
// Recreate the tasks view to update the task views.
view_ = widget_->SetContentsView(std::make_unique<GlanceablesTasksView>(
fake_glanceables_tasks_client_->task_lists()));
}
void CloseWidget() {
view_ = nullptr;
widget_.reset();
}
Combobox* GetComboBoxView() const {
return views::AsViewClass<Combobox>(view_->GetViewByID(
base::to_underlying(GlanceablesViewId::kTimeManagementBubbleComboBox)));
}
const IconButton* GetHeaderIconView() const {
return views::AsViewClass<IconButton>(
view_->GetViewByID(base::to_underlying(
GlanceablesViewId::kTimeManagementBubbleHeaderIcon)));
}
const CounterExpandButton* GetCounterExpandButton() const {
return views::AsViewClass<CounterExpandButton>(
view_->GetViewByID(base::to_underlying(
GlanceablesViewId::kTimeManagementBubbleExpandButton)));
}
views::ScrollView* GetScrollView() const {
return views::AsViewClass<views::ScrollView>(view_->GetViewByID(
base::to_underlying(GlanceablesViewId::kContentsScrollView)));
}
const views::View* GetTaskItemsContainerView() const {
return views::AsViewClass<views::View>(
view_->GetViewByID(base::to_underlying(
GlanceablesViewId::kTimeManagementBubbleListContainer)));
}
const views::View* GetEditInBrowserButton() const {
return views::AsViewClass<views::View>(view_->GetViewByID(
base::to_underlying(GlanceablesViewId::kTaskItemEditInBrowserLabel)));
}
const views::LabelButton* GetAddNewTaskButton() const {
return views::AsViewClass<views::LabelButton>(view_->GetViewByID(
base::to_underlying(GlanceablesViewId::kTasksBubbleAddNewButton)));
}
const GlanceablesListFooterView* GetListFooterView() const {
return views::AsViewClass<GlanceablesListFooterView>(
view_->GetViewByID(base::to_underlying(
GlanceablesViewId::kTimeManagementBubbleListFooter)));
}
const views::ProgressBar* GetProgressBar() const {
return views::AsViewClass<views::ProgressBar>(view_->GetViewByID(
base::to_underlying(GlanceablesViewId::kProgressBar)));
}
const ErrorMessageToast* GetErrorMessage() const {
return views::AsViewClass<ErrorMessageToast>(
view_->GetViewByID(base::to_underlying(
GlanceablesViewId::kTimeManagementErrorMessageToast)));
}
api::FakeTasksClient* tasks_client() const {
return fake_glanceables_tasks_client_.get();
}
const GlanceablesTestNewWindowDelegate* new_window_delegate() const {
return &new_window_delegate_;
}
GlanceablesTasksView* view() const { return view_; }
void MenuSelectionAt(int index) {
GetComboBoxView()->SelectMenuItemForTest(index);
}
private:
base::test::ScopedFeatureList feature_list_;
AccountId account_id_ = AccountId::FromUserEmail("[email protected]");
std::unique_ptr<api::FakeTasksClient> fake_glanceables_tasks_client_;
raw_ptr<GlanceablesTasksView, DanglingUntriaged> view_;
std::unique_ptr<views::Widget> widget_;
const GlanceablesTestNewWindowDelegate new_window_delegate_;
};
TEST_F(GlanceablesTasksViewTest, Basics) {
// Check that `GlanceablesTasksView` by itself doesn't have a background.
EXPECT_FALSE(view()->GetBackground());
// Check that the expand button does not exist when `GlanceablesTasksView` is
// created alone.
auto* expand_button = view()->GetViewByID(base::to_underlying(
GlanceablesViewId::kTimeManagementBubbleExpandButton));
EXPECT_TRUE(expand_button);
EXPECT_FALSE(expand_button->GetVisible());
}
TEST_F(GlanceablesTasksViewTest, RecordShowTimeHistogramOnClose) {
base::HistogramTester histogram_tester;
histogram_tester.ExpectTotalCount(
"Ash.Glanceables.TimeManagement.Tasks.TotalShowTime", 0);
CloseWidget();
histogram_tester.ExpectTotalCount(
"Ash.Glanceables.TimeManagement.Tasks.TotalShowTime", 1);
}
TEST_F(GlanceablesTasksViewTest, ShowsProgressBarWhileLoadingTasks) {
tasks_client()->set_paused(true);
// Initially progress bar is hidden.
EXPECT_FALSE(GetProgressBar()->GetVisible());
// Switch to another task list, the progress bar should become visible.
MenuSelectionAt(2);
EXPECT_TRUE(GetProgressBar()->GetVisible());
// After replying to pending callbacks, the progress bar should become hidden.
EXPECT_EQ(tasks_client()->RunPendingGetTasksCallbacks(), 1u);
EXPECT_FALSE(GetProgressBar()->GetVisible());
}
TEST_F(GlanceablesTasksViewTest, ShowsProgressBarWhileAddingTask) {
base::HistogramTester histogram_tester;
tasks_client()->set_paused(true);
// Initially progress bar is hidden.
EXPECT_FALSE(GetProgressBar()->GetVisible());
GestureTapOn(GetAddNewTaskButton());
PressAndReleaseKey(ui::VKEY_N, ui::EF_SHIFT_DOWN);
PressAndReleaseKey(ui::VKEY_E);
PressAndReleaseKey(ui::VKEY_W);
// Progress bar becomes visible during saving.
PressAndReleaseKey(ui::VKEY_ESCAPE);
EXPECT_TRUE(GetProgressBar()->GetVisible());
// After replying to pending callbacks, the progress bar should become hidden.
EXPECT_EQ(tasks_client()->RunPendingAddTaskCallbacks(), 1u);
EXPECT_FALSE(GetProgressBar()->GetVisible());
histogram_tester.ExpectUniqueSample(
"Ash.Glanceables.TimeManagement.Tasks.UserAction", 3, 1);
}
TEST_F(GlanceablesTasksViewTest, ShowsProgressBarWhileEditingTask) {
base::HistogramTester histogram_tester;
tasks_client()->set_paused(true);
// Initially progress bar is hidden.
EXPECT_FALSE(GetProgressBar()->GetVisible());
const auto* const task_items_container_view = GetTaskItemsContainerView();
EXPECT_EQ(GetCounterExpandButton()->counter_for_test(), 2u);
EXPECT_EQ(task_items_container_view->children().size(), 2u);
const auto* const title_label = views::AsViewClass<views::Label>(
task_items_container_view->children()[0]->GetViewByID(
base::to_underlying(GlanceablesViewId::kTaskItemTitleLabel)));
GestureTapOn(title_label);
GetEventGenerator()->PressAndReleaseKey(ui::VKEY_SPACE);
GetEventGenerator()->PressAndReleaseKey(ui::VKEY_U);
GetEventGenerator()->PressAndReleaseKey(ui::VKEY_P);
GetEventGenerator()->PressAndReleaseKey(ui::VKEY_D);
// Progress bar becomes visible during saving.
PressAndReleaseKey(ui::VKEY_ESCAPE);
EXPECT_TRUE(GetProgressBar()->GetVisible());
// After replying to pending callbacks, the progress bar should become hidden.
EXPECT_EQ(tasks_client()->RunPendingUpdateTaskCallbacks(), 1u);
EXPECT_FALSE(GetProgressBar()->GetVisible());
histogram_tester.ExpectUniqueSample(
"Ash.Glanceables.TimeManagement.Tasks.UserAction", 4, 1);
}
TEST_F(GlanceablesTasksViewTest, ScrollViewResetPositionAfterSwitchingLists) {
PopulateTasks(20, "TaskListID1");
PopulateTasks(20, "TaskListID2");
auto* scroll_bar = GetScrollView()->vertical_scroll_bar();
EXPECT_EQ(scroll_bar->GetPosition(), scroll_bar->GetMinPosition());
ASSERT_TRUE(scroll_bar->GetVisible());
scroll_bar->ScrollByAmount(views::ScrollBar::ScrollAmount::kEnd);
EXPECT_GT(scroll_bar->GetPosition(), scroll_bar->GetMinPosition());
GetComboBoxView()->SelectMenuItemForTest(1);
EXPECT_EQ(scroll_bar->GetPosition(), scroll_bar->GetMinPosition());
}
TEST_F(GlanceablesTasksViewTest, OnlyShowsFooterIfAtLeast100Tasks) {
ASSERT_TRUE(GetListFooterView());
EXPECT_FALSE(GetListFooterView()->GetVisible());
const auto initial_tasks_count =
GetTaskItemsContainerView()->children().size();
// Add tasks to make the list contain 99 tasks.
PopulateTasks(99u - initial_tasks_count);
view()->GetWidget()->LayoutRootViewIfNecessary();
EXPECT_FALSE(GetListFooterView()->GetVisible());
// Creates the 100th task.
GestureTapOn(GetAddNewTaskButton());
PressAndReleaseKey(ui::VKEY_N, ui::EF_SHIFT_DOWN);
PressAndReleaseKey(ui::VKEY_E);
PressAndReleaseKey(ui::VKEY_W);
PressAndReleaseKey(ui::VKEY_ESCAPE);
view()->GetWidget()->LayoutRootViewIfNecessary();
EXPECT_TRUE(GetListFooterView()->GetVisible());
}
TEST_F(GlanceablesTasksViewTest, SupportsEditingRightAfterAdding) {
base::HistogramTester histogram_tester;
tasks_client()->set_paused(true);
// Add a task.
GestureTapOn(GetAddNewTaskButton());
PressAndReleaseKey(ui::VKEY_N, ui::EF_SHIFT_DOWN);
PressAndReleaseKey(ui::VKEY_E);
PressAndReleaseKey(ui::VKEY_W);
PressAndReleaseKey(ui::VKEY_ESCAPE);
base::RunLoop().RunUntilIdle();
// Verify executed callbacks number.
EXPECT_EQ(tasks_client()->RunPendingAddTaskCallbacks(), 1u);
EXPECT_EQ(tasks_client()->RunPendingUpdateTaskCallbacks(), 0u);
view()->GetWidget()->LayoutRootViewIfNecessary();
// Edit the same task.
const auto* const title_label = views::AsViewClass<views::Label>(
GetTaskItemsContainerView()->children()[0]->GetViewByID(
base::to_underlying(GlanceablesViewId::kTaskItemTitleLabel)));
GestureTapOn(title_label);
GetEventGenerator()->PressAndReleaseKey(ui::VKEY_SPACE);
GetEventGenerator()->PressAndReleaseKey(ui::VKEY_1);
PressAndReleaseKey(ui::VKEY_ESCAPE);
base::RunLoop().RunUntilIdle();
// Verify executed callbacks number.
EXPECT_EQ(tasks_client()->RunPendingAddTaskCallbacks(), 0u);
EXPECT_EQ(tasks_client()->RunPendingUpdateTaskCallbacks(), 1u);
histogram_tester.ExpectTotalCount(
"Ash.Glanceables.TimeManagement.Tasks.UserAction", 2);
histogram_tester.ExpectBucketCount(
"Ash.Glanceables.TimeManagement.Tasks.UserAction", 3, 1);
histogram_tester.ExpectBucketCount(
"Ash.Glanceables.TimeManagement.Tasks.UserAction", 4, 1);
}
TEST_F(GlanceablesTasksViewTest, TabbingOutOfNewTaskTextfieldAddsTask) {
base::HistogramTester histogram_tester;
tasks_client()->set_paused(true);
// Add a task.
GestureTapOn(GetAddNewTaskButton());
const auto* task_view = GetTaskItemsContainerView()->children()[0].get();
EXPECT_TRUE(task_view->GetViewByID(
base::to_underlying(GlanceablesViewId::kTaskItemTitleTextField)));
PressAndReleaseKey(ui::VKEY_N, ui::EF_SHIFT_DOWN);
PressAndReleaseKey(ui::VKEY_E);
PressAndReleaseKey(ui::VKEY_W);
PressAndReleaseKey(ui::VKEY_TAB);
base::RunLoop().RunUntilIdle();
// Verify that edit in browser button is visible and focused.
const auto* const edit_in_browser_button = GetEditInBrowserButton();
ASSERT_TRUE(edit_in_browser_button);
EXPECT_TRUE(edit_in_browser_button->GetVisible());
EXPECT_TRUE(edit_in_browser_button->HasFocus());
const auto* title_label =
views::AsViewClass<views::Label>(task_view->GetViewByID(
base::to_underlying(GlanceablesViewId::kTaskItemTitleLabel)));
EXPECT_FALSE(title_label);
// Verify executed callbacks number.
EXPECT_EQ(tasks_client()->RunPendingAddTaskCallbacks(), 1u);
EXPECT_EQ(tasks_client()->RunPendingUpdateTaskCallbacks(), 0u);
// Tab back to the Add task textfield, and update the text.
PressAndReleaseKey(ui::VKEY_TAB, ui::EF_SHIFT_DOWN);
base::RunLoop().RunUntilIdle();
const auto* title_text_field =
views::AsViewClass<views::Textfield>(task_view->GetViewByID(
base::to_underlying(GlanceablesViewId::kTaskItemTitleTextField)));
ASSERT_TRUE(title_text_field);
EXPECT_TRUE(title_text_field->HasFocus());
EXPECT_EQ(u"New", title_text_field->GetText());
PressAndReleaseKey(ui::VKEY_RIGHT);
PressAndReleaseKey(ui::VKEY_1);
// Focus edit in browser button.
PressAndReleaseKey(ui::VKEY_TAB);
EXPECT_EQ(tasks_client()->RunPendingAddTaskCallbacks(), 0u);
EXPECT_EQ(tasks_client()->RunPendingUpdateTaskCallbacks(), 1u);
// Focus the next task, which exits the task editing state.
PressAndReleaseKey(ui::VKEY_TAB);
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(GetEditInBrowserButton());
task_view = GetTaskItemsContainerView()->children()[0].get();
title_text_field =
views::AsViewClass<views::Textfield>(task_view->GetViewByID(
base::to_underlying(GlanceablesViewId::kTaskItemTitleTextField)));
EXPECT_FALSE(title_text_field);
title_label = views::AsViewClass<views::Label>(task_view->GetViewByID(
base::to_underlying(GlanceablesViewId::kTaskItemTitleLabel)));
ASSERT_TRUE(title_label);
EXPECT_TRUE(title_label->IsDrawn());
EXPECT_EQ(u"New1", title_label->GetText());
// Edit the same task.
view()->GetWidget()->LayoutRootViewIfNecessary();
GestureTapOn(title_label);
GetEventGenerator()->PressAndReleaseKey(ui::VKEY_SPACE);
GetEventGenerator()->PressAndReleaseKey(ui::VKEY_1);
PressAndReleaseKey(ui::VKEY_ESCAPE);
base::RunLoop().RunUntilIdle();
// Verify executed callbacks number.
EXPECT_EQ(tasks_client()->RunPendingAddTaskCallbacks(), 0u);
EXPECT_EQ(tasks_client()->RunPendingUpdateTaskCallbacks(), 1u);
title_label = views::AsViewClass<views::Label>(
GetTaskItemsContainerView()->children()[0]->GetViewByID(
base::to_underlying(GlanceablesViewId::kTaskItemTitleLabel)));
ASSERT_TRUE(title_label);
EXPECT_TRUE(title_label->IsDrawn());
EXPECT_EQ(u"New1 1", title_label->GetText());
histogram_tester.ExpectTotalCount(
"Ash.Glanceables.TimeManagement.Tasks.UserAction", 2);
histogram_tester.ExpectBucketCount(
"Ash.Glanceables.TimeManagement.Tasks.UserAction", 3, 1);
histogram_tester.ExpectBucketCount(
"Ash.Glanceables.TimeManagement.Tasks.UserAction", 4, 1);
}
TEST_F(GlanceablesTasksViewTest, AllowsPressingAddNewTaskButtonWhileAdding) {
const auto initial_tasks_count =
GetTaskItemsContainerView()->children().size();
// Pressing the "Add new task" button should add another "pending" view.
GestureTapOn(GetAddNewTaskButton());
EXPECT_EQ(GetTaskItemsContainerView()->children().size(),
initial_tasks_count + 1);
// Enter text without explicitly committing it.
PressAndReleaseKey(ui::VKEY_N, ui::EF_SHIFT_DOWN);
PressAndReleaseKey(ui::VKEY_E);
PressAndReleaseKey(ui::VKEY_W);
// Pressing the "Add new task" button again adds another "pending" view.
GestureTapOn(GetAddNewTaskButton());
EXPECT_EQ(GetTaskItemsContainerView()->children().size(),
initial_tasks_count + 2);
base::RunLoop().RunUntilIdle();
// But the previous task becomes automatically committed due to losing focus.
const auto* const previous_task_label = views::AsViewClass<views::Label>(
GetTaskItemsContainerView()->children()[1]->GetViewByID(
base::to_underlying(GlanceablesViewId::kTaskItemTitleLabel)));
ASSERT_TRUE(previous_task_label);
EXPECT_EQ(previous_task_label->GetText(), u"New");
}
TEST_F(GlanceablesTasksViewTest,
DoesNotSendRequestAfterEditingWithUnchangedTitle) {
tasks_client()->set_paused(true);
const auto* const title_label = views::AsViewClass<views::Label>(
GetTaskItemsContainerView()->children()[0]->GetViewByID(
base::to_underlying(GlanceablesViewId::kTaskItemTitleLabel)));
// Enter and exit editing mode, the task's title should stay the same.
GestureTapOn(title_label);
PressAndReleaseKey(ui::VKEY_ESCAPE);
// Verify executed callbacks number.
EXPECT_EQ(tasks_client()->RunPendingUpdateTaskCallbacks(), 0u);
}
TEST_F(GlanceablesTasksViewTest, DoesNotAllowEditingToBlankTitle) {
tasks_client()->set_paused(true);
const auto* const task_view =
GetTaskItemsContainerView()->children()[0].get();
{
const auto* const title_label =
views::AsViewClass<views::Label>(task_view->GetViewByID(
base::to_underlying(GlanceablesViewId::kTaskItemTitleLabel)));
EXPECT_FALSE(title_label->GetText().empty());
// Enter editing mode.
GestureTapOn(title_label);
}
{
const auto* const title_text_field =
views::AsViewClass<views::Textfield>(task_view->GetViewByID(
base::to_underlying(GlanceablesViewId::kTaskItemTitleTextField)));
EXPECT_FALSE(title_text_field->GetText().empty());
// Clear `title_text_field`.
PressAndReleaseKey(ui::VKEY_A, ui::EF_CONTROL_DOWN);
PressAndReleaseKey(ui::VKEY_DELETE);
EXPECT_TRUE(title_text_field->GetText().empty());
// Commit changes.
PressAndReleaseKey(ui::VKEY_ESCAPE);
base::RunLoop().RunUntilIdle();
}
{
const auto* const title_label =
views::AsViewClass<views::Label>(task_view->GetViewByID(
base::to_underlying(GlanceablesViewId::kTaskItemTitleLabel)));
// `title_label` is back with non-empty title.
EXPECT_FALSE(title_label->GetText().empty());
}
// Verify executed callbacks number.
EXPECT_EQ(tasks_client()->RunPendingUpdateTaskCallbacks(), 0u);
}
TEST_F(GlanceablesTasksViewTest, DoesNotAddTaskWithBlankTitle) {
tasks_client()->set_paused(true);
const auto initial_tasks_count =
GetTaskItemsContainerView()->children().size();
// Add a task with blank title.
GestureTapOn(GetAddNewTaskButton());
EXPECT_EQ(GetTaskItemsContainerView()->children().size(),
initial_tasks_count + 1);
PressAndReleaseKey(ui::VKEY_ESCAPE);
base::RunLoop().RunUntilIdle();
// Verify executed callbacks number.
EXPECT_EQ(GetCounterExpandButton()->counter_for_test(), initial_tasks_count);
EXPECT_EQ(GetTaskItemsContainerView()->children().size(),
initial_tasks_count);
EXPECT_EQ(tasks_client()->RunPendingAddTaskCallbacks(), 0u);
}
TEST_F(GlanceablesTasksViewTest, ComboboxExpandedCollapsedAccessibleState) {
auto* combobox = GetComboBoxView();
ui::AXNodeData node_data;
combobox->GetViewAccessibility().GetAccessibleNodeData(&node_data);
EXPECT_FALSE(node_data.HasState(ax::mojom::State::kExpanded));
EXPECT_TRUE(node_data.HasState(ax::mojom::State::kCollapsed));
// Check accessibility of combobox while it's open.
LeftClickOn(combobox);
node_data = ui::AXNodeData();
combobox->GetViewAccessibility().GetAccessibleNodeData(&node_data);
EXPECT_TRUE(node_data.HasState(ax::mojom::State::kExpanded));
EXPECT_FALSE(node_data.HasState(ax::mojom::State::kCollapsed));
// Check accessibility of combobox while it's closed.
GetEventGenerator()->PressAndReleaseKey(ui::VKEY_ESCAPE);
node_data = ui::AXNodeData();
combobox->GetViewAccessibility().GetAccessibleNodeData(&node_data);
EXPECT_FALSE(node_data.HasState(ax::mojom::State::kExpanded));
EXPECT_TRUE(node_data.HasState(ax::mojom::State::kCollapsed));
}
TEST_F(GlanceablesTasksViewTest, OpenBrowserWithEmptyNewTaskDoesntCrash) {
base::UserActionTester user_actions;
// Add a task with blank title.
GestureTapOn(GetAddNewTaskButton());
GestureTapOn(GetHeaderIconView());
EXPECT_EQ(1, user_actions.GetActionCount(
"Glanceables_Tasks_LaunchTasksApp_HeaderButton"));
// Simulate that the widget is hidden safely after opening a browser window.
view()->GetWidget()->Hide();
EXPECT_FALSE(view()->GetWidget()->GetNativeWindow()->IsVisible());
}
TEST_F(GlanceablesTasksViewTest, HandlesErrorAfterAdding) {
tasks_client()->set_paused(true);
tasks_client()->set_http_error(
google_apis::ApiErrorCode::HTTP_INTERNAL_SERVER_ERROR);
const auto* const task_items_container_view = GetTaskItemsContainerView();
ASSERT_TRUE(task_items_container_view);
EXPECT_EQ(task_items_container_view->children().size(), 2u);
EXPECT_FALSE(GetErrorMessage());
GestureTapOn(GetAddNewTaskButton());
PressAndReleaseKey(ui::VKEY_N, ui::EF_SHIFT_DOWN);
PressAndReleaseKey(ui::VKEY_E);
PressAndReleaseKey(ui::VKEY_W);
PressAndReleaseKey(ui::VKEY_ESCAPE);
base::RunLoop().RunUntilIdle();
EXPECT_EQ(task_items_container_view->children().size(), 3u);
EXPECT_FALSE(GetErrorMessage());
EXPECT_EQ(tasks_client()->RunPendingAddTaskCallbacks(), 1u);
EXPECT_EQ(task_items_container_view->children().size(), 2u);
EXPECT_TRUE(GetErrorMessage());
EXPECT_EQ(GetErrorMessage()->GetMessageForTest(), u"Couldn't edit task.");
EXPECT_EQ(GetErrorMessage()->GetButtonForTest()->GetText(), u"Dismiss");
}
TEST_F(GlanceablesTasksViewTest, HandlesErrorAfterEditing) {
tasks_client()->set_paused(true);
tasks_client()->set_http_error(
google_apis::ApiErrorCode::HTTP_INTERNAL_SERVER_ERROR);
const auto* const task_items_container_view = GetTaskItemsContainerView();
ASSERT_TRUE(task_items_container_view);
EXPECT_EQ(task_items_container_view->children().size(), 2u);
EXPECT_FALSE(GetErrorMessage());
const auto* title_label = views::AsViewClass<views::Label>(
task_items_container_view->children()[0]->GetViewByID(
base::to_underlying(GlanceablesViewId::kTaskItemTitleLabel)));
GestureTapOn(title_label);
GetEventGenerator()->PressAndReleaseKey(ui::VKEY_SPACE);
GetEventGenerator()->PressAndReleaseKey(ui::VKEY_U);
GetEventGenerator()->PressAndReleaseKey(ui::VKEY_P);
GetEventGenerator()->PressAndReleaseKey(ui::VKEY_D);
PressAndReleaseKey(ui::VKEY_ESCAPE);
base::RunLoop().RunUntilIdle();
EXPECT_EQ(task_items_container_view->children().size(), 2u);
EXPECT_FALSE(GetErrorMessage());
EXPECT_EQ(tasks_client()->RunPendingUpdateTaskCallbacks(), 1u);
EXPECT_EQ(task_items_container_view->children().size(), 2u);
EXPECT_TRUE(GetErrorMessage());
EXPECT_EQ(GetErrorMessage()->GetMessageForTest(), u"Couldn't edit task.");
EXPECT_EQ(GetErrorMessage()->GetButtonForTest()->GetText(), u"Dismiss");
// Revert the task title to the one before editing.
title_label = views::AsViewClass<views::Label>(
task_items_container_view->children()[0]->GetViewByID(
base::to_underlying(GlanceablesViewId::kTaskItemTitleLabel)));
EXPECT_EQ(title_label->GetText(), u"Task List 1 Item 1 Title");
}
TEST_F(GlanceablesTasksViewTest, HandlesErrorAfterChangingTaskList) {
const auto* const task_items_container_view = GetTaskItemsContainerView();
ASSERT_TRUE(task_items_container_view);
EXPECT_FALSE(GetErrorMessage());
// Disconnect the network for test.
glanceables_util::SetIsNetworkConnectedForTest(false);
// Switch to another task list. The error message should show up immediately
// and ask users to try again after connecting to the network.
MenuSelectionAt(2);
EXPECT_TRUE(GetErrorMessage());
EXPECT_EQ(GetErrorMessage()->GetMessageForTest(),
u"Couldn't load items. Try again when online.");
EXPECT_EQ(GetErrorMessage()->GetButtonForTest()->GetText(), u"Dismiss");
// The task list should be reset to the one before switch.
const std::optional<size_t> selected_index =
GetComboBoxView()->GetSelectedIndex();
ASSERT_TRUE(selected_index.has_value());
EXPECT_EQ(GetComboBoxView()->GetTextForRow(selected_index.value()),
u"Task List 1 Title");
}
TEST_F(GlanceablesTasksViewTest, TasksContainerIsInvisibleWhenNoTask) {
// Check that task list items from the first list are shown.
auto* combobox = GetComboBoxView();
EXPECT_EQ(combobox->GetTextForRow(combobox->GetSelectedIndex().value()),
u"Task List 1 Title");
// Click on the combo box to show the task lists.
LeftClickOn(combobox);
// Go to the list with no task in it.
auto* third_menu_item_label = combobox->MenuItemAtIndex(2);
ASSERT_TRUE(third_menu_item_label);
LeftClickOn(third_menu_item_label);
base::RunLoop().RunUntilIdle();
EXPECT_EQ(combobox->GetTextForRow(combobox->GetSelectedIndex().value()),
u"Task List 3 Title (empty)");
const auto* const task_items_container = GetTaskItemsContainerView();
EXPECT_EQ(GetCounterExpandButton()->counter_for_test(), 0u);
EXPECT_EQ(task_items_container->children().size(), 0u);
EXPECT_FALSE(task_items_container->GetVisible());
// Click on the "Add a task" button. The task container should be visible now.
auto* add_task_button = GetAddNewTaskButton();
ASSERT_TRUE(add_task_button);
LeftClickOn(add_task_button);
EXPECT_EQ(task_items_container->children().size(), 1u);
EXPECT_TRUE(task_items_container->GetVisible());
// Commit the empty new task, which removes the temporary task view. The task
// container is reset to invisible.
GetEventGenerator()->PressAndReleaseKey(ui::VKEY_ESCAPE);
base::RunLoop().RunUntilIdle();
EXPECT_EQ(task_items_container->children().size(), 0u);
EXPECT_FALSE(task_items_container->GetVisible());
}
TEST_F(GlanceablesTasksViewTest, ShowTasksWebUIFromHeaderView) {
base::UserActionTester user_actions;
const auto* const header_icon_button = GetHeaderIconView();
GestureTapOn(header_icon_button);
EXPECT_EQ(new_window_delegate()->GetLastOpenedUrl(),
"https://tasks.google.com/");
EXPECT_EQ(1, user_actions.GetActionCount(
"Glanceables_Tasks_LaunchTasksApp_HeaderButton"));
EXPECT_EQ(0, user_actions.GetActionCount(
"Glanceables_Tasks_ActiveTaskListChanged"));
}
TEST_F(GlanceablesTasksViewTest, ShowTasksWebUIFromEditInBrowserView) {
base::AddFeatureIdTagToTestResult(
"screenplay-75d32091-1825-49cb-843b-8bb9d998a47d");
base::HistogramTester histogram_tester;
base::UserActionTester user_actions;
const auto* const title_label = views::AsViewClass<views::Label>(
GetTaskItemsContainerView()->children()[0]->GetViewByID(
base::to_underlying(GlanceablesViewId::kTaskItemTitleLabel)));
// Tap the title label to enter the edit mode. The enter in browser button
// should be visible.
GestureTapOn(title_label);
view()->GetWidget()->LayoutRootViewIfNecessary();
const auto* const edit_in_browser_button = GetEditInBrowserButton();
ASSERT_TRUE(edit_in_browser_button);
EXPECT_TRUE(edit_in_browser_button->GetVisible());
// Verify that tapping on the button will record the action.
GestureTapOn(edit_in_browser_button);
EXPECT_EQ(new_window_delegate()->GetLastOpenedUrl(),
"https://tasks.google.com/task/TaskListItem1");
EXPECT_EQ(1, user_actions.GetActionCount(
"Glanceables_Tasks_LaunchTasksApp_EditInGoogleTasksButton"));
histogram_tester.ExpectTotalCount(
"Ash.Glanceables.TimeManagement.Tasks.UserAction", 2);
histogram_tester.ExpectBucketCount(
"Ash.Glanceables.TimeManagement.Tasks.UserAction", 4, 1);
histogram_tester.ExpectBucketCount(
"Ash.Glanceables.TimeManagement.Tasks.UserAction", 8, 1);
// Simulate that the widget is hidden safely after opening a browser window.
view()->GetWidget()->Hide();
EXPECT_FALSE(view()->GetWidget()->GetNativeWindow()->IsVisible());
}
TEST_F(GlanceablesTasksViewTest, ComboboxAccessibleActiveDescendantId) {
auto* combobox = GetComboBoxView();
ui::AXNodeData node_data;
base::test::TaskEnvironment* task_environment_ = task_environment();
// Combobox is closed initially.
ASSERT_FALSE(
node_data.HasIntAttribute(ax::mojom::IntAttribute::kActivedescendantId));
// Check accessibility of combobox when it is open.
LeftClickOn(combobox);
combobox->GetViewAccessibility().GetAccessibleNodeData(&node_data);
ASSERT_TRUE(combobox->MenuItemAtIndex(0));
ASSERT_TRUE(
node_data.HasIntAttribute(ax::mojom::IntAttribute::kActivedescendantId));
ASSERT_EQ(
node_data.GetIntAttribute(ax::mojom::IntAttribute::kActivedescendantId),
combobox->MenuItemAtIndex(0)->GetViewAccessibility().GetUniqueId());
// Select second item in combobox menu items.
MenuSelectionAt(1);
// Advance time so that subsequent mouse click is considered valid.
task_environment_->AdvanceClock(views::kMinimumTimeBetweenButtonClicks +
base::Milliseconds(10));
LeftClickOn(combobox); // Open combobox.
node_data = ui::AXNodeData();
combobox->GetViewAccessibility().GetAccessibleNodeData(&node_data);
ASSERT_TRUE(combobox->MenuItemAtIndex(1));
ASSERT_TRUE(
node_data.HasIntAttribute(ax::mojom::IntAttribute::kActivedescendantId));
ASSERT_EQ(
node_data.GetIntAttribute(ax::mojom::IntAttribute::kActivedescendantId),
combobox->MenuItemAtIndex(1)->GetViewAccessibility().GetUniqueId());
// Check accessibility of combobox when it is closed.
GetEventGenerator()->PressAndReleaseKey(ui::VKEY_ESCAPE);
node_data = ui::AXNodeData();
combobox->GetViewAccessibility().GetAccessibleNodeData(&node_data);
ASSERT_FALSE(
node_data.HasIntAttribute(ax::mojom::IntAttribute::kActivedescendantId));
}
TEST_F(GlanceablesTasksViewTest, ComboboxAccessibleValue) {
auto* combobox = GetComboBoxView();
// default selection is first item in combobox
ui::AXNodeData node_data;
combobox->GetViewAccessibility().GetAccessibleNodeData(&node_data);
EXPECT_EQ("Task List 1 Title",
node_data.GetStringAttribute(ax::mojom::StringAttribute::kValue));
// Select second item in combobox menu items.
MenuSelectionAt(1);
node_data = ui::AXNodeData();
combobox->GetViewAccessibility().GetAccessibleNodeData(&node_data);
EXPECT_EQ("Task List 2 Title",
node_data.GetStringAttribute(ax::mojom::StringAttribute::kValue));
// Select third item in combobox menu items.
MenuSelectionAt(2);
node_data = ui::AXNodeData();
combobox->GetViewAccessibility().GetAccessibleNodeData(&node_data);
EXPECT_EQ("Task List 3 Title (empty)",
node_data.GetStringAttribute(ax::mojom::StringAttribute::kValue));
}
} // namespace ash