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 {
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")

View File

@@ -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<RootActivity>(
) {
@get:Rule
val composeTestRule = createAndroidComposeRule<RootActivity>()
@get:Rule
val serverRule = MockWebServerRule(8090)
@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(
"/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<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) {
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<MainScreen.ListItem> {
roomText.assertTextEquals("row 1")
timeText.assertTextEquals("end row 1")
}
childAt<MainScreen.ListItem>(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 роверка_кнопки_выхода() = runWithInit(1) {
serverRule.mockResponses(
"/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("Проверяем корректное заполнение контента") {
nameText {
assertIsDisplayed()
assertTextEquals("Ivan Ivanov")
step("Нажимаем на кнопку выхода") {
logoutButton.performClick()
}
}
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()
}
list {
firstChild<MainScreen.ListItem> {
roomText.assertTextEquals("row 3")
timeText.assertTextEquals("end row 3")
}
}
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
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<MainScreen>(composeTestRule) {
step("Проверка наличия всех элементов на экране") {
flakySafely(timeoutMs = 5000L, intervalMs = 100L) {
warmUpCompose(composeTestRule)
errorText.assertIsNotDisplayed()
addButton.assertIsDisplayed()
refreshButton.assertIsDisplayed()
logoutButton.assertIsDisplayed()
profileNameText.assertIsDisplayed()
}
}
step("Нажимаем на кнопку добавления") {
addButton.performClick()
}
}
onComposeScreen<MainScreen>(composeTestRule) {
step("Нажимаем на кнопку загрузки данных") {
loadButton {
assertIsDisplayed()
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()
assertIsSelected()
}
getNodePlaceByPosition(0).assertIsNotSelected()
}
step("Проверяем корректное заполнение контента") {
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()
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<MainScreen>(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<BookScreen>(composeTestRule) {
step("Проверка наличия всех элементов на экране") {
flakySafely(timeoutMs = 5000L, intervalMs = 100L) {
warmUpCompose(composeTestRule)
backButton.assertIsDisplayed()
bookButton.assertIsDisplayed()
}
loadButton {
assertIsDisplayed()
performClick()
}
step("Оформление брони") {
bookButton.performClick()
}
}
onComposeScreen<MainScreen>(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 роверка_пустых_слотов() = 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<MainScreen>(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<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("Нажимаем на кнопку добавления") {
addButton.performClick()
}
}
onComposeScreen<BookScreen>(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<MainScreen>(composeTestRule) {
step("Нажимаем на кнопку добавления") {
addButton.performClick()
}
}
onComposeScreen<BookScreen>(composeTestRule) {
step("Проверка наличия всех элементов на экране") {
flakySafely(timeoutMs = 5000L, intervalMs = 100L) {
warmUpCompose(composeTestRule)
backButton.assertIsDisplayed()
bookButton.assertIsNotDisplayed()
}
}
step("Проверяем обработку ошибки") {
errorSend {
assertTextEquals("TEST Error 1 TEST")
}
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
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<MainScreen>(semanticsProvider) {
val loadButton = child<KNode> {
hasTestTag("load_button")
}
val nameText: KNode = child<KNode> {
hasTestTag("name")
}
val roomInput: KNode = child<KNode> {
hasTestTag("input_room")
}
val timeInput: KNode = child<KNode> {
hasTestTag("input_time")
val errorText = child<KNode> {
hasTestTag(TestIds.Main.ERROR)
}
val addButton: KNode = child<KNode> {
hasTestTag("add")
hasTestTag(TestIds.Main.ADD_BUTTON)
}
val errorContent: KNode = child<KNode> {
hasTestTag("error_text_content")
val refreshButton: KNode = child<KNode> {
hasTestTag(TestIds.Main.REFRESH_BUTTON)
}
val errorSend: KNode = child<KNode> {
hasTestTag("error_text_send")
val logoutButton: KNode = child<KNode> {
hasTestTag(TestIds.Main.LOGOUT_BUTTON)
}
val profileNameText: KNode = child<KNode> {
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<ListItem>(semanticsNode, semanticsProvider) {
val roomText: KNode = child {
hasTestTag("room")
}
val timeText: KNode = child {
hasTestTag("time")
}
fun getItemByPosition(position: Int) = child<KNode> {
hasTestTag(TestIds.Main.getIdItemByPosition(position))
}
}
}
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",
"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"}
}
}

View File

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

View File

@@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".App"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_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 {
const val HOST = "http://localhost:8090"
const val USER_URL = "/user"
const val FULL_USER_URL = "$HOST$USER_URL"
const val AUTH_URL = "/auth"
const val INFO_URL = "/info"
const val BOOKING_URL = "/booking"
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>
<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>