diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d5f68f3..97059dd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -35,12 +35,19 @@ android { dependencies { defaultComposeLibrary() + implementation("androidx.datastore:datastore-preferences:1.1.7") + implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0") + implementation("androidx.navigation:navigation-compose:2.9.6") + val coil = "3.3.0" + implementation("io.coil-kt.coil3:coil-compose:$coil") + implementation("io.coil-kt.coil3:coil-network-ktor3:$coil") val ktor = "3.3.1" implementation("io.ktor:ktor-client-core:$ktor") implementation("io.ktor:ktor-client-cio:$ktor") implementation("io.ktor:ktor-client-content-negotiation:$ktor") implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") + androidTestImplementation(project(path = ":testLib")) androidTestImplementation("io.github.kakaocup:compose:1.0.0") androidTestImplementation("com.squareup.okhttp3:mockwebserver:4.12.0") diff --git a/app/src/androidTest/java/ru/myitschool/work/Tests.kt b/app/src/androidTest/java/ru/myitschool/work/Tests.kt index 7774de8..c998ce8 100755 --- a/app/src/androidTest/java/ru/myitschool/work/Tests.kt +++ b/app/src/androidTest/java/ru/myitschool/work/Tests.kt @@ -4,6 +4,7 @@ package ru.myitschool.work import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.espresso.Espresso.pressBack import androidx.test.ext.junit.runners.AndroidJUnit4 import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen import org.junit.FixMethodOrder @@ -11,12 +12,16 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters +import ru.myitschool.work.screens.AuthScreen +import ru.myitschool.work.screens.BookScreen import ru.myitschool.work.screens.MainScreen +import ru.myitschool.work.screens.getDateText +import ru.myitschool.work.screens.getPlaceText import ru.myitschool.work.ui.root.RootActivity import ru.myitschool.work.utils.MockWebServerRule import ru.myitschool.work.utils.Response import ru.samsung.test.core.core.BaseTest -import java.nio.charset.Charset +import ru.samsung.test.core.utils.warmUpCompose @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @@ -26,203 +31,443 @@ class Tests : BaseTest( ) { @get:Rule val composeTestRule = createAndroidComposeRule() + @get:Rule val serverRule = MockWebServerRule(8090) @Test - fun aПроверка_контента_на_экране() = runWithInit(1) { + fun aПроверка_экрана_авторизации_валидация() = runWithInit(1) { + onComposeScreen(composeTestRule) { + step("Проверка наличия всех элементов на экране") { + errorText.assertIsNotDisplayed() + signButton.assertIsDisplayed() + codeInput.assertIsDisplayed() + codeInput.assertTextEquals("Код", includeEditableText = false) + } + + step("Проверка состояния при пустом поле ввода") { + signButton.assertIsNotEnabled() + } + + step("Проверка состояния при вводе неверной комбинации 1") { + codeInput.performTextReplacement("1234567890") + signButton.assertIsNotEnabled() + } + + step("Проверка состояния при вводе верной комбинации 1") { + codeInput.performTextReplacement("1230") + signButton.assertIsEnabled() + } + + step("Проверка состояния при вводе неверной комбинации 2") { + codeInput.performTextReplacement("прив") + signButton.assertIsNotEnabled() + } + + step("Проверка состояния при вводе верной комбинации 2") { + codeInput.performTextReplacement("aAzZ") + signButton.assertIsEnabled() + } + + step("Проверка состояния при вводе неверной комбинации 3") { + codeInput.performTextReplacement("1пLK") + signButton.assertIsNotEnabled() + } + } + } + + @Test + fun bПроверка_экрана_авторизации_статусы() = runWithInit(1) { serverRule.mockResponses( - "/user" to Response(assetFile = "profile.json"), - "/user" to Response(assetFile = "profile2.json") + "/api/abcd/auth" to Response(statusCode = 401), + "/api/1234/auth" to Response(statusCode = 400), + "/api/4321/auth" to Response(statusCode = 500), + "/api/abc1/auth" to Response(statusCode = 200), + "/api/abc1/info" to Response(statusCode = 400), + ) + onComposeScreen(composeTestRule) { + step("Проверка наличия всех элементов на экране") { + errorText.assertIsNotDisplayed() + signButton.assertIsDisplayed() + codeInput.assertIsDisplayed() + } + + listOf( + "abcd", + "1234", + "4321" + ).forEachIndexed { index, code -> + warmUpCompose(composeTestRule) + step("Проверка статуса ошибки $index") { + codeInput.performTextReplacement(code) + flakySafely(timeoutMs = 500L, intervalMs = 100L) { + warmUpCompose(composeTestRule) + errorText.assertIsNotDisplayed() + } + signButton.performClick() + flakySafely(timeoutMs = 500L, intervalMs = 100L) { + warmUpCompose(composeTestRule) + errorText.assertIsDisplayed() + } + } + } + + step("Проверка верного статуса") { + codeInput.performTextReplacement("abc1") + errorText.assertIsNotDisplayed() + signButton.assertIsEnabled() + signButton.performClick() + } + } + onComposeScreen(composeTestRule) { + step("Проверка перехода на главный экран") { + errorText.assertIsDisplayed() + refreshButton.assertIsDisplayed() + } + } + } + + @Test + fun cПроверка_главного_экрана() = runWithInit(1) { + serverRule.mockResponses( + "/api/abc1/info" to Response(assetFile = "profile.json"), + "/api/abc1/info" to Response(assetFile = "profile2.json"), ) onComposeScreen(composeTestRule) { - step("Нажимаем на кнопку загрузки данных") { - loadButton { - assertIsDisplayed() - performClick() + step("Проверка наличия всех элементов на экране") { + flakySafely(timeoutMs = 5000L, intervalMs = 100L) { + warmUpCompose(composeTestRule) + errorText.assertIsNotDisplayed() + addButton.assertIsDisplayed() + refreshButton.assertIsDisplayed() + logoutButton.assertIsDisplayed() + profileNameText.assertIsDisplayed() } } - step("Проверяем корректное заполнение контента") { - nameText { - assertIsDisplayed() - assertTextEquals("Test Testovich") + step("Проверка контента на экране") { + profileNameText.assertTextEquals("Иван А") + getItemByPosition(0).invoke { + getDateText().assertTextEquals("05.01.2025") + getPlaceText().assertTextEquals("102") } - list { - firstChild { - roomText.assertTextEquals("row 1") - timeText.assertTextEquals("end row 1") - } - childAt(1) { - roomText.assertTextEquals("row 2") - timeText.assertTextEquals("end row 2") - } + getItemByPosition(1).invoke { + getDateText().assertTextEquals("06.01.2025") + getPlaceText().assertTextEquals("209.13") + } + getItemByPosition(2).invoke { + getDateText().assertTextEquals("09.01.2025") + getPlaceText().assertTextEquals("Зона 51. 50") } } - step("Обновляем страницу") { - loadButton { - assertIsDisplayed() - performClick() + step("Обновляем контент и проверяем повторно") { + refreshButton.performClick() + flakySafely(timeoutMs = 5000L, intervalMs = 100L) { + warmUpCompose(composeTestRule) + profileNameText.assertTextEquals("Вова Б") + } + getItemByPosition(0).invoke { + getDateText().assertTextEquals("01.01.2001") + getPlaceText().assertTextEquals("1") + } + getItemByPosition(1).invoke { + getDateText().assertTextEquals("01.01.2002") + getPlaceText().assertTextEquals("2") + } + getItemByPosition(2).invoke { + getDateText().assertTextEquals("01.01.2003") + getPlaceText().assertTextEquals("3") + } + getItemByPosition(3).assertIsNotDisplayed() + } + } + } + + @Test + fun dПроверка_кнопки_выхода() = runWithInit(1) { + serverRule.mockResponses( + "/api/abc1/info" to Response(assetFile = "profile.json"), + ) + onComposeScreen(composeTestRule) { + step("Проверка наличия всех элементов на экране") { + flakySafely(timeoutMs = 5000L, intervalMs = 100L) { + warmUpCompose(composeTestRule) + errorText.assertIsNotDisplayed() + addButton.assertIsDisplayed() + refreshButton.assertIsDisplayed() + logoutButton.assertIsDisplayed() + profileNameText.assertIsDisplayed() } } - step("Проверяем корректное заполнение контента") { - nameText { - assertIsDisplayed() - assertTextEquals("Ivan Ivanov") + step("Нажимаем на кнопку выхода") { + logoutButton.performClick() + } + } + + onComposeScreen(composeTestRule) { + step("Проверка наличия всех элементов на экране") { + errorText.assertIsNotDisplayed() + signButton.assertIsDisplayed() + codeInput.assertIsDisplayed() + codeInput.assertTextEquals("Код", includeEditableText = false) + } + } + } + + @Test + fun eПроверка_навигации_брони() = runWithInit(1) { + serverRule.mockResponses( + "/api/abc1/auth" to Response(statusCode = 200), + "/api/abc1/info" to Response(assetFile = "profile.json"), + "/api/abc1/booking" to Response(statusCode = 400), + ) + + onComposeScreen(composeTestRule) { + step("Проверка наличия всех элементов на экране") { + errorText.assertIsNotDisplayed() + signButton.assertIsDisplayed() + codeInput.assertIsDisplayed() + } + + step("Вход") { + codeInput.performTextReplacement("abc1") + errorText.assertIsNotDisplayed() + signButton.assertIsEnabled() + signButton.performClick() + } + } + onComposeScreen(composeTestRule) { + step("Проверка наличия всех элементов на экране") { + flakySafely(timeoutMs = 5000L, intervalMs = 100L) { + warmUpCompose(composeTestRule) + errorText.assertIsNotDisplayed() + addButton.assertIsDisplayed() + refreshButton.assertIsDisplayed() + logoutButton.assertIsDisplayed() + profileNameText.assertIsDisplayed() } - list { - firstChild { - roomText.assertTextEquals("row 3") - timeText.assertTextEquals("end row 3") - } + } + + step("Нажимаем на кнопку добавления") { + addButton.performClick() + } + } + onComposeScreen(composeTestRule) { + step("Проверка наличия всех элементов на экране") { + flakySafely(timeoutMs = 5000L, intervalMs = 100L) { + warmUpCompose(composeTestRule) + errorText.assertIsNotDisplayed() + refreshButton.assertIsDisplayed() + } + pressBack() + } + } + onComposeScreen(composeTestRule) { + step("Проверка контента на экране (обратная навигация)") { + profileNameText.assertTextEquals("Иван А") + getItemByPosition(0).invoke { + getDateText().assertTextEquals("05.01.2025") + getPlaceText().assertTextEquals("102") + } + getItemByPosition(1).invoke { + getDateText().assertTextEquals("06.01.2025") + getPlaceText().assertTextEquals("209.13") + } + getItemByPosition(2).invoke { + getDateText().assertTextEquals("09.01.2025") + getPlaceText().assertTextEquals("Зона 51. 50") } } } } @Test - fun bПроверка_добавления_данных() = runWithInit(1) { - val postResponseRoom = "Content-Disposition: form-data; name=room\nContent-Length: 8\n\nRoomTest" - val postResponseTime = "Content-Disposition: form-data; name=time\nContent-Length: 8\n\nTimeTest" - var requestComplete = false + fun fПроверка_контента_брони() = runWithInit(1) { serverRule.mockResponses( - "/user" to Response(assetFile = "profile.json"), - "/book" to Response(statusCode = 200), - "/user" to Response(assetFile = "profile2.json"), + "/api/abc1/info" to Response(assetFile = "profile.json"), + "/api/abc1/booking" to Response(statusCode = 400), + "/api/abc1/booking" to Response(assetFile = "booking.json"), ) - serverRule.setRecorderListener { request -> - if (request.path == "/book") { - val text = request.body.readString(Charset.defaultCharset()) - .replace("\r", "") - println(text) - assert(text.contains(postResponseRoom)) { "Значение Room не найдено" } - assert(text.contains(postResponseTime)) { "Значение Time не найдено" } - requestComplete = true + onComposeScreen(composeTestRule) { + step("Проверка наличия всех элементов на экране") { + flakySafely(timeoutMs = 5000L, intervalMs = 100L) { + warmUpCompose(composeTestRule) + errorText.assertIsNotDisplayed() + addButton.assertIsDisplayed() + refreshButton.assertIsDisplayed() + logoutButton.assertIsDisplayed() + profileNameText.assertIsDisplayed() + } + } + + step("Нажимаем на кнопку добавления") { + addButton.performClick() } } - onComposeScreen(composeTestRule) { - step("Нажимаем на кнопку загрузки данных") { - loadButton { - assertIsDisplayed() + onComposeScreen(composeTestRule) { + step("Проверка наличия всех элементов на экране") { + flakySafely(timeoutMs = 5000L, intervalMs = 100L) { + warmUpCompose(composeTestRule) + errorText.assertIsNotDisplayed() + refreshButton.assertIsDisplayed() + } + refreshButton.performClick() + } + step("Проверка контента") { + getNodeDateByPosition(0).invoke { + assertTextEquals("05.01") + assertIsSelected() + } + getNodeDateByPosition(1).assertTextEquals("06.01") + getNodeDateByPosition(2).assertTextEquals("07.02") + getNodeDateByPosition(3).assertTextEquals("08.03") + getNodePlaceByPosition(0).invoke { + assertTextEquals("102") + assertIsSelected() + } + getNodePlaceByPosition(1).assertTextEquals("666") + getNodePlaceByPosition(2).assertIsNotDisplayed() + } + step("Проверка выбора переговорок") { + getNodePlaceByPosition(1).invoke { performClick() + assertIsSelected() } + getNodePlaceByPosition(0).assertIsNotSelected() } - - step("Проверяем корректное заполнение контента") { - nameText { - assertIsDisplayed() - assertTextEquals("Test Testovich") - } - list { - firstChild { - roomText.assertTextEquals("row 1") - timeText.assertTextEquals("end row 1") - } - childAt(1) { - roomText.assertTextEquals("row 2") - timeText.assertTextEquals("end row 2") - } - } - } - - step("Заполняем поля для ввода") { - roomInput { - assertIsDisplayed() - performTextInput("RoomTest") - } - timeInput { - assertIsDisplayed() - performTextInput("TimeTest") - } - addButton { - assertIsDisplayed() + step("Проверка переключения дат") { + getNodeDateByPosition(2).invoke { performClick() + assertIsSelected() } - } - flakySafely(timeoutMs = 2_000, intervalMs = 100) { - assert(requestComplete) { "/book запроса не было" } - } - - step("Проверяем обработку результата") { - nameText { - assertIsDisplayed() - assertTextEquals("Ivan Ivanov") + getNodePlaceByPosition(0).invoke { + assertTextEquals("102") + assertIsSelected() } + getNodePlaceByPosition(1).assertTextEquals("209.13") + getNodePlaceByPosition(2).assertIsNotDisplayed() } } } @Test - fun cПроверка_обработки_ошибок() = runWithInit(1) { + fun gПроверка_возможности_брони() = runWithInit(1) { serverRule.mockResponses( - "/user" to Response(assetFile = "error.json", statusCode = 400), - "/user" to Response(assetFile = "error2.json", statusCode = 500), - "/user" to Response(assetFile = "profile.json"), - "/book" to Response(assetFile = "error.json", statusCode = 400) + "/api/abc1/info" to Response(assetFile = "profile.json"), + "/api/abc1/booking" to Response(assetFile = "booking.json"), + "/api/abc1/book" to Response(statusCode = 201), + "/api/abc1/info" to Response(assetFile = "profile2.json"), ) onComposeScreen(composeTestRule) { - step("Нажимаем на кнопку загрузки данных") { - loadButton { - assertIsDisplayed() - performClick() + step("Проверка наличия всех элементов на экране") { + flakySafely(timeoutMs = 5000L, intervalMs = 100L) { + warmUpCompose(composeTestRule) + errorText.assertIsNotDisplayed() + addButton.assertIsDisplayed() + refreshButton.assertIsDisplayed() + logoutButton.assertIsDisplayed() + profileNameText.assertIsDisplayed() } } - step("Проверяем ошибку и повторно выполняем запрос") { - errorContent { - assertTextEquals("TEST Error 1 TEST") + step("Нажимаем на кнопку добавления") { + addButton.performClick() + } + } + onComposeScreen(composeTestRule) { + step("Проверка наличия всех элементов на экране") { + flakySafely(timeoutMs = 5000L, intervalMs = 100L) { + warmUpCompose(composeTestRule) + backButton.assertIsDisplayed() + bookButton.assertIsDisplayed() } - loadButton { - assertIsDisplayed() - performClick() + } + step("Оформление брони") { + bookButton.performClick() + } + } + onComposeScreen(composeTestRule) { + step("Проверяем данные с главного экрана") { + flakySafely(timeoutMs = 5000L, intervalMs = 100L) { + warmUpCompose(composeTestRule) + profileNameText.assertTextEquals("Вова Б") } - errorContent { - assertTextEquals("TEST Error 2 TEST") + getItemByPosition(0).invoke { + getDateText().assertTextEquals("01.01.2001") + getPlaceText().assertTextEquals("1") } - loadButton { - assertIsDisplayed() - performClick() + getItemByPosition(1).invoke { + getDateText().assertTextEquals("01.01.2002") + getPlaceText().assertTextEquals("2") + } + getItemByPosition(2).invoke { + getDateText().assertTextEquals("01.01.2003") + getPlaceText().assertTextEquals("3") + } + getItemByPosition(3).assertIsNotDisplayed() + } + } + } + + @Test + fun hПроверка_пустых_слотов() = runWithInit(1) { + serverRule.mockResponses( + "/api/abc1/info" to Response(assetFile = "profile.json"), + "/api/abc1/booking" to Response(assetFile = "booking2.json"), + "/api/abc1/booking" to Response(assetFile = "booking3.json") + ) + onComposeScreen(composeTestRule) { + step("Проверка наличия всех элементов на экране") { + flakySafely(timeoutMs = 5000L, intervalMs = 100L) { + warmUpCompose(composeTestRule) + errorText.assertIsNotDisplayed() + addButton.assertIsDisplayed() + refreshButton.assertIsDisplayed() + logoutButton.assertIsDisplayed() + profileNameText.assertIsDisplayed() } } - step("Проверяем корректное заполнение контента") { - nameText { - assertIsDisplayed() - assertTextEquals("Test Testovich") - } - list { - firstChild { - roomText.assertTextEquals("row 1") - timeText.assertTextEquals("end row 1") - } - childAt(1) { - roomText.assertTextEquals("row 2") - timeText.assertTextEquals("end row 2") - } + step("Нажимаем на кнопку добавления") { + addButton.performClick() + } + } + onComposeScreen(composeTestRule) { + step("Проверка наличия всех элементов на экране") { + flakySafely(timeoutMs = 5000L, intervalMs = 100L) { + warmUpCompose(composeTestRule) + backButton.assertIsDisplayed() + bookButton.assertIsDisplayed() } } - - step("Заполняем поля для ввода") { - roomInput { - assertIsDisplayed() - performTextInput("RoomTest") - } - timeInput { - assertIsDisplayed() - performTextInput("TimeTest") - } - addButton { - assertIsDisplayed() - performClick() + step("Проверка пустого состояния") { + emptyText.assertIsNotDisplayed() + getNodeDateByPosition(0).assertIsDisplayed() + getNodeDateByPosition(1).assertIsDisplayed() + getNodeDateByPosition(2).assertIsDisplayed() + getNodeDateByPosition(3).assertIsNotDisplayed() + } + step("Возврат на главную") { + backButton.performClick() + } + } + onComposeScreen(composeTestRule) { + step("Нажимаем на кнопку добавления") { + addButton.performClick() + } + } + onComposeScreen(composeTestRule) { + step("Проверка наличия всех элементов на экране") { + flakySafely(timeoutMs = 5000L, intervalMs = 100L) { + warmUpCompose(composeTestRule) + backButton.assertIsDisplayed() + bookButton.assertIsNotDisplayed() } } - - step("Проверяем обработку ошибки") { - errorSend { - assertTextEquals("TEST Error 1 TEST") - } + step("Проверка пустого состояния") { + emptyText.assertIsDisplayed() } } } diff --git a/app/src/androidTest/java/ru/myitschool/work/screens/AuthScreen.kt b/app/src/androidTest/java/ru/myitschool/work/screens/AuthScreen.kt new file mode 100755 index 0000000..a15496c --- /dev/null +++ b/app/src/androidTest/java/ru/myitschool/work/screens/AuthScreen.kt @@ -0,0 +1,19 @@ +package ru.myitschool.work.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.kaspersky.components.composesupport.core.KNode +import ru.myitschool.work.core.TestIds + +class AuthScreen( + semanticsProvider: SemanticsNodeInteractionsProvider +) : UiBaseScreen(semanticsProvider) { + val errorText = child { + hasTestTag(TestIds.Auth.ERROR) + } + val signButton: KNode = child { + hasTestTag(TestIds.Auth.SIGN_BUTTON) + } + val codeInput: KNode = child { + hasTestTag(TestIds.Auth.CODE_INPUT) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/ru/myitschool/work/screens/BookScreen.kt b/app/src/androidTest/java/ru/myitschool/work/screens/BookScreen.kt new file mode 100755 index 0000000..8fcccba --- /dev/null +++ b/app/src/androidTest/java/ru/myitschool/work/screens/BookScreen.kt @@ -0,0 +1,33 @@ +package ru.myitschool.work.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.kaspersky.components.composesupport.core.KNode +import ru.myitschool.work.core.TestIds + +class BookScreen( + semanticsProvider: SemanticsNodeInteractionsProvider +) : UiBaseScreen(semanticsProvider) { + val errorText = child { + hasTestTag(TestIds.Book.ERROR) + } + val emptyText: KNode = child { + hasTestTag(TestIds.Book.EMPTY) + } + val refreshButton: KNode = child { + hasTestTag(TestIds.Book.REFRESH_BUTTON) + } + val backButton: KNode = child { + hasTestTag(TestIds.Book.BACK_BUTTON) + } + val bookButton: KNode = child { + hasTestTag(TestIds.Book.BOOK_BUTTON) + } + + fun getNodeDateByPosition(position: Int) = child { + hasTestTag(TestIds.Book.getIdDateItemByPosition(position)) + } + + fun getNodePlaceByPosition(position: Int) = child { + hasTestTag(TestIds.Book.getIdPlaceItemByPosition(position)) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/ru/myitschool/work/screens/MainScreen.kt b/app/src/androidTest/java/ru/myitschool/work/screens/MainScreen.kt index 1f627ba..60d6dc8 100755 --- a/app/src/androidTest/java/ru/myitschool/work/screens/MainScreen.kt +++ b/app/src/androidTest/java/ru/myitschool/work/screens/MainScreen.kt @@ -1,55 +1,33 @@ package ru.myitschool.work.screens -import androidx.compose.ui.semantics.SemanticsNode import androidx.compose.ui.test.SemanticsNodeInteractionsProvider -import androidx.compose.ui.test.hasTestTag import com.kaspersky.components.composesupport.core.KNode -import io.github.kakaocup.compose.node.element.lazylist.KLazyListItemNode -import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode +import io.github.kakaocup.compose.node.core.BaseNode +import ru.myitschool.work.core.TestIds class MainScreen( semanticsProvider: SemanticsNodeInteractionsProvider ) : UiBaseScreen(semanticsProvider) { - val loadButton = child { - hasTestTag("load_button") - } - val nameText: KNode = child { - hasTestTag("name") - } - val roomInput: KNode = child { - hasTestTag("input_room") - } - val timeInput: KNode = child { - hasTestTag("input_time") + val errorText = child { + hasTestTag(TestIds.Main.ERROR) } val addButton: KNode = child { - hasTestTag("add") + hasTestTag(TestIds.Main.ADD_BUTTON) } - val errorContent: KNode = child { - hasTestTag("error_text_content") + val refreshButton: KNode = child { + hasTestTag(TestIds.Main.REFRESH_BUTTON) } - val errorSend: KNode = child { - hasTestTag("error_text_send") + val logoutButton: KNode = child { + hasTestTag(TestIds.Main.LOGOUT_BUTTON) + } + val profileNameText: KNode = child { + hasTestTag(TestIds.Main.PROFILE_NAME) } - val list = KLazyListNode( - semanticsProvider = semanticsProvider, - viewBuilderAction = { hasTestTag("booking") }, - itemTypeBuilder = { - itemType(::ListItem) - }, - positionMatcher = { position -> hasTestTag("position=$position") } - ) - - class ListItem( - semanticsNode: SemanticsNode, - semanticsProvider: SemanticsNodeInteractionsProvider, - ) : KLazyListItemNode(semanticsNode, semanticsProvider) { - val roomText: KNode = child { - hasTestTag("room") - } - val timeText: KNode = child { - hasTestTag("time") - } + fun getItemByPosition(position: Int) = child { + hasTestTag(TestIds.Main.getIdItemByPosition(position)) } -} \ No newline at end of file +} +fun KNode.getDateText() = child { hasTestTag(TestIds.Main.ITEM_DATE) } + +fun KNode.getPlaceText() = child { hasTestTag(TestIds.Main.ITEM_PLACE) } \ No newline at end of file diff --git a/app/src/androidTest/resources/booking.json b/app/src/androidTest/resources/booking.json new file mode 100755 index 0000000..803db0d --- /dev/null +++ b/app/src/androidTest/resources/booking.json @@ -0,0 +1,6 @@ +{ + "2025-01-05": [{"id": 1, "place": "102"},{"id": 2, "place": "666"}], + "2025-01-06": [{"id": 3, "place": "Зона 51. 50"}], + "2025-02-07": [{"id": 1, "place": "102"},{"id": 2, "place": "209.13"}], + "2025-03-08": [{"id": 2, "place": "209.13"}] +} \ No newline at end of file diff --git a/app/src/androidTest/resources/booking2.json b/app/src/androidTest/resources/booking2.json new file mode 100755 index 0000000..f1855d8 --- /dev/null +++ b/app/src/androidTest/resources/booking2.json @@ -0,0 +1,6 @@ +{ + "2000-01-08": [{"id": 1, "place": "102"},{"id": 2, "place": "666"}], + "2000-01-07": [{"id": 3, "place": "Зона 51. 50"}], + "2000-01-05": [{"id": 1, "place": "102"},{"id": 2, "place": "209.13"}], + "2000-01-01": [] +} \ No newline at end of file diff --git a/app/src/androidTest/resources/booking3.json b/app/src/androidTest/resources/booking3.json new file mode 100755 index 0000000..9e26dfe --- /dev/null +++ b/app/src/androidTest/resources/booking3.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/app/src/androidTest/resources/error.json b/app/src/androidTest/resources/error.json deleted file mode 100755 index ea06ac0..0000000 --- a/app/src/androidTest/resources/error.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "error": "TEST Error 1 TEST" -} \ No newline at end of file diff --git a/app/src/androidTest/resources/error2.json b/app/src/androidTest/resources/error2.json deleted file mode 100755 index 155850b..0000000 --- a/app/src/androidTest/resources/error2.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "error": "TEST Error 2 TEST" -} \ No newline at end of file diff --git a/app/src/androidTest/resources/profile.json b/app/src/androidTest/resources/profile.json index 39249b9..c6b5620 100755 --- a/app/src/androidTest/resources/profile.json +++ b/app/src/androidTest/resources/profile.json @@ -1,13 +1,9 @@ { - "name": "Test Testovich", - "booking": [ - { - "room": "row 1", - "time": "end row 1" - }, - { - "room": "row 2", - "time": "end row 2" - } - ] + "name": "Иван А", + "photoUrl": "https://funnyducks.ru/upload/iblock/0cd/0cdeb7ec3ed6fddda0f90fccee05557d.jpg", + "booking": { + "2025-01-05": {"id": 1, "place": "102"}, + "2025-01-06": {"id": 2, "place": "209.13"}, + "2025-01-09": {"id": 3, "place": "Зона 51. 50"} + } } \ No newline at end of file diff --git a/app/src/androidTest/resources/profile2.json b/app/src/androidTest/resources/profile2.json index 1467fee..34bcb37 100755 --- a/app/src/androidTest/resources/profile2.json +++ b/app/src/androidTest/resources/profile2.json @@ -1,9 +1,9 @@ { - "name": "Ivan Ivanov", - "booking": [ - { - "room": "row 3", - "time": "end row 3" - } - ] + "name": "Вова Б", + "photoUrl": "https://funnyducks.ru/upload/iblock/0cd/0cdeb7ec3ed6fddda0f90fccee05557d.jpg", + "booking": { + "2003-01-01": {"id": 4, "place": "3"}, + "2002-01-01": {"id": 5, "place": "2"}, + "2001-01-01": {"id": 6, "place": "1"} + } } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5531bab..a2c02bd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ -) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/root/RootActivity.kt b/app/src/main/java/ru/myitschool/work/ui/root/RootActivity.kt deleted file mode 100644 index cc35f34..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/root/RootActivity.kt +++ /dev/null @@ -1,198 +0,0 @@ -package ru.myitschool.work.ui.root - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge -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.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material3.Button -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -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 androidx.lifecycle.viewmodel.compose.viewModel -import ru.myitschool.work.ui.theme.WorkTheme - -class RootActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - setContent { - WorkTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - Screen( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - ) - } - } - } - } -} - -@Composable -fun Screen( - modifier: Modifier = Modifier, - viewModel: RootViewModel = viewModel() -) { - val state by viewModel.uiState.collectAsState() - - when (val currentState = state) { - is RootState.Content -> { - Column( - modifier = modifier, - ) { - Row( - modifier = Modifier.padding(horizontal = 8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - modifier = Modifier.testTag("name").weight(1f), - text = currentState.userEntity.name, - style = MaterialTheme.typography.headlineSmall - ) - ButtonGetData(viewModel) - } - LazyColumn( - modifier = Modifier.testTag("booking").weight(1f) - ) { - itemsIndexed(currentState.userEntity.bookingList) { index, book -> - Row( - modifier = Modifier - .testTag("position=$index") - .padding(vertical = 16.dp, horizontal = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - modifier = Modifier.testTag("room").weight(1f), - text = book.roomName, - style = MaterialTheme.typography.bodyLarge - ) - Text( - modifier = Modifier.testTag("time"), - text = book.time, - style = MaterialTheme.typography.bodySmall - ) - } - } - } - Row( - modifier = Modifier.padding(horizontal = 8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - var roomText by remember { mutableStateOf("") } - var timeText by remember { mutableStateOf("") } - - Column(modifier = Modifier.weight(1f)) { - TextField( - modifier = Modifier.testTag("input_room").fillMaxWidth(), - value = roomText, - onValueChange = { roomText = it }, - label = { Text("Room") } - ) - TextField( - modifier = Modifier - .testTag("input_time") - .padding(top = 8.dp) - .fillMaxWidth(), - value = timeText, - onValueChange = { timeText = it }, - label = { Text("Time") } - ) - } - Column( - modifier = Modifier.padding(start = 8.dp), - ) { - Button( - modifier = Modifier.testTag("add"), - onClick = { - viewModel.onIntent( - RootIntent.AddBook(room = roomText, time = timeText) - ) - roomText = "" - timeText = "" - } - ) { - Text(text = "Add") - } - if (currentState.errorText != null) { - Text( - modifier = Modifier.testTag("error_text_send"), - text = currentState.errorText - ) - } - } - } - } - - } - - is RootState.Error -> { - Column( - modifier = modifier, - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - modifier = Modifier.testTag("error_text_content"), - text = currentState.message - ) - ButtonGetData(viewModel) - } - } - - is RootState.Loading -> { - Box( - modifier = modifier, - contentAlignment = Alignment.Center, - ) { - CircularProgressIndicator(modifier = Modifier.size(64.dp)) - } - } - - is RootState.NotLoaded -> { - Box( - modifier = modifier, - contentAlignment = Alignment.Center, - ) { - ButtonGetData(viewModel) - } - - } - } -} - -@Composable -private fun ButtonGetData( - viewModel: RootViewModel -) { - Button( - modifier = Modifier.testTag("load_button"), - onClick = { viewModel.onIntent(RootIntent.LoadData) } - ) { - Text( - text = "Get load" - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/root/RootIntent.kt b/app/src/main/java/ru/myitschool/work/ui/root/RootIntent.kt deleted file mode 100644 index 9804e07..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/root/RootIntent.kt +++ /dev/null @@ -1,9 +0,0 @@ -package ru.myitschool.work.ui.root - -sealed interface RootIntent { - data object LoadData: RootIntent - data class AddBook( - val room: String, - val time: String - ): RootIntent -} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/root/RootState.kt b/app/src/main/java/ru/myitschool/work/ui/root/RootState.kt deleted file mode 100644 index 1c5d32f..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/root/RootState.kt +++ /dev/null @@ -1,13 +0,0 @@ -package ru.myitschool.work.ui.root - -import ru.myitschool.work.domain.entities.UserEntity - -sealed interface RootState { - data object NotLoaded: RootState - data object Loading: RootState - data class Error(val message: String): RootState - data class Content( - val userEntity: UserEntity, - val errorText: String?, - ): RootState -} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/root/RootViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/root/RootViewModel.kt deleted file mode 100644 index d95ac89..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/root/RootViewModel.kt +++ /dev/null @@ -1,68 +0,0 @@ -package ru.myitschool.work.ui.root - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import ru.myitschool.work.data.AppRepository -import ru.myitschool.work.domain.AddBookUseCase -import ru.myitschool.work.domain.GetUserDataUseCase - -class RootViewModel : ViewModel() { - private val getUserDataUseCase by lazy { - GetUserDataUseCase( - repository = AppRepository - ) - } - private val addBookUseCase by lazy { - AddBookUseCase( - repository = AppRepository - ) - } - private val _uiState = MutableStateFlow(RootState.NotLoaded) - val uiState: StateFlow = _uiState.asStateFlow() - - fun onIntent(intent: RootIntent) { - when (intent) { - is RootIntent.LoadData -> loadData() - is RootIntent.AddBook -> addBook(intent) - } - } - - private fun loadData() { - viewModelScope.launch { - _uiState.emit(RootState.Loading) - getUserDataUseCase.invoke().fold( - onSuccess = { value -> - _uiState.emit(RootState.Content(userEntity = value, errorText = null)) - }, - onFailure = { error -> - _uiState.emit(RootState.Error(error.message.orEmpty())) - } - ) - } - } - - private fun addBook(intent: RootIntent.AddBook) { - viewModelScope.launch { - addBookUseCase.invoke( - room = intent.room, - time = intent.time - ).fold( - onSuccess = { - loadData() - }, - onFailure = { error -> - _uiState.update { state -> - (state as? RootState.Content)?.copy( - errorText = error.message - ) ?: state - } - } - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/theme/Color.kt b/app/src/main/java/ru/myitschool/work/ui/theme/Color.kt deleted file mode 100644 index 22226f4..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/theme/Color.kt +++ /dev/null @@ -1,11 +0,0 @@ -package ru.myitschool.work.ui.theme - -import androidx.compose.ui.graphics.Color - -val Purple80 = Color(0xFFD0BCFF) -val PurpleGrey80 = Color(0xFFCCC2DC) -val Pink80 = Color(0xFFEFB8C8) - -val Purple40 = Color(0xFF6650a4) -val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/theme/Theme.kt b/app/src/main/java/ru/myitschool/work/ui/theme/Theme.kt deleted file mode 100644 index d9cc58f..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/theme/Theme.kt +++ /dev/null @@ -1,57 +0,0 @@ -package ru.myitschool.work.ui.theme - -import android.os.Build -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext - -private val DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80 -) - -private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40 - - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ -) - -@Composable -fun WorkTheme( - darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ - dynamicColor: Boolean = true, - content: @Composable () -> Unit -) { - val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } - - darkTheme -> DarkColorScheme - else -> LightColorScheme - } - - MaterialTheme( - colorScheme = colorScheme, - typography = Typography, - content = content - ) -} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/theme/Type.kt b/app/src/main/java/ru/myitschool/work/ui/theme/Type.kt deleted file mode 100644 index 61b2923..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/theme/Type.kt +++ /dev/null @@ -1,34 +0,0 @@ -package ru.myitschool.work.ui.theme - -import androidx.compose.material3.Typography -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp - -// Set of Material typography styles to start with -val Typography = Typography( - bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp - ) - /* Other default text styles to override - titleLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), - labelSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ) - */ -) \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e88fc68..a9273cf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,15 @@ Work - NTO-2025 + RootActivity + Привет! Введи код для авторизации + Код + Войти + + Обновить + Выйти + Добавить + + Забронировать + Назад + Всё забронировано \ No newline at end of file