mirror of
https://github.com/avinal/nikki.git
synced 2026-07-03 21:40:09 +05:30
Add archived view, comment delete, accessibility, entity tests
Archived memos:
- "view archived memos" in explorer navigates to memo list with
archived filter active
- Fetches archived memos from API (state == ARCHIVED)
- MemoCard shows "restore" instead of "archive" in context menu
- Restore calls updateMemo(state=NORMAL), upserts to local cache
- Compose card hidden when viewing archived
- Filter banner shows "archived memos" with clear option
Comment deletion:
- Each comment shows "delete" link next to creator/date
- Calls deleteMemo on the comment (child memo) and removes locally
Accessibility:
- Task group collapse icons now have contentDescription
("Expand"/"Collapse")
Test suite: 107 tests (5 new entity mapper tests)
- MemoEntityMappersTest: round-trip field preservation, empty tags,
cachedAt, all visibility variants, timestamp fidelity
Co-Authored-By: Claude Opus 4.6 (1M context)
Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
This commit is contained in:
@@ -167,6 +167,18 @@ class MemoRepository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun restoreMemo(id: String): ApiResult<Memo> {
|
||||||
|
return when (val result = apiClient.updateMemo(id = id, state = "NORMAL")) {
|
||||||
|
is ApiResult.Success -> {
|
||||||
|
val memo = result.data.toDomain()
|
||||||
|
memoDao.upsert(memo.toEntity(nowMillis()))
|
||||||
|
ApiResult.Success(memo)
|
||||||
|
}
|
||||||
|
is ApiResult.Error -> result
|
||||||
|
is ApiResult.NetworkError -> result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun deleteMemo(id: String): ApiResult<Unit> {
|
suspend fun deleteMemo(id: String): ApiResult<Unit> {
|
||||||
return when (val result = apiClient.deleteMemo(id)) {
|
return when (val result = apiClient.deleteMemo(id)) {
|
||||||
is ApiResult.Success -> {
|
is ApiResult.Success -> {
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ fun MemoCard(
|
|||||||
onSave: ((String, MemoVisibility) -> Unit)? = null,
|
onSave: ((String, MemoVisibility) -> Unit)? = null,
|
||||||
onReact: ((String) -> Unit)? = null,
|
onReact: ((String) -> Unit)? = null,
|
||||||
onTaskToggle: ((Int, Boolean) -> Unit)? = null,
|
onTaskToggle: ((Int, Boolean) -> Unit)? = null,
|
||||||
|
onRestore: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val accent = LocalAccentColor.current
|
val accent = LocalAccentColor.current
|
||||||
val textColor = MaterialTheme.colorScheme.onBackground
|
val textColor = MaterialTheme.colorScheme.onBackground
|
||||||
@@ -94,15 +95,23 @@ fun MemoCard(
|
|||||||
title = null,
|
title = null,
|
||||||
text = {
|
text = {
|
||||||
Column {
|
Column {
|
||||||
MetroMenuItem(if (memo.pinned) "unpin" else "pin", textColor) { showMenu = false; onPin?.invoke() }
|
if (onRestore != null) {
|
||||||
MetroMenuItem("edit", textColor) {
|
MetroMenuItem("restore", textColor) { showMenu = false; onRestore.invoke() }
|
||||||
showMenu = false; editContent = memo.content; editVisibility = memo.visibility; isEditing = true
|
MetroMenuItem("copy content", textColor) { showMenu = false; clipboardManager.setText(AnnotatedString(memo.content)) }
|
||||||
|
MetroMenuItem("share", textColor) { showMenu = false; sharePlainText(memo.content) }
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
MetroMenuItem("delete", MaterialTheme.colorScheme.error) { showMenu = false; showDeleteDialog = true }
|
||||||
|
} else {
|
||||||
|
MetroMenuItem(if (memo.pinned) "unpin" else "pin", textColor) { showMenu = false; onPin?.invoke() }
|
||||||
|
MetroMenuItem("edit", textColor) {
|
||||||
|
showMenu = false; editContent = memo.content; editVisibility = memo.visibility; isEditing = true
|
||||||
|
}
|
||||||
|
MetroMenuItem("copy content", textColor) { showMenu = false; clipboardManager.setText(AnnotatedString(memo.content)) }
|
||||||
|
MetroMenuItem("share", textColor) { showMenu = false; sharePlainText(memo.content) }
|
||||||
|
MetroMenuItem("archive", textColor) { showMenu = false; onArchive?.invoke() }
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
MetroMenuItem("delete", MaterialTheme.colorScheme.error) { showMenu = false; showDeleteDialog = true }
|
||||||
}
|
}
|
||||||
MetroMenuItem("copy content", textColor) { showMenu = false; clipboardManager.setText(AnnotatedString(memo.content)) }
|
|
||||||
MetroMenuItem("share", textColor) { showMenu = false; sharePlainText(memo.content) }
|
|
||||||
MetroMenuItem("archive", textColor) { showMenu = false; onArchive?.invoke() }
|
|
||||||
Spacer(Modifier.height(8.dp))
|
|
||||||
MetroMenuItem("delete", MaterialTheme.colorScheme.error) { showMenu = false; showDeleteDialog = true }
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {},
|
confirmButton = {},
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ fun MainScreen(
|
|||||||
var dateFilter by remember { mutableStateOf<String?>(null) }
|
var dateFilter by remember { mutableStateOf<String?>(null) }
|
||||||
var tagFilter by remember { mutableStateOf<String?>(null) }
|
var tagFilter by remember { mutableStateOf<String?>(null) }
|
||||||
var searchFilter by remember { mutableStateOf<String?>(null) }
|
var searchFilter by remember { mutableStateOf<String?>(null) }
|
||||||
|
var showArchived by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val navigateToMemosWithFilter: () -> Unit = { scope.launch { pagerState.animateScrollToPage(1) } }
|
val navigateToMemosWithFilter: () -> Unit = { scope.launch { pagerState.animateScrollToPage(1) } }
|
||||||
|
|
||||||
@@ -135,15 +136,19 @@ fun MainScreen(
|
|||||||
deps = deps,
|
deps = deps,
|
||||||
onMemoClick = onMemoClick,
|
onMemoClick = onMemoClick,
|
||||||
onDateSelected = { date ->
|
onDateSelected = { date ->
|
||||||
dateFilter = date; tagFilter = null; searchFilter = null
|
dateFilter = date; tagFilter = null; searchFilter = null; showArchived = false
|
||||||
navigateToMemosWithFilter()
|
navigateToMemosWithFilter()
|
||||||
},
|
},
|
||||||
onTagSelected = { tag ->
|
onTagSelected = { tag ->
|
||||||
tagFilter = tag; dateFilter = null; searchFilter = null
|
tagFilter = tag; dateFilter = null; searchFilter = null; showArchived = false
|
||||||
navigateToMemosWithFilter()
|
navigateToMemosWithFilter()
|
||||||
},
|
},
|
||||||
onSearchSubmit = { query ->
|
onSearchSubmit = { query ->
|
||||||
searchFilter = query; dateFilter = null; tagFilter = null
|
searchFilter = query; dateFilter = null; tagFilter = null; showArchived = false
|
||||||
|
navigateToMemosWithFilter()
|
||||||
|
},
|
||||||
|
onShowArchived = {
|
||||||
|
showArchived = true; dateFilter = null; tagFilter = null; searchFilter = null
|
||||||
navigateToMemosWithFilter()
|
navigateToMemosWithFilter()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -154,7 +159,8 @@ fun MainScreen(
|
|||||||
dateFilter = dateFilter,
|
dateFilter = dateFilter,
|
||||||
tagFilter = tagFilter,
|
tagFilter = tagFilter,
|
||||||
searchFilter = searchFilter,
|
searchFilter = searchFilter,
|
||||||
onClearFilter = { dateFilter = null; tagFilter = null; searchFilter = null },
|
showArchived = showArchived,
|
||||||
|
onClearFilter = { dateFilter = null; tagFilter = null; searchFilter = null; showArchived = false },
|
||||||
)
|
)
|
||||||
2 -> TaskListScreen(deps = deps, onMemoClick = onMemoClick)
|
2 -> TaskListScreen(deps = deps, onMemoClick = onMemoClick)
|
||||||
3 -> SettingsScreen(deps = deps, onLogout = onLogout)
|
3 -> SettingsScreen(deps = deps, onLogout = onLogout)
|
||||||
@@ -171,6 +177,7 @@ private fun ExplorerPage(
|
|||||||
onDateSelected: (String) -> Unit,
|
onDateSelected: (String) -> Unit,
|
||||||
onTagSelected: (String) -> Unit,
|
onTagSelected: (String) -> Unit,
|
||||||
onSearchSubmit: (String) -> Unit,
|
onSearchSubmit: (String) -> Unit,
|
||||||
|
onShowArchived: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val memos by deps.memoRepository.observeMemos().collectAsState(initial = emptyList())
|
val memos by deps.memoRepository.observeMemos().collectAsState(initial = emptyList())
|
||||||
val accent = LocalAccentColor.current
|
val accent = LocalAccentColor.current
|
||||||
@@ -252,7 +259,7 @@ private fun ExplorerPage(
|
|||||||
"view archived memos",
|
"view archived memos",
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
color = accent,
|
color = accent,
|
||||||
modifier = Modifier.clickable { /* navigate to archived */ }.padding(vertical = 4.dp),
|
modifier = Modifier.clickable { onShowArchived() }.padding(vertical = 4.dp),
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|||||||
@@ -192,9 +192,18 @@ private fun CommentsSection(
|
|||||||
} else {
|
} else {
|
||||||
comments.forEach { comment ->
|
comments.forEach { comment ->
|
||||||
Column(modifier = Modifier.padding(bottom = 12.dp)) {
|
Column(modifier = Modifier.padding(bottom = 12.dp)) {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
|
||||||
Text(comment.creator, fontSize = 12.sp, color = subtleColor)
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
Text(formatDateTime(comment.createTime), fontSize = 12.sp, color = subtleColor)
|
Text(comment.creator, fontSize = 12.sp, color = subtleColor)
|
||||||
|
Text(formatDateTime(comment.createTime), fontSize = 12.sp, color = subtleColor)
|
||||||
|
}
|
||||||
|
Text("delete", fontSize = 12.sp, color = subtleColor.copy(alpha = 0.5f),
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
scope.launch {
|
||||||
|
deps.apiClient.deleteMemo(comment.id)
|
||||||
|
comments = comments.filter { it.id != comment.id }
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(2.dp))
|
Spacer(Modifier.height(2.dp))
|
||||||
MarkdownText(markdown = comment.content)
|
MarkdownText(markdown = comment.content)
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.avinal.memos.AppDependencies
|
import com.avinal.memos.AppDependencies
|
||||||
|
import com.avinal.memos.api.ApiResult
|
||||||
|
import com.avinal.memos.api.model.toDomain
|
||||||
|
import com.avinal.memos.domain.Memo
|
||||||
import com.avinal.memos.domain.MemoVisibility
|
import com.avinal.memos.domain.MemoVisibility
|
||||||
import com.avinal.memos.ui.components.MemoCard
|
import com.avinal.memos.ui.components.MemoCard
|
||||||
import com.avinal.memos.ui.theme.LocalAccentColor
|
import com.avinal.memos.ui.theme.LocalAccentColor
|
||||||
@@ -59,22 +62,41 @@ fun MemoListScreen(
|
|||||||
dateFilter: String? = null,
|
dateFilter: String? = null,
|
||||||
tagFilter: String? = null,
|
tagFilter: String? = null,
|
||||||
searchFilter: String? = null,
|
searchFilter: String? = null,
|
||||||
|
showArchived: Boolean = false,
|
||||||
onClearFilter: (() -> Unit)? = null,
|
onClearFilter: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val viewModel = viewModel { MemoListViewModel(deps.memoRepository) }
|
val viewModel = viewModel { MemoListViewModel(deps.memoRepository) }
|
||||||
val allMemos by viewModel.memos.collectAsState()
|
val allMemos by viewModel.memos.collectAsState()
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
val hasFilter = dateFilter != null || tagFilter != null || searchFilter != null
|
var archivedMemos by remember { mutableStateOf<List<Memo>>(emptyList()) }
|
||||||
|
var isLoadingArchived by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LaunchedEffect(showArchived) {
|
||||||
|
if (showArchived) {
|
||||||
|
isLoadingArchived = true
|
||||||
|
when (val result = deps.apiClient.listArchivedMemos()) {
|
||||||
|
is ApiResult.Success -> {
|
||||||
|
archivedMemos = result.data.memos.map { it.toDomain() }
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
isLoadingArchived = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val hasFilter = dateFilter != null || tagFilter != null || searchFilter != null || showArchived
|
||||||
val filterLabel = when {
|
val filterLabel = when {
|
||||||
|
showArchived -> "archived memos"
|
||||||
dateFilter != null -> "date: $dateFilter"
|
dateFilter != null -> "date: $dateFilter"
|
||||||
tagFilter != null -> "tag: #$tagFilter"
|
tagFilter != null -> "tag: #$tagFilter"
|
||||||
searchFilter != null -> "search: $searchFilter"
|
searchFilter != null -> "search: $searchFilter"
|
||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
|
|
||||||
val memos = remember(allMemos, dateFilter, tagFilter, searchFilter) {
|
val memos = remember(allMemos, dateFilter, tagFilter, searchFilter, showArchived, archivedMemos) {
|
||||||
when {
|
when {
|
||||||
|
showArchived -> archivedMemos
|
||||||
dateFilter != null -> {
|
dateFilter != null -> {
|
||||||
val parts = dateFilter.split("-")
|
val parts = dateFilter.split("-")
|
||||||
if (parts.size == 3) {
|
if (parts.size == 3) {
|
||||||
@@ -114,7 +136,7 @@ fun MemoListScreen(
|
|||||||
uploadScope.launch {
|
uploadScope.launch {
|
||||||
val base64 = Base64.encode(pickedFile.bytes)
|
val base64 = Base64.encode(pickedFile.bytes)
|
||||||
when (val result = deps.memoRepository.uploadAttachment(pickedFile.name, pickedFile.mimeType, base64)) {
|
when (val result = deps.memoRepository.uploadAttachment(pickedFile.name, pickedFile.mimeType, base64)) {
|
||||||
is com.avinal.memos.api.ApiResult.Success -> {
|
is ApiResult.Success -> {
|
||||||
uploadedAttachmentNames = uploadedAttachmentNames + result.data
|
uploadedAttachmentNames = uploadedAttachmentNames + result.data
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
@@ -185,6 +207,7 @@ fun MemoListScreen(
|
|||||||
state = listState,
|
state = listState,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
|
if (!showArchived) {
|
||||||
item(key = "compose") {
|
item(key = "compose") {
|
||||||
var showInsertMenu by remember { mutableStateOf(false) }
|
var showInsertMenu by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
@@ -348,8 +371,15 @@ fun MemoListScreen(
|
|||||||
.background(MaterialTheme.colorScheme.outline.copy(alpha = 0.2f))
|
.background(MaterialTheme.colorScheme.outline.copy(alpha = 0.2f))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (uiState.isInitialLoading && memos.isEmpty()) {
|
if (showArchived && isLoadingArchived) {
|
||||||
|
item {
|
||||||
|
Box(Modifier.fillMaxWidth().padding(top = 48.dp), contentAlignment = Alignment.Center) {
|
||||||
|
androidx.compose.material3.CircularProgressIndicator(color = accent, strokeWidth = 2.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (uiState.isInitialLoading && memos.isEmpty() && !showArchived) {
|
||||||
item {
|
item {
|
||||||
Box(Modifier.fillMaxWidth().padding(top = 48.dp), contentAlignment = Alignment.Center) {
|
Box(Modifier.fillMaxWidth().padding(top = 48.dp), contentAlignment = Alignment.Center) {
|
||||||
androidx.compose.material3.CircularProgressIndicator(color = accent, strokeWidth = 2.dp)
|
androidx.compose.material3.CircularProgressIndicator(color = accent, strokeWidth = 2.dp)
|
||||||
@@ -358,7 +388,7 @@ fun MemoListScreen(
|
|||||||
} else if (memos.isEmpty() && !uiState.isRefreshing) {
|
} else if (memos.isEmpty() && !uiState.isRefreshing) {
|
||||||
item {
|
item {
|
||||||
Box(Modifier.fillMaxWidth().padding(top = 48.dp), contentAlignment = Alignment.Center) {
|
Box(Modifier.fillMaxWidth().padding(top = 48.dp), contentAlignment = Alignment.Center) {
|
||||||
Text("no memos yet", fontSize = 15.sp, color = subtleColor)
|
Text(if (showArchived) "no archived memos" else "no memos yet", fontSize = 15.sp, color = subtleColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -373,19 +403,37 @@ fun MemoListScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
items(memos, key = { it.id }) { memo ->
|
items(memos, key = { it.id }) { memo ->
|
||||||
MemoCard(
|
if (showArchived) {
|
||||||
memo = memo,
|
MemoCard(
|
||||||
onClick = { onMemoClick(memo.id) },
|
memo = memo,
|
||||||
serverUrl = serverUrl,
|
onClick = { onMemoClick(memo.id) },
|
||||||
onPin = { viewModel.togglePin(memo) },
|
serverUrl = serverUrl,
|
||||||
onArchive = { viewModel.archiveMemo(memo.id) },
|
onPin = null,
|
||||||
onDelete = { viewModel.deleteMemo(memo.id) },
|
onArchive = null,
|
||||||
onSave = { content, visibility ->
|
onDelete = { viewModel.deleteMemo(memo.id) },
|
||||||
viewModel.updateMemo(memo.id, content, visibility)
|
onSave = null,
|
||||||
},
|
onReact = null,
|
||||||
onReact = { emoji -> viewModel.reactToMemo(memo.id, emoji) },
|
onTaskToggle = null,
|
||||||
onTaskToggle = { lineIndex, checked -> viewModel.toggleTask(memo.id, lineIndex, checked) },
|
onRestore = {
|
||||||
)
|
viewModel.restoreMemo(memo.id)
|
||||||
|
archivedMemos = archivedMemos.filter { it.id != memo.id }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
MemoCard(
|
||||||
|
memo = memo,
|
||||||
|
onClick = { onMemoClick(memo.id) },
|
||||||
|
serverUrl = serverUrl,
|
||||||
|
onPin = { viewModel.togglePin(memo) },
|
||||||
|
onArchive = { viewModel.archiveMemo(memo.id) },
|
||||||
|
onDelete = { viewModel.deleteMemo(memo.id) },
|
||||||
|
onSave = { content, visibility ->
|
||||||
|
viewModel.updateMemo(memo.id, content, visibility)
|
||||||
|
},
|
||||||
|
onReact = { emoji -> viewModel.reactToMemo(memo.id, emoji) },
|
||||||
|
onTaskToggle = { lineIndex, checked -> viewModel.toggleTask(memo.id, lineIndex, checked) },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,6 +122,10 @@ class MemoListViewModel(private val memoRepository: MemoRepository) : ViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun restoreMemo(id: String) {
|
||||||
|
viewModelScope.launch { memoRepository.restoreMemo(id) }
|
||||||
|
}
|
||||||
|
|
||||||
fun reactToMemo(memoId: String, emoji: String) {
|
fun reactToMemo(memoId: String, emoji: String) {
|
||||||
viewModelScope.launch { memoRepository.reactToMemo(memoId, emoji) }
|
viewModelScope.launch { memoRepository.reactToMemo(memoId, emoji) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ fun TaskListScreen(
|
|||||||
Spacer(Modifier.width(6.dp))
|
Spacer(Modifier.width(6.dp))
|
||||||
Icon(
|
Icon(
|
||||||
if (group.collapsed) Icons.Default.KeyboardArrowDown else Icons.Default.KeyboardArrowUp,
|
if (group.collapsed) Icons.Default.KeyboardArrowDown else Icons.Default.KeyboardArrowUp,
|
||||||
contentDescription = null,
|
contentDescription = if (group.collapsed) "Expand" else "Collapse",
|
||||||
modifier = Modifier.size(18.dp),
|
modifier = Modifier.size(18.dp),
|
||||||
tint = subtleColor,
|
tint = subtleColor,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package com.avinal.memos
|
||||||
|
|
||||||
|
import com.avinal.memos.db.entity.MemoEntity
|
||||||
|
import com.avinal.memos.db.entity.toDomain
|
||||||
|
import com.avinal.memos.db.entity.toEntity
|
||||||
|
import com.avinal.memos.domain.Memo
|
||||||
|
import com.avinal.memos.domain.MemoVisibility
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
import kotlin.time.Instant
|
||||||
|
|
||||||
|
class MemoEntityMappersTest {
|
||||||
|
|
||||||
|
private fun makeMemo(
|
||||||
|
id: String = "abc",
|
||||||
|
content: String = "hello",
|
||||||
|
tags: List<String> = listOf("work"),
|
||||||
|
pinned: Boolean = false,
|
||||||
|
commentCount: Int = 0,
|
||||||
|
) = Memo(
|
||||||
|
id = id, uid = "uid1", content = content, visibility = MemoVisibility.PRIVATE,
|
||||||
|
pinned = pinned, state = "NORMAL",
|
||||||
|
createTime = Instant.fromEpochMilliseconds(1000000),
|
||||||
|
updateTime = Instant.fromEpochMilliseconds(2000000),
|
||||||
|
displayTime = Instant.fromEpochMilliseconds(2000000),
|
||||||
|
creator = "test",
|
||||||
|
hasTaskList = false, hasIncompleteTasks = false, title = "Title",
|
||||||
|
tags = tags, snippet = "hello...",
|
||||||
|
commentCount = commentCount,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun roundTripPreservesAllFields() {
|
||||||
|
val original = makeMemo(tags = listOf("work", "urgent"), pinned = true, commentCount = 3)
|
||||||
|
val entity = original.toEntity(999L)
|
||||||
|
val restored = entity.toDomain()
|
||||||
|
assertEquals(original.id, restored.id)
|
||||||
|
assertEquals(original.content, restored.content)
|
||||||
|
assertEquals(original.visibility, restored.visibility)
|
||||||
|
assertEquals(original.pinned, restored.pinned)
|
||||||
|
assertEquals(original.tags, restored.tags)
|
||||||
|
assertEquals(original.createTime, restored.createTime)
|
||||||
|
assertEquals(original.commentCount, restored.commentCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun roundTripPreservesEmptyTags() {
|
||||||
|
val original = makeMemo(tags = emptyList())
|
||||||
|
val restored = original.toEntity(0).toDomain()
|
||||||
|
assertTrue(restored.tags.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun cachedAtIsStored() {
|
||||||
|
val entity = makeMemo().toEntity(12345L)
|
||||||
|
assertEquals(12345L, entity.cachedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun visibilityRoundTrips() {
|
||||||
|
MemoVisibility.entries.forEach { vis ->
|
||||||
|
val memo = makeMemo().copy(visibility = vis)
|
||||||
|
val restored = memo.toEntity(0).toDomain()
|
||||||
|
assertEquals(vis, restored.visibility)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun timestampsPreserved() {
|
||||||
|
val memo = makeMemo()
|
||||||
|
val entity = memo.toEntity(0)
|
||||||
|
assertEquals(1000000L, entity.createTime)
|
||||||
|
assertEquals(2000000L, entity.updateTime)
|
||||||
|
val restored = entity.toDomain()
|
||||||
|
assertEquals(memo.createTime, restored.createTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user