diff --git a/Core/TestsFoundation/UITestHelperNamespaces/LearnerDashboardHelper.swift b/Core/TestsFoundation/UITestHelperNamespaces/LearnerDashboardHelper.swift
new file mode 100644
index 0000000000..1ab2799a94
--- /dev/null
+++ b/Core/TestsFoundation/UITestHelperNamespaces/LearnerDashboardHelper.swift
@@ -0,0 +1,89 @@
+//
+// This file is part of Canvas.
+// Copyright (C) 2025-present Instructure, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+//
+
+import XCTest
+
+public class LearnerDashboardHelper: BaseHelper {
+ public static var bottomSettingsButton: XCUIElement { app.find(id: "Dashboard.bottomSettingsButton", type: .button) }
+
+ public struct Settings {
+ public static var doneButton: XCUIElement { app.find(id: "DashboardSettings.doneButton", type: .button) }
+ public static var newDashboardToggle: XCUIElement { app.find(id: "DashboardSettings.newDashboardToggle") }
+ public static var feedbackButton: XCUIElement { app.find(id: "Dashboard.Settings.feedbackButton") }
+ public static var showGradesToggle: XCUIElement { app.find(id: "Dashboard.Settings.showGradesToggle") }
+
+ public enum WidgetID: String {
+ case helloWidget
+ case coursesAndGroups
+ case weeklySummary
+ case todo
+ }
+
+ public static func widgetToggle(id: WidgetID) -> XCUIElement {
+ app.find(id: "Dashboard.Settings.widgetToggle.\(id.rawValue)")
+ }
+ }
+
+ public struct CoursesWidget {
+ public static var coursesHeader: XCUIElement { app.find(id: "Dashboard.Courses.coursesHeader") }
+ public static var groupsHeader: XCUIElement { app.find(id: "Dashboard.Courses.groupsHeader") }
+ public static var allCoursesButton: XCUIElement { app.find(id: "Dashboard.Courses.allCoursesButton") }
+ public static var courseCardGradePill: XCUIElement { app.find(id: "Dashboard.Courses.CourseCard.gradePill") }
+ public static var courseCardCustomizeButton: XCUIElement { app.find(id: "Dashboard.Courses.CourseCard.customizeButton") }
+ public static var courseCardAnnouncementsButton: XCUIElement { app.find(id: "Dashboard.Courses.CourseCard.announcementsButton") }
+
+ public static func courseCard(courseID: String) -> XCUIElement {
+ app.find(id: "Dashboard.Courses.CourseCard.cardButton.\(courseID)")
+ }
+ }
+
+ public struct AnnouncementsWidget {
+ public static var cardButton: XCUIElement { app.find(id: "Dashboard.Announcements.GlobalAnnouncement.cardButton") }
+ public static var detailsDismissButton: XCUIElement { app.find(id: "Dashboard.Announcements.GlobalAnnouncementDetails.dismissButton") }
+
+ public static func announcementCard(announcement: DSAccountNotification) -> XCUIElement {
+ app.find(id: "Dashboard.Announcements.GlobalAnnouncement.Id.\(announcement.id)")
+ }
+ }
+
+ public struct WeeklySummaryWidget {
+ public static var currentWeekButton: XCUIElement { app.find(id: "Dashboard.Forecast.currentWeekButton") }
+ public static var prevWeekButton: XCUIElement { app.find(id: "Dashboard.Forecast.prevWeekButton") }
+ public static var nextWeekButton: XCUIElement { app.find(id: "Dashboard.Forecast.nextWeekButton") }
+ public static var dueButton: XCUIElement { app.find(id: "Dashboard.Forecast.CategorySelector.dueButton") }
+ public static var missingButton: XCUIElement { app.find(id: "Dashboard.Forecast.CategorySelector.missingButton") }
+ public static var newGradesButton: XCUIElement { app.find(id: "Dashboard.Forecast.CategorySelector.newGradesButton") }
+ public static var itemCellButton: XCUIElement { app.find(id: "Dashboard.Forecast.Item.cellButton") }
+ }
+
+ public struct ToDoWidget {
+ public static var todayButton: XCUIElement { app.find(id: "Dashboard.Todo.todayButton") }
+ public static var showCompletedToggle: XCUIElement { app.find(id: "Dashboard.Todo.showCompletedToggle") }
+ public static var addTodoButton: XCUIElement { app.find(id: "Dashboard.Todo.TodoList.addTodoButton") }
+ public static var todoItem: XCUIElement { app.find(id: "Dashboard.Todo.TodoList.Item") }
+ }
+
+ public struct InvitationsWidget {
+ public static var acceptButton: XCUIElement { app.find(id: "Dashboard.Invitations.CourseInvitation.acceptButton") }
+ public static var declineButton: XCUIElement { app.find(id: "Dashboard.Invitations.CourseInvitation.declineButton") }
+
+ public static func invitationCard(enrollmentId: String) -> XCUIElement {
+ app.find(id: "Dashboard.Invitations.CourseInvitation.Id.\(enrollmentId)")
+ }
+ }
+}
diff --git a/Student/Student/LearnerDashboard/Settings/View/LearnerDashboardSettingsWidgetCardView.swift b/Student/Student/LearnerDashboard/Settings/View/LearnerDashboardSettingsWidgetCardView.swift
index debb6e9bf8..5b380cc0a0 100644
--- a/Student/Student/LearnerDashboard/Settings/View/LearnerDashboardSettingsWidgetCardView.swift
+++ b/Student/Student/LearnerDashboard/Settings/View/LearnerDashboardSettingsWidgetCardView.swift
@@ -44,6 +44,7 @@ struct LearnerDashboardSettingsWidgetCardView: View {
localized: "\(config.id.settingsTitle(username: username)) widget visibility",
bundle: .student
))
+ .identifier("Dashboard.Settings.widgetToggle.\(config.id.rawValue)")
}
.padding(.top, 12)
.padding(.bottom, 14)
diff --git a/Student/Student/LearnerDashboard/Widgets/CoursesAndGroupsWidget/View/CourseCardView.swift b/Student/Student/LearnerDashboard/Widgets/CoursesAndGroupsWidget/View/CourseCardView.swift
index 13bd514776..fae585e9f6 100644
--- a/Student/Student/LearnerDashboard/Widgets/CoursesAndGroupsWidget/View/CourseCardView.swift
+++ b/Student/Student/LearnerDashboard/Widgets/CoursesAndGroupsWidget/View/CourseCardView.swift
@@ -68,7 +68,7 @@ struct CourseCardView: View {
.animation(.dashboardWidget, value: viewModel)
.accessibilityElement(children: .combine)
.accessibilityLabel(a11yLabel)
- .identifier("Dashboard.Courses.CourseCard.cardButton")
+ .identifier("Dashboard.Courses.CourseCard.cardButton.\(viewModel.id)")
}
// MARK: - Card
diff --git a/Student/Student/LearnerDashboard/Widgets/CoursesAndGroupsWidget/View/CoursesAndGroupsWidgetSettingsView.swift b/Student/Student/LearnerDashboard/Widgets/CoursesAndGroupsWidget/View/CoursesAndGroupsWidgetSettingsView.swift
index 5957414425..517a91eb9e 100644
--- a/Student/Student/LearnerDashboard/Widgets/CoursesAndGroupsWidget/View/CoursesAndGroupsWidgetSettingsView.swift
+++ b/Student/Student/LearnerDashboard/Widgets/CoursesAndGroupsWidget/View/CoursesAndGroupsWidgetSettingsView.swift
@@ -28,6 +28,7 @@ struct CoursesAndGroupsWidgetSettingsView: View {
value: $viewModel.showGrades,
dividerStyle: .padded
)
+ .identifier("Dashboard.Settings.showGradesToggle")
AUI.ToggleCell(
label: Text("Show Color Overlay", bundle: .student),
value: $viewModel.showColorOverlay,
diff --git a/Student/Student/LearnerDashboard/Widgets/GlobalAnnouncementsWidget/View/GlobalAnnouncementsWidgetView.swift b/Student/Student/LearnerDashboard/Widgets/GlobalAnnouncementsWidget/View/GlobalAnnouncementsWidgetView.swift
index d1c0aa8939..1349cba687 100644
--- a/Student/Student/LearnerDashboard/Widgets/GlobalAnnouncementsWidget/View/GlobalAnnouncementsWidgetView.swift
+++ b/Student/Student/LearnerDashboard/Widgets/GlobalAnnouncementsWidget/View/GlobalAnnouncementsWidgetView.swift
@@ -40,7 +40,7 @@ struct GlobalAnnouncementsWidgetView: View {
) { cardViewModel in
GlobalAnnouncementCardView(viewModel: cardViewModel)
.accessibilityElement(children: .contain)
- .identifier("Dashboard.Announcements.GlobalAnnouncement.Id.\(cardViewModel.id)")
+ .identifier("Dashboard.Announcements.GlobalAnnouncement.Id.\(cardViewModel.accessibilityId)")
}
AUI.PageIndicator(currentIndex: currentPage, count: totalPages)
diff --git a/Student/Student/LearnerDashboard/Widgets/GlobalAnnouncementsWidget/ViewModel/GlobalAnnouncementCardViewModel.swift b/Student/Student/LearnerDashboard/Widgets/GlobalAnnouncementsWidget/ViewModel/GlobalAnnouncementCardViewModel.swift
index c9cb2f70fd..678c82e4e5 100644
--- a/Student/Student/LearnerDashboard/Widgets/GlobalAnnouncementsWidget/ViewModel/GlobalAnnouncementCardViewModel.swift
+++ b/Student/Student/LearnerDashboard/Widgets/GlobalAnnouncementsWidget/ViewModel/GlobalAnnouncementCardViewModel.swift
@@ -31,6 +31,7 @@ final class GlobalAnnouncementCardViewModel: Identifiable, Equatable {
// Including the whole model to ensure any change triggers a view update.
// Using only `model.id` would make the ForEach ignore a title change upon a refresh.
var id: GlobalAnnouncementsWidgetItem { model }
+ var accessibilityId: String { model.id }
private let model: GlobalAnnouncementsWidgetItem
private let onCardTap: (WeakViewController) -> Void
diff --git a/Student/Student/StudentAppDelegate.swift b/Student/Student/StudentAppDelegate.swift
index 5832ad321c..1d23ce492a 100644
--- a/Student/Student/StudentAppDelegate.swift
+++ b/Student/Student/StudentAppDelegate.swift
@@ -128,6 +128,10 @@ class StudentAppDelegate: UIResponder, UIApplicationDelegate, AppEnvironmentDele
.flatMap { unownedSelf.getFeatureFlags() }
.map { featureFlags in
unownedSelf.isLearnerDashboardEnabledOnInstance = featureFlags.isFeatureEnabled(.widget_dashboard)
+
+ if ProcessInfo.isUITest {
+ unownedSelf.isLearnerDashboardEnabledOnInstance = true
+ }
}
.flatMap {
unownedSelf.analyticsHandler.initializeTracking(environment: unownedSelf.environment) {
diff --git a/Student/StudentE2ETests/Dashboard/DashboardTests.swift b/Student/StudentE2ETests/Dashboard/DashboardTests.swift
index e9217dbd3a..850cf56bfc 100644
--- a/Student/StudentE2ETests/Dashboard/DashboardTests.swift
+++ b/Student/StudentE2ETests/Dashboard/DashboardTests.swift
@@ -18,12 +18,17 @@
import TestsFoundation
import XCTest
+import Core
class DashboardTests: E2ETestCase {
typealias Helper = DashboardHelper
typealias CourseInvitations = Helper.CourseInvitations
typealias AccountNotifications = Helper.AccountNotifications
+ override var experimentalFeatures: [ExperimentalFeature] {
+ [.revertToOldStudentDashboard]
+ }
+
func testDashboardFavoriteCourse() {
// MARK: Seed the usual stuff
let student = seeder.createUser()
diff --git a/Student/StudentE2ETests/LearnerDashboard/LearnerDashboardTests.swift b/Student/StudentE2ETests/LearnerDashboard/LearnerDashboardTests.swift
new file mode 100644
index 0000000000..5038299190
--- /dev/null
+++ b/Student/StudentE2ETests/LearnerDashboard/LearnerDashboardTests.swift
@@ -0,0 +1,176 @@
+//
+// This file is part of Canvas.
+// Copyright (C) 2025-present Instructure, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+//
+
+import TestsFoundation
+import XCTest
+
+class LearnerDashboardTests: E2ETestCase {
+ typealias Helper = LearnerDashboardHelper
+
+ func testCoursesWidgetShowsCourses() {
+ // MARK: Seed the usual stuff
+ let student = seeder.createUser()
+ let course = seeder.createCourse()
+ seeder.enrollStudent(student, in: course)
+
+ // MARK: Login and check courses widget shows the course
+ logInDSUser(student)
+ let coursesHeader = Helper.CoursesWidget.coursesHeader.waitUntil(.visible)
+ XCTAssertVisible(coursesHeader)
+
+ let courseCard = Helper.CoursesWidget.courseCard(courseID: course.id).waitUntil(.visible)
+ XCTAssertVisible(courseCard)
+ XCTAssertContains(courseCard.label, course.name)
+ }
+
+ func testCustomizeDashboardButtonOpensSettings() {
+ // MARK: Seed the usual stuff
+ let student = seeder.createUser()
+ let course = seeder.createCourse()
+ seeder.enrollStudent(student, in: course)
+
+ // MARK: Login and check for Customize Dashboard button
+ logInDSUser(student)
+ let bottomSettingsButton = Helper.bottomSettingsButton.waitUntil(.visible)
+ XCTAssertVisible(bottomSettingsButton)
+
+ // MARK: Tap button and verify settings screen
+ bottomSettingsButton.hit()
+ let doneButton = Helper.Settings.doneButton.waitUntil(.visible)
+ XCTAssertVisible(doneButton)
+
+ let newDashboardToggle = Helper.Settings.newDashboardToggle.waitUntil(.visible)
+ XCTAssertVisible(newDashboardToggle)
+ XCTAssertEqual(newDashboardToggle.stringValue, "on")
+
+ // MARK: Dismiss settings
+ doneButton.hit()
+ XCTAssertTrue(doneButton.waitUntil(.vanish).isVanished)
+ }
+
+ func testAllCoursesButtonNavigation() {
+ // MARK: Seed the usual stuff
+ let student = seeder.createUser()
+ let course = seeder.createCourse()
+ seeder.enrollStudent(student, in: course)
+
+ // MARK: Login and tap All Courses button
+ logInDSUser(student)
+ let allCoursesButton = Helper.CoursesWidget.allCoursesButton.waitUntil(.visible)
+ XCTAssertVisible(allCoursesButton)
+
+ // MARK: Verify All Courses screen shows the enrolled course
+ allCoursesButton.hit()
+ let courseItem = DashboardHelper.AllCourses.courseItem(course: course).waitUntil(.visible)
+ XCTAssertVisible(courseItem)
+ XCTAssertContains(courseItem.label, course.name)
+ }
+
+ func testToggleCoursesWidgetOffHidesWidget() {
+ // MARK: Seed the usual stuff
+ let student = seeder.createUser()
+ let course = seeder.createCourse()
+ seeder.enrollStudent(student, in: course)
+
+ // MARK: Login and verify courses widget is visible
+ logInDSUser(student)
+ var coursesHeader = Helper.CoursesWidget.coursesHeader.waitUntil(.visible)
+ XCTAssertVisible(coursesHeader)
+
+ // MARK: Open settings and toggle Courses & Groups widget off
+ Helper.bottomSettingsButton.hit()
+ let coursesWidgetToggle = Helper.Settings.widgetToggle(id: .coursesAndGroups).waitUntil(.visible)
+ XCTAssertVisible(coursesWidgetToggle)
+ XCTAssertEqual(coursesWidgetToggle.stringValue, "on")
+
+ coursesWidgetToggle.hit()
+ XCTAssertEqual(coursesWidgetToggle.stringValue, "off")
+
+ Helper.Settings.doneButton.hit()
+
+ // MARK: Verify courses header is no longer visible on dashboard
+ coursesHeader = Helper.CoursesWidget.coursesHeader.waitUntil(.vanish)
+ XCTAssertTrue(coursesHeader.isVanished)
+ }
+
+ func testCourseCardGradeVisibility() {
+ // MARK: Seed the usual stuff with a graded assignment
+ let student = seeder.createUser()
+ let course = seeder.createCourse()
+ let pointsPossible: Float = 10
+ let totalGrade = "100%"
+ seeder.enrollStudent(student, in: course)
+
+ let assignment = AssignmentsHelper.createAssignment(course: course, pointsPossible: Float(pointsPossible), gradingType: .percent)
+ GradesHelper.submitAssignment(course: course, student: student, assignment: assignment)
+ GradesHelper.gradeAssignment(grade: String(pointsPossible), course: course, assignment: assignment, user: student)
+
+ // MARK: Login and verify grade pill is not shown by default
+ logInDSUser(student)
+ Helper.CoursesWidget.courseCard(courseID: course.id).waitUntil(.visible)
+
+ var gradePill = Helper.CoursesWidget.courseCardGradePill.waitUntil(.vanish)
+ XCTAssertTrue(gradePill.isVanished)
+
+ // MARK: Open settings and enable Show Grades
+ Helper.bottomSettingsButton.hit()
+ let showGradesToggle = Helper.Settings.showGradesToggle.waitUntil(.visible)
+ XCTAssertVisible(showGradesToggle)
+ XCTAssertEqual(showGradesToggle.stringValue, "off")
+
+ showGradesToggle.hit()
+ XCTAssertEqual(showGradesToggle.stringValue, "on")
+
+ Helper.Settings.doneButton.hit()
+
+ // MARK: Verify grade pill appears with correct grade value
+ gradePill = Helper.CoursesWidget.courseCardGradePill.waitUntil(.visible)
+ XCTAssertVisible(gradePill)
+ gradePill.actionUntilElementCondition(action: .pullToRefresh, condition: .labelHasPrefix(expected: totalGrade))
+ XCTAssertTrue(gradePill.label.hasPrefix(totalGrade))
+ }
+
+ func testGlobalAnnouncementShownAndDismissed() {
+ // MARK: Seed the usual stuff
+ let student = seeder.createUser()
+ let course = seeder.createCourse()
+ seeder.enrollStudent(student, in: course)
+
+ // MARK: Login and post a global announcement
+ logInDSUser(student)
+ let announcement = AnnouncementsHelper.postAccountNotification()
+ app.pullToRefresh()
+
+ // MARK: Verify the announcement card is visible in the widget
+ let announcementCard = Helper.AnnouncementsWidget.announcementCard(announcement: announcement).waitUntil(.visible)
+ XCTAssertVisible(announcementCard)
+
+ // MARK: Tap the card button and verify details screen opens
+ let cardButton = Helper.AnnouncementsWidget.cardButton.waitUntil(.visible)
+ XCTAssertVisible(cardButton)
+
+ cardButton.hit()
+
+ let dismissButton = Helper.AnnouncementsWidget.detailsDismissButton.waitUntil(.visible)
+ XCTAssertVisible(dismissButton)
+
+ // MARK: Dismiss the details screen
+ dismissButton.hit()
+ XCTAssertTrue(dismissButton.waitUntil(.vanish).isVanished)
+ }
+}