Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
8585145
some progress on notebook list
andrasmaczak Nov 6, 2025
8b9256e
finished DropdownChip, modifications on NotebookScreen
andrasmaczak Nov 7, 2025
cf003d0
infinite scrolling
andrasmaczak Nov 10, 2025
9051fe2
course filtering implemented, ui changes
andrasmaczak Nov 11, 2025
af1a5a5
notebook ui changes
andrasmaczak Nov 12, 2025
5f65a67
Merge branch 'master' into CLX-3201-notebook-list
andrasmaczak Nov 12, 2025
32947af
notebook ui changes
andrasmaczak Nov 13, 2025
fb09d48
notebook ui changes
andrasmaczak Nov 13, 2025
d0b9ac4
notebook unit tests
andrasmaczak Nov 13, 2025
b322962
removed unused import
andrasmaczak Nov 13, 2025
31f38b9
Merge branch 'master' into CLX-3201-notebook-list
andrasmaczak Nov 17, 2025
75ce829
fix PR findings
andrasmaczak Nov 18, 2025
c518105
pull to refresh logic to notebook
andrasmaczak Nov 19, 2025
f9b8b36
Fix findings
domonkosadam Nov 24, 2025
edb8000
UI improvements
domonkosadam Nov 25, 2025
8121172
Merge branch 'master' into CLX-3201-notebook-list
domonkosadam Nov 25, 2025
230729d
Implement dropdown icons
domonkosadam Nov 25, 2025
4a7696f
Fix tests
domonkosadam Nov 25, 2025
e6542fa
Fix test
domonkosadam Nov 25, 2025
322aafd
Implement custom border
domonkosadam Nov 25, 2025
7dd36a0
Use custom shadow
domonkosadam Nov 25, 2025
9daf837
Fix shadows
domonkosadam Nov 25, 2025
df137d9
Implement correct shadow handling
domonkosadam Nov 25, 2025
99f7d6a
Fix shadow attributes
domonkosadam Nov 25, 2025
3cef77c
Fix colors
domonkosadam Nov 25, 2025
1b309f2
Border improvements
domonkosadam Nov 26, 2025
e280c2b
Implement ui layout
domonkosadam Nov 26, 2025
59715e1
Implement delete
domonkosadam Nov 26, 2025
8e8d23d
Fix UI
domonkosadam Nov 26, 2025
f694bfb
implement tests
domonkosadam Nov 26, 2025
e64d47d
Merge branch 'master' into CLX-3202-Notebook-list-item-changes
domonkosadam Nov 26, 2025
95603f3
Fix top app bar style
domonkosadam Nov 26, 2025
8d3ee76
a11y improvements
domonkosadam Nov 27, 2025
9a51409
Implement navigation animations
domonkosadam Nov 27, 2025
fd37e21
Fix animations
domonkosadam Nov 27, 2025
4d0da04
Refactor navigation transition handling
domonkosadam Nov 27, 2025
acf6e0e
Fix findings
domonkosadam Nov 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,339 @@
/*
* 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 General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.instructure.horizon.ui.features.notebook

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.navigation.compose.rememberNavController
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.instructure.canvasapi2.managers.graphql.horizon.CourseWithProgress
import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedData
import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedDataRange
import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedDataTextPosition
import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteObjectType
import com.instructure.canvasapi2.utils.ContextKeeper
import com.instructure.horizon.R
import com.instructure.horizon.features.notebook.NotebookScreen
import com.instructure.horizon.features.notebook.NotebookUiState
import com.instructure.horizon.features.notebook.common.model.Note
import com.instructure.horizon.features.notebook.common.model.NotebookType
import com.instructure.horizon.horizonui.platform.LoadingState
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.util.Date

@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class NotebookScreenUiTest {
@get:Rule
val composeTestRule = createComposeRule()

private val context = InstrumentationRegistry.getInstrumentation().targetContext

@Test
fun testEmptyStateDisplaysWhenNoNotes() {
val state = createEmptyState()

composeTestRule.setContent {
ContextKeeper.appContext = context
val navController = rememberNavController()
NotebookScreen(navController, state)
}

composeTestRule.onNodeWithText(context.getString(R.string.notesEmptyContentTitle))
.assertIsDisplayed()
}

@Test
fun testNoteCardDisplaysHighlightedText() {
val highlightedText = "This is important highlighted text from the course material"
val state = createStateWithNotes(
notes = listOf(createTestNote(highlightedText = highlightedText))
)

composeTestRule.setContent {
ContextKeeper.appContext = context
val navController = rememberNavController()
NotebookScreen(navController, state)
}

composeTestRule.onNodeWithText(highlightedText, substring = true)
.assertIsDisplayed()
}

@Test
fun testNoteCardDisplaysUserComment() {
val userComment = "My personal note about this concept"
val state = createStateWithNotes(
notes = listOf(createTestNote(userText = userComment))
)

composeTestRule.setContent {
ContextKeeper.appContext = context
val navController = rememberNavController()
NotebookScreen(navController, state)
}

composeTestRule.onNodeWithText(userComment, substring = true)
.assertIsDisplayed()
}

@Test
fun testNoteCardDisplaysDate() {
val state = createStateWithNotes(
notes = listOf(createTestNote())
)

composeTestRule.setContent {
ContextKeeper.appContext = context
val navController = rememberNavController()
NotebookScreen(navController, state)
}

composeTestRule.onNode(hasText("Jan", substring = true))
.assertIsDisplayed()
}

@Test
fun testNoteCardDisplaysTypeImportant() {
val state = createStateWithNotes(
notes = listOf(createTestNote(type = NotebookType.Important))
)

composeTestRule.setContent {
ContextKeeper.appContext = context
val navController = rememberNavController()
NotebookScreen(navController, state)
}

composeTestRule.onNodeWithText("Important", useUnmergedTree = true)
.assertIsDisplayed()
}

@Test
fun testNoteCardDisplaysTypeConfusing() {
val state = createStateWithNotes(
notes = listOf(createTestNote(type = NotebookType.Confusing))
)

composeTestRule.setContent {
ContextKeeper.appContext = context
val navController = rememberNavController()
NotebookScreen(navController, state)
}

composeTestRule.onNodeWithText("Unclear", useUnmergedTree = true)
.assertIsDisplayed()
}

@Test
fun testCourseFilterDisplayedWhenEnabled() {
val state = createStateWithNotes(
showCourseFilter = true,
courses = listOf(createTestCourse())
)

composeTestRule.setContent {
ContextKeeper.appContext = context
val navController = rememberNavController()
NotebookScreen(navController, state)
}

composeTestRule.onNodeWithText(context.getString(R.string.notebookFilterCoursePlaceholder), useUnmergedTree = true)
.assertIsDisplayed()
}

@Test
fun testNoteTypeFilterDisplayed() {
val state = createStateWithNotes(
showNoteTypeFilter = true,
notes = listOf(createTestNote())
)

composeTestRule.setContent {
ContextKeeper.appContext = context
val navController = rememberNavController()
NotebookScreen(navController, state)
}

composeTestRule.onNodeWithText("All notes", useUnmergedTree = true)
.assertIsDisplayed()
}

@Test
fun testCourseNameDisplayedWhenCourseFilterVisible() {
val courseName = "Biology 101"
val state = createStateWithNotes(
showCourseFilter = true,
courses = listOf(createTestCourse(name = courseName, id = 123L)),
notes = listOf(createTestNote(courseId = 123L))
)

composeTestRule.setContent {
ContextKeeper.appContext = context
val navController = rememberNavController()
NotebookScreen(navController, state)
}

composeTestRule.onNodeWithText(courseName, substring = true)
.assertIsDisplayed()
}

@Test
fun testCourseNameNotDisplayedWhenCourseFilterHidden() {
val courseName = "Biology 101"
val state = createStateWithNotes(
showCourseFilter = false,
courses = listOf(createTestCourse(name = courseName, id = 123L)),
notes = listOf(createTestNote(courseId = 123L))
)

composeTestRule.setContent {
ContextKeeper.appContext = context
val navController = rememberNavController()
NotebookScreen(navController, state)
}

composeTestRule.onNode(hasText(courseName))
.assertDoesNotExist()
}

@Test
fun testEmptyFilteredStateDisplayedWhenFilterApplied() {
val state = createEmptyState(selectedFilter = NotebookType.Important)

composeTestRule.setContent {
ContextKeeper.appContext = context
val navController = rememberNavController()
NotebookScreen(navController, state)
}

composeTestRule.onNodeWithText(context.getString(R.string.notesEmptyFilteredContentTitle))
.assertIsDisplayed()
}

@Test
fun testMultipleNotesDisplayed() {
val note1 = createTestNote(
id = "1",
highlightedText = "First important concept"
)
val note2 = createTestNote(
id = "2",
highlightedText = "Second important concept"
)
val state = createStateWithNotes(notes = listOf(note1, note2))

composeTestRule.setContent {
ContextKeeper.appContext = context
val navController = rememberNavController()
NotebookScreen(navController, state)
}

composeTestRule.onNodeWithText("First important concept", substring = true)
.assertIsDisplayed()
composeTestRule.onNodeWithText("Second important concept", substring = true)
.assertIsDisplayed()
}

@Test
fun testShowMoreButtonDisplayedWhenHasNextPage() {
val state = createStateWithNotes(
notes = listOf(createTestNote()),
hasNextPage = true
)

composeTestRule.setContent {
ContextKeeper.appContext = context
val navController = rememberNavController()
NotebookScreen(navController, state)
}

composeTestRule.onNodeWithText(context.getString(R.string.showMore))
.assertIsDisplayed()
}

private fun createEmptyState(
selectedFilter: NotebookType? = null
): NotebookUiState {
return NotebookUiState(
loadingState = LoadingState(isLoading = false),
notes = emptyList(),
selectedFilter = selectedFilter,
showCourseFilter = true,
showNoteTypeFilter = true
)
}

private fun createStateWithNotes(
notes: List<Note> = emptyList(),
showCourseFilter: Boolean = false,
showNoteTypeFilter: Boolean = false,
courses: List<CourseWithProgress> = emptyList(),
hasNextPage: Boolean = false,
selectedFilter: NotebookType? = null
): NotebookUiState {
return NotebookUiState(
loadingState = LoadingState(isLoading = false),
notes = notes,
courses = courses,
showCourseFilter = showCourseFilter,
showNoteTypeFilter = showNoteTypeFilter,
hasNextPage = hasNextPage,
selectedFilter = selectedFilter
)
}

private fun createTestNote(
id: String = "note1",
highlightedText: String = "Test highlighted text from course material",
userText: String = "My personal annotation",
type: NotebookType = NotebookType.Important,
courseId: Long = 123L
): Note {
return Note(
id = id,
highlightedText = NoteHighlightedData(
selectedText = highlightedText,
range = NoteHighlightedDataRange(0, highlightedText.length, "", ""),
textPosition = NoteHighlightedDataTextPosition(0, highlightedText.length)
),
type = type,
userText = userText,
updatedAt = Date(1706140800000L),
courseId = courseId,
objectType = NoteObjectType.Assignment,
objectId = "assignment123"
)
}

private fun createTestCourse(
name: String = "Test Course",
id: Long = 123L
): CourseWithProgress {
return CourseWithProgress(
courseId = id,
courseName = name,
progress = 0.0
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,9 @@
*/
package com.instructure.horizon.features.account.navigation

