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) + } +}