Initial commit

This commit is contained in:
2025-11-23 23:45:24 +03:00
parent fd0f2f0af1
commit c44bef0554
26 changed files with 558 additions and 613 deletions

View File

@@ -35,12 +35,19 @@ android {
dependencies { dependencies {
defaultComposeLibrary() 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" val ktor = "3.3.1"
implementation("io.ktor:ktor-client-core:$ktor") implementation("io.ktor:ktor-client-core:$ktor")
implementation("io.ktor:ktor-client-cio:$ktor") implementation("io.ktor:ktor-client-cio:$ktor")
implementation("io.ktor:ktor-client-content-negotiation:$ktor") implementation("io.ktor:ktor-client-content-negotiation:$ktor")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor") implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
androidTestImplementation(project(path = ":testLib")) androidTestImplementation(project(path = ":testLib"))
androidTestImplementation("io.github.kakaocup:compose:1.0.0") androidTestImplementation("io.github.kakaocup:compose:1.0.0")
androidTestImplementation("com.squareup.okhttp3:mockwebserver:4.12.0") androidTestImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")

View File

@@ -4,6 +4,7 @@ package ru.myitschool.work
import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.espresso.Espresso.pressBack
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import org.junit.FixMethodOrder import org.junit.FixMethodOrder
@@ -11,12 +12,16 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.MethodSorters 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.MainScreen
import ru.myitschool.work.screens.getDateText
import ru.myitschool.work.screens.getPlaceText
import ru.myitschool.work.ui.root.RootActivity import ru.myitschool.work.ui.root.RootActivity
import ru.myitschool.work.utils.MockWebServerRule import ru.myitschool.work.utils.MockWebServerRule
import ru.myitschool.work.utils.Response import ru.myitschool.work.utils.Response
import ru.samsung.test.core.core.BaseTest import ru.samsung.test.core.core.BaseTest
import java.nio.charset.Charset import ru.samsung.test.core.utils.warmUpCompose
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING) @FixMethodOrder(MethodSorters.NAME_ASCENDING)
@@ -26,204 +31,444 @@ class Tests : BaseTest<RootActivity>(
) { ) {
@get:Rule @get:Rule
val composeTestRule = createAndroidComposeRule<RootActivity>() val composeTestRule = createAndroidComposeRule<RootActivity>()
@get:Rule @get:Rule
val serverRule = MockWebServerRule(8090) val serverRule = MockWebServerRule(8090)
@Test @Test
fun роверка_контентаа_экране() = runWithInit(1) { fun роверка_экрана_авторизацииалидация() = runWithInit(1) {
onComposeScreen<AuthScreen>(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 роверка_экрана_авторизации_статусы() = runWithInit(1) {
serverRule.mockResponses( serverRule.mockResponses(
"/user" to Response(assetFile = "profile.json"), "/api/abcd/auth" to Response(statusCode = 401),
"/user" to Response(assetFile = "profile2.json") "/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<AuthScreen>(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<MainScreen>(composeTestRule) {
step("Проверка перехода на главный экран") {
errorText.assertIsDisplayed()
refreshButton.assertIsDisplayed()
}
}
}
@Test
fun роверка_главного_экрана() = runWithInit(1) {
serverRule.mockResponses(
"/api/abc1/info" to Response(assetFile = "profile.json"),
"/api/abc1/info" to Response(assetFile = "profile2.json"),
) )
onComposeScreen<MainScreen>(composeTestRule) { onComposeScreen<MainScreen>(composeTestRule) {
step("Нажимаем на кнопку загрузки данных") { step("Проверка наличия всех элементов на экране") {
loadButton { flakySafely(timeoutMs = 5000L, intervalMs = 100L) {
assertIsDisplayed() warmUpCompose(composeTestRule)
performClick() errorText.assertIsNotDisplayed()
addButton.assertIsDisplayed()
refreshButton.assertIsDisplayed()
logoutButton.assertIsDisplayed()
profileNameText.assertIsDisplayed()
} }
} }
step("Проверяем корректное заполнение контента") { step("Проверка контента на экране") {
nameText { profileNameText.assertTextEquals("Иван А")
assertIsDisplayed() getItemByPosition(0).invoke {
assertTextEquals("Test Testovich") getDateText().assertTextEquals("05.01.2025")
getPlaceText().assertTextEquals("102")
} }
list { getItemByPosition(1).invoke {
firstChild<MainScreen.ListItem> { getDateText().assertTextEquals("06.01.2025")
roomText.assertTextEquals("row 1") getPlaceText().assertTextEquals("209.13")
timeText.assertTextEquals("end row 1")
} }
childAt<MainScreen.ListItem>(1) { getItemByPosition(2).invoke {
roomText.assertTextEquals("row 2") getDateText().assertTextEquals("09.01.2025")
timeText.assertTextEquals("end row 2") getPlaceText().assertTextEquals("Зона 51. 50")
}
}
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()
} }
} }
} }
step("Обновляем страницу") { @Test
loadButton { fun роверка_кнопки_выхода() = runWithInit(1) {
assertIsDisplayed() serverRule.mockResponses(
performClick() "/api/abc1/info" to Response(assetFile = "profile.json"),
)
onComposeScreen<MainScreen>(composeTestRule) {
step("Проверка наличия всех элементов на экране") {
flakySafely(timeoutMs = 5000L, intervalMs = 100L) {
warmUpCompose(composeTestRule)
errorText.assertIsNotDisplayed()
addButton.assertIsDisplayed()
refreshButton.assertIsDisplayed()
logoutButton.assertIsDisplayed()
profileNameText.assertIsDisplayed()
} }
} }
step("Проверяем корректное заполнение контента") { step("Нажимаем на кнопку выхода") {
nameText { logoutButton.performClick()
assertIsDisplayed()
assertTextEquals("Ivan Ivanov")
} }
list {
firstChild<MainScreen.ListItem> {
roomText.assertTextEquals("row 3")
timeText.assertTextEquals("end row 3")
} }
onComposeScreen<AuthScreen>(composeTestRule) {
step("Проверка наличия всех элементов на экране") {
errorText.assertIsNotDisplayed()
signButton.assertIsDisplayed()
codeInput.assertIsDisplayed()
codeInput.assertTextEquals("Код", includeEditableText = false)
}
}
}
@Test
fun роверкаавигации_брони() = 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<AuthScreen>(composeTestRule) {
step("Проверка наличия всех элементов на экране") {
errorText.assertIsNotDisplayed()
signButton.assertIsDisplayed()
codeInput.assertIsDisplayed()
}
step("Вход") {
codeInput.performTextReplacement("abc1")
errorText.assertIsNotDisplayed()
signButton.assertIsEnabled()
signButton.performClick()
}
}
onComposeScreen<MainScreen>(composeTestRule) {
step("Проверка наличия всех элементов на экране") {
flakySafely(timeoutMs = 5000L, intervalMs = 100L) {
warmUpCompose(composeTestRule)
errorText.assertIsNotDisplayed()
addButton.assertIsDisplayed()
refreshButton.assertIsDisplayed()
logoutButton.assertIsDisplayed()
profileNameText.assertIsDisplayed()
}
}
step("Нажимаем на кнопку добавления") {
addButton.performClick()
}
}
onComposeScreen<BookScreen>(composeTestRule) {
step("Проверка наличия всех элементов на экране") {
flakySafely(timeoutMs = 5000L, intervalMs = 100L) {
warmUpCompose(composeTestRule)
errorText.assertIsNotDisplayed()
refreshButton.assertIsDisplayed()
}
pressBack()
}
}
onComposeScreen<MainScreen>(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 @Test
fun bПроверка_добавленияанных() = runWithInit(1) { fun fПроверка_контента_брони() = 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
serverRule.mockResponses( serverRule.mockResponses(
"/user" to Response(assetFile = "profile.json"), "/api/abc1/info" to Response(assetFile = "profile.json"),
"/book" to Response(statusCode = 200), "/api/abc1/booking" to Response(statusCode = 400),
"/user" to Response(assetFile = "profile2.json"), "/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<MainScreen>(composeTestRule) { onComposeScreen<MainScreen>(composeTestRule) {
step("Нажимаем на кнопку загрузки данных") { step("Проверка наличия всех элементов на экране") {
loadButton { flakySafely(timeoutMs = 5000L, intervalMs = 100L) {
assertIsDisplayed() warmUpCompose(composeTestRule)
errorText.assertIsNotDisplayed()
addButton.assertIsDisplayed()
refreshButton.assertIsDisplayed()
logoutButton.assertIsDisplayed()
profileNameText.assertIsDisplayed()
}
}
step("Нажимаем на кнопку добавления") {
addButton.performClick()
}
}
onComposeScreen<BookScreen>(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() performClick()
assertIsSelected()
} }
getNodePlaceByPosition(0).assertIsNotSelected()
} }
step("Проверка переключения дат") {
step("Проверяем корректное заполнение контента") { getNodeDateByPosition(2).invoke {
nameText {
assertIsDisplayed()
assertTextEquals("Test Testovich")
}
list {
firstChild<MainScreen.ListItem> {
roomText.assertTextEquals("row 1")
timeText.assertTextEquals("end row 1")
}
childAt<MainScreen.ListItem>(1) {
roomText.assertTextEquals("row 2")
timeText.assertTextEquals("end row 2")
}
}
}
step("Заполняем поля для ввода") {
roomInput {
assertIsDisplayed()
performTextInput("RoomTest")
}
timeInput {
assertIsDisplayed()
performTextInput("TimeTest")
}
addButton {
assertIsDisplayed()
performClick() performClick()
assertIsSelected()
} }
getNodePlaceByPosition(0).invoke {
assertTextEquals("102")
assertIsSelected()
} }
flakySafely(timeoutMs = 2_000, intervalMs = 100) { getNodePlaceByPosition(1).assertTextEquals("209.13")
assert(requestComplete) { "/book запроса не было" } getNodePlaceByPosition(2).assertIsNotDisplayed()
}
step("Проверяем обработку результата") {
nameText {
assertIsDisplayed()
assertTextEquals("Ivan Ivanov")
}
} }
} }
} }
@Test @Test
fun cПроверка_обработки_ошибок() = runWithInit(1) { fun gПроверка_возможности_брони() = runWithInit(1) {
serverRule.mockResponses( serverRule.mockResponses(
"/user" to Response(assetFile = "error.json", statusCode = 400), "/api/abc1/info" to Response(assetFile = "profile.json"),
"/user" to Response(assetFile = "error2.json", statusCode = 500), "/api/abc1/booking" to Response(assetFile = "booking.json"),
"/user" to Response(assetFile = "profile.json"), "/api/abc1/book" to Response(statusCode = 201),
"/book" to Response(assetFile = "error.json", statusCode = 400) "/api/abc1/info" to Response(assetFile = "profile2.json"),
) )
onComposeScreen<MainScreen>(composeTestRule) { onComposeScreen<MainScreen>(composeTestRule) {
step("Нажимаем на кнопку загрузки данных") { step("Проверка наличия всех элементов на экране") {
loadButton { flakySafely(timeoutMs = 5000L, intervalMs = 100L) {
assertIsDisplayed() warmUpCompose(composeTestRule)
performClick() errorText.assertIsNotDisplayed()
addButton.assertIsDisplayed()
refreshButton.assertIsDisplayed()
logoutButton.assertIsDisplayed()
profileNameText.assertIsDisplayed()
} }
} }
step("Проверяем ошибку и повторно выполняем запрос") { step("Нажимаем на кнопку добавления") {
errorContent { addButton.performClick()
assertTextEquals("TEST Error 1 TEST")
}
loadButton {
assertIsDisplayed()
performClick()
}
errorContent {
assertTextEquals("TEST Error 2 TEST")
}
loadButton {
assertIsDisplayed()
performClick()
} }
} }
onComposeScreen<BookScreen>(composeTestRule) {
step("Проверяем корректное заполнение контента") { step("Проверка наличия всех элементов на экране") {
nameText { flakySafely(timeoutMs = 5000L, intervalMs = 100L) {
assertIsDisplayed() warmUpCompose(composeTestRule)
assertTextEquals("Test Testovich") backButton.assertIsDisplayed()
bookButton.assertIsDisplayed()
} }
list {
firstChild<MainScreen.ListItem> {
roomText.assertTextEquals("row 1")
timeText.assertTextEquals("end row 1")
} }
childAt<MainScreen.ListItem>(1) { step("Оформление брони") {
roomText.assertTextEquals("row 2") bookButton.performClick()
timeText.assertTextEquals("end row 2") }
}
onComposeScreen<MainScreen>(composeTestRule) {
step("Проверяем данные с главного экрана") {
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()
} }
} }
} }
step("Заполняем поля для ввода") { @Test
roomInput { fun роверка_пустых_слотов() = runWithInit(1) {
assertIsDisplayed() serverRule.mockResponses(
performTextInput("RoomTest") "/api/abc1/info" to Response(assetFile = "profile.json"),
} "/api/abc1/booking" to Response(assetFile = "booking2.json"),
timeInput { "/api/abc1/booking" to Response(assetFile = "booking3.json")
assertIsDisplayed() )
performTextInput("TimeTest") onComposeScreen<MainScreen>(composeTestRule) {
} step("Проверка наличия всех элементов на экране") {
addButton { flakySafely(timeoutMs = 5000L, intervalMs = 100L) {
assertIsDisplayed() warmUpCompose(composeTestRule)
performClick() errorText.assertIsNotDisplayed()
addButton.assertIsDisplayed()
refreshButton.assertIsDisplayed()
logoutButton.assertIsDisplayed()
profileNameText.assertIsDisplayed()
} }
} }
step("Проверяем обработку ошибки") { step("Нажимаем на кнопку добавления") {
errorSend { addButton.performClick()
assertTextEquals("TEST Error 1 TEST")
} }
} }
onComposeScreen<BookScreen>(composeTestRule) {
step("Проверка наличия всех элементов на экране") {
flakySafely(timeoutMs = 5000L, intervalMs = 100L) {
warmUpCompose(composeTestRule)
backButton.assertIsDisplayed()
bookButton.assertIsDisplayed()
}
}
step("Проверка пустого состояния") {
emptyText.assertIsNotDisplayed()
getNodeDateByPosition(0).assertIsDisplayed()
getNodeDateByPosition(1).assertIsDisplayed()
getNodeDateByPosition(2).assertIsDisplayed()
getNodeDateByPosition(3).assertIsNotDisplayed()
}
step("Возврат на главную") {
backButton.performClick()
}
}
onComposeScreen<MainScreen>(composeTestRule) {
step("Нажимаем на кнопку добавления") {
addButton.performClick()
}
}
onComposeScreen<BookScreen>(composeTestRule) {
step("Проверка наличия всех элементов на экране") {
flakySafely(timeoutMs = 5000L, intervalMs = 100L) {
warmUpCompose(composeTestRule)
backButton.assertIsDisplayed()
bookButton.assertIsNotDisplayed()
}
}
step("Проверка пустого состояния") {
emptyText.assertIsDisplayed()
}
} }
} }
} }

View File

@@ -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<AuthScreen>(semanticsProvider) {
val errorText = child<KNode> {
hasTestTag(TestIds.Auth.ERROR)
}
val signButton: KNode = child<KNode> {
hasTestTag(TestIds.Auth.SIGN_BUTTON)
}
val codeInput: KNode = child<KNode> {
hasTestTag(TestIds.Auth.CODE_INPUT)
}
}

View File

@@ -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<BookScreen>(semanticsProvider) {
val errorText = child<KNode> {
hasTestTag(TestIds.Book.ERROR)
}
val emptyText: KNode = child<KNode> {
hasTestTag(TestIds.Book.EMPTY)
}
val refreshButton: KNode = child<KNode> {
hasTestTag(TestIds.Book.REFRESH_BUTTON)
}
val backButton: KNode = child<KNode> {
hasTestTag(TestIds.Book.BACK_BUTTON)
}
val bookButton: KNode = child<KNode> {
hasTestTag(TestIds.Book.BOOK_BUTTON)
}
fun getNodeDateByPosition(position: Int) = child<KNode> {
hasTestTag(TestIds.Book.getIdDateItemByPosition(position))
}
fun getNodePlaceByPosition(position: Int) = child<KNode> {
hasTestTag(TestIds.Book.getIdPlaceItemByPosition(position))
}
}

View File

@@ -1,55 +1,33 @@
package ru.myitschool.work.screens package ru.myitschool.work.screens
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import androidx.compose.ui.test.hasTestTag
import com.kaspersky.components.composesupport.core.KNode import com.kaspersky.components.composesupport.core.KNode
import io.github.kakaocup.compose.node.element.lazylist.KLazyListItemNode import io.github.kakaocup.compose.node.core.BaseNode
import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode import ru.myitschool.work.core.TestIds
class MainScreen( class MainScreen(
semanticsProvider: SemanticsNodeInteractionsProvider semanticsProvider: SemanticsNodeInteractionsProvider
) : UiBaseScreen<MainScreen>(semanticsProvider) { ) : UiBaseScreen<MainScreen>(semanticsProvider) {
val loadButton = child<KNode> { val errorText = child<KNode> {
hasTestTag("load_button") hasTestTag(TestIds.Main.ERROR)
}
val nameText: KNode = child<KNode> {
hasTestTag("name")
}
val roomInput: KNode = child<KNode> {
hasTestTag("input_room")
}
val timeInput: KNode = child<KNode> {
hasTestTag("input_time")
} }
val addButton: KNode = child<KNode> { val addButton: KNode = child<KNode> {
hasTestTag("add") hasTestTag(TestIds.Main.ADD_BUTTON)
} }
val errorContent: KNode = child<KNode> { val refreshButton: KNode = child<KNode> {
hasTestTag("error_text_content") hasTestTag(TestIds.Main.REFRESH_BUTTON)
} }
val errorSend: KNode = child<KNode> { val logoutButton: KNode = child<KNode> {
hasTestTag("error_text_send") hasTestTag(TestIds.Main.LOGOUT_BUTTON)
}
val profileNameText: KNode = child<KNode> {
hasTestTag(TestIds.Main.PROFILE_NAME)
} }
val list = KLazyListNode( fun getItemByPosition(position: Int) = child<KNode> {
semanticsProvider = semanticsProvider, hasTestTag(TestIds.Main.getIdItemByPosition(position))
viewBuilderAction = { hasTestTag("booking") },
itemTypeBuilder = {
itemType(::ListItem)
},
positionMatcher = { position -> hasTestTag("position=$position") }
)
class ListItem(
semanticsNode: SemanticsNode,
semanticsProvider: SemanticsNodeInteractionsProvider,
) : KLazyListItemNode<ListItem>(semanticsNode, semanticsProvider) {
val roomText: KNode = child {
hasTestTag("room")
}
val timeText: KNode = child {
hasTestTag("time")
}
} }
} }
fun KNode.getDateText() = child<KNode> { hasTestTag(TestIds.Main.ITEM_DATE) }
fun KNode.getPlaceText() = child<KNode> { hasTestTag(TestIds.Main.ITEM_PLACE) }

View File

@@ -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"}]
}

View File

@@ -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": []
}

View File

@@ -0,0 +1 @@
{}

View File

@@ -1,3 +0,0 @@
{
"error": "TEST Error 1 TEST"
}

View File

@@ -1,3 +0,0 @@
{
"error": "TEST Error 2 TEST"
}

View File

@@ -1,13 +1,9 @@
{ {
"name": "Test Testovich", "name": "Иван А",
"booking": [ "photoUrl": "https://funnyducks.ru/upload/iblock/0cd/0cdeb7ec3ed6fddda0f90fccee05557d.jpg",
{ "booking": {
"room": "row 1", "2025-01-05": {"id": 1, "place": "102"},
"time": "end row 1" "2025-01-06": {"id": 2, "place": "209.13"},
}, "2025-01-09": {"id": 3, "place": "Зона 51. 50"}
{
"room": "row 2",
"time": "end row 2"
} }
]
} }

View File

@@ -1,9 +1,9 @@
{ {
"name": "Ivan Ivanov", "name": "Вова Б",
"booking": [ "photoUrl": "https://funnyducks.ru/upload/iblock/0cd/0cdeb7ec3ed6fddda0f90fccee05557d.jpg",
{ "booking": {
"room": "row 3", "2003-01-01": {"id": 4, "place": "3"},
"time": "end row 3" "2002-01-01": {"id": 5, "place": "2"},
"2001-01-01": {"id": 6, "place": "1"}
} }
]
} }

View File

@@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<application <application
android:name=".App"
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"

View File

@@ -0,0 +1,15 @@
package ru.myitschool.work
import android.app.Application
import android.content.Context
class App: Application() {
override fun onCreate() {
super.onCreate()
context = this
}
companion object {
lateinit var context: Context
}
}

View File

@@ -2,8 +2,8 @@ package ru.myitschool.work.core
object Constants { object Constants {
const val HOST = "http://localhost:8090" const val HOST = "http://localhost:8090"
const val USER_URL = "/user" const val AUTH_URL = "/auth"
const val FULL_USER_URL = "$HOST$USER_URL" const val INFO_URL = "/info"
const val BOOKING_URL = "/booking"
const val BOOK_URL = "/book" const val BOOK_URL = "/book"
const val FULL_BOOK_URL = "$HOST$BOOK_URL"
} }

View File

@@ -0,0 +1,35 @@
package ru.myitschool.work.core
object TestIds {
object Auth {
const val ERROR = "auth_error"
const val SIGN_BUTTON = "auth_sign_button"
const val CODE_INPUT = "auth_code_input"
}
object Main {
const val ERROR = "main_error"
const val ADD_BUTTON = "main_add_button"
const val REFRESH_BUTTON = "main_refresh_button"
const val LOGOUT_BUTTON = "main_logout_button"
const val PROFILE_IMAGE = "main_image"
const val PROFILE_NAME = "main_name"
const val ITEM_PLACE = "main_item_place"
const val ITEM_DATE = "main_item_date"
fun getIdItemByPosition(position: Int) = "main_book_pos_$position"
}
object Book {
const val ERROR = "book_error"
const val EMPTY = "book_empty"
const val REFRESH_BUTTON = "book_refresh_button"
const val BACK_BUTTON = "book_back_button"
const val BOOK_BUTTON = "book_book_button"
const val ITEM_DATE = "book_date"
const val ITEM_PLACE_TEXT = "book_place_text"
const val ITEM_PLACE_SELECTOR = "book_place_selector"
fun getIdDateItemByPosition(position: Int) = "book_date_pos_$position"
fun getIdPlaceItemByPosition(position: Int) = "book_place_pos_$position"
}
}

View File

@@ -1,6 +0,0 @@
package ru.myitschool.work.domain.entities
data class BookingEntity(
val roomName: String,
val time: String,
)

View File

@@ -1,6 +0,0 @@
package ru.myitschool.work.domain.entities
data class UserEntity(
val name: String,
val bookingList: List<BookingEntity>
)

View File

@@ -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"
)
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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>(RootState.NotLoaded)
val uiState: StateFlow<RootState> = _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
}
}
)
}
}
}

View File

@@ -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)

View File

@@ -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
)
}

View File

@@ -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
)
*/
)

View File

@@ -1,4 +1,15 @@
<resources> <resources>
<string name="app_name">Work</string> <string name="app_name">Work</string>
<string name="title_activity_root">NTO-2025</string> <string name="title_activity_root">RootActivity</string>
<string name="auth_title">Привет! Введи код для авторизации</string>
<string name="auth_label">Код</string>
<string name="auth_sign_in">Войти</string>
<string name="main_refresh">Обновить</string>
<string name="main_logout">Выйти</string>
<string name="main_add">Добавить</string>
<string name="book_add">Забронировать</string>
<string name="book_back">Назад</string>
<string name="book_empty">Всё забронировано</string>
</resources> </resources>