import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
Expand All @@ -38,10 +36,9 @@ import com.instructure.horizon.features.account.profile.AccountProfileScreen
import com.instructure.horizon.features.account.profile.AccountProfileViewModel
import com.instructure.horizon.features.account.reportabug.ReportABugWebView
import com.instructure.horizon.features.home.HomeNavigationRoute
import com.instructure.horizon.horizonui.animation.NavigationTransitionAnimation
import com.instructure.horizon.horizonui.animation.enterTransition
import com.instructure.horizon.horizonui.animation.exitTransition
import com.instructure.horizon.horizonui.animation.mainEnterTransition
import com.instructure.horizon.horizonui.animation.mainExitTransition
import com.instructure.horizon.horizonui.animation.popEnterTransition
import com.instructure.horizon.horizonui.animation.popExitTransition

Expand All @@ -51,10 +48,10 @@ fun NavGraphBuilder.accountNavigation(
navigation(
route = HomeNavigationRoute.Account.route,
startDestination = AccountRoute.Account.route,
enterTransition = { if (isBottomNavDestination()) mainEnterTransition else enterTransition },
exitTransition = { if (isBottomNavDestination()) mainExitTransition else exitTransition },
popEnterTransition = { if (isBottomNavDestination()) mainEnterTransition else popEnterTransition },
popExitTransition = { if (isBottomNavDestination()) mainExitTransition else popExitTransition },
enterTransition = { enterTransition(NavigationTransitionAnimation.SCALE) },
exitTransition = { exitTransition(NavigationTransitionAnimation.SCALE) },
popEnterTransition = { popEnterTransition(NavigationTransitionAnimation.SCALE) },
popExitTransition = { popExitTransition(NavigationTransitionAnimation.SCALE) },
) {
composable(
route = AccountRoute.Account.route,
Expand Down Expand Up @@ -96,15 +93,4 @@ fun NavGraphBuilder.accountNavigation(
ReportABugWebView(navController)
}
}
}

private fun AnimatedContentTransitionScope<NavBackStackEntry>.isBottomNavDestination(): Boolean {
val sourceRoute = this.initialState.destination.route ?: return false
val destinationRoute = this.targetState.destination.route ?: return false
return sourceRoute.contains(HomeNavigationRoute.Learn.route)
|| sourceRoute.contains(HomeNavigationRoute.Dashboard.route)
|| sourceRoute.contains(HomeNavigationRoute.Skillspace.route)
|| destinationRoute.contains(HomeNavigationRoute.Learn.route)
|| destinationRoute.contains(HomeNavigationRoute.Dashboard.route)
|| destinationRoute.contains(HomeNavigationRoute.Skillspace.route)
}
Loading
Loading