Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions foliary/src/commonMain/composeResources/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@
<string name="today_title">Foliary</string>
<!-- endregion -->

<!-- region Task Item -->
<string name="task_item_overdue">Overdue</string>
<!-- endregion -->

<!-- region System trat -->
<string name="open_app">Open Foliary</string>
<string name="quit">Quit</string>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package dev.appoutlet.foliary.core.ui.component.task

import androidx.compose.foundation.layout.Arrangement
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.Checkbox
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import dev.appoutlet.foliary.core.ui.component.card.FoliaryCard
import dev.appoutlet.foliary.core.ui.theme.FoliaryTheme
import foliary.foliary.generated.resources.Res
import foliary.foliary.generated.resources.task_item_overdue
import org.jetbrains.compose.resources.stringResource
import androidx.compose.ui.tooling.preview.Preview

@Composable
fun TaskItem(
title: String,
modifier: Modifier = Modifier,
description: String? = null,
isCompleted: Boolean = false,
isOverdue: Boolean = false,
) {
FoliaryCard(modifier = modifier.fillMaxWidth()) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
Checkbox(
checked = isCompleted,
onCheckedChange = null,
modifier = Modifier.testTag("TaskItem:Checkbox"),
)

Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text = title,
modifier = Modifier.testTag("TaskItem:Title"),
style = MaterialTheme.typography.titleMedium,
color = if (isCompleted) {
MaterialTheme.colorScheme.onSurface.copy(alpha = CompletedAlpha)
} else {
MaterialTheme.colorScheme.onSurface
},
textDecoration = if (isCompleted) TextDecoration.LineThrough else null,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)

description?.let {
Text(
text = it,
modifier = Modifier.testTag("TaskItem:Description"),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = MaxDescriptionLines,
overflow = TextOverflow.Ellipsis,
)
}

if (isOverdue && !isCompleted) {
OverduePill()
}
}
}
}
}

@Composable
private fun OverduePill() {
Surface(
modifier = Modifier
.padding(top = 4.dp)
.testTag("TaskItem:OverduePill"),
shape = MaterialTheme.shapes.small,
color = MaterialTheme.colorScheme.errorContainer,
contentColor = MaterialTheme.colorScheme.onErrorContainer,
) {
Text(
text = stringResource(Res.string.task_item_overdue),
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
style = MaterialTheme.typography.labelSmall,
)
}
}

private const val CompletedAlpha = 0.38f
private const val MaxDescriptionLines = 3

@Preview
@Composable
private fun TaskItemPreview() {
FoliaryTheme {
TaskItem(
title = "Write API Documentation",
description = "Draft the OpenAPI spec for the new endpoints",
)
}
}

@Preview
@Composable
private fun TaskItemOverduePreview() {
FoliaryTheme {
TaskItem(
title = "Pay electricity bill",
isOverdue = true,
)
}
}

@Preview
@Composable
private fun TaskItemCompletedPreview() {
FoliaryTheme {
TaskItem(
title = "Inbox Zero",
description = "Process all unread emails and archive newsletters",
isCompleted = true,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package dev.appoutlet.foliary.core.ui.component.task

import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.runComposeUiTest
import foliary.foliary.generated.resources.Res
import foliary.foliary.generated.resources.task_item_overdue
import org.jetbrains.compose.resources.getString
import kotlin.test.Test

@OptIn(ExperimentalTestApi::class)
class TaskItemTest {

@Test
fun `should render title and description`() = runComposeUiTest {
val title = "Task title"
val description = "Task description"

setContent {
TaskItem(
title = title,
description = description,
)
}

onNodeWithTag("TaskItem:Title").assertIsDisplayed().assertTextEquals(title)
onNodeWithTag("TaskItem:Description").assertIsDisplayed().assertTextEquals(description)
}

@Test
fun `should not show description when null`() = runComposeUiTest {
setContent {
TaskItem(title = "Task title")
}

onNodeWithTag("TaskItem:Description").assertDoesNotExist()
}

@Test
fun `should show unchecked checkbox by default`() = runComposeUiTest {
setContent {
TaskItem(title = "Task title")
}

onNodeWithTag("TaskItem:Checkbox").assertIsDisplayed()
}

@Test
fun `should show overdue pill when isOverdue is true and not completed`() = runComposeUiTest {
setContent {
TaskItem(
title = "Task title",
isOverdue = true,
isCompleted = false,
)
}

onNodeWithTag("TaskItem:OverduePill").assertIsDisplayed()
onNodeWithText(getString(Res.string.task_item_overdue)).assertIsDisplayed()
}

@Test
fun `should not show overdue pill when not overdue`() = runComposeUiTest {
setContent {
TaskItem(
title = "Task title",
isOverdue = false,
)
}

onNodeWithTag("TaskItem:OverduePill").assertDoesNotExist()
}

@Test
fun `should not show overdue pill when completed`() = runComposeUiTest {
setContent {
TaskItem(
title = "Task title",
isOverdue = true,
isCompleted = true,
)
}

onNodeWithTag("TaskItem:OverduePill").assertDoesNotExist()
}

@Test
fun `should show checked checkbox and distinct title when completed`() = runComposeUiTest {
setContent {
TaskItem(
title = "Task title",
isCompleted = true,
)
}

onNodeWithTag("TaskItem:Checkbox").assertIsDisplayed()
onNodeWithTag("TaskItem:Title").assertIsDisplayed()
}
}
Loading