diff --git a/apps/student/src/main/java/com/instructure/student/features/dashboard/compose/DashboardScreen.kt b/apps/student/src/main/java/com/instructure/student/features/dashboard/compose/DashboardScreen.kt
index bb57b76751..e4dfb14d3f 100644
--- a/apps/student/src/main/java/com/instructure/student/features/dashboard/compose/DashboardScreen.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/dashboard/compose/DashboardScreen.kt
@@ -59,10 +59,10 @@ import com.instructure.pandautils.compose.composables.Loading
import com.instructure.pandautils.features.dashboard.notifications.DashboardRouter
import com.instructure.pandautils.features.dashboard.widget.WidgetMetadata
import com.instructure.pandautils.features.dashboard.widget.courseinvitation.CourseInvitationsWidget
+import com.instructure.pandautils.features.dashboard.widget.welcome.WelcomeWidget
import com.instructure.pandautils.features.dashboard.widget.institutionalannouncements.InstitutionalAnnouncementsWidget
import com.instructure.student.R
import com.instructure.student.activity.NavigationActivity
-import com.instructure.student.features.dashboard.widget.welcome.WelcomeWidget
import kotlinx.coroutines.flow.SharedFlow
@Composable
diff --git a/libs/pandares/src/main/res/values/strings.xml b/libs/pandares/src/main/res/values/strings.xml
index e3e761ce13..590a0883f9 100644
--- a/libs/pandares/src/main/res/values/strings.xml
+++ b/libs/pandares/src/main/res/values/strings.xml
@@ -2230,6 +2230,123 @@
Decline Invitation
Are you sure you want to decline the invitation to %s?
+
+
+ Good morning, %s!
+ Good afternoon, %s!
+ Good evening, %s!
+ Good night, %s!
+
+
+ Good morning!
+ Good afternoon!
+ Good evening!
+ Good night!
+
+
+
+ - You\'ve got this.
+ - Keep going — you\'re stronger than you feel right now.
+ - One step at a time is still progress.
+ - Don\'t give up — future you will thank you.
+ - You\'re capable of more than you think.
+ - Even on tough days, you\'re moving forward.
+ - Trust yourself — you\'ve done hard things before.
+ - Progress, not perfection. You\'re doing great.
+ - Hang in there — you\'re not alone in this.
+ - You\'re learning, growing, and doing better than you realize.
+ - It\'s okay to stumble — you\'re still on the right path.
+ - Keep pushing — you\'re closer than you think.
+ - Small wins count too.
+ - Keep going — you\'re closer than you think.
+ - It\'s okay to pause. Breaks are part of learning.
+ - Trying is already a win.
+ - Progress > perfection.
+ - Showing up matters more than you know.
+ - You\'re building skills, even on slow days.
+ - Don\'t forget to breathe — you\'re doing fine.
+ - Your pace is the right pace.
+ - Not everything needs to be figured out today.
+ - You belong here.
+ - Every effort you make adds up.
+ - It\'s okay to start again — as many times as you need.
+ - What feels hard now will feel easier later.
+ - Keep showing up — that\'s what counts.
+ - Small steps move big mountains.
+ - Rest is part of progress too.
+ - You\'re doing better than you realize.
+ - Even slow progress is still progress.
+ - The future isn\'t built in a day — but you\'re on the way.
+ - One assignment, one moment, one step at a time.
+ - You don\'t have to be perfect to make an impact.
+ - Learning is messy — and that\'s normal.
+ - Every try is growth, even if it doesn\'t feel like it.
+ - You\'ve done hard things before — you can do this too.
+ - Your effort matters, even if no one sees it.
+ - It\'s okay to take things slow.
+ - You\'re moving forward, even on quiet days.
+ - The path doesn\'t need to be clear yet — keep walking.
+ - You\'re stronger than you feel right now.
+ - Big goals are built from small steps.
+ - Keep going — future you will thank you.
+ - Even messy progress is still progress.
+ - You\'re not behind — you\'re on your path.
+ - It\'s okay to learn as you go.
+ - You\'re growing in ways you might not see yet.
+ - Your effort today is an investment in tomorrow.
+
+
+
+ - Morning! You\'ve got this — one class, one step at a time.
+ - Not feeling ready? That\'s normal. Just start where you are.
+ - Coffee helps, but kindness to yourself works better.
+ - Tech acting up? Happens to all of us — don\'t stress.
+ - Today doesn\'t need to be perfect, just possible.
+ - Good morning — today is a new chance to learn and grow.
+ - Even small steps this morning move you closer to your goals.
+ - Take a breath — you don\'t need to have everything figured out yet.
+ - Technology can be tricky, but you\'re not alone in learning it.
+
+
+
+ - Halfway there — you\'ve already done more than you think.
+ - Feeling stuck? Everyone hits walls, just don\'t stop climbing.
+ - Jobs, grades, the future… no one has it all figured out yet.
+ - Brain tired? Quick break = better focus later.
+ - Ask for help. Seriously, no one\'s doing this solo.
+ - You\'ve already made it this far today — that\'s something to be proud of.
+ - Need a pause? Recharging is part of learning too.
+ - It\'s okay if the path feels uncertain — skills build step by step.
+ - Reach out if you\'re stuck — support is always closer than it feels.
+
+
+
+ - Made it through the day — that\'s a win in itself.
+ - Missing people? Shoot someone a quick "hey" — it helps.
+ - Even if today felt messy, you showed up. That matters.
+ - Remember: no grade measures your worth.
+ - Relax, laugh, or scroll guilt-free — you earned it.
+ - Well done getting through the day — progress counts, even when it\'s quiet.
+ - Missing friends or mentors? Connection can come in small moments too.
+ - Evenings are for reflection — notice what you\'ve learned today, not just what\'s left to do.
+ - Your effort matters more than perfection.
+
+
+
+ - Still grinding? Respect — but don\'t forget sleep exists.
+ - Tomorrow you\'ll thank yourself for resting tonight.
+ - Anxiety gets louder at night — don\'t believe all its noise.
+ - You\'re not behind, you\'re just on your path.
+ - Close the laptop — your brain needs dreams too.
+ - It\'s okay to rest — tomorrow is waiting with new opportunities.
+ - Learning is a marathon, not a sprint. Be kind to yourself tonight.
+ - If worries feel heavy, remember you don\'t have to carry them alone.
+ - End the day knowing that trying is already an achievement.
+
+
+
+ Welcome message: %1$s. %2$s
+
Announcements (%d)
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/ApplicationModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/ApplicationModule.kt
index dbea27770a..4c4b89934b 100644
--- a/libs/pandautils/src/main/java/com/instructure/pandautils/di/ApplicationModule.kt
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/ApplicationModule.kt
@@ -52,6 +52,7 @@ import org.threeten.bp.Clock
import java.util.Locale
import java.util.TimeZone
import javax.inject.Singleton
+import kotlin.random.Random
/**
* Module that provides all the application scope dependencies, that are not related to other module.
@@ -189,4 +190,9 @@ class ApplicationModule {
fun provideFileCache(): FileCache {
return FileCache
}
+
+ @Provides
+ fun provideRandom(): Random {
+ return Random.Default
+ }
}
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/TimeOfDay.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/TimeOfDay.kt
new file mode 100644
index 0000000000..a5956121d2
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/TimeOfDay.kt
@@ -0,0 +1,24 @@
+/*
+ * 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 .
+ */
+
+package com.instructure.pandautils.features.dashboard.widget.welcome
+
+enum class TimeOfDay {
+ MORNING, // 4am - 12pm
+ AFTERNOON, // 12pm - 5pm
+ EVENING, // 5pm - 9pm
+ NIGHT // 9pm - 4am
+}
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/TimeOfDayCalculator.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/TimeOfDayCalculator.kt
new file mode 100644
index 0000000000..c44097f1e9
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/TimeOfDayCalculator.kt
@@ -0,0 +1,31 @@
+/*
+ * 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 .
+ */
+
+package com.instructure.pandautils.features.dashboard.widget.welcome
+
+class TimeOfDayCalculator(private val timeProvider: TimeProvider) {
+
+ fun getTimeOfDay(): TimeOfDay {
+ val hour = timeProvider.getCurrentHourOfDay()
+ return when {
+ hour < 4 -> TimeOfDay.NIGHT
+ hour < 12 -> TimeOfDay.MORNING
+ hour < 17 -> TimeOfDay.AFTERNOON
+ hour < 21 -> TimeOfDay.EVENING
+ else -> TimeOfDay.NIGHT
+ }
+ }
+}
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/TimeProvider.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/TimeProvider.kt
new file mode 100644
index 0000000000..7398c968bb
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/TimeProvider.kt
@@ -0,0 +1,29 @@
+/*
+ * 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 .
+ */
+
+package com.instructure.pandautils.features.dashboard.widget.welcome
+
+import java.util.Calendar
+
+interface TimeProvider {
+ fun getCurrentHourOfDay(): Int
+}
+
+class SystemTimeProvider : TimeProvider {
+ override fun getCurrentHourOfDay(): Int {
+ return Calendar.getInstance().get(Calendar.HOUR_OF_DAY)
+ }
+}
diff --git a/apps/student/src/main/java/com/instructure/student/features/dashboard/widget/welcome/WelcomeWidgetScreen.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/WelcomeWidget.kt
similarity index 70%
rename from apps/student/src/main/java/com/instructure/student/features/dashboard/widget/welcome/WelcomeWidgetScreen.kt
rename to libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/WelcomeWidget.kt
index 08bf943659..4f37e79851 100644
--- a/apps/student/src/main/java/com/instructure/student/features/dashboard/widget/welcome/WelcomeWidgetScreen.kt
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/WelcomeWidget.kt
@@ -14,23 +14,27 @@
* along with this program. If not, see .
*/
-package com.instructure.student.features.dashboard.widget.welcome
+package com.instructure.pandautils.features.dashboard.widget.welcome
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
-import com.instructure.student.R
+import com.instructure.pandautils.R
import kotlinx.coroutines.flow.SharedFlow
@Composable
@@ -41,6 +45,12 @@ fun WelcomeWidget(
val viewModel: WelcomeWidgetViewModel = hiltViewModel()
val uiState by viewModel.uiState.collectAsState()
+ LaunchedEffect(refreshSignal) {
+ refreshSignal.collect {
+ viewModel.refresh()
+ }
+ }
+
WelcomeContent(
modifier = modifier,
uiState = uiState
@@ -52,7 +62,17 @@ private fun WelcomeContent(
modifier: Modifier = Modifier,
uiState: WelcomeWidgetUiState
) {
- Column(modifier = modifier.padding(horizontal = 16.dp)) {
+ val contentDescriptionText = stringResource(
+ R.string.welcomeWidgetContentDescription,
+ uiState.greeting,
+ uiState.message
+ )
+
+ Column(
+ modifier = modifier
+ .padding(horizontal = 16.dp)
+ .semantics { contentDescription = contentDescriptionText }
+ ) {
Text(
modifier = Modifier.fillMaxWidth(),
text = uiState.greeting,
@@ -61,11 +81,15 @@ private fun WelcomeContent(
color = colorResource(R.color.textDarkest),
lineHeight = 29.sp
)
- Text(modifier = Modifier.fillMaxWidth(),
+ Text(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 2.dp),
text = uiState.message,
fontSize = 14.sp,
color = colorResource(R.color.textDarkest),
- lineHeight = 19.sp)
+ lineHeight = 19.sp
+ )
}
}
@@ -75,8 +99,8 @@ private fun WelcomeContent(
fun WelcomeContentPreview() {
WelcomeContent(
uiState = WelcomeWidgetUiState(
- greeting = "Welcome back, Student!",
- message = "Here's what's happening in your courses today."
+ greeting = "Good morning, Riley!",
+ message = "Every small step you take is progress. Keep going!"
)
)
-}
\ No newline at end of file
+}
diff --git a/apps/student/src/main/java/com/instructure/student/features/dashboard/widget/welcome/WelcomeWidgetUiState.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/WelcomeWidgetUiState.kt
similarity index 91%
rename from apps/student/src/main/java/com/instructure/student/features/dashboard/widget/welcome/WelcomeWidgetUiState.kt
rename to libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/WelcomeWidgetUiState.kt
index e69994e2a6..91b28b47bd 100644
--- a/apps/student/src/main/java/com/instructure/student/features/dashboard/widget/welcome/WelcomeWidgetUiState.kt
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/WelcomeWidgetUiState.kt
@@ -14,9 +14,9 @@
* along with this program. If not, see .
*/
-package com.instructure.student.features.dashboard.widget.welcome
+package com.instructure.pandautils.features.dashboard.widget.welcome
data class WelcomeWidgetUiState(
val greeting: String = "",
val message: String = ""
-)
\ No newline at end of file
+)
diff --git a/apps/student/src/main/java/com/instructure/student/features/dashboard/widget/welcome/WelcomeWidgetViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/WelcomeWidgetViewModel.kt
similarity index 62%
rename from apps/student/src/main/java/com/instructure/student/features/dashboard/widget/welcome/WelcomeWidgetViewModel.kt
rename to libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/WelcomeWidgetViewModel.kt
index 3cd2e293e5..56166dbea9 100644
--- a/apps/student/src/main/java/com/instructure/student/features/dashboard/widget/welcome/WelcomeWidgetViewModel.kt
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/WelcomeWidgetViewModel.kt
@@ -14,9 +14,11 @@
* along with this program. If not, see .
*/
-package com.instructure.student.features.dashboard.widget.welcome
+package com.instructure.pandautils.features.dashboard.widget.welcome
import androidx.lifecycle.ViewModel
+import com.instructure.pandautils.features.dashboard.widget.welcome.usecase.GetWelcomeGreetingUseCase
+import com.instructure.pandautils.features.dashboard.widget.welcome.usecase.GetWelcomeMessageUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -25,17 +27,28 @@ import kotlinx.coroutines.flow.update
import javax.inject.Inject
@HiltViewModel
-class WelcomeWidgetViewModel @Inject constructor() : ViewModel() {
+class WelcomeWidgetViewModel @Inject constructor(
+ private val getWelcomeGreetingUseCase: GetWelcomeGreetingUseCase,
+ private val getWelcomeMessageUseCase: GetWelcomeMessageUseCase
+) : ViewModel() {
private val _uiState = MutableStateFlow(WelcomeWidgetUiState())
val uiState: StateFlow = _uiState.asStateFlow()
init {
+ loadWelcomeContent()
+ }
+
+ fun refresh() {
+ loadWelcomeContent()
+ }
+
+ private fun loadWelcomeContent() {
_uiState.update {
it.copy(
- greeting = "Welcome back, Learner!",
- message = "Here you can find an overview of your courses and activities."
+ greeting = getWelcomeGreetingUseCase(),
+ message = getWelcomeMessageUseCase()
)
}
}
-}
\ No newline at end of file
+}
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/di/WelcomeWidgetModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/di/WelcomeWidgetModule.kt
new file mode 100644
index 0000000000..d5e68f782a
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/di/WelcomeWidgetModule.kt
@@ -0,0 +1,38 @@
+/*
+ * 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 .
+ */
+
+package com.instructure.pandautils.features.dashboard.widget.welcome.di
+
+import com.instructure.pandautils.features.dashboard.widget.welcome.SystemTimeProvider
+import com.instructure.pandautils.features.dashboard.widget.welcome.TimeOfDayCalculator
+import com.instructure.pandautils.features.dashboard.widget.welcome.TimeProvider
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+
+@Module
+@InstallIn(ViewModelComponent::class)
+class WelcomeWidgetModule {
+
+ @Provides
+ fun provideTimeProvider(): TimeProvider = SystemTimeProvider()
+
+ @Provides
+ fun provideTimeOfDayCalculator(timeProvider: TimeProvider): TimeOfDayCalculator {
+ return TimeOfDayCalculator(timeProvider)
+ }
+}
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/usecase/GetWelcomeGreetingUseCase.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/usecase/GetWelcomeGreetingUseCase.kt
new file mode 100644
index 0000000000..40937f7783
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/usecase/GetWelcomeGreetingUseCase.kt
@@ -0,0 +1,52 @@
+/*
+ * 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 .
+ */
+
+package com.instructure.pandautils.features.dashboard.widget.welcome.usecase
+
+import android.content.res.Resources
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.pandautils.R
+import com.instructure.pandautils.features.dashboard.widget.welcome.TimeOfDay
+import com.instructure.pandautils.features.dashboard.widget.welcome.TimeOfDayCalculator
+import javax.inject.Inject
+
+class GetWelcomeGreetingUseCase @Inject constructor(
+ private val resources: Resources,
+ private val timeOfDayCalculator: TimeOfDayCalculator,
+ private val apiPrefs: ApiPrefs
+) {
+
+ operator fun invoke(): String {
+ val timeOfDay = timeOfDayCalculator.getTimeOfDay()
+ val firstName = apiPrefs.user?.shortName
+
+ return if (!firstName.isNullOrBlank()) {
+ when (timeOfDay) {
+ TimeOfDay.MORNING -> resources.getString(R.string.welcomeGreetingMorningWithName, firstName)
+ TimeOfDay.AFTERNOON -> resources.getString(R.string.welcomeGreetingAfternoonWithName, firstName)
+ TimeOfDay.EVENING -> resources.getString(R.string.welcomeGreetingEveningWithName, firstName)
+ TimeOfDay.NIGHT -> resources.getString(R.string.welcomeGreetingNightWithName, firstName)
+ }
+ } else {
+ when (timeOfDay) {
+ TimeOfDay.MORNING -> resources.getString(R.string.welcomeGreetingMorning)
+ TimeOfDay.AFTERNOON -> resources.getString(R.string.welcomeGreetingAfternoon)
+ TimeOfDay.EVENING -> resources.getString(R.string.welcomeGreetingEvening)
+ TimeOfDay.NIGHT -> resources.getString(R.string.welcomeGreetingNight)
+ }
+ }
+ }
+}
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/usecase/GetWelcomeMessageUseCase.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/usecase/GetWelcomeMessageUseCase.kt
new file mode 100644
index 0000000000..924bcd6e02
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/usecase/GetWelcomeMessageUseCase.kt
@@ -0,0 +1,46 @@
+/*
+ * 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 .
+ */
+
+package com.instructure.pandautils.features.dashboard.widget.welcome.usecase
+
+import android.content.res.Resources
+import com.instructure.pandautils.R
+import com.instructure.pandautils.features.dashboard.widget.welcome.TimeOfDay
+import com.instructure.pandautils.features.dashboard.widget.welcome.TimeOfDayCalculator
+import javax.inject.Inject
+import kotlin.random.Random
+
+class GetWelcomeMessageUseCase @Inject constructor(
+ private val resources: Resources,
+ private val timeOfDayCalculator: TimeOfDayCalculator,
+ private val random: Random
+) {
+
+ operator fun invoke(): String {
+ val timeOfDay = timeOfDayCalculator.getTimeOfDay()
+
+ val genericMessages = resources.getStringArray(R.array.welcomeMessagesGeneric)
+ val timeSpecificMessages = when (timeOfDay) {
+ TimeOfDay.MORNING -> resources.getStringArray(R.array.welcomeMessagesMorning)
+ TimeOfDay.AFTERNOON -> resources.getStringArray(R.array.welcomeMessagesAfternoon)
+ TimeOfDay.EVENING -> resources.getStringArray(R.array.welcomeMessagesEvening)
+ TimeOfDay.NIGHT -> resources.getStringArray(R.array.welcomeMessagesNight)
+ }
+
+ val allMessages = genericMessages + timeSpecificMessages
+ return allMessages[random.nextInt(allMessages.size)]
+ }
+}
diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/widget/welcome/GetWelcomeGreetingUseCaseTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/widget/welcome/GetWelcomeGreetingUseCaseTest.kt
new file mode 100644
index 0000000000..24bb05c231
--- /dev/null
+++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/widget/welcome/GetWelcomeGreetingUseCaseTest.kt
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2025 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.instructure.pandautils.features.dashboard.widget.welcome
+
+import android.content.res.Resources
+import com.instructure.canvasapi2.models.User
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.pandautils.R
+import com.instructure.pandautils.features.dashboard.widget.welcome.usecase.GetWelcomeGreetingUseCase
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkObject
+import io.mockk.unmockkAll
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+
+class GetWelcomeGreetingUseCaseTest {
+
+ private val resources: Resources = mockk()
+ private val timeOfDayCalculator: TimeOfDayCalculator = mockk()
+ private val apiPrefs: ApiPrefs = mockk()
+
+ private lateinit var useCase: GetWelcomeGreetingUseCase
+
+ @Before
+ fun setUp() {
+ mockkObject(ApiPrefs)
+ useCase = GetWelcomeGreetingUseCase(resources, timeOfDayCalculator, apiPrefs)
+ }
+
+ @After
+ fun tearDown() {
+ unmockkAll()
+ }
+
+ @Test
+ fun `invoke returns morning greeting with name when user has short name`() {
+ val user = User(shortName = "Riley")
+ every { apiPrefs.user } returns user
+ every { timeOfDayCalculator.getTimeOfDay() } returns TimeOfDay.MORNING
+ every { resources.getString(R.string.welcomeGreetingMorningWithName, "Riley") } returns "Good morning, Riley!"
+
+ val result = useCase()
+
+ assertEquals("Good morning, Riley!", result)
+ }
+
+ @Test
+ fun `invoke returns afternoon greeting with name when user has short name`() {
+ val user = User(shortName = "Riley")
+ every { apiPrefs.user } returns user
+ every { timeOfDayCalculator.getTimeOfDay() } returns TimeOfDay.AFTERNOON
+ every { resources.getString(R.string.welcomeGreetingAfternoonWithName, "Riley") } returns "Good afternoon, Riley!"
+
+ val result = useCase()
+
+ assertEquals("Good afternoon, Riley!", result)
+ }
+
+ @Test
+ fun `invoke returns evening greeting with name when user has short name`() {
+ val user = User(shortName = "Riley")
+ every { apiPrefs.user } returns user
+ every { timeOfDayCalculator.getTimeOfDay() } returns TimeOfDay.EVENING
+ every { resources.getString(R.string.welcomeGreetingEveningWithName, "Riley") } returns "Good evening, Riley!"
+
+ val result = useCase()
+
+ assertEquals("Good evening, Riley!", result)
+ }
+
+ @Test
+ fun `invoke returns night greeting with name when user has short name`() {
+ val user = User(shortName = "Riley")
+ every { apiPrefs.user } returns user
+ every { timeOfDayCalculator.getTimeOfDay() } returns TimeOfDay.NIGHT
+ every { resources.getString(R.string.welcomeGreetingNightWithName, "Riley") } returns "Good night, Riley!"
+
+ val result = useCase()
+
+ assertEquals("Good night, Riley!", result)
+ }
+
+ @Test
+ fun `invoke returns morning greeting without name when user has null short name`() {
+ val user = User(shortName = null)
+ every { apiPrefs.user } returns user
+ every { timeOfDayCalculator.getTimeOfDay() } returns TimeOfDay.MORNING
+ every { resources.getString(R.string.welcomeGreetingMorning) } returns "Good morning!"
+
+ val result = useCase()
+
+ assertEquals("Good morning!", result)
+ }
+
+ @Test
+ fun `invoke returns morning greeting without name when user has blank short name`() {
+ val user = User(shortName = " ")
+ every { apiPrefs.user } returns user
+ every { timeOfDayCalculator.getTimeOfDay() } returns TimeOfDay.MORNING
+ every { resources.getString(R.string.welcomeGreetingMorning) } returns "Good morning!"
+
+ val result = useCase()
+
+ assertEquals("Good morning!", result)
+ }
+
+ @Test
+ fun `invoke returns morning greeting without name when user is null`() {
+ every { apiPrefs.user } returns null
+ every { timeOfDayCalculator.getTimeOfDay() } returns TimeOfDay.MORNING
+ every { resources.getString(R.string.welcomeGreetingMorning) } returns "Good morning!"
+
+ val result = useCase()
+
+ assertEquals("Good morning!", result)
+ }
+
+ @Test
+ fun `invoke returns afternoon greeting without name when user is null`() {
+ every { apiPrefs.user } returns null
+ every { timeOfDayCalculator.getTimeOfDay() } returns TimeOfDay.AFTERNOON
+ every { resources.getString(R.string.welcomeGreetingAfternoon) } returns "Good afternoon!"
+
+ val result = useCase()
+
+ assertEquals("Good afternoon!", result)
+ }
+
+ @Test
+ fun `invoke returns evening greeting without name when user is null`() {
+ every { apiPrefs.user } returns null
+ every { timeOfDayCalculator.getTimeOfDay() } returns TimeOfDay.EVENING
+ every { resources.getString(R.string.welcomeGreetingEvening) } returns "Good evening!"
+
+ val result = useCase()
+
+ assertEquals("Good evening!", result)
+ }
+
+ @Test
+ fun `invoke returns night greeting without name when user is null`() {
+ every { apiPrefs.user } returns null
+ every { timeOfDayCalculator.getTimeOfDay() } returns TimeOfDay.NIGHT
+ every { resources.getString(R.string.welcomeGreetingNight) } returns "Good night!"
+
+ val result = useCase()
+
+ assertEquals("Good night!", result)
+ }
+}
\ No newline at end of file
diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/widget/welcome/GetWelcomeMessageUseCaseTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/widget/welcome/GetWelcomeMessageUseCaseTest.kt
new file mode 100644
index 0000000000..b6d1efa130
--- /dev/null
+++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/widget/welcome/GetWelcomeMessageUseCaseTest.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2025 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.instructure.pandautils.features.dashboard.widget.welcome
+
+import android.content.res.Resources
+import com.instructure.pandautils.R
+import com.instructure.pandautils.features.dashboard.widget.welcome.usecase.GetWelcomeMessageUseCase
+import io.mockk.every
+import io.mockk.mockk
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import kotlin.random.Random
+
+class GetWelcomeMessageUseCaseTest {
+
+ private val resources: Resources = mockk()
+ private val timeOfDayCalculator: TimeOfDayCalculator = mockk()
+ private val random: Random = mockk()
+
+ private val useCase = GetWelcomeMessageUseCase(resources, timeOfDayCalculator, random)
+
+ @Test
+ fun `invoke returns first generic message when random returns 0 and time is morning`() {
+ val genericMessages = arrayOf("Generic message 1", "Generic message 2")
+ val morningMessages = arrayOf("Morning message 1", "Morning message 2")
+
+ every { timeOfDayCalculator.getTimeOfDay() } returns TimeOfDay.MORNING
+ every { resources.getStringArray(R.array.welcomeMessagesGeneric) } returns genericMessages
+ every { resources.getStringArray(R.array.welcomeMessagesMorning) } returns morningMessages
+ every { random.nextInt(4) } returns 0
+
+ val result = useCase()
+
+ assertEquals("Generic message 1", result)
+ }
+
+ @Test
+ fun `invoke returns first morning-specific message when random returns 2 and time is morning`() {
+ val genericMessages = arrayOf("Generic message 1", "Generic message 2")
+ val morningMessages = arrayOf("Morning message 1", "Morning message 2")
+
+ every { timeOfDayCalculator.getTimeOfDay() } returns TimeOfDay.MORNING
+ every { resources.getStringArray(R.array.welcomeMessagesGeneric) } returns genericMessages
+ every { resources.getStringArray(R.array.welcomeMessagesMorning) } returns morningMessages
+ every { random.nextInt(4) } returns 2
+
+ val result = useCase()
+
+ assertEquals("Morning message 1", result)
+ }
+
+ @Test
+ fun `invoke returns afternoon-specific message when random returns 3 and time is afternoon`() {
+ val genericMessages = arrayOf("Generic message 1", "Generic message 2")
+ val afternoonMessages = arrayOf("Afternoon message 1", "Afternoon message 2")
+
+ every { timeOfDayCalculator.getTimeOfDay() } returns TimeOfDay.AFTERNOON
+ every { resources.getStringArray(R.array.welcomeMessagesGeneric) } returns genericMessages
+ every { resources.getStringArray(R.array.welcomeMessagesAfternoon) } returns afternoonMessages
+ every { random.nextInt(4) } returns 3
+
+ val result = useCase()
+
+ assertEquals("Afternoon message 2", result)
+ }
+
+ @Test
+ fun `invoke returns evening-specific message when random returns 2 and time is evening`() {
+ val genericMessages = arrayOf("Generic message 1", "Generic message 2")
+ val eveningMessages = arrayOf("Evening message 1", "Evening message 2")
+
+ every { timeOfDayCalculator.getTimeOfDay() } returns TimeOfDay.EVENING
+ every { resources.getStringArray(R.array.welcomeMessagesGeneric) } returns genericMessages
+ every { resources.getStringArray(R.array.welcomeMessagesEvening) } returns eveningMessages
+ every { random.nextInt(4) } returns 2
+
+ val result = useCase()
+
+ assertEquals("Evening message 1", result)
+ }
+
+ @Test
+ fun `invoke returns night-specific message when random returns 3 and time is night`() {
+ val genericMessages = arrayOf("Generic message 1", "Generic message 2")
+ val nightMessages = arrayOf("Night message 1", "Night message 2")
+
+ every { timeOfDayCalculator.getTimeOfDay() } returns TimeOfDay.NIGHT
+ every { resources.getStringArray(R.array.welcomeMessagesGeneric) } returns genericMessages
+ every { resources.getStringArray(R.array.welcomeMessagesNight) } returns nightMessages
+ every { random.nextInt(4) } returns 3
+
+ val result = useCase()
+
+ assertEquals("Night message 2", result)
+ }
+}
\ No newline at end of file
diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/widget/welcome/TimeOfDayCalculatorTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/widget/welcome/TimeOfDayCalculatorTest.kt
new file mode 100644
index 0000000000..6ff12064d5
--- /dev/null
+++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/widget/welcome/TimeOfDayCalculatorTest.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2025 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.instructure.pandautils.features.dashboard.widget.welcome
+
+import io.mockk.every
+import io.mockk.mockk
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class TimeOfDayCalculatorTest {
+
+ private val timeProvider: TimeProvider = mockk()
+ private val calculator = TimeOfDayCalculator(timeProvider)
+
+ @Test
+ fun `getTimeOfDay returns NIGHT when hour is 0`() {
+ every { timeProvider.getCurrentHourOfDay() } returns 0
+ assertEquals(TimeOfDay.NIGHT, calculator.getTimeOfDay())
+ }
+
+ @Test
+ fun `getTimeOfDay returns NIGHT when hour is 3`() {
+ every { timeProvider.getCurrentHourOfDay() } returns 3
+ assertEquals(TimeOfDay.NIGHT, calculator.getTimeOfDay())
+ }
+
+ @Test
+ fun `getTimeOfDay returns MORNING when hour is 4`() {
+ every { timeProvider.getCurrentHourOfDay() } returns 4
+ assertEquals(TimeOfDay.MORNING, calculator.getTimeOfDay())
+ }
+
+ @Test
+ fun `getTimeOfDay returns MORNING when hour is 8`() {
+ every { timeProvider.getCurrentHourOfDay() } returns 8
+ assertEquals(TimeOfDay.MORNING, calculator.getTimeOfDay())
+ }
+
+ @Test
+ fun `getTimeOfDay returns MORNING when hour is 11`() {
+ every { timeProvider.getCurrentHourOfDay() } returns 11
+ assertEquals(TimeOfDay.MORNING, calculator.getTimeOfDay())
+ }
+
+ @Test
+ fun `getTimeOfDay returns AFTERNOON when hour is 12`() {
+ every { timeProvider.getCurrentHourOfDay() } returns 12
+ assertEquals(TimeOfDay.AFTERNOON, calculator.getTimeOfDay())
+ }
+
+ @Test
+ fun `getTimeOfDay returns AFTERNOON when hour is 14`() {
+ every { timeProvider.getCurrentHourOfDay() } returns 14
+ assertEquals(TimeOfDay.AFTERNOON, calculator.getTimeOfDay())
+ }
+
+ @Test
+ fun `getTimeOfDay returns AFTERNOON when hour is 16`() {
+ every { timeProvider.getCurrentHourOfDay() } returns 16
+ assertEquals(TimeOfDay.AFTERNOON, calculator.getTimeOfDay())
+ }
+
+ @Test
+ fun `getTimeOfDay returns EVENING when hour is 17`() {
+ every { timeProvider.getCurrentHourOfDay() } returns 17
+ assertEquals(TimeOfDay.EVENING, calculator.getTimeOfDay())
+ }
+
+ @Test
+ fun `getTimeOfDay returns EVENING when hour is 19`() {
+ every { timeProvider.getCurrentHourOfDay() } returns 19
+ assertEquals(TimeOfDay.EVENING, calculator.getTimeOfDay())
+ }
+
+ @Test
+ fun `getTimeOfDay returns EVENING when hour is 20`() {
+ every { timeProvider.getCurrentHourOfDay() } returns 20
+ assertEquals(TimeOfDay.EVENING, calculator.getTimeOfDay())
+ }
+
+ @Test
+ fun `getTimeOfDay returns NIGHT when hour is 21`() {
+ every { timeProvider.getCurrentHourOfDay() } returns 21
+ assertEquals(TimeOfDay.NIGHT, calculator.getTimeOfDay())
+ }
+
+ @Test
+ fun `getTimeOfDay returns NIGHT when hour is 22`() {
+ every { timeProvider.getCurrentHourOfDay() } returns 22
+ assertEquals(TimeOfDay.NIGHT, calculator.getTimeOfDay())
+ }
+
+ @Test
+ fun `getTimeOfDay returns NIGHT when hour is 23`() {
+ every { timeProvider.getCurrentHourOfDay() } returns 23
+ assertEquals(TimeOfDay.NIGHT, calculator.getTimeOfDay())
+ }
+}
diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/widget/welcome/WelcomeWidgetViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/widget/welcome/WelcomeWidgetViewModelTest.kt
new file mode 100644
index 0000000000..ca228f5ed6
--- /dev/null
+++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/widget/welcome/WelcomeWidgetViewModelTest.kt
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2025 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.instructure.pandautils.features.dashboard.widget.welcome
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import com.instructure.pandautils.features.dashboard.widget.welcome.usecase.GetWelcomeGreetingUseCase
+import com.instructure.pandautils.features.dashboard.widget.welcome.usecase.GetWelcomeMessageUseCase
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.unmockkAll
+import io.mockk.verify
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+@ExperimentalCoroutinesApi
+class WelcomeWidgetViewModelTest {
+
+ @get:Rule
+ var instantExecutorRule = InstantTaskExecutorRule()
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val getWelcomeGreetingUseCase: GetWelcomeGreetingUseCase = mockk()
+ private val getWelcomeMessageUseCase: GetWelcomeMessageUseCase = mockk()
+
+ private lateinit var viewModel: WelcomeWidgetViewModel
+
+ @Before
+ fun setUp() {
+ Dispatchers.setMain(testDispatcher)
+ }
+
+ @After
+ fun tearDown() {
+ Dispatchers.resetMain()
+ unmockkAll()
+ }
+
+ @Test
+ fun `init loads greeting and message`() {
+ every { getWelcomeGreetingUseCase() } returns "Good morning, Riley!"
+ every { getWelcomeMessageUseCase() } returns "Every small step you take is progress."
+
+ viewModel = createViewModel()
+
+ val state = viewModel.uiState.value
+ assertEquals("Good morning, Riley!", state.greeting)
+ assertEquals("Every small step you take is progress.", state.message)
+ verify(exactly = 1) { getWelcomeGreetingUseCase() }
+ verify(exactly = 1) { getWelcomeMessageUseCase() }
+ }
+
+ @Test
+ fun `init loads greeting without name when user has no short name`() {
+ every { getWelcomeGreetingUseCase() } returns "Good morning!"
+ every { getWelcomeMessageUseCase() } returns "Start your day with purpose."
+
+ viewModel = createViewModel()
+
+ val state = viewModel.uiState.value
+ assertEquals("Good morning!", state.greeting)
+ assertEquals("Start your day with purpose.", state.message)
+ }
+
+ @Test
+ fun `init loads afternoon greeting`() {
+ every { getWelcomeGreetingUseCase() } returns "Good afternoon, Riley!"
+ every { getWelcomeMessageUseCase() } returns "Keep up the great work."
+
+ viewModel = createViewModel()
+
+ val state = viewModel.uiState.value
+ assertEquals("Good afternoon, Riley!", state.greeting)
+ assertEquals("Keep up the great work.", state.message)
+ }
+
+ @Test
+ fun `init loads evening greeting`() {
+ every { getWelcomeGreetingUseCase() } returns "Good evening, Riley!"
+ every { getWelcomeMessageUseCase() } returns "Finish strong today."
+
+ viewModel = createViewModel()
+
+ val state = viewModel.uiState.value
+ assertEquals("Good evening, Riley!", state.greeting)
+ assertEquals("Finish strong today.", state.message)
+ }
+
+ @Test
+ fun `init loads night greeting`() {
+ every { getWelcomeGreetingUseCase() } returns "Good night, Riley!"
+ every { getWelcomeMessageUseCase() } returns "Rest well, you earned it."
+
+ viewModel = createViewModel()
+
+ val state = viewModel.uiState.value
+ assertEquals("Good night, Riley!", state.greeting)
+ assertEquals("Rest well, you earned it.", state.message)
+ }
+
+ @Test
+ fun `refresh updates greeting and message`() {
+ every { getWelcomeGreetingUseCase() } returns "Good morning, Riley!"
+ every { getWelcomeMessageUseCase() } returns "First message"
+
+ viewModel = createViewModel()
+
+ val initialState = viewModel.uiState.value
+ assertEquals("Good morning, Riley!", initialState.greeting)
+ assertEquals("First message", initialState.message)
+
+ every { getWelcomeGreetingUseCase() } returns "Good morning, Riley!"
+ every { getWelcomeMessageUseCase() } returns "Second message"
+
+ viewModel.refresh()
+
+ val refreshedState = viewModel.uiState.value
+ assertEquals("Good morning, Riley!", refreshedState.greeting)
+ assertEquals("Second message", refreshedState.message)
+ verify(exactly = 2) { getWelcomeGreetingUseCase() }
+ verify(exactly = 2) { getWelcomeMessageUseCase() }
+ }
+
+ @Test
+ fun `refresh updates greeting when time changes`() {
+ every { getWelcomeGreetingUseCase() } returns "Good morning, Riley!"
+ every { getWelcomeMessageUseCase() } returns "Morning message"
+
+ viewModel = createViewModel()
+
+ val initialState = viewModel.uiState.value
+ assertEquals("Good morning, Riley!", initialState.greeting)
+
+ every { getWelcomeGreetingUseCase() } returns "Good afternoon, Riley!"
+ every { getWelcomeMessageUseCase() } returns "Afternoon message"
+
+ viewModel.refresh()
+
+ val refreshedState = viewModel.uiState.value
+ assertEquals("Good afternoon, Riley!", refreshedState.greeting)
+ assertEquals("Afternoon message", refreshedState.message)
+ }
+
+ @Test
+ fun `multiple refresh calls update state correctly`() {
+ every { getWelcomeGreetingUseCase() } returns "Good morning, Riley!"
+ every { getWelcomeMessageUseCase() } returnsMany listOf(
+ "Message 1",
+ "Message 2",
+ "Message 3"
+ )
+
+ viewModel = createViewModel()
+
+ assertEquals("Message 1", viewModel.uiState.value.message)
+
+ viewModel.refresh()
+ assertEquals("Message 2", viewModel.uiState.value.message)
+
+ viewModel.refresh()
+ assertEquals("Message 3", viewModel.uiState.value.message)
+
+ verify(exactly = 3) { getWelcomeGreetingUseCase() }
+ verify(exactly = 3) { getWelcomeMessageUseCase() }
+ }
+
+ @Test
+ fun `uiState initial values are empty strings`() {
+ every { getWelcomeGreetingUseCase() } returns "Good morning, Riley!"
+ every { getWelcomeMessageUseCase() } returns "Test message"
+
+ // Create ViewModel but check state before init completes would show empty strings
+ // However, since init runs immediately, we verify the pattern is correct
+ viewModel = createViewModel()
+
+ // After init, state should be populated
+ val state = viewModel.uiState.value
+ assertEquals("Good morning, Riley!", state.greeting)
+ assertEquals("Test message", state.message)
+ }
+
+ private fun createViewModel(): WelcomeWidgetViewModel {
+ return WelcomeWidgetViewModel(
+ getWelcomeGreetingUseCase = getWelcomeGreetingUseCase,
+ getWelcomeMessageUseCase = getWelcomeMessageUseCase
+ )
+ }
+}
\ No newline at end of file