From 5888790ba70ad3ba5536cb462dd25b558ff27919 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 19 Oct 2026 10:18:47 +0000 Subject: [PATCH 01/14] Create task screen priority field Add priority selector UI component and integrate with CreateTask screen and ViewModel to allow selecting and persisting task priority. Closes #123 --- .../composeResources/values/strings.xml | 1 + .../prioritypicker/FoliaryPriorityPicker.kt | 131 ++++++++++++++++++ .../feature/createtask/CreateTaskScreen.kt | 11 ++ .../feature/createtask/CreateTaskViewModel.kt | 10 +- .../FoliaryPriorityPickerTest.kt | 103 ++++++++++++++ .../createtask/CreateTaskScreenTest.kt | 28 ++++ .../createtask/CreateTaskViewDataFixture.kt | 4 + .../createtask/CreateTaskViewModelTest.kt | 91 ++++++++++++ 8 files changed, 378 insertions(+), 1 deletion(-) create mode 100644 foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/prioritypicker/FoliaryPriorityPicker.kt create mode 100644 foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/prioritypicker/FoliaryPriorityPickerTest.kt diff --git a/foliary/src/commonMain/composeResources/values/strings.xml b/foliary/src/commonMain/composeResources/values/strings.xml index 2480cf4..3bbfc0b 100644 --- a/foliary/src/commonMain/composeResources/values/strings.xml +++ b/foliary/src/commonMain/composeResources/values/strings.xml @@ -71,6 +71,7 @@ Add description... Save Title is required + Priority Due date Add due date Clear due date diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/prioritypicker/FoliaryPriorityPicker.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/prioritypicker/FoliaryPriorityPicker.kt new file mode 100644 index 0000000..6b5b9d6 --- /dev/null +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/prioritypicker/FoliaryPriorityPicker.kt @@ -0,0 +1,131 @@ +package dev.appoutlet.foliary.core.ui.component.prioritypicker + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.dp +import com.composables.icons.lucide.Check +import com.composables.icons.lucide.ChevronDown +import com.composables.icons.lucide.Flag +import com.composables.icons.lucide.Lucide +import dev.appoutlet.foliary.core.ui.component.card.FoliaryCard +import dev.appoutlet.foliary.core.ui.component.pill.PriorityPill +import dev.appoutlet.foliary.data.task.database.entity.Priority +import foliary.foliary.generated.resources.Res +import foliary.foliary.generated.resources.priority_pill_blocker +import foliary.foliary.generated.resources.priority_pill_high +import foliary.foliary.generated.resources.priority_pill_highest +import foliary.foliary.generated.resources.priority_pill_low +import foliary.foliary.generated.resources.priority_pill_lowest +import foliary.foliary.generated.resources.priority_pill_medium +import org.jetbrains.compose.resources.StringResource +import org.jetbrains.compose.resources.stringResource + +@Composable +fun FoliaryPriorityPicker( + selectedPriority: Priority, + onPrioritySelected: (Priority) -> Unit, + modifier: Modifier = Modifier, + label: String, +) { + var showMenu by rememberSaveable { mutableStateOf(false) } + + Box(modifier = modifier) { + PriorityPickerCard( + label = label, + selectedPriority = selectedPriority, + onClick = { showMenu = true } + ) + + DropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false }, + containerColor = MaterialTheme.colorScheme.surface, + ) { + Priority.entries.forEach { priority -> + DropdownMenuItem( + text = { Text(text = stringResource(priority.stringResource)) }, + leadingIcon = { Icon(imageVector = Lucide.Flag, contentDescription = null) }, + trailingIcon = if (priority == selectedPriority) { + { Icon(imageVector = Lucide.Check, contentDescription = null) } + } else { + null + }, + onClick = { + showMenu = false + onPrioritySelected(priority) + } + ) + } + } + } +} + +@Composable +private fun PriorityPickerCard( + label: String, + selectedPriority: Priority, + onClick: () -> Unit, +) { + FoliaryCard(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier + .fillMaxWidth() + .testTag("FoliaryPriorityPicker:Card") + .clickable(onClick = onClick), + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier + .padding(16.dp) + .weight(1f), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = label, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onBackground + ) + + PriorityPill( + priority = selectedPriority, + modifier = Modifier.testTag("FoliaryPriorityPicker:Value") + ) + } + + Icon( + modifier = Modifier.padding(end = 16.dp), + imageVector = Lucide.ChevronDown, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface + ) + } + } +} + +private val Priority.stringResource: StringResource + get() = when (this) { + Priority.LOWEST -> Res.string.priority_pill_lowest + Priority.LOW -> Res.string.priority_pill_low + Priority.MEDIUM -> Res.string.priority_pill_medium + Priority.HIGH -> Res.string.priority_pill_high + Priority.HIGHEST -> Res.string.priority_pill_highest + Priority.BLOCKER -> Res.string.priority_pill_blocker + } diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreen.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreen.kt index 74a6ba2..4af14ce 100644 --- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreen.kt +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreen.kt @@ -24,6 +24,7 @@ import dev.appoutlet.foliary.core.ui.component.button.FoliaryBackIconButton import dev.appoutlet.foliary.core.ui.component.button.FoliaryPrimaryButton import dev.appoutlet.foliary.core.ui.component.datepicker.FoliaryDatePicker import dev.appoutlet.foliary.core.ui.component.modifier.widthInCompact +import dev.appoutlet.foliary.core.ui.component.prioritypicker.FoliaryPriorityPicker import dev.appoutlet.foliary.core.ui.component.textfield.FoliaryTextField import dev.appoutlet.foliary.feature.main.getWindowDecorationPadding import foliary.foliary.generated.resources.Res @@ -31,6 +32,7 @@ import foliary.foliary.generated.resources.create_task_description_label import foliary.foliary.generated.resources.create_task_description_placeholder import foliary.foliary.generated.resources.create_task_due_date_label import foliary.foliary.generated.resources.create_task_due_date_placeholder +import foliary.foliary.generated.resources.create_task_priority_label import foliary.foliary.generated.resources.create_task_save import foliary.foliary.generated.resources.create_task_title import foliary.foliary.generated.resources.create_task_title_label @@ -57,6 +59,15 @@ fun CreateTaskScreen(viewData: CreateTaskViewData, onEvent: (CreateTaskEvent) -> Spacer(Modifier.size(16.dp)) TitleField(viewData.title, onEvent) DescriptionField(viewData.description, onEvent) + FoliaryPriorityPicker( + modifier = Modifier.widthInCompact() + .fillMaxWidth() + .padding(horizontal = 16.dp) + .testTag("CreateTaskScreen:PriorityPicker"), + label = stringResource(Res.string.create_task_priority_label), + selectedPriority = viewData.priority, + onPrioritySelected = { onEvent(CreateTaskEvent.PriorityChanged(it)) }, + ) FoliaryDatePicker( modifier = Modifier.widthInCompact() .fillMaxWidth() diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModel.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModel.kt index 335ecb7..f6836f6 100644 --- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModel.kt +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModel.kt @@ -38,6 +38,7 @@ class CreateTaskViewModel( id = task.id.toString(), title = task.title, description = task.description, + priority = task.priority, dueDate = task.dueDate?.let { dueDate -> CreateTaskViewData.DueDateViewData( selectedDateMillis = dueDate.toEpochMilliseconds(), @@ -54,12 +55,17 @@ class CreateTaskViewModel( when (event) { is CreateTaskEvent.TitleChanged -> onTitleChange(event.title) is CreateTaskEvent.DescriptionChanged -> onDescriptionChange(event.description) + is CreateTaskEvent.PriorityChanged -> onPriorityChange(event.priority) is CreateTaskEvent.DueDateChanged -> onDueDateChange(event.dueDateMillis) CreateTaskEvent.SaveClicked -> onSaveClick() CreateTaskEvent.BackClicked -> onBackClick() } } + private fun onPriorityChange(priority: Priority) = intent { + reduce { state.copy(priority = priority) } + } + private fun onDueDateChange(dueDateMillis: Long?) = intent { reduce { state.copy( @@ -100,7 +106,7 @@ class CreateTaskViewModel( timeProvider.endOfDay(it) }, completionDate = null, - priority = Priority.MEDIUM, + priority = state.priority, url = null, location = null, ) @@ -121,6 +127,7 @@ data class CreateTaskViewData( val id: String? = null, val title: String = "", val description: String? = null, + val priority: Priority = Priority.MEDIUM, val dueDate: DueDateViewData? = null, val minDueDateMillis: Long, val saveButtonEnabled: Boolean = false, @@ -138,6 +145,7 @@ data class CreateTaskViewData( sealed interface CreateTaskEvent { data class TitleChanged(val title: String) : CreateTaskEvent data class DescriptionChanged(val description: String?) : CreateTaskEvent + data class PriorityChanged(val priority: Priority) : CreateTaskEvent data class DueDateChanged(val dueDateMillis: Long?) : CreateTaskEvent data object SaveClicked : CreateTaskEvent data object BackClicked : CreateTaskEvent diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/prioritypicker/FoliaryPriorityPickerTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/prioritypicker/FoliaryPriorityPickerTest.kt new file mode 100644 index 0000000..e91dc1b --- /dev/null +++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/prioritypicker/FoliaryPriorityPickerTest.kt @@ -0,0 +1,103 @@ +package dev.appoutlet.foliary.core.ui.component.prioritypicker + +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertCountEquals +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runComposeUiTest +import dev.appoutlet.foliary.data.task.database.entity.Priority +import foliary.foliary.generated.resources.Res +import foliary.foliary.generated.resources.priority_pill_blocker +import foliary.foliary.generated.resources.priority_pill_high +import foliary.foliary.generated.resources.priority_pill_highest +import foliary.foliary.generated.resources.priority_pill_low +import foliary.foliary.generated.resources.priority_pill_lowest +import foliary.foliary.generated.resources.priority_pill_medium +import io.kotest.matchers.shouldBe +import org.jetbrains.compose.resources.getString +import kotlin.test.Test + +@OptIn(ExperimentalTestApi::class) +class FoliaryPriorityPickerTest { + + @Test + fun `should render selected priority as pill`() = runComposeUiTest { + val label = "Priority" + val mediumText = getString(Res.string.priority_pill_medium) + + setContent { + FoliaryPriorityPicker( + selectedPriority = Priority.MEDIUM, + onPrioritySelected = {}, + label = label + ) + } + + onNodeWithText(label).assertIsDisplayed() + onNodeWithText(mediumText).assertIsDisplayed() + onNodeWithTag("FoliaryPriorityPicker:Value", useUnmergedTree = true).assertIsDisplayed() + } + + @Test + fun `no menu items should exist before card is clicked`() = runComposeUiTest { + setContent { + FoliaryPriorityPicker( + selectedPriority = Priority.MEDIUM, + onPrioritySelected = {}, + label = "Priority" + ) + } + + onNodeWithText(getString(Res.string.priority_pill_lowest)).assertDoesNotExist() + onNodeWithText(getString(Res.string.priority_pill_high)).assertDoesNotExist() + onNodeWithText(getString(Res.string.priority_pill_blocker)).assertDoesNotExist() + } + + @Test + fun `should open menu and list all six labels when card is clicked`() = runComposeUiTest { + setContent { + FoliaryPriorityPicker( + selectedPriority = Priority.MEDIUM, + onPrioritySelected = {}, + label = "Priority" + ) + } + + onNodeWithTag("FoliaryPriorityPicker:Card", useUnmergedTree = true).performClick() + waitForIdle() + + onNodeWithText(getString(Res.string.priority_pill_lowest)).assertIsDisplayed() + onNodeWithText(getString(Res.string.priority_pill_low)).assertIsDisplayed() + onAllNodesWithText(getString(Res.string.priority_pill_medium)).assertCountEquals(2) + onNodeWithText(getString(Res.string.priority_pill_high)).assertIsDisplayed() + onNodeWithText(getString(Res.string.priority_pill_highest)).assertIsDisplayed() + onNodeWithText(getString(Res.string.priority_pill_blocker)).assertIsDisplayed() + } + + @Test + fun `selecting an option should invoke callback and close menu`() = runComposeUiTest { + var selectedPriority: Priority? = null + + setContent { + FoliaryPriorityPicker( + selectedPriority = Priority.MEDIUM, + onPrioritySelected = { selectedPriority = it }, + label = "Priority" + ) + } + + onNodeWithTag("FoliaryPriorityPicker:Card", useUnmergedTree = true).performClick() + waitForIdle() + + onNodeWithText(getString(Res.string.priority_pill_high)).performClick() + waitForIdle() + + selectedPriority shouldBe Priority.HIGH + + onNodeWithText(getString(Res.string.priority_pill_lowest)).assertDoesNotExist() + onNodeWithText(getString(Res.string.priority_pill_blocker)).assertDoesNotExist() + } +} diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreenTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreenTest.kt index 2a73f93..c0996d7 100644 --- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreenTest.kt +++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreenTest.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.v2.runComposeUiTest +import dev.appoutlet.foliary.data.task.database.entity.Priority import foliary.foliary.generated.resources.Res import foliary.foliary.generated.resources.back_icon_button_a11y import foliary.foliary.generated.resources.create_task_description_label @@ -21,10 +22,13 @@ import foliary.foliary.generated.resources.create_task_description_placeholder import foliary.foliary.generated.resources.create_task_due_date_clear_a11y import foliary.foliary.generated.resources.create_task_due_date_label import foliary.foliary.generated.resources.create_task_due_date_placeholder +import foliary.foliary.generated.resources.create_task_priority_label import foliary.foliary.generated.resources.create_task_save import foliary.foliary.generated.resources.create_task_title import foliary.foliary.generated.resources.create_task_title_label import foliary.foliary.generated.resources.create_task_title_placeholder +import foliary.foliary.generated.resources.priority_pill_high +import foliary.foliary.generated.resources.priority_pill_medium import io.kotest.matchers.shouldBe import org.jetbrains.compose.resources.getString import kotlin.test.Test @@ -50,6 +54,8 @@ class CreateTaskScreenTest { onNodeWithText(getString(Res.string.create_task_title_placeholder)).assertIsDisplayed() onNodeWithText(getString(Res.string.create_task_description_label)).assertIsDisplayed() onNodeWithText(getString(Res.string.create_task_description_placeholder)).assertIsDisplayed() + onNodeWithTag("CreateTaskScreen:PriorityPicker").assertIsDisplayed() + onNodeWithText(getString(Res.string.priority_pill_medium)).assertIsDisplayed() onNodeWithText(getString(Res.string.create_task_due_date_label)).assertIsDisplayed() onNodeWithText(getString(Res.string.create_task_due_date_placeholder)).assertIsDisplayed() onNodeWithContentDescription(getString(Res.string.create_task_due_date_clear_a11y)) @@ -149,4 +155,26 @@ class CreateTaskScreenTest { onNodeWithContentDescription(getString(Res.string.create_task_due_date_clear_a11y)) .assertDoesNotExist() } + + @Test + fun `should change priority and emit event`() = runComposeUiTest { + val events = mutableListOf() + var viewData by mutableStateOf(CreateTaskViewData.fixture(priority = Priority.MEDIUM)) + + setContent { + CreateTaskScreen(viewData = viewData, onEvent = events::add) + } + + onNodeWithTag("CreateTaskScreen:PriorityPicker").performClick() + waitForIdle() + + onNodeWithText(getString(Res.string.priority_pill_high)).performClick() + + events.contains(CreateTaskEvent.PriorityChanged(Priority.HIGH)) shouldBe true + + viewData = viewData.copy(priority = Priority.HIGH) + waitForIdle() + + onNodeWithText(getString(Res.string.priority_pill_high)).assertIsDisplayed() + } } diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewDataFixture.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewDataFixture.kt index 0a9b3da..7a767f2 100644 --- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewDataFixture.kt +++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewDataFixture.kt @@ -1,9 +1,12 @@ package dev.appoutlet.foliary.feature.createtask +import dev.appoutlet.foliary.data.task.database.entity.Priority + fun CreateTaskViewData.Companion.fixture( id: String? = null, title: String = "Task fixture", description: String? = "Task description", + priority: Priority = Priority.MEDIUM, dueDate: CreateTaskViewData.DueDateViewData? = CreateTaskViewData.DueDateViewData.fixture(), minDueDateMillis: Long = 1_752_960_000_000, saveButtonEnabled: Boolean = true, @@ -11,6 +14,7 @@ fun CreateTaskViewData.Companion.fixture( id = id, title = title, description = description, + priority = priority, dueDate = dueDate, minDueDateMillis = minDueDateMillis, saveButtonEnabled = saveButtonEnabled, diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModelTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModelTest.kt index d140f15..106539c 100644 --- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModelTest.kt +++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModelTest.kt @@ -4,6 +4,7 @@ import dev.appoutlet.foliary.core.provider.time.TimeProvider import dev.appoutlet.foliary.core.provider.uuid.UuidProvider import dev.appoutlet.foliary.core.testing.ViewModelTest import dev.appoutlet.foliary.data.task.TaskRepository +import dev.appoutlet.foliary.data.task.database.entity.Priority import dev.appoutlet.foliary.data.task.database.entity.Task import dev.appoutlet.foliary.data.task.database.entity.fixture import dev.mokkery.MockMode @@ -75,6 +76,17 @@ class CreateTaskViewModelTest : expectSideEffect(CreateTaskAction.NavigateBack) } + @Test + fun `default state should have medium priority`() = test { + currentState.priority shouldBe Priority.MEDIUM + } + + @Test + fun `PriorityChanged - should update priority`() = test { + viewModel.onEvent(CreateTaskEvent.PriorityChanged(Priority.HIGH)) + expectState { copy(priority = Priority.HIGH) } + } + @Test fun `default state should have dueDate unset and minDueDate set to start of today`() = test { currentState.dueDate shouldBe null @@ -185,6 +197,85 @@ class CreateTaskViewModelTest : awaitSideEffect() shouldBe CreateTaskAction.NavigateBack } + @Test + fun `SaveClicked - should persist default medium priority`() = test { + val id = Uuid.random() + val title = "Task title" + val taskCapture = Capture.slot() + + every(mockUuidProvider::random) returns id + every(mockTimeProvider::now) returns Instant.parse("2026-07-21T12:00:00Z") + everySuspend { mockTaskRepository.save(capture(taskCapture)) } returns Unit + + viewModel.onEvent(CreateTaskEvent.TitleChanged(title)) + expectState { copy(title = title, saveButtonEnabled = true) } + + viewModel.onEvent(CreateTaskEvent.SaveClicked) + awaitState().saveButtonEnabled shouldBe false + + taskCapture.get().priority shouldBe Priority.MEDIUM + } + + @Test + fun `SaveClicked - should persist selected priority`() = test { + val id = Uuid.random() + val title = "Task title" + val taskCapture = Capture.slot() + + every(mockUuidProvider::random) returns id + every(mockTimeProvider::now) returns Instant.parse("2026-07-21T12:00:00Z") + everySuspend { mockTaskRepository.save(capture(taskCapture)) } returns Unit + + viewModel.onEvent(CreateTaskEvent.TitleChanged(title)) + expectState { copy(title = title, saveButtonEnabled = true) } + + viewModel.onEvent(CreateTaskEvent.PriorityChanged(Priority.BLOCKER)) + expectState { copy(title = title, priority = Priority.BLOCKER, saveButtonEnabled = true) } + + viewModel.onEvent(CreateTaskEvent.SaveClicked) + awaitState().saveButtonEnabled shouldBe false + + taskCapture.get().priority shouldBe Priority.BLOCKER + } + + @Test + fun `should preserve saved task priority on init`() { + val taskId = Uuid.random() + val savedTask = Task.fixture(id = taskId, priority = Priority.HIGHEST, dueDate = null) + val taskCapture = Capture.slot() + + everySuspend { mockTaskRepository.findById(taskId) } returns savedTask + every(mockTimeProvider::now) returns Instant.parse("2026-07-22T12:00:00Z") + everySuspend { mockTaskRepository.save(capture(taskCapture)) } returns Unit + + viewModel = CreateTaskViewModel( + taskId = taskId.toString(), + taskRepository = mockTaskRepository, + timeProvider = mockTimeProvider, + uuidProvider = mockUuidProvider, + ) + + test { + expectState { + copy( + id = taskId.toString(), + title = savedTask.title, + description = savedTask.description, + priority = Priority.HIGHEST, + dueDate = null, + saveButtonEnabled = true, + ) + } + + viewModel.onEvent(CreateTaskEvent.SaveClicked) + awaitState().saveButtonEnabled shouldBe false + + taskCapture.get().priority shouldBe Priority.HIGHEST + + awaitSideEffect() shouldBe CreateTaskAction.NavigateBack + } + } + @Test fun `SaveClicked - should keep creationDate when editing an existing task`() { val taskId = Uuid.random() From 1017a4f7598960c4321089b9a3ba9e971354d5cd Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 19 Oct 2026 10:52:13 +0000 Subject: [PATCH 02/14] Create task screen priority field Add priority selector UI component and integrate with CreateTask screen and ViewModel to allow selecting and persisting task priority. Closes #123 --- .../appoutlet/foliary/feature/createtask/CreateTaskScreenTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreenTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreenTest.kt index c0996d7..00b4927 100644 --- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreenTest.kt +++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreenTest.kt @@ -22,7 +22,6 @@ import foliary.foliary.generated.resources.create_task_description_placeholder import foliary.foliary.generated.resources.create_task_due_date_clear_a11y import foliary.foliary.generated.resources.create_task_due_date_label import foliary.foliary.generated.resources.create_task_due_date_placeholder -import foliary.foliary.generated.resources.create_task_priority_label import foliary.foliary.generated.resources.create_task_save import foliary.foliary.generated.resources.create_task_title import foliary.foliary.generated.resources.create_task_title_label From 7dfdcf213ab4a6e7fd5993385e85340d7dfa92a9 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 20 Oct 2026 08:20:41 +0000 Subject: [PATCH 03/14] Create task screen priority field Add priority selector UI component in feature package and integrate with CreateTask screen and ViewModel to allow selecting and persisting task priority. Closes #123 --- .../appoutlet/foliary/feature/createtask/CreateTaskScreen.kt | 1 - .../createtask}/FoliaryPriorityPicker.kt | 2 +- .../createtask}/FoliaryPriorityPickerTest.kt | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) rename foliary/src/commonMain/kotlin/dev/appoutlet/foliary/{core/ui/component/prioritypicker => feature/createtask}/FoliaryPriorityPicker.kt (98%) rename foliary/src/commonTest/kotlin/dev/appoutlet/foliary/{core/ui/component/prioritypicker => feature/createtask}/FoliaryPriorityPickerTest.kt (98%) diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreen.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreen.kt index 4af14ce..14d4ff1 100644 --- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreen.kt +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreen.kt @@ -24,7 +24,6 @@ import dev.appoutlet.foliary.core.ui.component.button.FoliaryBackIconButton import dev.appoutlet.foliary.core.ui.component.button.FoliaryPrimaryButton import dev.appoutlet.foliary.core.ui.component.datepicker.FoliaryDatePicker import dev.appoutlet.foliary.core.ui.component.modifier.widthInCompact -import dev.appoutlet.foliary.core.ui.component.prioritypicker.FoliaryPriorityPicker import dev.appoutlet.foliary.core.ui.component.textfield.FoliaryTextField import dev.appoutlet.foliary.feature.main.getWindowDecorationPadding import foliary.foliary.generated.resources.Res diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/prioritypicker/FoliaryPriorityPicker.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPicker.kt similarity index 98% rename from foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/prioritypicker/FoliaryPriorityPicker.kt rename to foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPicker.kt index 6b5b9d6..e909b80 100644 --- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/prioritypicker/FoliaryPriorityPicker.kt +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPicker.kt @@ -1,4 +1,4 @@ -package dev.appoutlet.foliary.core.ui.component.prioritypicker +package dev.appoutlet.foliary.feature.createtask import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/prioritypicker/FoliaryPriorityPickerTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPickerTest.kt similarity index 98% rename from foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/prioritypicker/FoliaryPriorityPickerTest.kt rename to foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPickerTest.kt index e91dc1b..56c2417 100644 --- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/prioritypicker/FoliaryPriorityPickerTest.kt +++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPickerTest.kt @@ -1,4 +1,4 @@ -package dev.appoutlet.foliary.core.ui.component.prioritypicker +package dev.appoutlet.foliary.feature.createtask import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertCountEquals From baac73189bc8d6e32b2b8a72d7c5d14da7d8f9c6 Mon Sep 17 00:00:00 2001 From: Messias Junior Date: Tue, 20 Oct 2026 11:02:45 +0100 Subject: [PATCH 04/14] priority ui --- .../core/ui/component/pill/PriorityPill.kt | 33 +------ .../core/ui/priority/PriorityViewData.kt | 96 +++++++++++++++++++ .../data/task/database/entity/Priority.kt | 2 +- .../createtask/FoliaryPriorityPicker.kt | 65 ++++++------- gradle/gradle-daemon-jvm.properties | 23 ++--- 5 files changed, 147 insertions(+), 72 deletions(-) create mode 100644 foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/priority/PriorityViewData.kt diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/pill/PriorityPill.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/pill/PriorityPill.kt index 09bb04c..c5605d9 100644 --- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/pill/PriorityPill.kt +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/pill/PriorityPill.kt @@ -2,48 +2,25 @@ package dev.appoutlet.foliary.core.ui.component.pill import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.FlowRow -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.composables.icons.lucide.Flag -import com.composables.icons.lucide.Lucide +import dev.appoutlet.foliary.core.ui.priority.viewData import dev.appoutlet.foliary.core.ui.theme.FoliaryTheme import dev.appoutlet.foliary.data.task.database.entity.Priority -import foliary.foliary.generated.resources.Res -import foliary.foliary.generated.resources.priority_pill_blocker -import foliary.foliary.generated.resources.priority_pill_high -import foliary.foliary.generated.resources.priority_pill_highest -import foliary.foliary.generated.resources.priority_pill_low -import foliary.foliary.generated.resources.priority_pill_lowest -import foliary.foliary.generated.resources.priority_pill_medium import org.jetbrains.compose.resources.stringResource -private val HighContentColor = Color(0xFFC4A319) -private val HighestContentColor = Color(0xFFAD5203) - @Composable fun PriorityPill(priority: Priority, modifier: Modifier = Modifier) { - val defaultContentColor = MaterialTheme.colorScheme.onSurface - val blockerContentColor = MaterialTheme.colorScheme.error - - val (text, contentColor) = when (priority) { - Priority.LOWEST -> Res.string.priority_pill_lowest to defaultContentColor - Priority.LOW -> Res.string.priority_pill_low to defaultContentColor - Priority.MEDIUM -> Res.string.priority_pill_medium to defaultContentColor - Priority.HIGH -> Res.string.priority_pill_high to HighContentColor - Priority.HIGHEST -> Res.string.priority_pill_highest to HighestContentColor - Priority.BLOCKER -> Res.string.priority_pill_blocker to blockerContentColor - } + val priorityViewData = priority.viewData() FoliaryPill( modifier = modifier.testTag("PriorityPill"), - icon = Lucide.Flag, - text = stringResource(text), - contentColor = contentColor + icon = priorityViewData.icon, + text = priorityViewData.text, + contentColor = priorityViewData.color ) } diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/priority/PriorityViewData.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/priority/PriorityViewData.kt new file mode 100644 index 0000000..f1a337c --- /dev/null +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/priority/PriorityViewData.kt @@ -0,0 +1,96 @@ +package dev.appoutlet.foliary.core.ui.priority + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.luminance +import androidx.compose.ui.graphics.vector.ImageVector +import com.composables.icons.lucide.ChevronDown +import com.composables.icons.lucide.ChevronUp +import com.composables.icons.lucide.ChevronsDown +import com.composables.icons.lucide.ChevronsUp +import com.composables.icons.lucide.ChevronsUpDown +import com.composables.icons.lucide.CircleMinus +import com.composables.icons.lucide.Lucide +import dev.appoutlet.foliary.data.task.database.entity.Priority +import foliary.foliary.generated.resources.Res +import foliary.foliary.generated.resources.priority_pill_blocker +import foliary.foliary.generated.resources.priority_pill_high +import foliary.foliary.generated.resources.priority_pill_highest +import foliary.foliary.generated.resources.priority_pill_low +import foliary.foliary.generated.resources.priority_pill_lowest +import foliary.foliary.generated.resources.priority_pill_medium +import org.jetbrains.compose.resources.stringResource + +data class FoliaryPriorityViewData( + val text: String, + val color: Color, + val icon: ImageVector, +) + +val PriorityHighLight = Color(0xFFBD8100) +val PriorityHighestLight = Color(0xFFA65200) +val PriorityBlockerLight = Color(0xFF93000A) + +val PriorityHighDark = Color(0xFFFFAA5C) +val PriorityHighestDark = Color(0xFFFF8080) +val PriorityBlockerDark = Color(0xFFFF5449) + +private data class PriorityColors( + val high: Color, + val highest: Color, + val blocker: Color +) + +@Composable +fun Priority.viewData(): FoliaryPriorityViewData { + val backgroundColor = MaterialTheme.colorScheme.background + + val colors = remember(this) { + val isDarkTheme = backgroundColor.luminance() < .5f + if (isDarkTheme) { + PriorityColors(PriorityHighDark, PriorityHighestDark, PriorityBlockerDark) + } else { + PriorityColors(PriorityHighLight, PriorityHighestLight, PriorityBlockerLight) + } + } + + return when (this) { + Priority.LOWEST -> FoliaryPriorityViewData( + text = stringResource(Res.string.priority_pill_lowest), + color = MaterialTheme.colorScheme.onSurface.copy(alpha = .4f), + icon = Lucide.ChevronsDown, + ) + + Priority.LOW -> FoliaryPriorityViewData( + text = stringResource(Res.string.priority_pill_low), + color = MaterialTheme.colorScheme.onSurface.copy(alpha = .7f), + icon = Lucide.ChevronDown, + ) + + Priority.MEDIUM -> FoliaryPriorityViewData( + text = stringResource(Res.string.priority_pill_medium), + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 1f), + icon = Lucide.ChevronsUpDown, + ) + + Priority.HIGH -> FoliaryPriorityViewData( + text = stringResource(Res.string.priority_pill_high), + color = colors.high, + icon = Lucide.ChevronUp, + ) + + Priority.HIGHEST -> FoliaryPriorityViewData( + text = stringResource(Res.string.priority_pill_highest), + color = colors.highest, + icon = Lucide.ChevronsUp, + ) + + Priority.BLOCKER -> FoliaryPriorityViewData( + text = stringResource(Res.string.priority_pill_blocker), + color = colors.blocker, + icon = Lucide.CircleMinus, + ) + } +} diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/data/task/database/entity/Priority.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/data/task/database/entity/Priority.kt index a8f8735..80e54c8 100644 --- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/data/task/database/entity/Priority.kt +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/data/task/database/entity/Priority.kt @@ -1,5 +1,5 @@ package dev.appoutlet.foliary.data.task.database.entity enum class Priority { - LOWEST, LOW, MEDIUM, HIGH, HIGHEST, BLOCKER + BLOCKER, HIGHEST, HIGH, MEDIUM, LOW, LOWEST } diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPicker.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPicker.kt index e909b80..670ff42 100644 --- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPicker.kt +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPicker.kt @@ -11,6 +11,7 @@ import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MenuDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -23,20 +24,10 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp import com.composables.icons.lucide.Check import com.composables.icons.lucide.ChevronDown -import com.composables.icons.lucide.Flag import com.composables.icons.lucide.Lucide import dev.appoutlet.foliary.core.ui.component.card.FoliaryCard -import dev.appoutlet.foliary.core.ui.component.pill.PriorityPill +import dev.appoutlet.foliary.core.ui.priority.viewData import dev.appoutlet.foliary.data.task.database.entity.Priority -import foliary.foliary.generated.resources.Res -import foliary.foliary.generated.resources.priority_pill_blocker -import foliary.foliary.generated.resources.priority_pill_high -import foliary.foliary.generated.resources.priority_pill_highest -import foliary.foliary.generated.resources.priority_pill_low -import foliary.foliary.generated.resources.priority_pill_lowest -import foliary.foliary.generated.resources.priority_pill_medium -import org.jetbrains.compose.resources.StringResource -import org.jetbrains.compose.resources.stringResource @Composable fun FoliaryPriorityPicker( @@ -60,18 +51,23 @@ fun FoliaryPriorityPicker( containerColor = MaterialTheme.colorScheme.surface, ) { Priority.entries.forEach { priority -> + val viewData = priority.viewData() DropdownMenuItem( - text = { Text(text = stringResource(priority.stringResource)) }, - leadingIcon = { Icon(imageVector = Lucide.Flag, contentDescription = null) }, - trailingIcon = if (priority == selectedPriority) { - { Icon(imageVector = Lucide.Check, contentDescription = null) } - } else { - null + text = { Text(text = viewData.text) }, + leadingIcon = { Icon(imageVector = viewData.icon, contentDescription = null) }, + trailingIcon = { + if (priority == selectedPriority) { + Icon(imageVector = Lucide.Check, contentDescription = null) + } }, onClick = { showMenu = false onPrioritySelected(priority) - } + }, + colors = MenuDefaults.itemColors( + textColor = viewData.color, + leadingIconColor = viewData.color + ) ) } } @@ -84,6 +80,7 @@ private fun PriorityPickerCard( selectedPriority: Priority, onClick: () -> Unit, ) { + val viewData = selectedPriority.viewData() FoliaryCard(modifier = Modifier.fillMaxWidth()) { Row( modifier = Modifier @@ -104,10 +101,24 @@ private fun PriorityPickerCard( color = MaterialTheme.colorScheme.onBackground ) - PriorityPill( - priority = selectedPriority, - modifier = Modifier.testTag("FoliaryPriorityPicker:Value") - ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + + Icon( + imageVector = viewData.icon, + contentDescription = null, + tint = viewData.color + ) + + Text( + modifier = Modifier.testTag("FoliaryPriorityPicker:Value"), + text = viewData.text, + color = viewData.color, + style = MaterialTheme.typography.bodyMedium + ) + } } Icon( @@ -119,13 +130,3 @@ private fun PriorityPickerCard( } } } - -private val Priority.stringResource: StringResource - get() = when (this) { - Priority.LOWEST -> Res.string.priority_pill_lowest - Priority.LOW -> Res.string.priority_pill_low - Priority.MEDIUM -> Res.string.priority_pill_medium - Priority.HIGH -> Res.string.priority_pill_high - Priority.HIGHEST -> Res.string.priority_pill_highest - Priority.BLOCKER -> Res.string.priority_pill_blocker - } diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties index 6c1139e..c610302 100644 --- a/gradle/gradle-daemon-jvm.properties +++ b/gradle/gradle-daemon-jvm.properties @@ -1,12 +1,13 @@ #This file is generated by updateDaemonJvm -toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect -toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect -toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect -toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect -toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/73bcfb608d1fde9fb62e462f834a3299/redirect -toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/846ee0d876d26a26f37aa1ce8de73224/redirect -toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect -toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect -toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/9482ddec596298c84656d31d16652665/redirect -toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/39701d92e1756bb2f141eb67cd4c660e/redirect -toolchainVersion=21 +toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d57918ebcf16bdd07b10454e594d88e4/redirect +toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/104aa12b204c38a5934ad22c8b1f1eb6/redirect +toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d57918ebcf16bdd07b10454e594d88e4/redirect +toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/104aa12b204c38a5934ad22c8b1f1eb6/redirect +toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/cba046b7dcb2d003be9d45bd846e7569/redirect +toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/28d2e80ff6ae2ef68c116d735e66a048/redirect +toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d57918ebcf16bdd07b10454e594d88e4/redirect +toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/104aa12b204c38a5934ad22c8b1f1eb6/redirect +toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/f947c0a30aa9016da9bb0912d406d3e8/redirect +toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c5047562b7a2d3671802428a0793774/redirect +toolchainVendor=JETBRAINS +toolchainVersion=25 From 2d6c4ea0219931cdcf90824e67d76dd1a371c493 Mon Sep 17 00:00:00 2001 From: Messias Junior Date: Tue, 20 Oct 2026 11:03:53 +0100 Subject: [PATCH 05/14] jvm 21 --- gradle/gradle-daemon-jvm.properties | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties index c610302..baa28d1 100644 --- a/gradle/gradle-daemon-jvm.properties +++ b/gradle/gradle-daemon-jvm.properties @@ -1,13 +1,13 @@ #This file is generated by updateDaemonJvm -toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d57918ebcf16bdd07b10454e594d88e4/redirect -toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/104aa12b204c38a5934ad22c8b1f1eb6/redirect -toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d57918ebcf16bdd07b10454e594d88e4/redirect -toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/104aa12b204c38a5934ad22c8b1f1eb6/redirect -toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/cba046b7dcb2d003be9d45bd846e7569/redirect -toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/28d2e80ff6ae2ef68c116d735e66a048/redirect -toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d57918ebcf16bdd07b10454e594d88e4/redirect -toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/104aa12b204c38a5934ad22c8b1f1eb6/redirect -toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/f947c0a30aa9016da9bb0912d406d3e8/redirect -toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c5047562b7a2d3671802428a0793774/redirect +toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect +toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect +toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect +toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect +toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/7083b89563e7ce20943037b8cd2b8cc2/redirect +toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/060bbb778a1f55ea705fdebd2ccfeab9/redirect +toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect +toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect +toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d09679dc60fe5aa05ef7d03efdefac20/redirect +toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/ed4e3bf2f5e7c5d9aabc4cbd8acd555e/redirect toolchainVendor=JETBRAINS -toolchainVersion=25 +toolchainVersion=21 From d4eb64ee178500a06c28df52ce113fd0b3b1b2de Mon Sep 17 00:00:00 2001 From: Messias Junior Date: Tue, 20 Oct 2026 11:04:46 +0100 Subject: [PATCH 06/14] refactor: rename FoliaryPriorityViewData to PriorityViewData --- .../core/ui/component/pill/PriorityPill.kt | 1 - .../foliary/core/ui/priority/PriorityViewData.kt | 16 ++++++++-------- .../feature/createtask/FoliaryPriorityPicker.kt | 1 - 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/pill/PriorityPill.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/pill/PriorityPill.kt index c5605d9..6045c00 100644 --- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/pill/PriorityPill.kt +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/pill/PriorityPill.kt @@ -10,7 +10,6 @@ import androidx.compose.ui.unit.dp import dev.appoutlet.foliary.core.ui.priority.viewData import dev.appoutlet.foliary.core.ui.theme.FoliaryTheme import dev.appoutlet.foliary.data.task.database.entity.Priority -import org.jetbrains.compose.resources.stringResource @Composable fun PriorityPill(priority: Priority, modifier: Modifier = Modifier) { diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/priority/PriorityViewData.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/priority/PriorityViewData.kt index f1a337c..44600f7 100644 --- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/priority/PriorityViewData.kt +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/priority/PriorityViewData.kt @@ -23,7 +23,7 @@ import foliary.foliary.generated.resources.priority_pill_lowest import foliary.foliary.generated.resources.priority_pill_medium import org.jetbrains.compose.resources.stringResource -data class FoliaryPriorityViewData( +data class PriorityViewData( val text: String, val color: Color, val icon: ImageVector, @@ -44,7 +44,7 @@ private data class PriorityColors( ) @Composable -fun Priority.viewData(): FoliaryPriorityViewData { +fun Priority.viewData(): PriorityViewData { val backgroundColor = MaterialTheme.colorScheme.background val colors = remember(this) { @@ -57,37 +57,37 @@ fun Priority.viewData(): FoliaryPriorityViewData { } return when (this) { - Priority.LOWEST -> FoliaryPriorityViewData( + Priority.LOWEST -> PriorityViewData( text = stringResource(Res.string.priority_pill_lowest), color = MaterialTheme.colorScheme.onSurface.copy(alpha = .4f), icon = Lucide.ChevronsDown, ) - Priority.LOW -> FoliaryPriorityViewData( + Priority.LOW -> PriorityViewData( text = stringResource(Res.string.priority_pill_low), color = MaterialTheme.colorScheme.onSurface.copy(alpha = .7f), icon = Lucide.ChevronDown, ) - Priority.MEDIUM -> FoliaryPriorityViewData( + Priority.MEDIUM -> PriorityViewData( text = stringResource(Res.string.priority_pill_medium), color = MaterialTheme.colorScheme.onSurface.copy(alpha = 1f), icon = Lucide.ChevronsUpDown, ) - Priority.HIGH -> FoliaryPriorityViewData( + Priority.HIGH -> PriorityViewData( text = stringResource(Res.string.priority_pill_high), color = colors.high, icon = Lucide.ChevronUp, ) - Priority.HIGHEST -> FoliaryPriorityViewData( + Priority.HIGHEST -> PriorityViewData( text = stringResource(Res.string.priority_pill_highest), color = colors.highest, icon = Lucide.ChevronsUp, ) - Priority.BLOCKER -> FoliaryPriorityViewData( + Priority.BLOCKER -> PriorityViewData( text = stringResource(Res.string.priority_pill_blocker), color = colors.blocker, icon = Lucide.CircleMinus, diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPicker.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPicker.kt index 670ff42..4d9fbe8 100644 --- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPicker.kt +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPicker.kt @@ -105,7 +105,6 @@ private fun PriorityPickerCard( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp) ) { - Icon( imageVector = viewData.icon, contentDescription = null, From d945666f4d08d14274653e978c822459110814d1 Mon Sep 17 00:00:00 2001 From: Messias Junior Date: Tue, 20 Oct 2026 11:10:10 +0100 Subject: [PATCH 07/14] refactor(ui): introduce LocalIsDarkTheme composition local - Added `LocalIsDarkTheme` `CompositionLocal` and provided it inside `Theme.kt`. - Updated `Priority.viewData()` to determine the theme mode using `LocalIsDarkTheme` instead of calculating background color luminance. --- .../foliary/core/ui/priority/PriorityViewData.kt | 7 +++---- .../dev/appoutlet/foliary/core/ui/theme/Color.kt | 3 +++ .../dev/appoutlet/foliary/core/ui/theme/Theme.kt | 15 +++++++++------ 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/priority/PriorityViewData.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/priority/PriorityViewData.kt index 44600f7..fe931b5 100644 --- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/priority/PriorityViewData.kt +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/priority/PriorityViewData.kt @@ -4,7 +4,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.luminance import androidx.compose.ui.graphics.vector.ImageVector import com.composables.icons.lucide.ChevronDown import com.composables.icons.lucide.ChevronUp @@ -13,6 +12,7 @@ import com.composables.icons.lucide.ChevronsUp import com.composables.icons.lucide.ChevronsUpDown import com.composables.icons.lucide.CircleMinus import com.composables.icons.lucide.Lucide +import dev.appoutlet.foliary.core.ui.theme.LocalIsDarkTheme import dev.appoutlet.foliary.data.task.database.entity.Priority import foliary.foliary.generated.resources.Res import foliary.foliary.generated.resources.priority_pill_blocker @@ -45,10 +45,9 @@ private data class PriorityColors( @Composable fun Priority.viewData(): PriorityViewData { - val backgroundColor = MaterialTheme.colorScheme.background + val isDarkTheme = LocalIsDarkTheme.current - val colors = remember(this) { - val isDarkTheme = backgroundColor.luminance() < .5f + val colors = remember(isDarkTheme) { if (isDarkTheme) { PriorityColors(PriorityHighDark, PriorityHighestDark, PriorityBlockerDark) } else { diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/theme/Color.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/theme/Color.kt index 3aafdec..243438d 100644 --- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/theme/Color.kt +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/theme/Color.kt @@ -1,4 +1,5 @@ package dev.appoutlet.foliary.core.ui.theme +import androidx.compose.runtime.compositionLocalOf import androidx.compose.ui.graphics.Color val primaryLight = Color(0xFF1A4D3E) @@ -72,3 +73,5 @@ val surfaceContainerLowDark = Color(0xFF22261E) val surfaceContainerDark = Color(0xFF272B22) val surfaceContainerHighDark = Color(0xFF32372B) val surfaceContainerHighestDark = Color(0xFF3D4235) + +val LocalIsDarkTheme = compositionLocalOf { false } diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/theme/Theme.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/theme/Theme.kt index 3b5de6d..c8073e2 100644 --- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/theme/Theme.kt +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/theme/Theme.kt @@ -5,6 +5,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider private val lightScheme = lightColorScheme( primary = primaryLight, @@ -91,12 +92,14 @@ fun FoliaryTheme( val colorScheme = if (isDarkTheme) darkScheme else lightScheme - MaterialTheme( - colorScheme = colorScheme, - typography = getTypography(colorScheme), - content = content, - shapes = shapes - ) + CompositionLocalProvider(LocalIsDarkTheme provides isDarkTheme) { + MaterialTheme( + colorScheme = colorScheme, + typography = getTypography(colorScheme), + content = content, + shapes = shapes + ) + } } @Composable From 94b79d44e50b8706a790f9cab117c3d5871b93ad Mon Sep 17 00:00:00 2001 From: Messias Junior Date: Tue, 20 Oct 2026 11:15:17 +0100 Subject: [PATCH 08/14] ```text refactor: reorder parameters in FoliaryPriorityPicker ``` --- .../foliary/feature/createtask/FoliaryPriorityPicker.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPicker.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPicker.kt index 4d9fbe8..6dd605c 100644 --- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPicker.kt +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPicker.kt @@ -31,10 +31,10 @@ import dev.appoutlet.foliary.data.task.database.entity.Priority @Composable fun FoliaryPriorityPicker( + label: String, selectedPriority: Priority, onPrioritySelected: (Priority) -> Unit, modifier: Modifier = Modifier, - label: String, ) { var showMenu by rememberSaveable { mutableStateOf(false) } From 13f0da42c374905dd57d928540e9d5eb36b9cdfa Mon Sep 17 00:00:00 2001 From: Messias Junior Date: Tue, 20 Oct 2026 11:27:41 +0100 Subject: [PATCH 09/14] ci: run allTests for test coverage check in PR workflow --- .github/workflows/pull-request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index a411a4d..b120bbe 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -36,7 +36,7 @@ jobs: run: ./gradlew detekt - name: Check code test coverage - run: ./gradlew jvmTest -Pkover koverVerify + run: ./gradlew allTests -Pkover koverVerify - name: Upload JVM test report if: always() From 98666dd92919e877c522d2e10218d5ab542190bd Mon Sep 17 00:00:00 2001 From: Messias Junior Date: Tue, 20 Oct 2026 11:30:58 +0100 Subject: [PATCH 10/14] CreateTaskViewModelTest --- .../createtask/CreateTaskViewModelTest.kt | 116 +++++++----------- 1 file changed, 41 insertions(+), 75 deletions(-) diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModelTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModelTest.kt index 106539c..ebb3f56 100644 --- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModelTest.kt +++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModelTest.kt @@ -17,7 +17,6 @@ import dev.mokkery.matcher.capture.capture import dev.mokkery.matcher.capture.get import dev.mokkery.mock import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe import kotlin.test.Test import kotlin.time.Instant import kotlin.uuid.Uuid @@ -42,14 +41,11 @@ class CreateTaskViewModelTest : } @Test - fun `TitleChanged - blank title button disabled`() = test { + fun `TitleChanged - should update title and save button enabled state`() = test { + val title = "Task" + viewModel.onEvent(CreateTaskEvent.TitleChanged("")) currentState.saveButtonEnabled shouldBe false - } - - @Test - fun `TitleChanged - entering a valid title enables the save button`() = test { - val title = "Task" viewModel.onEvent(CreateTaskEvent.TitleChanged(title)) @@ -77,8 +73,16 @@ class CreateTaskViewModelTest : } @Test - fun `default state should have medium priority`() = test { - currentState.priority shouldBe Priority.MEDIUM + fun `default state should have expected values`() = test { + currentState shouldBe CreateTaskViewData( + id = null, + title = "", + description = null, + priority = Priority.MEDIUM, + dueDate = null, + minDueDateMillis = startOfToday.toEpochMilliseconds(), + saveButtonEnabled = false, + ) } @Test @@ -87,18 +91,16 @@ class CreateTaskViewModelTest : expectState { copy(priority = Priority.HIGH) } } - @Test - fun `default state should have dueDate unset and minDueDate set to start of today`() = test { - currentState.dueDate shouldBe null - currentState.minDueDateMillis shouldBe startOfToday.toEpochMilliseconds() - } - @Test fun `should load saved task on init`() { val taskId = Uuid.random() - val savedTask = Task.fixture(id = taskId) + val savedTask = Task.fixture(id = taskId, priority = Priority.HIGHEST) + val taskCapture = Capture.slot() + val endOfDayDueDate = Instant.parse("2026-08-21T23:59:59.999999999Z") everySuspend { mockTaskRepository.findById(taskId) } returns savedTask + every { mockTimeProvider.endOfDay(any()) } returns endOfDayDueDate + everySuspend { mockTaskRepository.save(capture(taskCapture)) } returns Unit viewModel = CreateTaskViewModel( taskId = taskId.toString(), @@ -113,6 +115,7 @@ class CreateTaskViewModelTest : id = taskId.toString(), title = savedTask.title, description = savedTask.description, + priority = savedTask.priority, dueDate = savedTask.dueDate?.let { dueDate -> CreateTaskViewData.DueDateViewData.fixture( selectedDateMillis = dueDate.toEpochMilliseconds(), @@ -123,23 +126,21 @@ class CreateTaskViewModelTest : saveButtonEnabled = true, ) } - } - } - @Test - fun `DueDateChanged - should update dueDate`() = test { - val dueDateMillis = 1_752_996_000_000 + viewModel.onEvent(CreateTaskEvent.SaveClicked) + awaitState().saveButtonEnabled shouldBe false - viewModel.onEvent(CreateTaskEvent.DueDateChanged(dueDateMillis)) + taskCapture.get().also { task -> + task.priority shouldBe Priority.HIGHEST + task.dueDate shouldBe endOfDayDueDate + } - awaitState().dueDate.also { - it?.selectedDateMillis shouldBe dueDateMillis - it?.selectedDateDisplayText shouldNotBe null + awaitSideEffect() shouldBe CreateTaskAction.NavigateBack } } @Test - fun `DueDateChanged - null should clear dueDate`() = test { + fun `DueDateChanged - should update and clear dueDate`() = test { val dueDateMillis = 1_752_996_000_000 val expectedDueDate = CreateTaskViewData.DueDateViewData.fixture( selectedDateMillis = dueDateMillis, @@ -166,14 +167,6 @@ class CreateTaskViewModelTest : every(mockTimeProvider::now) returns creationDate everySuspend { mockTaskRepository.save(capture(taskCapture)) } returns Unit - currentState shouldBe CreateTaskViewData.fixture( - title = "", - description = null, - dueDate = null, - minDueDateMillis = startOfToday.toEpochMilliseconds(), - saveButtonEnabled = false, - ) - viewModel.onEvent(CreateTaskEvent.TitleChanged(title)) expectState { copy(title = title, saveButtonEnabled = true) } @@ -198,13 +191,14 @@ class CreateTaskViewModelTest : } @Test - fun `SaveClicked - should persist default medium priority`() = test { + fun `SaveClicked - should persist task with default values`() = test { val id = Uuid.random() val title = "Task title" + val creationDate = Instant.parse("2026-07-21T12:00:00Z") val taskCapture = Capture.slot() every(mockUuidProvider::random) returns id - every(mockTimeProvider::now) returns Instant.parse("2026-07-21T12:00:00Z") + every(mockTimeProvider::now) returns creationDate everySuspend { mockTaskRepository.save(capture(taskCapture)) } returns Unit viewModel.onEvent(CreateTaskEvent.TitleChanged(title)) @@ -213,7 +207,17 @@ class CreateTaskViewModelTest : viewModel.onEvent(CreateTaskEvent.SaveClicked) awaitState().saveButtonEnabled shouldBe false - taskCapture.get().priority shouldBe Priority.MEDIUM + taskCapture.get() shouldBe Task( + id = id, + title = title, + description = null, + creationDate = creationDate, + dueDate = null, + completionDate = null, + priority = Priority.MEDIUM, + url = null, + location = null, + ) } @Test @@ -238,44 +242,6 @@ class CreateTaskViewModelTest : taskCapture.get().priority shouldBe Priority.BLOCKER } - @Test - fun `should preserve saved task priority on init`() { - val taskId = Uuid.random() - val savedTask = Task.fixture(id = taskId, priority = Priority.HIGHEST, dueDate = null) - val taskCapture = Capture.slot() - - everySuspend { mockTaskRepository.findById(taskId) } returns savedTask - every(mockTimeProvider::now) returns Instant.parse("2026-07-22T12:00:00Z") - everySuspend { mockTaskRepository.save(capture(taskCapture)) } returns Unit - - viewModel = CreateTaskViewModel( - taskId = taskId.toString(), - taskRepository = mockTaskRepository, - timeProvider = mockTimeProvider, - uuidProvider = mockUuidProvider, - ) - - test { - expectState { - copy( - id = taskId.toString(), - title = savedTask.title, - description = savedTask.description, - priority = Priority.HIGHEST, - dueDate = null, - saveButtonEnabled = true, - ) - } - - viewModel.onEvent(CreateTaskEvent.SaveClicked) - awaitState().saveButtonEnabled shouldBe false - - taskCapture.get().priority shouldBe Priority.HIGHEST - - awaitSideEffect() shouldBe CreateTaskAction.NavigateBack - } - } - @Test fun `SaveClicked - should keep creationDate when editing an existing task`() { val taskId = Uuid.random() From cd84dd24504467886f23f6c16ea68fc7c1cb7f62 Mon Sep 17 00:00:00 2001 From: Messias Junior Date: Tue, 20 Oct 2026 11:40:09 +0100 Subject: [PATCH 11/14] string resource isntead of hardcoded --- .../feature/createtask/FoliaryPriorityPickerTest.kt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPickerTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPickerTest.kt index 56c2417..087f819 100644 --- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPickerTest.kt +++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPickerTest.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.test.performClick import androidx.compose.ui.test.v2.runComposeUiTest import dev.appoutlet.foliary.data.task.database.entity.Priority import foliary.foliary.generated.resources.Res +import foliary.foliary.generated.resources.create_task_priority_label import foliary.foliary.generated.resources.priority_pill_blocker import foliary.foliary.generated.resources.priority_pill_high import foliary.foliary.generated.resources.priority_pill_highest @@ -18,6 +19,7 @@ import foliary.foliary.generated.resources.priority_pill_lowest import foliary.foliary.generated.resources.priority_pill_medium import io.kotest.matchers.shouldBe import org.jetbrains.compose.resources.getString +import org.jetbrains.compose.resources.stringResource import kotlin.test.Test @OptIn(ExperimentalTestApi::class) @@ -25,8 +27,7 @@ class FoliaryPriorityPickerTest { @Test fun `should render selected priority as pill`() = runComposeUiTest { - val label = "Priority" - val mediumText = getString(Res.string.priority_pill_medium) + val label = getString(Res.string.create_task_priority_label) setContent { FoliaryPriorityPicker( @@ -37,7 +38,7 @@ class FoliaryPriorityPickerTest { } onNodeWithText(label).assertIsDisplayed() - onNodeWithText(mediumText).assertIsDisplayed() + onNodeWithText(getString(Res.string.priority_pill_medium)).assertIsDisplayed() onNodeWithTag("FoliaryPriorityPicker:Value", useUnmergedTree = true).assertIsDisplayed() } @@ -47,7 +48,7 @@ class FoliaryPriorityPickerTest { FoliaryPriorityPicker( selectedPriority = Priority.MEDIUM, onPrioritySelected = {}, - label = "Priority" + label = stringResource(Res.string.create_task_priority_label) ) } @@ -62,7 +63,7 @@ class FoliaryPriorityPickerTest { FoliaryPriorityPicker( selectedPriority = Priority.MEDIUM, onPrioritySelected = {}, - label = "Priority" + label = stringResource(Res.string.create_task_priority_label) ) } @@ -85,7 +86,7 @@ class FoliaryPriorityPickerTest { FoliaryPriorityPicker( selectedPriority = Priority.MEDIUM, onPrioritySelected = { selectedPriority = it }, - label = "Priority" + label = stringResource(Res.string.create_task_priority_label) ) } From 45803b376c1dc29e1f6eb5b20fd7536a44590347 Mon Sep 17 00:00:00 2001 From: Messias Junior Date: Tue, 20 Oct 2026 11:42:00 +0100 Subject: [PATCH 12/14] chore(deps): downgrade kotlin from 2.4.21 to 2.4.20 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b4322ab..e5c4dc7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ koin = "4.2.2" koin-compiler = "1.2.1" kotest = "6.2.5" ktor = "3.6.0" -kotlin = "2.4.21" +kotlin = "2.4.20" kotlinx-coroutines = "1.11.0" kotlinx-serialization = "1.11.0" ksp = "2.3.12" From 952ddd90768f797cbac71188b47431f71a4e9bde Mon Sep 17 00:00:00 2001 From: Messias Junior Date: Tue, 20 Oct 2026 12:05:39 +0100 Subject: [PATCH 13/14] run only jvm tests --- .github/workflows/pull-request.yml | 2 +- .../DefaultAuthenticationRepositoryTest.android.kt | 6 ++++++ .../core/ui/component/button/FoliaryPrimaryButtonTest.kt | 2 +- .../core/ui/component/button/FoliarySecondaryButtonTest.kt | 2 +- .../authentication/DefaultAuthenticationRepositoryTest.kt | 5 +++-- .../DefaultAuthenticationRepositoryTest.ios.kt | 5 +++++ .../DefaultAuthenticationRepositoryTest.jvm.kt | 5 +++++ gradle/libs.versions.toml | 2 +- 8 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 foliary/src/androidHostTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.android.kt create mode 100644 foliary/src/iosTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.ios.kt create mode 100644 foliary/src/jvmTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.jvm.kt diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index b120bbe..a411a4d 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -36,7 +36,7 @@ jobs: run: ./gradlew detekt - name: Check code test coverage - run: ./gradlew allTests -Pkover koverVerify + run: ./gradlew jvmTest -Pkover koverVerify - name: Upload JVM test report if: always() diff --git a/foliary/src/androidHostTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.android.kt b/foliary/src/androidHostTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.android.kt new file mode 100644 index 0000000..83db01d --- /dev/null +++ b/foliary/src/androidHostTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.android.kt @@ -0,0 +1,6 @@ +package dev.appoutlet.foliary.data.authentication + +import eu.anifantakis.lib.ksafe.KSafe +import kotlin.io.path.createTempDirectory + +actual fun platformKSafe(): KSafe = error("") diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/button/FoliaryPrimaryButtonTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/button/FoliaryPrimaryButtonTest.kt index cdb2f2b..7b2b2f5 100644 --- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/button/FoliaryPrimaryButtonTest.kt +++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/button/FoliaryPrimaryButtonTest.kt @@ -17,7 +17,7 @@ import kotlin.test.Test class FoliaryPrimaryButtonTest { @Test - fun `should show content, be enabled by default and invoke onClick when clicked`() = runComposeUiTest { + fun `should show content be enabled by default and invoke onClick when clicked`() = runComposeUiTest { var clicked = false val testTag = "PrimaryButton" val buttonText = "Click Me" diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/button/FoliarySecondaryButtonTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/button/FoliarySecondaryButtonTest.kt index d629684..dc07aab 100644 --- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/button/FoliarySecondaryButtonTest.kt +++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/button/FoliarySecondaryButtonTest.kt @@ -17,7 +17,7 @@ import kotlin.test.Test class FoliarySecondaryButtonTest { @Test - fun `should show content, be enabled by default and invoke onClick when clicked`() = runComposeUiTest { + fun `should show content be enabled by default and invoke onClick when clicked`() = runComposeUiTest { var clicked = false val testTag = "SecondaryButton" val buttonText = "Click Me" diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.kt index cebefc3..4eb77ba 100644 --- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.kt +++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.kt @@ -18,12 +18,11 @@ import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest -import kotlin.io.path.createTempDirectory import kotlin.test.Test class DefaultAuthenticationRepositoryTest { private val mockAuth = mock(mode = MockMode.autoUnit) - private val ksafe = KSafe(baseDir = createTempDirectory("ksafe-test").toFile()) + private val ksafe = platformKsafe() val subject = DefaultAuthenticationRepository( lazyAuth = lazy { mockAuth }, @@ -116,3 +115,5 @@ class DefaultAuthenticationRepositoryTest { subject.currentUser() shouldBe fixture } } + +expect fun platformKSafe(): KSafe diff --git a/foliary/src/iosTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.ios.kt b/foliary/src/iosTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.ios.kt new file mode 100644 index 0000000..c73788d --- /dev/null +++ b/foliary/src/iosTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.ios.kt @@ -0,0 +1,5 @@ +package dev.appoutlet.foliary.data.authentication + +import eu.anifantakis.lib.ksafe.KSafe + +actual fun platformKSafe() = KSafe() diff --git a/foliary/src/jvmTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.jvm.kt b/foliary/src/jvmTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.jvm.kt new file mode 100644 index 0000000..c73788d --- /dev/null +++ b/foliary/src/jvmTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.jvm.kt @@ -0,0 +1,5 @@ +package dev.appoutlet.foliary.data.authentication + +import eu.anifantakis.lib.ksafe.KSafe + +actual fun platformKSafe() = KSafe() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e5c4dc7..d6a039c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -87,7 +87,7 @@ kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-c kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } -ksafe = "eu.anifantakis:ksafe:3.2.0" +ksafe = "eu.anifantakis:ksafe:3.3.0" ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } From d05a7601aaae2d65936bfa9027906aff5e56ca4c Mon Sep 17 00:00:00 2001 From: Messias Junior Date: Tue, 20 Oct 2026 12:06:38 +0100 Subject: [PATCH 14/14] fix method call --- .../data/authentication/DefaultAuthenticationRepositoryTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.kt index 4eb77ba..04d89ff 100644 --- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.kt +++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.kt @@ -22,7 +22,7 @@ import kotlin.test.Test class DefaultAuthenticationRepositoryTest { private val mockAuth = mock(mode = MockMode.autoUnit) - private val ksafe = platformKsafe() + private val ksafe = platformKSafe() val subject = DefaultAuthenticationRepository( lazyAuth = lazy { mockAuth },