Skip to content

Commit d0954a6

Browse files
authored
[CLX-3743][Horizon] Learn course screen (#3499)
refs: CLX-3743 affects: Student release note: none
1 parent acc019a commit d0954a6

File tree

39 files changed

+2861
-142
lines changed

39 files changed

+2861
-142
lines changed

automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeGetHorizonCourseManager.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,19 @@ class FakeGetHorizonCourseManager(): HorizonGetCoursesManager {
3333
return DataResult.Success(getCourses())
3434
}
3535

36+
override suspend fun getCourseWithProgressById(
37+
courseId: Long,
38+
userId: Long,
39+
forceNetwork: Boolean
40+
): DataResult<CourseWithProgress> {
41+
val course = getCourses().find { it.courseId == courseId }
42+
return if (course != null) {
43+
DataResult.Success(course)
44+
} else {
45+
DataResult.Fail()
46+
}
47+
}
48+
3649
override suspend fun getEnrollments(
3750
userId: Long,
3851
forceNetwork: Boolean
@@ -84,6 +97,9 @@ class FakeGetHorizonCourseManager(): HorizonGetCoursesManager {
8497
CourseWithModuleItemDurations(
8598
courseId = courseId,
8699
courseName = "Program Course",
100+
moduleItemsDuration = emptyList(),
101+
startDate = null,
102+
endDate = null
87103
)
88104
)
89105
}
@@ -94,6 +110,7 @@ class FakeGetHorizonCourseManager(): HorizonGetCoursesManager {
94110
CourseWithProgress(
95111
courseId = courses[0].id,
96112
courseName = courses[0].name,
113+
courseImageUrl = null,
97114
courseSyllabus = "Syllabus for Course 1",
98115
progress = 0.25
99116
)
@@ -102,6 +119,7 @@ class FakeGetHorizonCourseManager(): HorizonGetCoursesManager {
102119
CourseWithProgress(
103120
courseId = courses[1].id,
104121
courseName = courses[1].name,
122+
courseImageUrl = null,
105123
courseSyllabus = "Syllabus for Course 2",
106124
progress = 1.0
107125
)
@@ -110,6 +128,7 @@ class FakeGetHorizonCourseManager(): HorizonGetCoursesManager {
110128
CourseWithProgress(
111129
courseId = courses[2].id,
112130
courseName = courses[2].name,
131+
courseImageUrl = null,
113132
courseSyllabus = null,
114133
progress = 0.0
115134
)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
query HorizonGetCourseById($courseId: ID!, $userId: ID!) {
2+
legacyNode(_id: $courseId, type: Course) {
3+
... on Course {
4+
id: _id
5+
name
6+
image_download_url: imageUrl
7+
syllabus_body: syllabusBody
8+
usersConnection(filter: {userIds: [$userId]}) {
9+
nodes {
10+
courseProgression {
11+
requirements {
12+
completionPercentage
13+
}
14+
}
15+
}
16+
}
17+
}
18+
}
19+
}

libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/horizon/HorizonGetCoursesManager.kt

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.instructure.canvasapi2.managers.graphql.horizon
1818
import com.apollographql.apollo.ApolloClient
1919
import com.apollographql.apollo.api.Optional
2020
import com.instructure.canvasapi2.GetCoursesQuery
21+
import com.instructure.canvasapi2.HorizonGetCourseByIdQuery
2122
import com.instructure.canvasapi2.HorizonGetProgramCourseByIdQuery
2223
import com.instructure.canvasapi2.QLClientConfig
2324
import com.instructure.canvasapi2.enqueueQuery
@@ -29,6 +30,8 @@ import java.util.Date
2930
interface HorizonGetCoursesManager {
3031
suspend fun getCoursesWithProgress(userId: Long, forceNetwork: Boolean = false): DataResult<List<CourseWithProgress>>
3132

33+
suspend fun getCourseWithProgressById(courseId: Long, userId: Long, forceNetwork: Boolean = false): DataResult<CourseWithProgress>
34+
3235
suspend fun getEnrollments(userId: Long, forceNetwork: Boolean = false): DataResult<List<GetCoursesQuery.Enrollment>>
3336

3437
suspend fun getProgramCourses(courseId: Long, forceNetwork: Boolean = false): DataResult<CourseWithModuleItemDurations>
@@ -50,14 +53,38 @@ class HorizonGetCoursesManagerImpl(private val apolloClient: ApolloClient): Hori
5053
}
5154
}
5255

56+
override suspend fun getCourseWithProgressById(courseId: Long, userId: Long, forceNetwork: Boolean): DataResult<CourseWithProgress> {
57+
return try {
58+
val query = HorizonGetCourseByIdQuery(courseId.toString(), userId.toString())
59+
val result = apolloClient.enqueueQuery(query, forceNetwork).dataAssertNoErrors
60+
61+
val progress = result.legacyNode?.onCourse?.usersConnection?.nodes?.firstOrNull()?.courseProgression?.requirements?.completionPercentage ?: 0.0
62+
val courseId = result.legacyNode?.onCourse?.id?.toLongOrNull() ?: -1L
63+
val courseName = result.legacyNode?.onCourse?.name ?: ""
64+
val courseSyllabus = result.legacyNode?.onCourse?.syllabus_body ?: ""
65+
val imageUrl = result.legacyNode?.onCourse?.image_download_url ?: ""
66+
val course = CourseWithProgress(courseId, courseName, imageUrl, courseSyllabus, progress)
67+
68+
return DataResult.Success(course)
69+
} catch (e: Exception) {
70+
DataResult.Fail(Failure.Exception(e))
71+
}
72+
}
73+
5374
private fun mapCourse(course: GetCoursesQuery.Course?): CourseWithProgress? {
5475
val progress = course?.usersConnection?.nodes?.firstOrNull()?.courseProgression?.requirements?.completionPercentage ?: 0.0
5576
val courseId = course?.id?.toLong()
5677
val courseName = course?.name
5778
val courseSyllabus = course?.syllabus_body
79+
val imageUrl = course?.image_download_url
5880

5981
return if (courseId != null && courseName != null) {
60-
CourseWithProgress(courseId, courseName, courseSyllabus, progress)
82+
CourseWithProgress(
83+
courseId,
84+
courseName,
85+
imageUrl,
86+
courseSyllabus,
87+
progress)
6188
} else {
6289
null
6390
}
@@ -113,7 +140,8 @@ class HorizonGetCoursesManagerImpl(private val apolloClient: ApolloClient): Hori
113140
data class CourseWithProgress(
114141
val courseId: Long,
115142
val courseName: String,
116-
val courseSyllabus: String? = null,
143+
val courseImageUrl: String?,
144+
val courseSyllabus: String?,
117145
val progress: Double,
118146
)
119147

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright (C) 2025 - present Instructure, Inc.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, version 3 of the License.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* GNU General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU General Public License
14+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
*
16+
*/
17+
package com.instructure.horizon.ui.features.learn
18+
19+
import androidx.compose.ui.test.assertIsDisplayed
20+
import androidx.compose.ui.test.junit4.createComposeRule
21+
import androidx.compose.ui.test.onNodeWithTag
22+
import androidx.compose.ui.test.onNodeWithText
23+
import androidx.navigation.compose.ComposeNavigator
24+
import androidx.navigation.compose.rememberNavController
25+
import androidx.test.ext.junit.runners.AndroidJUnit4
26+
import com.instructure.horizon.features.learn.course.details.CourseDetailsScreen
27+
import com.instructure.horizon.features.learn.course.details.CourseDetailsUiState
28+
import com.instructure.horizon.horizonui.platform.LoadingState
29+
import org.junit.Rule
30+
import org.junit.Test
31+
import org.junit.runner.RunWith
32+
33+
@RunWith(AndroidJUnit4::class)
34+
class CourseDetailsUiTest {
35+
@get:Rule
36+
val composeTestRule = createComposeRule()
37+
38+
@Test
39+
fun testLoadingStateDisplaysSpinner() {
40+
val state = CourseDetailsUiState(
41+
loadingState = LoadingState(isLoading = true),
42+
courseName = "",
43+
courseProgress = 0.0,
44+
courseId = 1L
45+
)
46+
47+
composeTestRule.setContent {
48+
val navController = rememberNavController()
49+
navController.navigatorProvider.addNavigator(ComposeNavigator())
50+
51+
CourseDetailsScreen(
52+
state = state,
53+
navController = navController
54+
)
55+
}
56+
57+
composeTestRule.onNodeWithTag("LoadingSpinner")
58+
.assertIsDisplayed()
59+
}
60+
61+
@Test
62+
fun testCourseDetailsDisplaysCourseName() {
63+
val state = CourseDetailsUiState(
64+
loadingState = LoadingState(isLoading = false),
65+
courseName = "Test Course Name",
66+
courseProgress = 75.0,
67+
courseId = 1L
68+
)
69+
70+
composeTestRule.setContent {
71+
val navController = rememberNavController()
72+
navController.navigatorProvider.addNavigator(ComposeNavigator())
73+
74+
CourseDetailsScreen(
75+
state = state,
76+
navController = navController
77+
)
78+
}
79+
80+
composeTestRule.onNodeWithText("Test Course Name")
81+
.assertIsDisplayed()
82+
}
83+
84+
@Test
85+
fun testErrorStateDisplayed() {
86+
val state = CourseDetailsUiState(
87+
loadingState = LoadingState(isLoading = false, isError = true, errorMessage = "Failed to load course"),
88+
courseName = "",
89+
courseProgress = 0.0,
90+
courseId = 1L
91+
)
92+
93+
composeTestRule.setContent {
94+
val navController = rememberNavController()
95+
navController.navigatorProvider.addNavigator(ComposeNavigator())
96+
97+
CourseDetailsScreen(
98+
state = state,
99+
navController = navController
100+
)
101+
}
102+
103+
composeTestRule.onNodeWithText("Failed to load course", substring = true)
104+
.assertIsDisplayed()
105+
}
106+
}

libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/learn/HorizonLearnUiTest.kt

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import androidx.compose.ui.Modifier
2121
import androidx.compose.ui.test.assertIsDisplayed
2222
import androidx.compose.ui.test.junit4.createComposeRule
2323
import androidx.compose.ui.test.onNodeWithTag
24-
import androidx.compose.ui.test.onNodeWithText
2524
import androidx.test.ext.junit.runners.AndroidJUnit4
2625
import com.instructure.horizon.horizonui.molecules.Spinner
2726
import org.junit.Rule
@@ -42,14 +41,4 @@ class HorizonLearnUiTest {
4241
composeTestRule.onNodeWithTag("LoadingSpinner")
4342
.assertIsDisplayed()
4443
}
45-
46-
@Test
47-
fun testLearnTitleDisplays() {
48-
composeTestRule.setContent {
49-
androidx.compose.material3.Text("Learn")
50-
}
51-
52-
composeTestRule.onNodeWithText("Learn")
53-
.assertIsDisplayed()
54-
}
5544
}

0 commit comments

Comments
 (0)