Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
7 changes: 4 additions & 3 deletions apps/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,10 @@ Apps support multiple languages. Translation tags are scanned at build time via
When creating a pull request, use the template located at `/PULL_REQUEST_TEMPLATE` in the repository root. The template includes:
- Test plan description
- Issue references (refs:)
- Impact scope (affects:)
- Impact scope (affects: - only Student, Teacher, or Parent; can be multiple if affecting multiple apps)
- Release note
- Screenshots table (Before/After)
- Checklist (E2E tests, dark/light mode, landscape/tablet, accessibility, product approval)
- Checklist (dark/light mode, landscape/tablet, accessibility, product approval)

Note: Do not include E2E tests or screenshots sections in the PR description unless specifically needed.

Use `gh pr create` with the template to create PRs from the command line.
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.instructure.student.ui.rendertests

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.instructure.student.features.dashboard.compose.DashboardScreenContent
import com.instructure.student.features.dashboard.compose.DashboardUiState
import com.instructure.student.ui.utils.StudentComposeTest
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class DashboardScreenTest {

@get:Rule
val composeTestRule = createComposeRule()

@Test
fun testDashboardScreenShowsLoadingState() {
val mockUiState = DashboardUiState(
loading = true,
error = null,
refreshing = false,
onRefresh = {},
onRetry = {}
)

composeTestRule.setContent {
DashboardScreenContent(uiState = mockUiState)
}

composeTestRule.waitForIdle()
composeTestRule.onNodeWithTag("loading").assertIsDisplayed()
}

@Test
fun testDashboardScreenShowsErrorState() {
val mockUiState = DashboardUiState(
loading = false,
error = "An error occurred",
refreshing = false,
onRefresh = {},
onRetry = {}
)

composeTestRule.setContent {
DashboardScreenContent(uiState = mockUiState)
}

composeTestRule.waitForIdle()
composeTestRule.onNodeWithTag("errorContent").assertIsDisplayed()
}

@Test
fun testDashboardScreenShowsEmptyState() {
val mockUiState = DashboardUiState(
loading = false,
error = null,
refreshing = false,
onRefresh = {},
onRetry = {}
)

composeTestRule.setContent {
DashboardScreenContent(uiState = mockUiState)
}

composeTestRule.waitForIdle()
composeTestRule.onNodeWithTag("emptyContent").assertIsDisplayed()
}

@Test
fun testDashboardScreenShowsRefreshIndicator() {
val mockUiState = DashboardUiState(
loading = false,
error = null,
refreshing = true,
onRefresh = {},
onRetry = {}
)

composeTestRule.setContent {
DashboardScreenContent(uiState = mockUiState)
}

composeTestRule.waitForIdle()
composeTestRule.onNodeWithTag("dashboardPullRefreshIndicator").assertIsDisplayed()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ import com.instructure.student.features.modules.progression.CourseModuleProgress
import com.instructure.student.features.navigation.NavigationRepository
import com.instructure.student.features.todolist.ToDoListFragment
import com.instructure.student.fragment.BookmarksFragment
import com.instructure.student.fragment.DashboardFragment
import com.instructure.student.fragment.OldDashboardFragment
import com.instructure.student.fragment.NotificationListFragment
import com.instructure.student.fragment.OldToDoListFragment
import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionUploadEffectHandler
Expand Down Expand Up @@ -1012,7 +1012,7 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
private fun selectBottomNavFragment(fragmentClass: Class<out Fragment>) {
val selectedFragment = supportFragmentManager.findFragmentByTag(fragmentClass.name)

(topFragment as? DashboardFragment)?.cancelCardDrag()
(topFragment as? OldDashboardFragment)?.cancelCardDrag()

if (selectedFragment == null) {
val fragment = createBottomNavFragment(fragmentClass.name)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright (C) 2024 - 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.student.features.dashboard.compose

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import com.instructure.interactions.router.Route
import com.instructure.pandautils.compose.CanvasTheme
import com.instructure.student.fragment.ParentFragment
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class DashboardFragment : ParentFragment() {

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
applyTheme()
return ComposeView(requireContext()).apply {
setContent {
CanvasTheme {
DashboardScreen()
}
}
}
}

override fun title(): String = ""

override fun applyTheme() {
navigation?.attachNavigationDrawer(this, null)
}

companion object Companion {
fun newInstance(route: Route): DashboardFragment {
val fragment = DashboardFragment()
fragment.arguments = route.arguments
return fragment
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright (C) 2024 - 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.student.features.dashboard.compose

import androidx.activity.compose.LocalActivity
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.instructure.pandautils.compose.composables.CanvasThemedAppBar
import com.instructure.pandautils.compose.composables.EmptyContent
import com.instructure.pandautils.compose.composables.ErrorContent
import com.instructure.pandautils.compose.composables.Loading
import com.instructure.student.R
import com.instructure.student.activity.NavigationActivity

@Composable
fun DashboardScreen() {
val viewModel: DashboardViewModel = hiltViewModel()
val uiState by viewModel.uiState.collectAsState()

DashboardScreenContent(uiState = uiState)
}

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun DashboardScreenContent(uiState: DashboardUiState) {
val activity = LocalActivity.current
val pullRefreshState = rememberPullRefreshState(
refreshing = uiState.refreshing,
onRefresh = uiState.onRefresh
)

Scaffold(
modifier = Modifier.background(colorResource(R.color.backgroundLightest)),
topBar = {
CanvasThemedAppBar(
title = stringResource(id = R.string.dashboard),
navIconRes = R.drawable.ic_hamburger,
navIconContentDescription = stringResource(id = R.string.navigation_drawer_open),
navigationActionClick = { (activity as? NavigationActivity)?.openNavigationDrawer() }
)
}
) { paddingValues ->
Box(
modifier = Modifier
.background(colorResource(R.color.backgroundLightest))
.padding(paddingValues)
.pullRefresh(pullRefreshState)
.fillMaxSize()
) {
when {
uiState.error != null -> {
ErrorContent(
errorMessage = uiState.error,
retryClick = uiState.onRetry,
modifier = Modifier
.fillMaxSize()
.testTag("errorContent")
)
}

uiState.loading -> {
Loading(modifier = Modifier
.fillMaxSize()
.testTag("loading"))
}

else -> {
EmptyContent(
emptyMessage = stringResource(id = R.string.noCoursesSubtext),
imageRes = R.drawable.ic_panda_nocourses,
modifier = Modifier
.fillMaxSize()
.testTag("emptyContent")
)
}
}

PullRefreshIndicator(
refreshing = uiState.refreshing,
state = pullRefreshState,
modifier = Modifier
.align(Alignment.TopCenter)
.testTag("dashboardPullRefreshIndicator")
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright (C) 2024 - 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.student.features.dashboard.compose

data class DashboardUiState(
val loading: Boolean = true,
val error: String? = null,
val refreshing: Boolean = false,
val onRefresh: () -> Unit = {},
val onRetry: () -> Unit = {}
)
Loading
Loading