mirror of
https://github.com/wismna/DoNext.git
synced 2026-07-25 20:23:06 -04:00
Update SDK, Gradle and packages
Fix timezone issues Add timezone tests
This commit is contained in:
+12
-12
@@ -14,7 +14,7 @@ ksp {
|
||||
|
||||
android {
|
||||
namespace = "com.wismna.geoffroy.donext"
|
||||
compileSdk = 36
|
||||
compileSdk = 37
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.wismna.geoffroy.donext"
|
||||
@@ -82,35 +82,35 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.9.4")
|
||||
implementation("androidx.activity:activity-compose:1.11.0")
|
||||
implementation(platform("androidx.compose:compose-bom:2025.11.00"))
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.11.0")
|
||||
implementation("androidx.activity:activity-compose:1.13.0")
|
||||
implementation(platform("androidx.compose:compose-bom:2026.06.01"))
|
||||
implementation("androidx.compose.ui:ui")
|
||||
implementation("androidx.compose.ui:ui-graphics")
|
||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||
implementation("androidx.compose.material3:material3")
|
||||
implementation("androidx.compose.material3:material3-window-size-class:1.4.0")
|
||||
implementation("androidx.compose.material:material-icons-extended:1.7.8")
|
||||
implementation("androidx.navigation:navigation-compose:2.9.6")
|
||||
implementation("androidx.hilt:hilt-navigation-compose:1.3.0")
|
||||
implementation("sh.calvin.reorderable:reorderable:3.0.0")
|
||||
implementation("androidx.navigation:navigation-compose:2.9.8")
|
||||
implementation("androidx.hilt:hilt-navigation-compose:1.4.0")
|
||||
implementation("sh.calvin.reorderable:reorderable:3.1.0")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
testImplementation("io.mockk:mockk:1.14.6")
|
||||
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2")
|
||||
testImplementation("io.mockk:mockk:1.14.11")
|
||||
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0")
|
||||
testImplementation("com.google.truth:truth:1.4.5")
|
||||
androidTestImplementation("androidx.test.ext:junit-ktx:1.3.0")
|
||||
androidTestImplementation(platform("androidx.compose:compose-bom:2025.11.00"))
|
||||
androidTestImplementation(platform("androidx.compose:compose-bom:2026.06.01"))
|
||||
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
||||
androidTestImplementation("com.google.truth:truth:1.4.5")
|
||||
debugImplementation("androidx.compose.ui:ui-tooling")
|
||||
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
||||
|
||||
val roomVersion = "2.8.3"
|
||||
val roomVersion = "2.8.4"
|
||||
implementation("androidx.room:room-runtime:$roomVersion")
|
||||
androidTestImplementation("androidx.room:room-testing:$roomVersion")
|
||||
ksp("androidx.room:room-compiler:$roomVersion")
|
||||
|
||||
val hiltVersion = "2.57.2"
|
||||
val hiltVersion = "2.60.1"
|
||||
implementation("com.google.dagger:hilt-android:$hiltVersion")
|
||||
ksp("com.google.dagger:hilt-android-compiler:$hiltVersion")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.wismna.geoffroy.donext.data.injection
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import java.time.Clock
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object ClockModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideClock(): Clock = Clock.systemDefaultZone()
|
||||
}
|
||||
+10
-6
@@ -3,20 +3,24 @@ package com.wismna.geoffroy.donext.domain.usecase
|
||||
import com.wismna.geoffroy.donext.domain.model.Task
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.time.Clock
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneOffset
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetDueTodayTasksUseCase @Inject constructor(private val repository: TaskRepository) {
|
||||
class GetDueTodayTasksUseCase @Inject constructor(
|
||||
private val repository: TaskRepository,
|
||||
private val clock: Clock
|
||||
) {
|
||||
operator fun invoke(): Flow<List<Task>> {
|
||||
val todayStart = LocalDate.now()
|
||||
.atStartOfDay(ZoneOffset.UTC)
|
||||
val today = LocalDate.now(clock)
|
||||
val todayStart = today
|
||||
.atStartOfDay(clock.zone)
|
||||
.toInstant()
|
||||
.toEpochMilli()
|
||||
|
||||
val todayEnd = LocalDate.now()
|
||||
val todayEnd = today
|
||||
.plusDays(1)
|
||||
.atStartOfDay(ZoneOffset.UTC)
|
||||
.atStartOfDay(clock.zone)
|
||||
.toInstant()
|
||||
.toEpochMilli() - 1
|
||||
return repository.getDueTodayTasks(
|
||||
|
||||
+9
-6
@@ -3,17 +3,20 @@ package com.wismna.geoffroy.donext.domain.usecase
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.time.Clock
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneOffset
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetTaskListsWithOverdueUseCase @Inject constructor(private val taskRepository: TaskRepository) {
|
||||
class GetTaskListsWithOverdueUseCase @Inject constructor(
|
||||
private val taskRepository: TaskRepository,
|
||||
private val clock: Clock
|
||||
) {
|
||||
operator fun invoke(): Flow<List<TaskListWithOverdue>> {
|
||||
return taskRepository.getTaskListsWithOverdue(
|
||||
LocalDate.now()
|
||||
.atStartOfDay(ZoneOffset.UTC)
|
||||
.toInstant()
|
||||
.toEpochMilli()
|
||||
LocalDate.now(clock)
|
||||
.atStartOfDay(clock.zone)
|
||||
.toInstant()
|
||||
.toEpochMilli()
|
||||
)
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package com.wismna.geoffroy.donext.domain.usecase
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.wismna.geoffroy.donext.domain.model.Priority
|
||||
import com.wismna.geoffroy.donext.domain.model.Task
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import java.time.Clock
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
|
||||
class GetDueTodayTasksUseCaseTest {
|
||||
|
||||
@Test
|
||||
fun `computes today's window using the clock's own zone, not UTC`() {
|
||||
// Paris in summer is UTC+2: local midnight for this date is 22:00 UTC
|
||||
// the *previous* day. A window anchored to UTC midnight would miss it.
|
||||
val zone = ZoneId.of("Europe/Paris")
|
||||
val today = LocalDate.of(2026, 7, 16)
|
||||
val clock = Clock.fixed(today.atStartOfDay(zone).plusHours(10).toInstant(), zone)
|
||||
|
||||
val repository = mockk<TaskRepository>()
|
||||
every { repository.getDueTodayTasks(any(), any()) } returns flowOf(emptyList())
|
||||
|
||||
GetDueTodayTasksUseCase(repository, clock).invoke()
|
||||
|
||||
val expectedStart = today.atStartOfDay(zone).toInstant().toEpochMilli()
|
||||
val expectedEnd = today.plusDays(1).atStartOfDay(zone).toInstant().toEpochMilli() - 1
|
||||
|
||||
verify { repository.getDueTodayTasks(expectedStart, expectedEnd) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `window covers exactly local midnight to the millisecond before the next local midnight`() {
|
||||
val zone = ZoneId.of("America/Montreal")
|
||||
val today = LocalDate.of(2026, 1, 5)
|
||||
val clock = Clock.fixed(today.atStartOfDay(zone).plusHours(6).toInstant(), zone)
|
||||
|
||||
val repository = mockk<TaskRepository>()
|
||||
every { repository.getDueTodayTasks(any(), any()) } returns flowOf(emptyList())
|
||||
|
||||
GetDueTodayTasksUseCase(repository, clock).invoke()
|
||||
|
||||
val expectedStart = today.atStartOfDay(zone).toInstant().toEpochMilli()
|
||||
val expectedEnd = today.plusDays(1).atStartOfDay(zone).toInstant().toEpochMilli() - 1
|
||||
assertThat(expectedEnd - expectedStart).isEqualTo(24L * 60 * 60 * 1000 - 1)
|
||||
|
||||
verify { repository.getDueTodayTasks(expectedStart, expectedEnd) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `passes through the tasks emitted by the repository`() = runTest {
|
||||
val zone = ZoneId.systemDefault()
|
||||
val clock = Clock.fixed(LocalDate.of(2026, 1, 1).atStartOfDay(zone).toInstant(), zone)
|
||||
val tasks = listOf(
|
||||
Task(
|
||||
id = 1L,
|
||||
taskListId = 1L,
|
||||
name = "Task",
|
||||
description = "",
|
||||
priority = Priority.NORMAL,
|
||||
isDone = false,
|
||||
isDeleted = false
|
||||
)
|
||||
)
|
||||
|
||||
val repository = mockk<TaskRepository>()
|
||||
every { repository.getDueTodayTasks(any(), any()) } returns flowOf(tasks)
|
||||
|
||||
val result = GetDueTodayTasksUseCase(repository, clock).invoke().first()
|
||||
|
||||
assertThat(result).isEqualTo(tasks)
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package com.wismna.geoffroy.donext.domain.usecase
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import java.time.Clock
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
|
||||
class GetTaskListsWithOverdueUseCaseTest {
|
||||
|
||||
@Test
|
||||
fun `computes the overdue cutoff at local midnight, not UTC midnight`() {
|
||||
// Paris in summer is UTC+2: local midnight for this date is 22:00 UTC
|
||||
// the *previous* day. A cutoff anchored to UTC midnight would flag
|
||||
// today's tasks as overdue a full day too early.
|
||||
val zone = ZoneId.of("Europe/Paris")
|
||||
val today = LocalDate.of(2026, 7, 16)
|
||||
val clock = Clock.fixed(today.atStartOfDay(zone).plusHours(10).toInstant(), zone)
|
||||
|
||||
val repository = mockk<TaskRepository>()
|
||||
every { repository.getTaskListsWithOverdue(any()) } returns flowOf(emptyList())
|
||||
|
||||
GetTaskListsWithOverdueUseCase(repository, clock).invoke()
|
||||
|
||||
val expectedCutoff = today.atStartOfDay(zone).toInstant().toEpochMilli()
|
||||
verify { repository.getTaskListsWithOverdue(expectedCutoff) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `passes through the lists emitted by the repository`() = runTest {
|
||||
val zone = ZoneId.systemDefault()
|
||||
val clock = Clock.fixed(LocalDate.of(2026, 1, 1).atStartOfDay(zone).toInstant(), zone)
|
||||
val lists = listOf(TaskListWithOverdue(id = 1L, name = "Work", overdueCount = 2))
|
||||
|
||||
val repository = mockk<TaskRepository>()
|
||||
every { repository.getTaskListsWithOverdue(any()) } returns flowOf(lists)
|
||||
|
||||
val result = GetTaskListsWithOverdueUseCase(repository, clock).invoke().first()
|
||||
|
||||
assertThat(result).isEqualTo(lists)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user