Skip to content

Commit 4e43218

Browse files
committed
Дорабатываю резервные копии
И обновил документацию
1 parent f163a75 commit 4e43218

30 files changed

Lines changed: 1547 additions & 1504 deletions

KODA.md

Lines changed: 0 additions & 464 deletions
This file was deleted.

app/src/androidTest/java/com/dayscounter/ui/screens/createedit/ColorSelectorUiTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class ColorSelectorUiTest {
2626
fun colorSelector_whenCustomColorSelected_thenShowsCustomColorChip() {
2727
// Given
2828
@Suppress("MagicNumber")
29-
val customColor = Color(0xFFFF6600) // Оранжевый — не в preset
29+
val customColor = Color(0xFF123456) // Синий — точно не в preset
3030
val selectedColor = mutableStateOf<Color?>(customColor)
3131

3232
// When
@@ -92,7 +92,7 @@ class ColorSelectorUiTest {
9292
fun colorSelector_whenCustomColorClicked_thenDeselects() {
9393
// Given
9494
@Suppress("MagicNumber")
95-
val customColor = Color(0xFFFF6600) // Оранжевый
95+
val customColor = Color(0xFF123456) // Синий
9696
val selectedColor = mutableStateOf<Color?>(customColor)
9797

9898
composeTestRule.setContent {

app/src/androidTest/java/com/dayscounter/ui/screens/createedit/CreateEditScreenCustomColorTest.kt

Lines changed: 28 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.dayscounter.ui.screens.createedit
22

33
import androidx.compose.foundation.layout.PaddingValues
4+
import androidx.compose.runtime.MutableState
45
import androidx.compose.runtime.mutableStateOf
56
import androidx.compose.runtime.remember
67
import androidx.compose.runtime.saveable.rememberSaveable
@@ -21,8 +22,8 @@ import org.junit.runner.RunWith
2122
/**
2223
* UI тесты для кастомного цвета в CreateEditScreen.
2324
*
24-
* Проверяет отображение кастомного цвета в форме редактирования
25-
* и его поведение при выборе preset-цвета.
25+
* Проверяет поведение кастомного цвета при взаимодействии с формой.
26+
* Статические тесты (начальное состояние) находятся в ColorSelectorUiTest.
2627
*/
2728
@RunWith(AndroidJUnit4::class)
2829
class CreateEditScreenCustomColorTest {
@@ -32,70 +33,23 @@ class CreateEditScreenCustomColorTest {
3233
private val context = InstrumentationRegistry.getInstrumentation().targetContext
3334

3435
/**
35-
* Проверяет, что при открытии формы с кастомным цветом
36-
* отображается 7 чипов (6 preset + 1 custom).
36+
* Проверяет, что при выборе preset-цвета он становится выбранным.
3737
*/
3838
@Test
39-
fun createEditForm_whenCustomColorInitiallySet_thenShowsCustomColorChip() {
40-
// Given - кастомный цвет (оранжевый, не в preset)
41-
@Suppress("MagicNumber")
42-
val customColor = Color(0xFFFF6600)
43-
44-
composeTestRule.setContent {
45-
JetpackDaysTheme {
46-
val title = rememberSaveable { mutableStateOf("Test Title") }
47-
val details = rememberSaveable { mutableStateOf("") }
48-
val selectedDate = rememberSaveable { mutableStateOf(java.time.LocalDate.now()) }
49-
val selectedColor = remember { mutableStateOf<Color?>(customColor) }
50-
val selectedDisplayOption =
51-
rememberSaveable { mutableStateOf(DisplayOption.DAY) }
52-
val showDatePicker = remember { mutableStateOf(false) }
53-
54-
val uiStates =
55-
CreateEditUiState(
56-
title = title,
57-
details = details,
58-
selectedDate = selectedDate,
59-
selectedColor = selectedColor,
60-
selectedDisplayOption = selectedDisplayOption,
61-
)
62-
63-
val params =
64-
CreateEditFormParams(
65-
itemId = 1L,
66-
paddingValues = PaddingValues(),
67-
uiStates = uiStates,
68-
showDatePicker = showDatePicker,
69-
viewModel = createTestViewModel(),
70-
onBackClick = {},
71-
)
72-
73-
CreateEditFormContent(params)
74-
}
75-
}
76-
77-
// Then - должен быть 7 чипов (6 preset + 1 custom)
78-
val colorDescription = context.getString(R.string.color)
79-
composeTestRule
80-
.onAllNodesWithContentDescription(colorDescription)
81-
.assertCountEquals(7)
82-
}
83-
84-
/**
85-
* Проверяет, что при выборе preset-цвета кастомный чип исчезает.
86-
*/
87-
@Test
88-
fun createEditForm_whenPresetColorSelected_thenHidesCustomColorChip() {
39+
fun createEditForm_whenPresetColorSelected_thenSelectsPresetColor() {
8940
// Given - сначала выбран кастомный цвет
9041
@Suppress("MagicNumber")
91-
val customColor = Color(0xFFFF6600)
42+
val customColor = Color(0xFF123456)
43+
44+
var selectedColorStateHolder: MutableState<Color?>? = null
9245

9346
composeTestRule.setContent {
9447
JetpackDaysTheme {
9548
val title = rememberSaveable { mutableStateOf("Test Title") }
9649
val details = rememberSaveable { mutableStateOf("") }
9750
val selectedDate = rememberSaveable { mutableStateOf(java.time.LocalDate.now()) }
9851
val selectedColorState = remember { mutableStateOf<Color?>(customColor) }
52+
selectedColorStateHolder = selectedColorState
9953
val selectedDisplayOption =
10054
rememberSaveable { mutableStateOf(DisplayOption.DAY) }
10155
val showDatePicker = remember { mutableStateOf(false) }
@@ -123,33 +77,33 @@ class CreateEditScreenCustomColorTest {
12377
}
12478
}
12579

126-
// When - кликаем на последний чип (preset цвет)
80+
// When - кликаем на Red (индекс 1 в списке: custom, Red, Teal, Blue, Green, Yellow, Purple)
12781
val colorDescription = context.getString(R.string.color)
12882
composeTestRule
129-
.onAllNodesWithContentDescription(colorDescription)[6] // Последний preset
83+
.onAllNodesWithContentDescription(colorDescription)[1] // Red
13084
.performClick()
13185

132-
// Then - должен быть только 6 preset чипов (кастомный исчез)
133-
composeTestRule
134-
.onAllNodesWithContentDescription(colorDescription)
135-
.assertCountEquals(6)
86+
// Then - выбранный цвет должен быть Red
87+
assert(selectedColorStateHolder?.value == PresetColors.Red) {
88+
"Expected Red but got ${selectedColorStateHolder?.value}"
89+
}
13690
}
13791

13892
/**
139-
* Проверяет, что при открытии формы с preset-цветом
140-
* отображается только 6 чипов.
93+
* Проверяет, что при выборе preset-цвета кастомный чип исчезает.
14194
*/
14295
@Test
143-
fun createEditForm_whenPresetColorInitiallySet_thenShowsOnlyPresetChips() {
144-
// Given - preset цвет (красный)
145-
val presetColor = PresetColors.Red
96+
fun createEditForm_whenPresetColorSelected_thenHidesCustomColorChip() {
97+
// Given - сначала выбран кастомный цвет (7 чипов)
98+
@Suppress("MagicNumber")
99+
val customColor = Color(0xFF123456)
146100

147101
composeTestRule.setContent {
148102
JetpackDaysTheme {
149103
val title = rememberSaveable { mutableStateOf("Test Title") }
150104
val details = rememberSaveable { mutableStateOf("") }
151105
val selectedDate = rememberSaveable { mutableStateOf(java.time.LocalDate.now()) }
152-
val selectedColor = remember { mutableStateOf<Color?>(presetColor) }
106+
val selectedColor = remember { mutableStateOf<Color?>(customColor) }
153107
val selectedDisplayOption =
154108
rememberSaveable { mutableStateOf(DisplayOption.DAY) }
155109
val showDatePicker = remember { mutableStateOf(false) }
@@ -177,56 +131,18 @@ class CreateEditScreenCustomColorTest {
177131
}
178132
}
179133

180-
// Then - должен быть только 6 preset чипов
134+
// Сначала проверяем, что есть 7 чипов (6 preset + 1 custom)
181135
val colorDescription = context.getString(R.string.color)
182136
composeTestRule
183137
.onAllNodesWithContentDescription(colorDescription)
184-
.assertCountEquals(6)
185-
}
186-
187-
/**
188-
* Проверяет, что при открытии формы без цвета
189-
* отображается только 6 preset чипов.
190-
*/
191-
@Test
192-
fun createEditForm_whenNoColorInitiallySet_thenShowsOnlyPresetChips() {
193-
// Given - цвет не выбран
194-
195-
composeTestRule.setContent {
196-
JetpackDaysTheme {
197-
val title = rememberSaveable { mutableStateOf("Test Title") }
198-
val details = rememberSaveable { mutableStateOf("") }
199-
val selectedDate = rememberSaveable { mutableStateOf(java.time.LocalDate.now()) }
200-
val selectedColor = remember { mutableStateOf<Color?>(null) }
201-
val selectedDisplayOption =
202-
rememberSaveable { mutableStateOf(DisplayOption.DAY) }
203-
val showDatePicker = remember { mutableStateOf(false) }
204-
205-
val uiStates =
206-
CreateEditUiState(
207-
title = title,
208-
details = details,
209-
selectedDate = selectedDate,
210-
selectedColor = selectedColor,
211-
selectedDisplayOption = selectedDisplayOption,
212-
)
213-
214-
val params =
215-
CreateEditFormParams(
216-
itemId = null,
217-
paddingValues = PaddingValues(),
218-
uiStates = uiStates,
219-
showDatePicker = showDatePicker,
220-
viewModel = createTestViewModel(),
221-
onBackClick = {},
222-
)
138+
.assertCountEquals(7)
223139

224-
CreateEditFormContent(params)
225-
}
226-
}
140+
// When - кликаем на Red (индекс 1 в списке: custom, Red, Teal, Blue, Green, Yellow, Purple)
141+
composeTestRule
142+
.onAllNodesWithContentDescription(colorDescription)[1]
143+
.performClick()
227144

228-
// Then - должен быть только 6 preset чипов
229-
val colorDescription = context.getString(R.string.color)
145+
// Then - должен остаться только 6 preset чипов (кастомный исчез)
230146
composeTestRule
231147
.onAllNodesWithContentDescription(colorDescription)
232148
.assertCountEquals(6)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.dayscounter.domain.usecase
2+
3+
import kotlinx.serialization.Serializable
4+
5+
/**
6+
* Формат резервной копии.
7+
*
8+
* Используется для определения платформы-источника бэкапа и правильной конвертации данных.
9+
* Сериализуется в lowercase строки ("android", "ios") для совместимости с iOS.
10+
*/
11+
@Serializable(with = BackupFormatSerializer::class)
12+
enum class BackupFormat {
13+
/**
14+
* Android формат.
15+
* - timestamp: миллисекунды с 1970-01-01
16+
* - colorTag: hex-строка (#RRGGBB)
17+
*/
18+
ANDROID,
19+
20+
/**
21+
* iOS формат.
22+
* - timestamp: секунды с 2001-01-01 (timeIntervalSinceReferenceDate)
23+
* - colorTag: Base64 NSKeyedArchiver (UIColor)
24+
*/
25+
IOS,
26+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.dayscounter.domain.usecase
2+
3+
import kotlinx.serialization.KSerializer
4+
import kotlinx.serialization.descriptors.PrimitiveKind
5+
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
6+
import kotlinx.serialization.descriptors.SerialDescriptor
7+
import kotlinx.serialization.encoding.Decoder
8+
import kotlinx.serialization.encoding.Encoder
9+
10+
/**
11+
* Сериализатор для BackupFormat.
12+
*
13+
* Сериализует enum в lowercase строки ("android", "ios") для совместимости с iOS.
14+
* Десериализует оба формата: lowercase и uppercase.
15+
*/
16+
object BackupFormatSerializer : KSerializer<BackupFormat> {
17+
override val descriptor: SerialDescriptor =
18+
PrimitiveSerialDescriptor("BackupFormat", PrimitiveKind.STRING)
19+
20+
override fun serialize(
21+
encoder: Encoder,
22+
value: BackupFormat,
23+
) {
24+
encoder.encodeString(value.serialName)
25+
}
26+
27+
override fun deserialize(decoder: Decoder): BackupFormat {
28+
val string = decoder.decodeString()
29+
return BackupFormat.entries.find {
30+
it.serialName == string || it.name.equals(
31+
string,
32+
ignoreCase = true
33+
)
34+
}
35+
?: throw IllegalArgumentException("Unknown BackupFormat: $string")
36+
}
37+
38+
private val BackupFormat.serialName: String
39+
get() =
40+
when (this) {
41+
BackupFormat.ANDROID -> "android"
42+
BackupFormat.IOS -> "ios"
43+
}
44+
}

app/src/main/java/com/dayscounter/domain/usecase/BackupItem.kt

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,6 @@ fun String?.parseColorTag(): Int? {
9797
return null
9898
}
9999

100-
/**
101-
* Конвертирует ARGB цвет в Base64 NSKeyedArchiver (iOS формат).
102-
*
103-
* @return Base64-закодированный NSKeyedArchiver или null, если конвертация не удалась
104-
*/
105-
fun Int.toIosColorTag(): String = NsKeyedArchiverBuilder.buildFromArgb(this)
106-
107100
/**
108101
* Расширение для конвертации Item в BackupItem (Android формат - hex).
109102
*/
@@ -121,23 +114,6 @@ fun Item.toBackupItem(): BackupItem =
121114
},
122115
)
123116

124-
/**
125-
* Расширение для конвертации Item в BackupItem (iOS формат - Base64 NSKeyedArchiver).
126-
*/
127-
fun Item.toIosBackupItem(): BackupItem =
128-
BackupItem(
129-
title = title,
130-
details = details,
131-
timestamp = timestamp,
132-
colorTag = colorTag?.toIosColorTag(), // Конвертируем Int? в Base64 NSKeyedArchiver
133-
displayOption =
134-
when (displayOption) {
135-
DisplayOption.DAY -> "day"
136-
DisplayOption.MONTH_DAY -> "monthDay"
137-
DisplayOption.YEAR_MONTH_DAY -> "yearMonthDay"
138-
},
139-
)
140-
141117
/**
142118
* Расширение для конвертации BackupItem в Item.
143119
*
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.dayscounter.domain.usecase
2+
3+
import kotlinx.serialization.Serializable
4+
5+
/**
6+
* Обёртка для резервной копии с указанием формата.
7+
*
8+
* Используется для импорта/экспорта бэкапов с явным указанием платформы-источника.
9+
* Поле `format` nullable для обратной совместимости со старыми файлами без этого поля.
10+
*
11+
* @property format Формат бэкапа (android, ios) или null для старых файлов
12+
* @property items Список элементов резервной копии
13+
*/
14+
@Serializable
15+
data class BackupWrapper(
16+
val format: BackupFormat? = null,
17+
val items: List<BackupItem>,
18+
)

0 commit comments

Comments
 (0)