0

[gtm] Update error states

This cl adds additional error states to cover the cases where the
cached list is shown but the new tasks can not be loaded.

Other changes:
1. Reset the task list if switching between task list through combobox
failed because the tasks fetching failed.
2. Reset the task title if committing the change failed after editing
the task title.

Bug: b/323959143
Change-Id: I5d43c6df519596086da95c05be5890a911954e2d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5367150
Reviewed-by: Toni Barzic <tbarzic@chromium.org>
Commit-Queue: Wen-Chien Wang <wcwang@chromium.org>
Reviewed-by: Artsiom Mitrokhin <amitrokhin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1272910}
This commit is contained in:
Wen-Chien Wang
2024-03-14 18:01:22 +00:00
committed by Chromium LUCI CQ
parent 80e84fc7bd
commit dd8a883dff
21 changed files with 314 additions and 86 deletions

@ -698,7 +698,8 @@ component("ash") {
"glanceables/common/glanceables_list_footer_view.h",
"glanceables/common/glanceables_progress_bar_view.cc",
"glanceables/common/glanceables_progress_bar_view.h",
"glanceables/common/glanceables_tasks_error_type.h",
"glanceables/common/glanceables_util.cc",
"glanceables/common/glanceables_util.h",
"glanceables/common/glanceables_view_id.h",
"glanceables/glanceables_controller.cc",
"glanceables/glanceables_controller.h",
@ -710,6 +711,7 @@ component("ash") {
"glanceables/tasks/glanceables_task_view.h",
"glanceables/tasks/glanceables_task_view_v2.cc",
"glanceables/tasks/glanceables_task_view_v2.h",
"glanceables/tasks/glanceables_tasks_error_type.h",
"glanceables/tasks/glanceables_tasks_view.cc",
"glanceables/tasks/glanceables_tasks_view.h",
"host/ash_window_tree_host.cc",

@ -39,8 +39,7 @@ size_t RunPendingCallbacks(std::list<base::OnceClosure>& pending_callbacks) {
FakeTasksClient::FakeTasksClient()
: task_lists_(std::make_unique<ui::ListModel<TaskList>>()),
cached_task_lists_(std::make_unique<ui::ListModel<TaskList>>()),
cached_tasks_(std::make_unique<ui::ListModel<Task>>()) {}
cached_task_lists_(std::make_unique<ui::ListModel<TaskList>>()) {}
FakeTasksClient::~FakeTasksClient() = default;
@ -60,12 +59,12 @@ void FakeTasksClient::GetTaskLists(bool force_fetch,
if (paused_ || (paused_on_fetch_ && need_fetch)) {
pending_get_task_lists_callbacks_.push_back(base::BindOnce(
[](ui::ListModel<TaskList>* task_lists, GetTaskListsCallback callback) {
std::move(callback).Run(/*success=*/true, task_lists);
},
task_lists_returned, std::move(callback)));
[](ui::ListModel<TaskList>* task_lists, GetTaskListsCallback callback,
bool success) { std::move(callback).Run(success, task_lists); },
task_lists_returned, std::move(callback), !get_task_lists_error_));
} else {
std::move(callback).Run(/*success=*/true, task_lists_returned);
std::move(callback).Run(/*success=*/!get_task_lists_error_,
task_lists_returned);
}
}
@ -92,12 +91,11 @@ void FakeTasksClient::GetTasks(const std::string& task_list_id,
if (paused_ || (paused_on_fetch_ && need_fetch)) {
pending_get_tasks_callbacks_.push_back(base::BindOnce(
[](ui::ListModel<Task>* tasks, GetTasksCallback callback) {
std::move(callback).Run(/*success=*/true, tasks);
},
tasks_returned, std::move(callback)));
[](ui::ListModel<Task>* tasks, GetTasksCallback callback,
bool success) { std::move(callback).Run(success, tasks); },
tasks_returned, std::move(callback), !get_tasks_error_));
} else {
std::move(callback).Run(/*success=*/true, tasks_returned);
std::move(callback).Run(/*success=*/!get_tasks_error_, tasks_returned);
}
}
@ -218,7 +216,7 @@ size_t FakeTasksClient::RunPendingUpdateTaskCallbacks() {
void FakeTasksClient::AddTaskImpl(const std::string& task_list_id,
const std::string& title,
TasksClient::OnTaskSavedCallback callback) {
if (run_with_errors_) {
if (update_errors_) {
std::move(callback).Run(/*task=*/nullptr);
return;
}
@ -247,7 +245,7 @@ void FakeTasksClient::UpdateTaskImpl(
const std::string& title,
bool completed,
TasksClient::OnTaskSavedCallback callback) {
if (run_with_errors_) {
if (update_errors_) {
std::move(callback).Run(/*task=*/nullptr);
return;
}
@ -276,9 +274,16 @@ void FakeTasksClient::CacheTaskLists() {
void FakeTasksClient::CacheTasks() {
auto iter = tasks_in_task_lists_.find(cached_task_list_id_);
CHECK(iter != tasks_in_task_lists_.end());
if (iter == tasks_in_task_lists_.end()) {
return;
}
if (!cached_tasks_) {
cached_tasks_ = std::make_unique<ui::ListModel<Task>>();
} else {
cached_tasks_->DeleteAll();
}
cached_tasks_->DeleteAll();
for (const auto& task : *iter->second) {
cached_tasks_->Add(std::make_unique<Task>(
task->id, task->title, task->due, task->completed, task->has_subtasks,

@ -87,9 +87,9 @@ class ASH_EXPORT FakeTasksClient : public TasksClient {
void set_paused(bool paused) { paused_ = paused; }
void set_paused_on_fetch(bool paused) { paused_on_fetch_ = paused; }
void set_run_with_errors(bool run_with_errors) {
run_with_errors_ = run_with_errors;
}
void set_update_errors(bool update_errors) { update_errors_ = update_errors; }
void set_get_task_lists_error(bool error) { get_task_lists_error_ = error; }
void set_get_tasks_error(bool error) { get_tasks_error_ = error; }
ui::ListModel<TaskList>* task_lists() { return task_lists_.get(); }
@ -136,8 +136,16 @@ class ASH_EXPORT FakeTasksClient : public TasksClient {
int completed_tasks_ = 0;
// If `false` - callbacks are executed normally; if `true` - executed with
// simulated error (currently works for `AddTask` and `UpdateTask` only).
bool run_with_errors_ = false;
// simulated error. This only works for `AddTask` and `UpdateTask` functions.
bool update_errors_ = false;
// If `true`, GetTaskListsCallback run with failure after data fetching in
// `GetTaskLists()` is done. This should be set before `GetTaskLists()` is
// called.
bool get_task_lists_error_ = false;
// If `true`, GetTasksCallback run with failure after data fetching in
// `GetTasks()` is done. This should be set before `GetTasks()` is called.
bool get_tasks_error_ = false;
// The last time when the tasks were updated. This is manually set by
// `SetTasksLastUpdateTime`.

@ -7378,12 +7378,24 @@ New install
<message name="IDS_GLANCEABLES_TASKS_ERROR_LAST_UPDATE_TIME" desc="The error message used in the task glanceable to notify users that the task list is not updated since the shown time today.">
Tasks last updated: <ph name="TIME">$1<ex>3:17 PM</ex></ph>.
</message>
<message name="IDS_GLANCEABLES_TASKS_ERROR_LOAD_ITEMS_FAILED" desc="The error message used in the glanceables to notify users that the glanceables failed to load the tasks or classroom data.">
Couldn't load items.
</message>
<message name="IDS_GLANCEABLES_TASKS_ERROR_LOAD_ITEMS_FAILED_WHILE_OFFLINE" desc="The error message used in the glanceables to notify users that the glanceables failed to load the tasks or classroom data because the device is offline. Also guide the user to try again when the device is connected to the network.">
Couldn't load items. Try again when online.
</message>
<message name="IDS_GLANCEABLES_TASKS_ERROR_MARK_COMPLETE_FAILED" desc="The error message used in the task glanceable to notify users that clicking on the check button to mark a task as complete failed.">
Couldn't mark as complete.
</message>
<message name="IDS_GLANCEABLES_TASKS_ERROR_MARK_COMPLETE_FAILED_WHILE_OFFLINE" desc="The error message used in the task glanceable to notify users that clicking on the check button to mark a task as complete failed because the device is offline. Also guide the user to try again when the device is connected to the network.">
Couldn't mark as complete. Try again when online.
</message>
<message name="IDS_GLANCEABLES_TASKS_ERROR_EDIT_TASK_FAILED" desc="The error message used in the task glanceable to notify users that the glanceables failed to commit the task that they edited.">
Couldn't edit task.
</message>
<message name="IDS_GLANCEABLES_TASKS_ERROR_EDIT_TASK_FAILED_WHILE_OFFLINE" desc="The error message used in the task glanceable to notify users that the glanceables failed to commit the task that they edited because the device is offline. Also guide the user to try again when the device is connected to the network.">
Couldn't edit task. Try again when online.
</message>
<!-- Do Not Disturb notification -->
<message name="IDS_ASH_DO_NOT_DISTURB_NOTIFICATION_TITLE" desc="Label used for the notification that shows up when the 'Do Not Disturb' feature is enabled.">
Do Not Disturb is on

@ -0,0 +1 @@
89bcfab26c75f0f26f2dcba6cc642d8cb0a40c29

@ -0,0 +1 @@
a8be99d40764a85b4d5a831be479a86693bbfa77

@ -0,0 +1 @@
641c7d25fffc2671fc0cbd6d752f06e40e5b1406

@ -0,0 +1 @@
d8cb8127c93f6fff1846577488ba3dd7f32a2d2c

@ -104,6 +104,10 @@ void GlanceablesErrorMessageView::UpdateBoundsToContainer(
SetBoundsRect(preferred_bounds);
}
std::u16string GlanceablesErrorMessageView::GetMessageForTest() const {
return error_message_label_->GetText();
}
BEGIN_METADATA(GlanceablesErrorMessageView)
END_METADATA

@ -37,6 +37,8 @@ class ASH_EXPORT GlanceablesErrorMessageView : public views::FlexLayoutView {
// `container_bounds`.
void UpdateBoundsToContainer(const gfx::Rect& container_bounds);
std::u16string GetMessageForTest() const;
private:
raw_ptr<views::Label> error_message_label_ = nullptr;
raw_ptr<views::LabelButton> dismiss_button_ = nullptr;

@ -0,0 +1,33 @@
// Copyright 2024 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/common/glanceables_util.h"
#include "chromeos/ash/components/network/network_handler.h"
#include "chromeos/ash/components/network/network_state_handler.h"
namespace ash::glanceables_util {
namespace {
// A global flag for tests to manually set the connection of the network.
std::optional<bool> g_is_network_connected_for_test = std::nullopt;
} // namespace
bool IsNetworkConnected() {
if (g_is_network_connected_for_test.has_value()) {
return g_is_network_connected_for_test.value();
}
const auto* const network =
NetworkHandler::Get()->network_state_handler()->DefaultNetwork();
return network && network->IsConnectedState();
}
void SetIsNetworkConnectedForTest(bool connected) {
g_is_network_connected_for_test = connected;
}
} // namespace ash::glanceables_util

@ -0,0 +1,21 @@
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef ASH_GLANCEABLES_COMMON_GLANCEABLES_UTIL_H_
#define ASH_GLANCEABLES_COMMON_GLANCEABLES_UTIL_H_
#include "ash/ash_export.h"
namespace ash::glanceables_util {
// Checks if the default network is connected or not.
bool IsNetworkConnected();
// Manually sets the network connected state used `IsNetworkConnected()` in
// for testing.
ASH_EXPORT void SetIsNetworkConnectedForTest(bool connected);
} // namespace ash::glanceables_util
#endif // ASH_GLANCEABLES_COMMON_GLANCEABLES_UTIL_H_

@ -10,6 +10,7 @@
#include "ash/api/tasks/tasks_types.h"
#include "ash/constants/ash_features.h"
#include "ash/glanceables/common/glanceables_util.h"
#include "ash/glanceables/common/glanceables_view_id.h"
#include "ash/glanceables/glanceables_metrics.h"
#include "ash/resources/vector_icons/vector_icons.h"
@ -18,8 +19,6 @@
#include "ash/style/system_textfield.h"
#include "ash/style/system_textfield_controller.h"
#include "ash/style/typography.h"
#include "ash/system/model/system_tray_model.h"
#include "ash/system/network/tray_network_state_model.h"
#include "ash/system/time/calendar_utils.h"
#include "ash/system/time/date_helper.h"
#include "base/functional/bind.h"
@ -28,9 +27,6 @@
#include "base/strings/utf_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/types/cxx23_to_underlying.h"
#include "chromeos/ash/components/network/network_handler.h"
#include "chromeos/ash/components/network/network_state_handler.h"
#include "chromeos/services/network_config/public/cpp/cros_network_config_util.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_header_macros.h"
@ -58,9 +54,6 @@
namespace ash {
namespace {
// A global flag for tests to manually set the connection of the network.
std::optional<bool> g_is_network_connected_for_test = std::nullopt;
constexpr char kFormatterPattern[] = "EEE, MMM d"; // "Wed, Feb 28"
// Margins between icons and labels in `tasks_details_view_`.
@ -122,17 +115,6 @@ std::unique_ptr<views::ImageView> CreateSecondRowIcon(
return icon_view;
}
bool IsNetworkConnected() {
if (g_is_network_connected_for_test.has_value()) {
return g_is_network_connected_for_test.value();
}
const auto* const network =
NetworkHandler::Get()->network_state_handler()->DefaultNetwork();
return network && network->IsConnectedState();
}
class TaskViewTextField : public SystemTextfield,
public SystemTextfieldController {
METADATA_HEADER(TaskViewTextField, SystemTextfield)
@ -472,6 +454,7 @@ void GlanceablesTaskViewV2::UpdateTaskTitleViewForState(
/*completed=*/check_button_->checked());
break;
case TaskTitleViewState::kEdit:
task_title_before_edit_ = task_title_;
task_title_textfield_ =
tasks_title_view_->AddChildView(std::make_unique<TaskViewTextField>(
task_title_,
@ -490,11 +473,6 @@ void GlanceablesTaskViewV2::UpdateTaskTitleViewForState(
UpdateContentsMargins(state);
}
// static
void GlanceablesTaskViewV2::SetIsNetworkConnectedForTest(bool connected) {
g_is_network_connected_for_test = connected;
}
void GlanceablesTaskViewV2::UpdateContentsMargins(TaskTitleViewState state) {
switch (state) {
case TaskTitleViewState::kNotInitialized:
@ -518,7 +496,7 @@ void GlanceablesTaskViewV2::UpdateContentsMargins(TaskTitleViewState state) {
}
void GlanceablesTaskViewV2::CheckButtonPressed() {
if (!IsNetworkConnected()) {
if (!glanceables_util::IsNetworkConnected()) {
show_error_message_callback_.Run(
GlanceablesTasksErrorType::kCantMarkCompleteNoNetwork);
return;
@ -539,7 +517,7 @@ void GlanceablesTaskViewV2::CheckButtonPressed() {
}
void GlanceablesTaskViewV2::TaskTitleButtonPressed() {
if (!IsNetworkConnected()) {
if (!glanceables_util::IsNetworkConnected()) {
show_error_message_callback_.Run(
GlanceablesTasksErrorType::kCantUpdateTitleNoNetwork);
return;
@ -550,7 +528,6 @@ void GlanceablesTaskViewV2::TaskTitleButtonPressed() {
}
void GlanceablesTaskViewV2::OnFinishedEditing(const std::u16string& title) {
const auto old_title = task_title_;
if (!title.empty()) {
task_title_ = title;
}
@ -559,7 +536,7 @@ void GlanceablesTaskViewV2::OnFinishedEditing(const std::u16string& title) {
GetFocusManager()->ClearFocus();
}
if (task_id_.empty() || task_title_ != old_title) {
if (task_id_.empty() || task_title_ != task_title_before_edit_) {
saving_task_changes_ = true;
if (task_title_button_) {
task_title_button_->SetEnabled(false);
@ -598,7 +575,12 @@ void GlanceablesTaskViewV2::OnSaved(const api::Task* task) {
}
if (task) {
task_id_ = task->id;
} else {
// If `task` is a nullptr, the change failed to commit.
task_title_ = task_title_before_edit_;
task_title_button_->SetText(task_title_);
}
task_title_before_edit_ = u"";
}
BEGIN_METADATA(GlanceablesTaskViewV2)

@ -9,7 +9,7 @@
#include "ash/api/tasks/tasks_client.h"
#include "ash/ash_export.h"
#include "ash/glanceables/common/glanceables_tasks_error_type.h"
#include "ash/glanceables/tasks/glanceables_tasks_error_type.h"
#include "base/functional/callback_forward.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
@ -122,6 +122,10 @@ class ASH_EXPORT GlanceablesTaskViewV2 : public views::FlexLayoutView,
// Title of the task.
std::u16string task_title_;
// Cached to reset the value of `task_title_` when the new title failed to
// commit after editing.
std::u16string task_title_before_edit_ = u"";
bool saving_task_changes_ = false;
// Marks the task as completed.

@ -11,6 +11,7 @@
#include "ash/api/tasks/tasks_client.h"
#include "ash/api/tasks/tasks_types.h"
#include "ash/constants/ash_features.h"
#include "ash/glanceables/common/glanceables_util.h"
#include "ash/glanceables/common/glanceables_view_id.h"
#include "ash/system/time/calendar_unittest_utils.h"
#include "ash/test/ash_test_base.h"
@ -41,7 +42,7 @@ class GlanceablesTaskViewStableLaunchTest : public AshTestBase {
feature_list_.InitWithFeatures(
/*enabled_features=*/{features::kGlanceablesTimeManagementTasksView},
/*disabled_features=*/{});
GlanceablesTaskViewV2::SetIsNetworkConnectedForTest(true);
glanceables_util::SetIsNetworkConnectedForTest(true);
}
private:
@ -138,7 +139,7 @@ TEST_F(GlanceablesTaskViewStableLaunchTest,
TEST_F(GlanceablesTaskViewStableLaunchTest,
UpdatingTaskTriggersErrorMessageIfNoNetwork) {
// Simulate that the network is disabled.
GlanceablesTaskViewV2::SetIsNetworkConnectedForTest(false);
glanceables_util::SetIsNetworkConnectedForTest(false);
const auto task = api::Task("task-id", "Task title",
/*due=*/std::nullopt, /*completed=*/false,

@ -2,15 +2,21 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef ASH_GLANCEABLES_COMMON_GLANCEABLES_TASKS_ERROR_TYPE_H_
#define ASH_GLANCEABLES_COMMON_GLANCEABLES_TASKS_ERROR_TYPE_H_
#ifndef ASH_GLANCEABLES_TASKS_GLANCEABLES_TASKS_ERROR_TYPE_H_
#define ASH_GLANCEABLES_TASKS_GLANCEABLES_TASKS_ERROR_TYPE_H_
namespace ash {
enum class GlanceablesTasksErrorType {
// The tasks view data wasn't successfully fetched so the list can not be
// updated.
kCantUpdateList,
// updated. This is only used when the cached task data is shown.
kCantUpdateTasks,
// The new tasks data can not be loaded. These are used in the cases where
// users have never loaded the tasks or are switching between lists and
// failed.
kCantLoadTasks,
kCantLoadTasksNoNetwork,
// The tasks weren't marked as completed because of failing to commit the
// change. This normally shows when the user reopens the glanceables after
@ -34,4 +40,4 @@ enum class GlanceablesTasksErrorType {
} // namespace ash
#endif // ASH_GLANCEABLES_COMMON_GLANCEABLES_TASKS_ERROR_TYPE_H_
#endif // ASH_GLANCEABLES_TASKS_GLANCEABLES_TASKS_ERROR_TYPE_H_

@ -12,6 +12,7 @@
#include "ash/api/tasks/tasks_types.h"
#include "ash/glanceables/common/glanceables_list_footer_view.h"
#include "ash/glanceables/common/glanceables_progress_bar_view.h"
#include "ash/glanceables/common/glanceables_util.h"
#include "ash/glanceables/common/glanceables_view_id.h"
#include "ash/glanceables/glanceables_controller.h"
#include "ash/glanceables/glanceables_metrics.h"
@ -353,6 +354,15 @@ std::unique_ptr<GlanceablesTaskViewV2> GlanceablesTasksView::CreateTaskView(
}
void GlanceablesTasksView::SelectedTasksListChanged() {
if (!glanceables_util::IsNetworkConnected()) {
// If the network is disconnected, cancel the list change and show the error
// message.
ShowErrorMessageWithType(
GlanceablesTasksErrorType::kCantLoadTasksNoNetwork);
task_list_combo_box_view_->SetSelectedIndex(cached_selected_list_index_);
return;
}
weak_ptr_factory_.InvalidateWeakPtrs();
tasks_requested_time_ = base::TimeTicks::Now();
tasks_list_change_count_++;
@ -389,26 +399,36 @@ void GlanceablesTasksView::UpdateTasksInTaskList(
std::move(recreate_combobox_callback_).Run();
}
if (!fetch_success) {
switch (context) {
case ListShownContext::kCachedList:
// Cached list should always considered as successfully fetched.
NOTREACHED_NORETURN();
case ListShownContext::kInitialList: {
if (GetTasksClient()->GetCachedTasksInTaskList(task_list_id)) {
// Notify users the last updated time of the tasks with the cached
// data.
ShowErrorMessageWithType(GlanceablesTasksErrorType::kCantUpdateTasks);
} else {
// Notify users that the target list of tasks wasn't loaded and guide
// them to retry.
ShowErrorMessageWithType(GlanceablesTasksErrorType::kCantLoadTasks);
}
return;
}
case ListShownContext::kUserSelectedList:
ShowErrorMessageWithType(GlanceablesTasksErrorType::kCantLoadTasks);
task_list_combo_box_view_->SetSelectedIndex(
cached_selected_list_index_);
return;
}
}
// Discard the fetched tasks that is not shown now.
if (task_list_id != GetActiveTaskList()->id) {
return;
}
if (!fetch_success) {
if (!GetTasksClient()->GetCachedTasksInTaskList(task_list_id) &&
context == ListShownContext::kInitialList) {
// TODO(b/323959143): Show "Couldn't load item" view if there is no cached
// view shown.
return;
} else {
// TODO(b/323959143): The error message should only be shown after we
// implement caching the fetched tasks and show cached tasks in UI.
// Revisit this to see if it works.
ShowErrorMessageWithType(GlanceablesTasksErrorType::kCantUpdateList);
return;
}
}
switch (context) {
case ListShownContext::kCachedList:
break;
@ -426,6 +446,7 @@ void GlanceablesTasksView::UpdateTasksInTaskList(
add_new_task_button_->SetVisible(true);
task_items_container_view_->RemoveAllChildViews();
cached_selected_list_index_ = task_list_combo_box_view_->GetSelectedIndex();
size_t num_tasks_shown = 0;
user_with_no_tasks_ =
@ -583,12 +604,18 @@ void GlanceablesTasksView::ShowErrorMessageWithType(
std::u16string GlanceablesTasksView::GetErrorString(
GlanceablesTasksErrorType error_type) const {
switch (error_type) {
case GlanceablesTasksErrorType::kCantUpdateList: {
case GlanceablesTasksErrorType::kCantUpdateTasks: {
auto last_modified_time =
GetTasksClient()->GetTasksLastUpdateTime(GetActiveTaskList()->id);
CHECK(last_modified_time.has_value());
return GetLastUpdateTimeMessage(last_modified_time.value());
}
case GlanceablesTasksErrorType::kCantLoadTasks:
return l10n_util::GetStringUTF16(
IDS_GLANCEABLES_TASKS_ERROR_LOAD_ITEMS_FAILED);
case GlanceablesTasksErrorType::kCantLoadTasksNoNetwork:
return l10n_util::GetStringUTF16(
IDS_GLANCEABLES_TASKS_ERROR_LOAD_ITEMS_FAILED_WHILE_OFFLINE);
case GlanceablesTasksErrorType::kCantMarkComplete:
return l10n_util::GetStringUTF16(
IDS_GLANCEABLES_TASKS_ERROR_MARK_COMPLETE_FAILED);
@ -596,9 +623,11 @@ std::u16string GlanceablesTasksView::GetErrorString(
return l10n_util::GetStringUTF16(
IDS_GLANCEABLES_TASKS_ERROR_MARK_COMPLETE_FAILED_WHILE_OFFLINE);
case GlanceablesTasksErrorType::kCantUpdateTitle:
return l10n_util::GetStringUTF16(
IDS_GLANCEABLES_TASKS_ERROR_EDIT_TASK_FAILED);
case GlanceablesTasksErrorType::kCantUpdateTitleNoNetwork:
// TODO(b/323959143): Add the string when it is ready.
return u"Error";
return l10n_util::GetStringUTF16(
IDS_GLANCEABLES_TASKS_ERROR_EDIT_TASK_FAILED_WHILE_OFFLINE);
}
}

@ -10,8 +10,8 @@
#include "ash/api/tasks/tasks_client.h"
#include "ash/api/tasks/tasks_types.h"
#include "ash/ash_export.h"
#include "ash/glanceables/common/glanceables_tasks_error_type.h"
#include "ash/glanceables/glanceables_metrics.h"
#include "ash/glanceables/tasks/glanceables_tasks_error_type.h"
#include "ash/system/unified/glanceable_tray_child_bubble.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
@ -183,6 +183,10 @@ class ASH_EXPORT GlanceablesTasksView : public GlanceablesTasksViewBase,
// metrics.
base::TimeTicks tasks_requested_time_;
// Cached to reset the value of the index of `task_list_combo_box_view_` when
// the target task list failed to be loaded.
std::optional<size_t> cached_selected_list_index_ = std::nullopt;
// Number of tasks added by the user for the currently selected task list.
// Task is considered "added" if task creation was requested via tasks API.
// The count is reset when the selected task list changes.

@ -8,7 +8,9 @@
#include "ash/api/tasks/fake_tasks_client.h"
#include "ash/constants/ash_features.h"
#include "ash/glanceables/common/glanceables_error_message_view.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"
@ -64,7 +66,7 @@ class GlanceablesTasksViewTest : public AshTestBase {
view_ = widget_->SetContentsView(std::make_unique<GlanceablesTasksView>(
fake_glanceables_tasks_client_->task_lists()));
GlanceablesTaskViewV2::SetIsNetworkConnectedForTest(true);
glanceables_util::SetIsNetworkConnectedForTest(true);
}
void TearDown() override {
@ -131,8 +133,8 @@ class GlanceablesTasksViewTest : public AshTestBase {
base::to_underlying(GlanceablesViewId::kProgressBar)));
}
const views::View* GetErrorMessage() const {
return views::AsViewClass<views::View>(view_->GetViewByID(
const GlanceablesErrorMessageView* GetErrorMessage() const {
return views::AsViewClass<GlanceablesErrorMessageView>(view_->GetViewByID(
base::to_underlying(GlanceablesViewId::kGlanceablesErrorMessageView)));
}
@ -509,7 +511,7 @@ TEST_F(GlanceablesTasksViewTest, OpenBrowserWithEmptyNewTaskDoesntCrash) {
TEST_F(GlanceablesTasksViewTest, HandlesErrorAfterAdding) {
tasks_client()->set_paused(true);
tasks_client()->set_run_with_errors(true);
tasks_client()->set_update_errors(true);
const auto* const task_items_container_view = GetTaskItemsContainerView();
ASSERT_TRUE(task_items_container_view);
@ -530,11 +532,12 @@ TEST_F(GlanceablesTasksViewTest, HandlesErrorAfterAdding) {
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.");
}
TEST_F(GlanceablesTasksViewTest, HandlesErrorAfterEditing) {
tasks_client()->set_paused(true);
tasks_client()->set_run_with_errors(true);
tasks_client()->set_update_errors(true);
const auto* const task_items_container_view = GetTaskItemsContainerView();
ASSERT_TRUE(task_items_container_view);
@ -542,9 +545,10 @@ TEST_F(GlanceablesTasksViewTest, HandlesErrorAfterEditing) {
EXPECT_EQ(task_items_container_view->children().size(), 2u);
EXPECT_FALSE(GetErrorMessage());
const auto* const title_label = views::AsViewClass<views::Label>(
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);
@ -559,9 +563,36 @@ TEST_F(GlanceablesTasksViewTest, HandlesErrorAfterEditing) {
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.");
// TODO(b/308446582): Confirm if the title needs to be reverted back in case
// of error.
// 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.");
// 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, ShowTasksWebUIFromHeaderView) {

@ -395,9 +395,10 @@ void GlanceableTrayBubbleView::AddClassroomBubbleStudentViewIfNeeded(
void GlanceableTrayBubbleView::AddTaskBubbleViewIfNeeded(
bool fetch_success,
const ui::ListModel<api::TaskList>* task_lists) {
if (task_lists->item_count() == 0) {
if (!fetch_success || task_lists->item_count() == 0) {
return;
}
// Add tasks bubble before everything.
if (features::IsGlanceablesTimeManagementTasksViewEnabled()) {
time_management_container_view_ =

@ -9,6 +9,7 @@
#include "ash/constants/ash_features.h"
#include "ash/glanceables/classroom/fake_glanceables_classroom_client.h"
#include "ash/glanceables/classroom/glanceables_classroom_item_view.h"
#include "ash/glanceables/common/glanceables_error_message_view.h"
#include "ash/glanceables/common/glanceables_view_id.h"
#include "ash/glanceables/glanceables_controller.h"
#include "ash/glanceables/tasks/glanceables_task_view.h"
@ -842,4 +843,82 @@ IN_PROC_BROWSER_TEST_F(GlanceablesWithAddEditBrowserTest,
"Task List 2 Item 3 Title"}));
}
IN_PROC_BROWSER_TEST_F(GlanceablesWithAddEditBrowserTest,
DontShowTasksIfNoNetwork) {
fake_glanceables_tasks_client()->set_get_task_lists_error(true);
// Click the date tray to show the glanceable bubbles.
ToggleDateTray();
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(GetGlanceableTrayBubble());
EXPECT_FALSE(GetTasksView());
}
IN_PROC_BROWSER_TEST_F(GlanceablesWithAddEditBrowserTest,
ShowFailedToLoadViewIfNoNetwork) {
fake_glanceables_tasks_client()->set_get_tasks_error(true);
// Click the date tray to show the glanceable bubbles.
ToggleDateTray();
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(GetGlanceableTrayBubble());
EXPECT_TRUE(GetTasksView());
auto* error_view = views::AsViewClass<GlanceablesErrorMessageView>(
GetTasksView()->GetViewByID(base::to_underlying(
GlanceablesViewId::kGlanceablesErrorMessageView)));
ASSERT_TRUE(error_view);
EXPECT_EQ(error_view->GetMessageForTest(), u"Couldn't load items.");
}
IN_PROC_BROWSER_TEST_F(GlanceablesWithAddEditBrowserTest,
SwitchTaskListsWithError) {
ToggleDateTray();
EXPECT_TRUE(GetGlanceableTrayBubble());
EXPECT_TRUE(GetTasksView());
// Check that the tasks glanceable is completely shown on the primary screen.
GetTasksView()->ScrollViewToVisible();
EXPECT_TRUE(
Shell::Get()->GetPrimaryRootWindow()->GetBoundsInScreen().Contains(
GetTasksView()->GetBoundsInScreen()));
// Set the error flag to true so that it fails on the next time the tasks are
// fetched.
fake_glanceables_tasks_client()->set_get_tasks_error(true);
// Check that task list items from the first list are shown.
const auto* combobox = GetTasksComboBoxView();
EXPECT_EQ(combobox->GetTextForRow(combobox->GetSelectedIndex().value()),
u"Task List 1 Title");
// Click on the combo box to show the task lists.
GetEventGenerator()->MoveMouseTo(combobox->GetBoundsInScreen().CenterPoint());
GetEventGenerator()->ClickLeftButton();
views::Label* second_menu_item_label =
FindMenuItemLabelWithString(u"Task List 2 Title");
// Click on the second menu item label to switch to the second task list.
ASSERT_TRUE(second_menu_item_label);
GetEventGenerator()->MoveMouseTo(
second_menu_item_label->GetBoundsInScreen().CenterPoint());
GetEventGenerator()->ClickLeftButton();
base::RunLoop().RunUntilIdle();
// Failing to update the task list will reset the combobox to the task list
// before switching.
EXPECT_EQ(combobox->GetTextForRow(combobox->GetSelectedIndex().value()),
u"Task List 1 Title");
auto* error_view = views::AsViewClass<GlanceablesErrorMessageView>(
GetTasksView()->GetViewByID(base::to_underlying(
GlanceablesViewId::kGlanceablesErrorMessageView)));
ASSERT_TRUE(error_view);
EXPECT_EQ(error_view->GetMessageForTest(), u"Couldn't load items.");
}
} // namespace ash