diff --git a/playground/sandbox-integration-test/build.gradle.kts b/playground/sandbox-integration-test/build.gradle.kts new file mode 100644 index 000000000..34d925d34 --- /dev/null +++ b/playground/sandbox-integration-test/build.gradle.kts @@ -0,0 +1,35 @@ +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + id("convention.sandbox-app") + id("convention.compose") + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "com.sdds.playground.integrationtest" + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } +} + +dependencies { + implementation("io.github.salute-developers:sdds-serv-compose:+") + implementation("sdds-core:uikit-compose") + implementation("sdds-core:icons") + implementation(libs.base.androidX.compose.foundation) + implementation(libs.base.androidX.activity.compose) + implementation(libs.base.androidX.appcompat) + implementation(libs.base.android.material) + implementation(libs.base.androidX.activity) + implementation(libs.base.androidX.constraintLayout) + androidTestImplementation(libs.base.test.integration.jUnit) + androidTestImplementation(libs.base.test.ui.espresso.core) + androidTestImplementation(libs.base.test.ui.compose.jUnit4) + androidTestImplementation(libs.base.test.integration.rules) + androidTestImplementation(libs.base.test.integration.runner) + androidTestImplementation(platform(libs.base.androidX.compose.bom)) + + debugImplementation(platform(libs.base.androidX.compose.bom)) + debugImplementation(libs.base.test.ui.compose.uiTestManifest) + +} diff --git a/playground/sandbox-integration-test/gradle.properties b/playground/sandbox-integration-test/gradle.properties new file mode 100644 index 000000000..3fbbed464 --- /dev/null +++ b/playground/sandbox-integration-test/gradle.properties @@ -0,0 +1,3 @@ +versionMajor=0 +versionMinor=1 +versionPatch=0 diff --git a/playground/sandbox-integration-test/src/androidTest/kotlin/com/sdds/playground/integrationtest/LoginFormTest.kt b/playground/sandbox-integration-test/src/androidTest/kotlin/com/sdds/playground/integrationtest/LoginFormTest.kt new file mode 100644 index 000000000..5df650c12 --- /dev/null +++ b/playground/sandbox-integration-test/src/androidTest/kotlin/com/sdds/playground/integrationtest/LoginFormTest.kt @@ -0,0 +1,42 @@ +package com.sdds.playground.integrationtest +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performImeAction +import androidx.compose.ui.test.performTextInput +import com.sdds.playground.integrationtest.testtags.LoginFormTags +import com.sdds.playground.sandboxhelper.SandboxScenariosIds +import com.sdds.playground.sandboxhelper.createSandboxComposeRule +import org.junit.Rule +import org.junit.Test + +class LoginFormTest { + + @get:Rule + val composeTestRule = createSandboxComposeRule(SandboxScenariosIds.LOGIN_FORM) + + @Test + fun test_input_correct() { + composeTestRule.onNodeWithTag("login_form_open_sheet").performClick() + composeTestRule.onNodeWithTag(LoginFormTags.CONTINUE).assertIsNotEnabled() + composeTestRule.onNodeWithTag(LoginFormTags.EMAIL).performTextInput("demo") + composeTestRule.onNodeWithTag(LoginFormTags.PASSWORD).performTextInput("demo") + composeTestRule.onNodeWithTag(LoginFormTags.CONTINUE).assertIsEnabled() + composeTestRule.onNodeWithTag(LoginFormTags.CONTINUE).performClick() + composeTestRule.onNodeWithTag(LoginFormTags.LOADING_HINT).assertExists() + } + + @Test + fun test_input_incorrect() { + composeTestRule.onNodeWithTag("login_form_open_sheet").performClick() + composeTestRule.onNodeWithTag(LoginFormTags.CONTINUE).assertIsNotEnabled() + composeTestRule.onNodeWithTag(LoginFormTags.EMAIL).performTextInput("demodemo") + composeTestRule.onNodeWithTag(LoginFormTags.PASSWORD).performTextInput("demodemo") + composeTestRule.onNodeWithTag(LoginFormTags.CONTINUE).assertIsEnabled() + composeTestRule.onNodeWithTag(LoginFormTags.CONTINUE).performClick() + composeTestRule.onNodeWithTag(LoginFormTags.PASSWORD).performImeAction() + composeTestRule.onNodeWithTag(LoginFormTags.ERROR).assertIsDisplayed() + } +} diff --git a/playground/sandbox-integration-test/src/androidTest/kotlin/com/sdds/playground/integrationtest/ModalScrollbarTest.kt b/playground/sandbox-integration-test/src/androidTest/kotlin/com/sdds/playground/integrationtest/ModalScrollbarTest.kt new file mode 100644 index 000000000..823dc20d5 --- /dev/null +++ b/playground/sandbox-integration-test/src/androidTest/kotlin/com/sdds/playground/integrationtest/ModalScrollbarTest.kt @@ -0,0 +1,34 @@ +package com.sdds.playground.integrationtest + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.isDisplayed +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.test.swipeUp +import com.sdds.playground.integrationtest.testtags.ModalTags +import com.sdds.playground.sandboxhelper.SandboxScenariosIds +import com.sdds.playground.sandboxhelper.createSandboxComposeRule +import org.junit.Rule +import org.junit.Test + +class ModalScrollbarTest { + + @get:Rule + val composeTestRule = createSandboxComposeRule(SandboxScenariosIds.MODAL_SCROLLBAR) + + @Test + fun test_modal_scroll_after_close() { + composeTestRule.onNodeWithTag(ModalTags.FIRST_OPEN_BUTTON).performClick() + composeTestRule.onNodeWithTag(ModalTags.CHECK_FIRST_OPENED).isDisplayed() + composeTestRule.onNodeWithTag(ModalTags.FIRST_CLOSE_BUTTON).performClick() + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag(ModalTags.FIRST_SCROLL_CONTAINER).performTouchInput { + repeat(5) { + swipeUp() + } + } + composeTestRule.onNodeWithText("scroll passed").assertIsDisplayed() + } +} diff --git a/playground/sandbox-integration-test/src/androidTest/kotlin/com/sdds/playground/integrationtest/ToastModalTest.kt b/playground/sandbox-integration-test/src/androidTest/kotlin/com/sdds/playground/integrationtest/ToastModalTest.kt new file mode 100644 index 000000000..bcc92868c --- /dev/null +++ b/playground/sandbox-integration-test/src/androidTest/kotlin/com/sdds/playground/integrationtest/ToastModalTest.kt @@ -0,0 +1,29 @@ +package com.sdds.playground.integrationtest + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import com.sdds.playground.integrationtest.testtags.LoginFormTags +import com.sdds.playground.integrationtest.testtags.ModalTags +import com.sdds.playground.integrationtest.testtags.ToastTags +import com.sdds.playground.sandboxhelper.SandboxScenariosIds +import com.sdds.playground.sandboxhelper.createSandboxComposeRule +import org.junit.Rule +import org.junit.Test + +class ToastModalTest { + + @get:Rule + val composeTestRule = createSandboxComposeRule(SandboxScenariosIds.TOAST_MODAL_LOGIN) + + @Test + fun test_toast_modal_overlay() { + composeTestRule.onNodeWithTag(ModalTags.FIRST_OPEN_BUTTON).performClick() + composeTestRule.onNodeWithTag(LoginFormTags.EMAIL).performTextInput("demo") + composeTestRule.onNodeWithTag(LoginFormTags.PASSWORD).performTextInput("demo") + composeTestRule.onNodeWithTag(LoginFormTags.CONTINUE).performClick() + composeTestRule.onNodeWithTag(ToastTags.CHECK_TOAST_AFTER_VALID_SUBMIT).assertIsDisplayed() + composeTestRule.onNodeWithTag(ToastTags.TOAST).assertIsDisplayed() + } +} diff --git a/playground/sandbox-integration-test/src/androidTest/kotlin/com/sdds/playground/integrationtest/TooltipTest.kt b/playground/sandbox-integration-test/src/androidTest/kotlin/com/sdds/playground/integrationtest/TooltipTest.kt new file mode 100644 index 000000000..7e63ffcb5 --- /dev/null +++ b/playground/sandbox-integration-test/src/androidTest/kotlin/com/sdds/playground/integrationtest/TooltipTest.kt @@ -0,0 +1,47 @@ +package com.sdds.playground.integrationtest + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotDisplayed +import androidx.compose.ui.test.click +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTouchInput +import androidx.test.espresso.Espresso.pressBack +import com.sdds.playground.integrationtest.testtags.TooltipTags +import com.sdds.playground.sandboxhelper.SandboxScenariosIds +import com.sdds.playground.sandboxhelper.createSandboxComposeRule +import org.junit.Rule +import org.junit.Test + +class TooltipTest { + + @get:Rule + val composeTestRule = createSandboxComposeRule(SandboxScenariosIds.TOOLTIP_CLOSE) + + @Test + fun test_close_tooltip_with_btn_in_shadow() { + composeTestRule.onNodeWithTag(TooltipTags.FIRST_OPEN_BUTTON).performClick() + composeTestRule.onNodeWithTag(TooltipTags.CHECK_FIRST_OPENED).assertIsDisplayed() + composeTestRule.onNodeWithTag(TooltipTags.FIRST_CLOSE_BUTTON).performClick() + composeTestRule.onNodeWithTag(TooltipTags.FIRST_TOOLTIP).assertIsNotDisplayed() + } + + @Test + fun test_close_tooltip_with_tap_outside() { + composeTestRule.onNodeWithTag(TooltipTags.SECOND_OPEN_BUTTON).performClick() + composeTestRule.onNodeWithTag(TooltipTags.SECOND_TOOLTIP).assertIsDisplayed() + composeTestRule.onNodeWithTag(TooltipTags.ROOT).performTouchInput { + click(Offset(x = center.x, y = bottom - 10f)) + } + composeTestRule.onNodeWithTag(TooltipTags.SECOND_TOOLTIP).assertIsNotDisplayed() + } + + @Test + fun test_close_tooltip_with_pressBack() { + composeTestRule.onNodeWithTag(TooltipTags.SECOND_OPEN_BUTTON).performClick() + composeTestRule.onNodeWithTag(TooltipTags.SECOND_TOOLTIP).assertIsDisplayed() + pressBack() + composeTestRule.onNodeWithTag(TooltipTags.SECOND_TOOLTIP).assertIsNotDisplayed() + } +} diff --git a/playground/sandbox-integration-test/src/androidTest/kotlin/com/sdds/playground/sandboxhelper/SandboxComposeRule.kt b/playground/sandbox-integration-test/src/androidTest/kotlin/com/sdds/playground/sandboxhelper/SandboxComposeRule.kt new file mode 100644 index 000000000..963da726a --- /dev/null +++ b/playground/sandbox-integration-test/src/androidTest/kotlin/com/sdds/playground/sandboxhelper/SandboxComposeRule.kt @@ -0,0 +1,34 @@ +package com.sdds.playground.sandboxhelper + +import android.content.Context +import android.content.Intent +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.rules.ActivityScenarioRule +import com.sdds.playground.integrationtest.sandbox.AppActivity + +fun createSandboxComposeRule( + scenarioId: String, +): AndroidComposeTestRule, AppActivity> { + val context = ApplicationProvider.getApplicationContext() + + val intent = Intent(context, AppActivity::class.java).apply { + putExtra(AppActivity.EXTRA_SCENARIO_ID, scenarioId) + } + + return AndroidComposeTestRule( + activityRule = ActivityScenarioRule(intent), + activityProvider = { rule -> + var activity: AppActivity? = null + rule.scenario.onActivity { activity = it } + activity ?: error("Activity не запущен") + }, + ) +} + +internal object SandboxScenariosIds { + const val LOGIN_FORM = "login-form-basic" + const val TOOLTIP_CLOSE = "popup-tooltip-basic" + const val MODAL_SCROLLBAR = "modal-scrollbar-basic" + const val TOAST_MODAL_LOGIN = "toast-modal-login-basic" +} diff --git a/playground/sandbox-integration-test/src/main/AndroidManifest.xml b/playground/sandbox-integration-test/src/main/AndroidManifest.xml new file mode 100644 index 000000000..256092db3 --- /dev/null +++ b/playground/sandbox-integration-test/src/main/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/app/IntegrationSandboxApp.kt b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/app/IntegrationSandboxApp.kt new file mode 100644 index 000000000..faf963bda --- /dev/null +++ b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/app/IntegrationSandboxApp.kt @@ -0,0 +1,140 @@ +package com.sdds.playground.integrationtest.app + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +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.statusBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +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.unit.dp +import com.sdds.compose.uikit.Button +import com.sdds.compose.uikit.Text +import com.sdds.compose.uikit.style.style +import com.sdds.playground.integrationtest.scenarios.catalog.IntegrationScenario +import com.sdds.playground.integrationtest.scenarios.catalog.IntegrationScenarioRegistry +import com.sdds.playground.integrationtest.theme.IntegrationSandboxTheme +import com.sdds.serv.styles.basicbutton.BasicButton +import com.sdds.serv.styles.basicbutton.S +import com.sdds.serv.styles.basicbutton.Secondary + +/** + * Приложение для интеграционных сценариев + */ +@Composable +internal fun IntegrationSandboxApp( + initialScenarioId: String? = null, +) { + var selectedScenario by remember { + mutableStateOf( + IntegrationScenarioRegistry.scenarios + .firstOrNull { it.id == initialScenarioId }, + ) + } + + IntegrationSandboxTheme { + Box( + modifier = Modifier + .fillMaxSize(), + ) { + if (selectedScenario == null) { + ScenarioListScreen( + scenarios = IntegrationScenarioRegistry.scenarios, + onScenarioClick = { selectedScenario = it }, + ) + } else { + ScenarioDetailsScreen( + scenario = selectedScenario!!, + onBack = { selectedScenario = null }, + ) + } + } + } +} + +@Composable +private fun ScenarioListScreen( + scenarios: List, + onScenarioClick: (IntegrationScenario) -> Unit, +) { + Column( + modifier = Modifier + .fillMaxSize() + .statusBarsPadding(), + ) { + Text( + text = "Integration Sandbox", + modifier = Modifier.padding(horizontal = 20.dp, vertical = 24.dp), + ) + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(horizontal = 20.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + items(scenarios, key = { it.id }) { scenario -> + ScenarioListItem( + scenario = scenario, + onClick = { onScenarioClick(scenario) }, + ) + } + } + } +} + +@Composable +private fun ScenarioListItem( + scenario: IntegrationScenario, + onClick: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + Text(text = scenario.category.title) + Text(text = scenario.title) + Text(text = scenario.description) + } +} + +@Composable +private fun ScenarioDetailsScreen( + scenario: IntegrationScenario, + onBack: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxSize() + .statusBarsPadding() + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text(text = scenario.title) + Button( + label = "Back", + style = BasicButton.S.Secondary.style(), + onClick = onBack, + ) + } + scenario.screen() + } +} diff --git a/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/components/scenario/ScenarioButton.kt b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/components/scenario/ScenarioButton.kt new file mode 100644 index 000000000..1f176c397 --- /dev/null +++ b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/components/scenario/ScenarioButton.kt @@ -0,0 +1,31 @@ +package com.sdds.playground.integrationtest.components.scenario + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import com.sdds.compose.uikit.Button +import com.sdds.compose.uikit.ButtonStyle +import com.sdds.playground.integrationtest.uistate.ButtonUiState + +/** + * BasicButton для сценария + */ +@Composable +internal fun ScenarioButton( + state: ButtonUiState, + style: ButtonStyle, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Button( + modifier = modifier + .fillMaxWidth() + .testTag(state.testTag), + label = state.label, + style = style, + enabled = state.enabled, + loading = state.loading, + onClick = onClick, + ) +} diff --git a/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/components/scenario/ScenarioModal.kt b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/components/scenario/ScenarioModal.kt new file mode 100644 index 000000000..faa1c9ddb --- /dev/null +++ b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/components/scenario/ScenarioModal.kt @@ -0,0 +1,78 @@ +package com.sdds.playground.integrationtest.components.scenario + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.sdds.compose.uikit.Button +import com.sdds.compose.uikit.ButtonStyle +import com.sdds.compose.uikit.Modal +import com.sdds.compose.uikit.ModalStyle +import com.sdds.icons.R +import com.sdds.playground.integrationtest.uistate.ModalUiState + +/** + * Modal для сценария + */ +@Composable +internal fun ScenarioModal( + state: ModalUiState, + style: ModalStyle, + buttonStyle: ButtonStyle, + isVisible: Boolean, + onOpenClick: () -> Unit, + onDismissRequest: () -> Unit, + openButtonTag: String, + modalTag: String, + closeButtonTag: String, + modifier: Modifier = Modifier, + openButtonLabel: String = "Show modal", + closeButtonLabel: String = "Close modal", + content: @Composable () -> Unit, +) { + Box(modifier = modifier) { + Button( + style = buttonStyle, + modifier = Modifier + .align(Alignment.Center) + .testTag(openButtonTag), + label = openButtonLabel, + onClick = onOpenClick, + ) + Modal( + show = isVisible, + style = style, + modifier = Modifier.width(300.dp), + gravity = state.gravity, + onDismissRequest = onDismissRequest, + dimBackground = state.hasDimBackground, + hasClose = state.hasClose, + edgeToEdge = state.edgeToEdge, + useNativeBlackout = state.useNativeBlackout, + closeIcon = painterResource(R.drawable.ic_close_24), + ) { + Column( + modifier = Modifier + .width(300.dp) + .testTag(modalTag), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + content() + + Button( + style = buttonStyle, + modifier = Modifier.testTag(closeButtonTag), + label = closeButtonLabel, + onClick = onDismissRequest, + ) + } + } + } +} diff --git a/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/components/scenario/ScenarioScrollBar.kt b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/components/scenario/ScenarioScrollBar.kt new file mode 100644 index 000000000..209e021a2 --- /dev/null +++ b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/components/scenario/ScenarioScrollBar.kt @@ -0,0 +1,75 @@ +package com.sdds.playground.integrationtest.components.scenario + +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.dp +import com.sdds.compose.uikit.ScrollBar +import com.sdds.compose.uikit.ScrollBarStyle +import com.sdds.compose.uikit.Text + +/** + * Scrollbar для сценария + */ +@Composable +internal fun ScenarioScrollBar( + style: ScrollBarStyle, + scrollContainerTag: String, + scrollTargetTag: String, + modifier: Modifier = Modifier, + onScrolled: () -> Unit = {}, +) { + val scrollState = rememberScrollState() + + LaunchedEffect(scrollState.value) { + if (scrollState.value > 0) { + onScrolled() + } + } + + Box( + modifier = modifier + .fillMaxWidth() + .height(240.dp), + ) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState) + .testTag(scrollContainerTag), + ) { + repeat(30) { index -> + Text( + text = "Item $index", + modifier = if (index == 29) { + Modifier + .height(48.dp) + .testTag(scrollTargetTag) + } else { + Modifier.height(48.dp) + }, + ) + } + Text("scroll passed") + } + ScrollBar( + scrollState = scrollState, + modifier = Modifier.align(Alignment.TopEnd), + style = style, + orientation = Orientation.Vertical, + hasTrack = true, + hoverExpand = true, + alwaysShowScrollbar = true, + ) + } +} diff --git a/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/components/scenario/ScenarioTextField.kt b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/components/scenario/ScenarioTextField.kt new file mode 100644 index 000000000..7c70c2b2c --- /dev/null +++ b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/components/scenario/ScenarioTextField.kt @@ -0,0 +1,36 @@ +package com.sdds.playground.integrationtest.components.scenario + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.input.VisualTransformation +import com.sdds.compose.uikit.TextField +import com.sdds.compose.uikit.TextFieldStyle +import com.sdds.compose.uikit.fs.FocusSelectorSettings +import com.sdds.playground.integrationtest.uistate.TextFieldUiState + +/** + * TextField для сценария + */ +@Composable +internal fun ScenarioTextField( + state: TextFieldUiState, + style: TextFieldStyle, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + visualTransformation: VisualTransformation = VisualTransformation.None, +) { + TextField( + value = state.value, + onValueChange = onValueChange, + modifier = modifier.testTag(state.testTag), + style = style, + placeholderText = state.placeholder, + labelText = state.label, + focusSelectorSettings = FocusSelectorSettings.None, + keyboardOptions = keyboardOptions, + visualTransformation = visualTransformation, + ) +} diff --git a/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/components/scenario/ScenarioTooltip.kt b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/components/scenario/ScenarioTooltip.kt new file mode 100644 index 000000000..1393cddc2 --- /dev/null +++ b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/components/scenario/ScenarioTooltip.kt @@ -0,0 +1,93 @@ +package com.sdds.playground.integrationtest.components.scenario + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.offset +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.PopupProperties +import com.sdds.compose.uikit.Button +import com.sdds.compose.uikit.ButtonStyle +import com.sdds.compose.uikit.IconButton +import com.sdds.compose.uikit.PopoverAlignment +import com.sdds.compose.uikit.PopoverPlacement +import com.sdds.compose.uikit.PopoverPlacementMode +import com.sdds.compose.uikit.Text +import com.sdds.compose.uikit.Tooltip +import com.sdds.compose.uikit.TooltipStyle +import com.sdds.compose.uikit.TriggerInfo +import com.sdds.compose.uikit.popoverTrigger + +/** + * Tooltip для сценария + */ +@Composable +internal fun ScenarioTooltip( + style: TooltipStyle, + buttonStyle: ButtonStyle, + isVisible: Boolean, + text: String, + onOpenClick: () -> Unit, + onDismissRequest: () -> Unit, + openButtonTag: String, + tooltipTag: String, + modifier: Modifier = Modifier, + triggerLabel: String = "Show tooltip", + popupProperties: PopupProperties, + inlineCloseButtonStyle: ButtonStyle, + inlineCloseButtonTag: String? = null, + onInlineCloseClick: (() -> Unit)? = null, +) { + val triggerInfo = remember { mutableStateOf(TriggerInfo()) } + + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text(text = "Tooltip case") + + Box { + Button( + style = buttonStyle, + modifier = Modifier + .popoverTrigger(triggerInfo) + .testTag(openButtonTag), + label = triggerLabel, + onClick = onOpenClick, + ) + if ( + isVisible && inlineCloseButtonTag != null && onInlineCloseClick != null + ) { + IconButton( + style = inlineCloseButtonStyle, + modifier = Modifier + .align(Alignment.Center) + .offset(x = 12.dp, y = (-12).dp) + .testTag(inlineCloseButtonTag), + iconRes = com.sdds.icons.R.drawable.ic_close_16, + onClick = onInlineCloseClick, + ) + } + Tooltip( + style = style, + modifier = Modifier.testTag(tooltipTag), + show = isVisible, + triggerInfo = { triggerInfo.value }, + placement = PopoverPlacement.Top, + placementMode = PopoverPlacementMode.Loose, + triggerCentered = false, + alignment = PopoverAlignment.Start, + onDismissRequest = onDismissRequest, + popupProperties = popupProperties, + text = AnnotatedString(text), + ) + } + } +} diff --git a/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/sandbox/AppActivity.kt b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/sandbox/AppActivity.kt new file mode 100644 index 000000000..65ba65ed1 --- /dev/null +++ b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/sandbox/AppActivity.kt @@ -0,0 +1,32 @@ +package com.sdds.playground.integrationtest.sandbox + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.core.view.WindowCompat +import com.sdds.playground.integrationtest.app.IntegrationSandboxApp + +/** + * Activity для приложения + */ +@Suppress("UndocumentedPublicProperty") +class AppActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val initialScenarioId = intent.getStringExtra(EXTRA_SCENARIO_ID) + + WindowCompat.setDecorFitsSystemWindows(window, false) + + setContent { + IntegrationSandboxApp(initialScenarioId = initialScenarioId) + } + } + + /** + * Id сценария + */ + companion object { + const val EXTRA_SCENARIO_ID = "scenario_id" + } +} diff --git a/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/scaffold/ScenarioScaffold.kt b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/scaffold/ScenarioScaffold.kt new file mode 100644 index 000000000..79b9c23e2 --- /dev/null +++ b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/scaffold/ScenarioScaffold.kt @@ -0,0 +1,55 @@ +package com.sdds.playground.integrationtest.scaffold + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.widthIn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.dp +import com.sdds.compose.uikit.Text +import com.sdds.playground.integrationtest.testtags.CommonScenarioTags +import com.sdds.playground.integrationtest.uistate.ScenarioCheckUiState + +@Composable +internal fun ScenarioScaffold( + title: String, + description: String, + checks: List, + rootTestTag: String, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + Column( + modifier = modifier + .widthIn(max = 360.dp) + .testTag(rootTestTag), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text( + text = title, + modifier = Modifier.testTag(CommonScenarioTags.TITLE), + ) + Text( + text = description, + modifier = Modifier.testTag(CommonScenarioTags.DESCRIPTION), + ) + Text( + text = "Progress: ${checks.count { it.passed }}/${checks.size}", + modifier = Modifier.testTag(CommonScenarioTags.PROGRESS), + ) + Column( + modifier = Modifier.testTag(CommonScenarioTags.CHECKS_SECTION), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + checks.forEachIndexed { index, check -> + val prefix = if (check.passed) "PASS" else "WAIT" + Text( + text = "$prefix ${index + 1}. ${check.title}", + modifier = Modifier.testTag(check.testTag), + ) + } + } + content() + } +} diff --git a/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/scenarios/catalog/IntegrationScenario.kt b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/scenarios/catalog/IntegrationScenario.kt new file mode 100644 index 000000000..59d0551d1 --- /dev/null +++ b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/scenarios/catalog/IntegrationScenario.kt @@ -0,0 +1,22 @@ +package com.sdds.playground.integrationtest.scenarios.catalog + +import androidx.compose.runtime.Composable + +/** + * Категория сценария + */ +internal enum class ScenarioCategory(val title: String) { + Input("Input & Validation"), + Popup("Popup components"), +} + +/** + * Сценарий + */ +internal data class IntegrationScenario( + val id: String, + val title: String, + val description: String, + val category: ScenarioCategory, + val screen: @Composable () -> Unit, +) diff --git a/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/scenarios/catalog/IntegrationScenarioRegistry.kt b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/scenarios/catalog/IntegrationScenarioRegistry.kt new file mode 100644 index 000000000..eebd5b1cd --- /dev/null +++ b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/scenarios/catalog/IntegrationScenarioRegistry.kt @@ -0,0 +1,39 @@ +package com.sdds.playground.integrationtest.scenarios.catalog + +import com.sdds.playground.integrationtest.scenarios.login.LoginFormScenarioScreen +import com.sdds.playground.integrationtest.scenarios.popup.ModalScenarioScreen +import com.sdds.playground.integrationtest.scenarios.popup.ToastModalLoginFormScenarioScreen +import com.sdds.playground.integrationtest.scenarios.popup.TooltipScenarioScreen + +internal object IntegrationScenarioRegistry { + val scenarios: List = listOf( + IntegrationScenario( + id = "login-form-basic", + title = "Login Form", + description = "A realistic form flow with CTA gating, inline error and loading transition.", + category = ScenarioCategory.Input, + screen = { LoginFormScenarioScreen() }, + ), + IntegrationScenario( + id = "popup-tooltip-basic", + title = "Tooltip popup", + description = "Testing tooltip", + category = ScenarioCategory.Popup, + screen = { TooltipScenarioScreen() }, + ), + IntegrationScenario( + id = "modal-scrollbar-basic", + title = "Modal Scrollbar", + description = "Two modal flows that verify content can scroll again after closing overlays.", + category = ScenarioCategory.Popup, + screen = { ModalScenarioScreen() }, + ), + IntegrationScenario( + id = "toast-modal-login-basic", + title = "Toast Modal TextField", + description = "After input in two text fields inside modal toast appears", + category = ScenarioCategory.Popup, + screen = { ToastModalLoginFormScenarioScreen() }, + ), + ) +} diff --git a/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/scenarios/login/LoginFormScenarioScreen.kt b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/scenarios/login/LoginFormScenarioScreen.kt new file mode 100644 index 000000000..784d2c2f5 --- /dev/null +++ b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/scenarios/login/LoginFormScenarioScreen.kt @@ -0,0 +1,187 @@ +package com.sdds.playground.integrationtest.scenarios.login + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +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.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.unit.dp +import com.sdds.compose.uikit.BottomSheetHandlePlacement +import com.sdds.compose.uikit.Divider +import com.sdds.compose.uikit.IconButton +import com.sdds.compose.uikit.ModalBottomSheet +import com.sdds.compose.uikit.Text +import com.sdds.compose.uikit.internal.modal.BottomSheetValue +import com.sdds.compose.uikit.internal.modal.rememberModalBottomSheetState +import com.sdds.compose.uikit.style.style +import com.sdds.playground.integrationtest.components.scenario.ScenarioButton +import com.sdds.playground.integrationtest.components.scenario.ScenarioTextField +import com.sdds.playground.integrationtest.scaffold.ScenarioScaffold +import com.sdds.playground.integrationtest.scenarios.login.state.LoginFormScenarioUiState +import com.sdds.playground.integrationtest.scenarios.login.state.ValidationState +import com.sdds.playground.integrationtest.testtags.LoginFormTags +import com.sdds.serv.styles.basicbutton.BasicButton +import com.sdds.serv.styles.basicbutton.Default +import com.sdds.serv.styles.basicbutton.L +import com.sdds.serv.styles.basicbutton.Secondary +import com.sdds.serv.styles.basicbutton.Xs +import com.sdds.serv.styles.bottomsheet.Default +import com.sdds.serv.styles.bottomsheet.ModalBottomSheet +import com.sdds.serv.styles.textfield.Default +import com.sdds.serv.styles.textfield.Error +import com.sdds.serv.styles.textfield.InnerLabel +import com.sdds.serv.styles.textfield.L +import com.sdds.serv.styles.textfield.RequiredEnd +import com.sdds.serv.styles.textfield.Success +import com.sdds.serv.styles.textfield.TextField +import kotlinx.coroutines.launch + +/** + * Экран для интеграционного сценария: Login Form + */ +@Composable +internal fun LoginFormScenarioScreen() { + var uiState by remember { mutableStateOf(LoginFormScenarioUiState.initial()) } + val sheetState = rememberModalBottomSheetState(initialValue = BottomSheetValue.Hidden) + val scope = rememberCoroutineScope() + val bodyScrollState = rememberScrollState() + + val emailStyle = when (uiState.emailValidation) { + ValidationState.Default -> TextField.L.InnerLabel.Default.style() + ValidationState.Success -> TextField.L.InnerLabel.Success.style() + ValidationState.Error -> TextField.L.InnerLabel.Error.style() + } + + val passwordStyle = when (uiState.passwordValidation) { + ValidationState.Default -> TextField.L.InnerLabel.RequiredEnd.Default.style() + ValidationState.Success -> TextField.L.InnerLabel.RequiredEnd.Success.style() + ValidationState.Error -> TextField.L.InnerLabel.RequiredEnd.Error.style() + } + + ScenarioScaffold( + title = "Интеграционный сценарий: Login Form", + description = "A realistic form flow that checks field composition, CTA gating, " + + "error rendering and loading transition.", + checks = uiState.checks, + rootTestTag = LoginFormTags.ROOT, + ) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + ScenarioButton( + state = uiState.resetButton.copy( + label = "Open login sheet", + testTag = "login_form_open_sheet", + ), + style = BasicButton.L.Default.style(), + onClick = { + scope.launch { sheetState.show() } + }, + ) + + ModalBottomSheet( + style = ModalBottomSheet.Default.style(), + edgeToEdge = false, + sheetState = sheetState, + handlePlacement = BottomSheetHandlePlacement.Auto, + fitContent = false, + header = { + Column { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text(text = "Login Form") + IconButton( + modifier = Modifier.testTag("login_form_close_sheet_btn"), + iconRes = com.sdds.icons.R.drawable.ic_close_24, + ) { + scope.launch { sheetState.hide() } + } + } + Spacer(modifier = Modifier.padding(6.dp)) + Divider() + Spacer(modifier = Modifier.padding(6.dp)) + } + }, + body = { + Column( + modifier = Modifier + .verticalScroll(bodyScrollState), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + ScenarioTextField( + modifier = Modifier.fillMaxWidth(), + state = uiState.emailField, + style = emailStyle, + onValueChange = { uiState = uiState.updateEmail(it) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email), + ) + ScenarioTextField( + modifier = Modifier.fillMaxWidth(), + state = uiState.passwordField, + style = passwordStyle, + onValueChange = { uiState = uiState.updatePassword(it) }, + visualTransformation = PasswordVisualTransformation(), + ) + + uiState.errorMessage?.let { + Text( + text = it, + modifier = Modifier.testTag(LoginFormTags.ERROR), + ) + } + uiState.loadingMessage?.let { + Text( + text = it, + modifier = Modifier.testTag(LoginFormTags.LOADING_HINT), + ) + } + } + }, + footer = { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + ScenarioButton( + state = uiState.continueButton, + style = BasicButton.Xs.Default.style(), + onClick = { uiState = uiState.submit() }, + ) + ScenarioButton( + state = uiState.useInvalidSampleButton, + style = BasicButton.Xs.Secondary.style(), + onClick = { uiState = uiState.applyInvalidSample() }, + ) + ScenarioButton( + state = uiState.useValidSampleButton, + style = BasicButton.Xs.Secondary.style(), + onClick = { uiState = uiState.applyValidSample() }, + ) + ScenarioButton( + state = uiState.resetButton, + style = BasicButton.Xs.Secondary.style(), + onClick = { uiState = uiState.reset() }, + ) + } + }, + ) + } + } +} diff --git a/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/scenarios/login/state/LoginFormScenarioUiState.kt b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/scenarios/login/state/LoginFormScenarioUiState.kt new file mode 100644 index 000000000..424075e60 --- /dev/null +++ b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/scenarios/login/state/LoginFormScenarioUiState.kt @@ -0,0 +1,220 @@ +package com.sdds.playground.integrationtest.scenarios.login.state + +import com.sdds.playground.integrationtest.testtags.LoginFormTags +import com.sdds.playground.integrationtest.uistate.ButtonUiState +import com.sdds.playground.integrationtest.uistate.ScenarioCheckUiState +import com.sdds.playground.integrationtest.uistate.TextFieldUiState + +/** + * Состояние сценария логина + */ +internal enum class ValidationState { + Default, + Success, + Error, +} + +internal data class LoginFormScenarioUiState( + val emailField: TextFieldUiState, + val passwordField: TextFieldUiState, + val emailValidation: ValidationState, + val passwordValidation: ValidationState, + val continueButton: ButtonUiState, + val useInvalidSampleButton: ButtonUiState, + val useValidSampleButton: ButtonUiState, + val resetButton: ButtonUiState, + val checks: List, + val errorMessage: String? = null, + val loadingMessage: String? = null, +) { + + fun updateEmail(value: String): LoginFormScenarioUiState = from( + email = value, + password = passwordField.value, + showError = false, + loading = false, + ) + + fun updatePassword(value: String): LoginFormScenarioUiState = from( + email = emailField.value, + password = value, + showError = false, + loading = false, + ) + + fun submit(): LoginFormScenarioUiState = + if (emailField.value == "demo" && passwordField.value == "demo") { + from( + email = emailField.value, + password = passwordField.value, + showError = false, + loading = true, + ) + } else { + from( + email = emailField.value, + password = passwordField.value, + showError = true, + loading = false, + ) + } + + fun applyInvalidSample(): LoginFormScenarioUiState = from( + email = "demo", + password = "wrong", + showError = false, + loading = false, + ) + + fun applyValidSample(): LoginFormScenarioUiState = from( + email = "demo", + password = "demo", + showError = false, + loading = false, + ) + + fun reset(): LoginFormScenarioUiState = initial() + + companion object { + fun initial(): LoginFormScenarioUiState = from( + email = "", + password = "", + showError = false, + loading = false, + ) + + private fun from( + email: String, + password: String, + showError: Boolean, + loading: Boolean, + ): LoginFormScenarioUiState { + val isSubmitEnabled = isSubmitEnabled(email, password, loading) + val isValidCredentials = hasValidCredentials(email, password) + + val emailValidation = resolveValidation( + value = email, + showError = showError, + loading = loading, + isValidCredentials = isValidCredentials, + ) + + val passwordValidation = resolveValidation( + value = password, + showError = showError, + loading = loading, + isValidCredentials = isValidCredentials, + ) + + return LoginFormScenarioUiState( + emailField = buildEmailField(email), + passwordField = buildPasswordField(password), + emailValidation = emailValidation, + passwordValidation = passwordValidation, + continueButton = buildContinueButton(isSubmitEnabled, loading), + useInvalidSampleButton = buildInvalidSampleButton(), + useValidSampleButton = buildValidSampleButton(), + resetButton = buildResetButton(), + checks = buildChecks( + email = email, + password = password, + showError = showError, + loading = loading, + isSubmitEnabled = isSubmitEnabled, + ), + errorMessage = buildErrorMessage(showError), + loadingMessage = buildLoadingMessage(loading), + ) + } + } +} + +private fun resolveValidation( + value: String, + showError: Boolean, + loading: Boolean, + isValidCredentials: Boolean, +): ValidationState = when { + value.isBlank() -> ValidationState.Default + showError -> ValidationState.Error + loading && isValidCredentials -> ValidationState.Success + else -> ValidationState.Default +} + +private fun isSubmitEnabled(email: String, password: String, loading: Boolean): Boolean = + email.isNotBlank() && password.isNotBlank() && !loading + +private fun hasValidCredentials(email: String, password: String): Boolean = + email == "demo" && password == "demo" + +private fun buildChecks( + email: String, + password: String, + showError: Boolean, + loading: Boolean, + isSubmitEnabled: Boolean, +): List = listOf( + ScenarioCheckUiState( + title = "Кнопка выключена когда форма не заполнена", + passed = email.isBlank() && password.isBlank() && !isSubmitEnabled && !showError && !loading, + testTag = LoginFormTags.check(1), + ), + ScenarioCheckUiState( + title = "Кнопка активна когда обе формы заполнены", + passed = email.isNotBlank() && password.isNotBlank() && isSubmitEnabled && !showError && !loading, + testTag = LoginFormTags.check(2), + ), + ScenarioCheckUiState( + title = "Неправильный ввод показывает ошибку", + passed = showError && !loading && email.isNotBlank() && password.isNotBlank(), + testTag = LoginFormTags.check(3), + ), + ScenarioCheckUiState( + title = "Правильный ввод переводит кнопку в статус Loading", + passed = loading && email == "demo" && password == "demo", + testTag = LoginFormTags.check(4), + ), +) + +private fun buildEmailField(email: String): TextFieldUiState = TextFieldUiState( + value = email, + label = "Email", + placeholder = "введите ваш email, например: name@example.com", + testTag = LoginFormTags.EMAIL, +) + +private fun buildPasswordField(password: String): TextFieldUiState = TextFieldUiState( + value = password, + label = "Пароль", + placeholder = "Введите пароль", + testTag = LoginFormTags.PASSWORD, + isPassword = true, +) + +private fun buildContinueButton(enabled: Boolean, loading: Boolean): ButtonUiState = ButtonUiState( + label = "Continue", + testTag = LoginFormTags.CONTINUE, + enabled = enabled, + loading = loading, +) + +private fun buildInvalidSampleButton(): ButtonUiState = ButtonUiState( + label = "Use invalid sample", + testTag = LoginFormTags.USE_INVALID, +) + +private fun buildValidSampleButton(): ButtonUiState = ButtonUiState( + label = "Use valid sample", + testTag = LoginFormTags.USE_VALID, +) + +private fun buildResetButton(): ButtonUiState = ButtonUiState( + label = "Reset scenario", + testTag = LoginFormTags.RESET, +) + +private fun buildErrorMessage(showError: Boolean): String? = + if (showError) "Invalid credentials" else null + +private fun buildLoadingMessage(loading: Boolean): String? = + if (loading) "Loading state reached for a valid submit." else null diff --git a/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/scenarios/popup/ModalScenarioScreen.kt b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/scenarios/popup/ModalScenarioScreen.kt new file mode 100644 index 000000000..994a6b727 --- /dev/null +++ b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/scenarios/popup/ModalScenarioScreen.kt @@ -0,0 +1,86 @@ +package com.sdds.playground.integrationtest.scenarios.popup + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.sdds.compose.uikit.Text +import com.sdds.compose.uikit.style.style +import com.sdds.playground.integrationtest.components.scenario.ScenarioButton +import com.sdds.playground.integrationtest.components.scenario.ScenarioModal +import com.sdds.playground.integrationtest.components.scenario.ScenarioScrollBar +import com.sdds.playground.integrationtest.scaffold.ScenarioScaffold +import com.sdds.playground.integrationtest.scenarios.popup.state.ModalScenarioUiState +import com.sdds.playground.integrationtest.testtags.ModalTags +import com.sdds.playground.integrationtest.testtags.TooltipTags +import com.sdds.playground.integrationtest.uistate.ButtonUiState +import com.sdds.playground.integrationtest.uistate.ModalUiState +import com.sdds.serv.styles.basicbutton.BasicButton +import com.sdds.serv.styles.basicbutton.Default +import com.sdds.serv.styles.basicbutton.M +import com.sdds.serv.styles.basicbutton.S +import com.sdds.serv.styles.basicbutton.Secondary +import com.sdds.serv.styles.modal.Default +import com.sdds.serv.styles.modal.Modal +import com.sdds.serv.styles.scrollbar.M +import com.sdds.serv.styles.scrollbar.ScrollBar + +@Composable +internal fun ModalScenarioScreen() { + var uiState by remember { mutableStateOf(ModalScenarioUiState.initial()) } + val screenScrollState = rememberScrollState() + + ScenarioScaffold( + title = "Интеграционный сценарий: Modal + Scrollbar", + description = "Проверяем скролится ли контент после открытия и закрытия модального окна", + checks = uiState.checks, + rootTestTag = ModalTags.ROOT, + ) { + Column( + modifier = Modifier.verticalScroll(screenScrollState), + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + Text(text = "Сценарий: Открываем модал, закрываем, затем скроллим контент") + + ScenarioModal( + state = ModalUiState(), + style = Modal.Default.style(), + buttonStyle = BasicButton.M.Default.style(), + isVisible = uiState.isFirstModalVisible, + onOpenClick = { uiState = uiState.openFirstModal() }, + onDismissRequest = { uiState = uiState.closeFirstModal() }, + openButtonTag = ModalTags.FIRST_OPEN_BUTTON, + modalTag = ModalTags.FIRST_MODAL, + closeButtonTag = ModalTags.FIRST_CLOSE_BUTTON, + openButtonLabel = "Open modal", + content = {}, + ) + + ScenarioScrollBar( + style = ScrollBar.M.style(), + scrollContainerTag = ModalTags.FIRST_SCROLL_CONTAINER, + scrollTargetTag = ModalTags.FIRST_SCROLL_TARGET, + onScrolled = { uiState = uiState.markFirstScrolled() }, + ) + Spacer(modifier = Modifier.height(12.dp)) + + ScenarioButton( + state = ButtonUiState( + label = "Reset scenario", + testTag = TooltipTags.RESET, + ), + style = BasicButton.S.Secondary.style(), + onClick = { uiState = uiState.reset() }, + ) + } + } +} diff --git a/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/scenarios/popup/ToastModalLoginScenarioScreen.kt b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/scenarios/popup/ToastModalLoginScenarioScreen.kt new file mode 100644 index 000000000..05b11ac97 --- /dev/null +++ b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/scenarios/popup/ToastModalLoginScenarioScreen.kt @@ -0,0 +1,200 @@ +package com.sdds.playground.integrationtest.scenarios.popup + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.unit.dp +import com.sdds.compose.uikit.Icon +import com.sdds.compose.uikit.Text +import com.sdds.compose.uikit.Toast +import com.sdds.compose.uikit.overlay.LocalOverlayManager +import com.sdds.compose.uikit.overlay.OverlayHost +import com.sdds.compose.uikit.overlay.OverlayManager +import com.sdds.compose.uikit.overlay.OverlayPosition +import com.sdds.compose.uikit.overlay.showToast +import com.sdds.compose.uikit.style.style +import com.sdds.playground.integrationtest.components.scenario.ScenarioButton +import com.sdds.playground.integrationtest.components.scenario.ScenarioModal +import com.sdds.playground.integrationtest.components.scenario.ScenarioTextField +import com.sdds.playground.integrationtest.scaffold.ScenarioScaffold +import com.sdds.playground.integrationtest.scenarios.login.state.LoginFormScenarioUiState +import com.sdds.playground.integrationtest.scenarios.login.state.ValidationState +import com.sdds.playground.integrationtest.testtags.LoginFormTags +import com.sdds.playground.integrationtest.testtags.ModalTags +import com.sdds.playground.integrationtest.testtags.ToastTags +import com.sdds.playground.integrationtest.uistate.ButtonUiState +import com.sdds.playground.integrationtest.uistate.ModalUiState +import com.sdds.playground.integrationtest.uistate.ScenarioCheckUiState +import com.sdds.serv.styles.basicbutton.BasicButton +import com.sdds.serv.styles.basicbutton.Default +import com.sdds.serv.styles.basicbutton.M +import com.sdds.serv.styles.basicbutton.S +import com.sdds.serv.styles.basicbutton.Secondary +import com.sdds.serv.styles.basicbutton.Xs +import com.sdds.serv.styles.modal.Default +import com.sdds.serv.styles.modal.Modal +import com.sdds.serv.styles.textfield.Default +import com.sdds.serv.styles.textfield.Error +import com.sdds.serv.styles.textfield.InnerLabel +import com.sdds.serv.styles.textfield.L +import com.sdds.serv.styles.textfield.RequiredEnd +import com.sdds.serv.styles.textfield.Success +import com.sdds.serv.styles.textfield.TextField +import com.sdds.serv.styles.toast.Positive +import com.sdds.serv.styles.toast.Rounded +import com.sdds.serv.styles.toast.Toast + +@Composable +internal fun ToastModalLoginFormScenarioScreen() { + var uiState by remember { mutableStateOf(LoginFormScenarioUiState.initial()) } + var isModalVisible by remember { mutableStateOf(false) } + var toastShownAfterValidSubmit by remember { mutableStateOf(false) } + + val emailStyle = when (uiState.emailValidation) { + ValidationState.Default -> TextField.L.InnerLabel.Default.style() + ValidationState.Success -> TextField.L.InnerLabel.Success.style() + ValidationState.Error -> TextField.L.InnerLabel.Error.style() + } + + val passwordStyle = when (uiState.passwordValidation) { + ValidationState.Default -> TextField.L.InnerLabel.RequiredEnd.Default.style() + ValidationState.Success -> TextField.L.InnerLabel.RequiredEnd.Success.style() + ValidationState.Error -> TextField.L.InnerLabel.RequiredEnd.Error.style() + } + + val checks = uiState.checks + ScenarioCheckUiState( + title = "Toast появляется после успешного ввода", + passed = toastShownAfterValidSubmit, + testTag = ToastTags.CHECK_TOAST_AFTER_VALID_SUBMIT, + ) + + ScenarioScaffold( + title = "Интеграционный сценарий: Toast показывается после ввода", + description = "Toast появляется после ввода и закрытия модал", + checks = checks, + rootTestTag = ToastTags.ROOT, + ) { + OverlayHost { + val overlayManager = LocalOverlayManager.current + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + ScenarioModal( + state = ModalUiState( + hasDimBackground = true, + useNativeBlackout = true, + edgeToEdge = true, + hasClose = false, + ), + style = Modal.Default.style(), + buttonStyle = BasicButton.M.Default.style(), + isVisible = isModalVisible, + onOpenClick = { isModalVisible = true }, + onDismissRequest = { isModalVisible = false }, + openButtonTag = ModalTags.FIRST_OPEN_BUTTON, + modalTag = ModalTags.FIRST_MODAL, + closeButtonTag = ModalTags.FIRST_CLOSE_BUTTON, + openButtonLabel = "Open modal", + ) { + ScenarioTextField( + modifier = Modifier.fillMaxWidth(), + state = uiState.emailField, + style = emailStyle, + onValueChange = { + toastShownAfterValidSubmit = false + uiState = uiState.updateEmail(it) + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email), + ) + ScenarioTextField( + modifier = Modifier.fillMaxWidth(), + state = uiState.passwordField, + style = passwordStyle, + onValueChange = { + toastShownAfterValidSubmit = false + uiState = uiState.updatePassword(it) + }, + visualTransformation = PasswordVisualTransformation(), + ) + + uiState.errorMessage?.let { + Text( + text = it, + modifier = Modifier.testTag(LoginFormTags.ERROR), + ) + } + + ScenarioButton( + state = uiState.continueButton, + style = BasicButton.Xs.Default.style(), + onClick = { + val nextState = uiState.submit() + uiState = nextState + val isSuccess = nextState.errorMessage == null && + nextState.loadingMessage != null + if (isSuccess) { + toastShownAfterValidSubmit = true + + overlayManager.showToast( + onDismiss = {}, + position = OverlayPosition.Center, + durationMillis = OverlayManager.OVERLAY_DURATION_SLOW_MILLIS, + ) { + id -> + Toast( + modifier = Modifier.testTag(ToastTags.TOAST), + style = Toast.Rounded.Positive.style(), + text = "Успешный логин", + contentStart = { + Icon( + painterResource(com.sdds.icons.R.drawable.ic_shazam_16), + contentDescription = "", + ) + }, + contentEnd = { + Icon( + modifier = Modifier.clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + ) { + overlayManager.remove(id) + }, + painter = painterResource(com.sdds.icons.R.drawable.ic_close_16), + contentDescription = "", + ) + }, + ) + } + } + }, + ) + } + + ScenarioButton( + state = ButtonUiState( + label = "Reset scenario", + testTag = ToastTags.RESET, + ), + style = BasicButton.S.Secondary.style(), + onClick = { + toastShownAfterValidSubmit = false + uiState = uiState.reset() + }, + ) + } + } + } +} diff --git a/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/scenarios/popup/TooltipScenarioScreen.kt b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/scenarios/popup/TooltipScenarioScreen.kt new file mode 100644 index 000000000..1fb8db793 --- /dev/null +++ b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/scenarios/popup/TooltipScenarioScreen.kt @@ -0,0 +1,98 @@ +package com.sdds.playground.integrationtest.scenarios.popup + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.PopupProperties +import com.sdds.compose.uikit.style.style +import com.sdds.playground.integrationtest.components.scenario.ScenarioButton +import com.sdds.playground.integrationtest.components.scenario.ScenarioTooltip +import com.sdds.playground.integrationtest.scaffold.ScenarioScaffold +import com.sdds.playground.integrationtest.scenarios.popup.state.TooltipScenarioUiState +import com.sdds.playground.integrationtest.testtags.TooltipTags +import com.sdds.playground.integrationtest.uistate.ButtonUiState +import com.sdds.serv.styles.basicbutton.BasicButton +import com.sdds.serv.styles.basicbutton.Default +import com.sdds.serv.styles.basicbutton.M +import com.sdds.serv.styles.basicbutton.S +import com.sdds.serv.styles.basicbutton.Secondary +import com.sdds.serv.styles.iconbutton.IconButton +import com.sdds.serv.styles.iconbutton.Positive +import com.sdds.serv.styles.iconbutton.S +import com.sdds.serv.styles.iconbutton.Xs +import com.sdds.serv.styles.tooltip.M +import com.sdds.serv.styles.tooltip.Tooltip + +@Composable +internal fun TooltipScenarioScreen() { + var uiState by remember { mutableStateOf(TooltipScenarioUiState.initial()) } + + ScenarioScaffold( + title = "Интеграционный сценарий: Tooltip popupProperties", + description = "Первый тултип закрывается по нажатию на встроенную кнопку. " + + "Второй тултип закрывается по тапу вне области тултипа.", + checks = uiState.checks, + rootTestTag = TooltipTags.ROOT, + ) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + ScenarioTooltip( + style = Tooltip.M.style(), + buttonStyle = BasicButton.M.Default.style(), + isVisible = uiState.isFirstTooltipVisible, + text = "Нажмите на кнопку с крестиком для продолжения", + onOpenClick = { uiState = uiState.openFirstTooltip() }, + onDismissRequest = { uiState = uiState.dismissActiveTooltip() }, + openButtonTag = TooltipTags.FIRST_OPEN_BUTTON, + tooltipTag = TooltipTags.FIRST_TOOLTIP, + triggerLabel = "Show tooltip with close button", + popupProperties = PopupProperties( + focusable = false, + dismissOnBackPress = true, + dismissOnClickOutside = false, + ), + inlineCloseButtonStyle = IconButton.Xs.Positive.style(), + inlineCloseButtonTag = TooltipTags.FIRST_CLOSE_BUTTON, + onInlineCloseClick = { uiState = uiState.closeFirstByButton() }, + ) + + Spacer(modifier = Modifier.size(36.dp)) + + ScenarioTooltip( + style = Tooltip.M.style(), + buttonStyle = BasicButton.M.Default.style(), + isVisible = uiState.isSecondTooltipVisible, + text = "Нажмите на область вне тултипа для закрытия", + onOpenClick = { uiState = uiState.openSecondTooltip() }, + onDismissRequest = { uiState = uiState.closeSecondByOutside() }, + openButtonTag = TooltipTags.SECOND_OPEN_BUTTON, + tooltipTag = TooltipTags.SECOND_TOOLTIP, + triggerLabel = "Show tooltip with outside dismiss", + popupProperties = PopupProperties( + focusable = true, + dismissOnBackPress = true, + dismissOnClickOutside = true, + ), + inlineCloseButtonStyle = IconButton.S.Positive.style(), + ) + + ScenarioButton( + state = ButtonUiState( + label = "Reset scenario", + testTag = TooltipTags.RESET, + ), + style = BasicButton.S.Secondary.style(), + onClick = { uiState = uiState.reset() }, + ) + } + } +} diff --git a/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/scenarios/popup/state/ModalScenarioUiState.kt b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/scenarios/popup/state/ModalScenarioUiState.kt new file mode 100644 index 000000000..3622a6c88 --- /dev/null +++ b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/scenarios/popup/state/ModalScenarioUiState.kt @@ -0,0 +1,105 @@ +package com.sdds.playground.integrationtest.scenarios.popup.state + +import com.sdds.playground.integrationtest.testtags.ModalTags +import com.sdds.playground.integrationtest.uistate.ScenarioCheckUiState + +/** + * State для сценария Modal + */ +internal data class ModalScenarioUiState( + val firstModalVisible: Boolean, + val firstModalOpened: Boolean, + val firstModalClosed: Boolean, + val firstScrolled: Boolean, + val secondModalVisible: Boolean, + val secondModalOpened: Boolean, + val secondModalClosed: Boolean, + val secondScrolled: Boolean, +) { + val isFirstModalVisible: Boolean + get() = firstModalVisible + + val isSecondModalVisible: Boolean + get() = secondModalVisible + + val checks: List + get() = listOf( + ScenarioCheckUiState( + title = "First modal opens", + passed = firstModalOpened, + testTag = ModalTags.CHECK_FIRST_OPENED, + ), + ScenarioCheckUiState( + title = "First modal closes before scrolling", + passed = firstModalClosed && !firstModalVisible, + testTag = ModalTags.CHECK_FIRST_CLOSED, + ), + ScenarioCheckUiState( + title = "First scenario content scrolls after modal close", + passed = firstScrolled, + testTag = ModalTags.CHECK_FIRST_SCROLLED, + ), + ScenarioCheckUiState( + title = "Second modal opens", + passed = secondModalOpened, + testTag = ModalTags.CHECK_SECOND_OPENED, + ), + ScenarioCheckUiState( + title = "Second modal closes before scrolling", + passed = secondModalClosed && !secondModalVisible, + testTag = ModalTags.CHECK_SECOND_CLOSED, + ), + ScenarioCheckUiState( + title = "Second scenario content scrolls after modal close", + passed = secondScrolled, + testTag = ModalTags.CHECK_SECOND_SCROLLED, + ), + ) + + fun openFirstModal() = copy( + firstModalVisible = true, + firstModalOpened = true, + ) + + fun closeFirstModal() = copy( + firstModalVisible = false, + firstModalClosed = true, + ) + + fun markFirstScrolled(): ModalScenarioUiState = if (firstModalClosed) { + copy(firstScrolled = true) + } else { + this + } + + fun openSecondModal() = copy( + secondModalVisible = true, + secondModalOpened = true, + ) + + fun closeSecondModal() = copy( + secondModalVisible = false, + secondModalClosed = true, + ) + + fun markSecondScrolled(): ModalScenarioUiState = if (secondModalClosed) { + copy(secondScrolled = true) + } else { + this + } + + fun reset(): ModalScenarioUiState = initial() + + companion object { + fun initial() = ModalScenarioUiState( + firstModalVisible = false, + firstModalOpened = false, + firstModalClosed = false, + firstScrolled = false, + secondModalVisible = false, + secondModalOpened = false, + secondModalClosed = false, + secondScrolled = false, + ) + } +} diff --git a/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/scenarios/popup/state/TooltipScenarioUiState.kt b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/scenarios/popup/state/TooltipScenarioUiState.kt new file mode 100644 index 000000000..ccc544c88 --- /dev/null +++ b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/scenarios/popup/state/TooltipScenarioUiState.kt @@ -0,0 +1,82 @@ +package com.sdds.playground.integrationtest.scenarios.popup.state + +import com.sdds.playground.integrationtest.testtags.TooltipTags +import com.sdds.playground.integrationtest.uistate.ScenarioCheckUiState + +internal enum class ActiveTooltip { + NONE, + FIRST, + SECOND, +} + +internal data class TooltipScenarioUiState( + val activeTooltip: ActiveTooltip, + val firstWasOpened: Boolean, + val firstClosedByButton: Boolean, + val secondWasOpened: Boolean, + val secondClosedByOutside: Boolean, +) { + val isFirstTooltipVisible: Boolean + get() = activeTooltip == ActiveTooltip.FIRST + + val isSecondTooltipVisible: Boolean + get() = activeTooltip == ActiveTooltip.SECOND + + val checks: List + get() = listOf( + ScenarioCheckUiState( + title = "Первый тултип открывается после нажатия на кнопку", + passed = firstWasOpened, + testTag = TooltipTags.CHECK_FIRST_OPENED, + ), + ScenarioCheckUiState( + title = "Первый тултип закрывается по нажатию на кнопку закрытия в зоне тултипа", + passed = firstClosedByButton && !isFirstTooltipVisible, + testTag = TooltipTags.CHECK_FIRST_CLOSED_BY_BUTTON, + ), + ScenarioCheckUiState( + title = "Второй тултип открывается после нажатия на кнопку", + passed = secondWasOpened, + testTag = TooltipTags.CHECK_SECOND_OPENED, + ), + ScenarioCheckUiState( + title = "Второй тултип закрывается по тапу вне области тултипа", + passed = secondClosedByOutside && !isSecondTooltipVisible, + testTag = TooltipTags.CHECK_SECOND_CLOSED_BY_OUTSIDE, + ), + ) + + fun openFirstTooltip() = copy( + activeTooltip = ActiveTooltip.FIRST, + firstWasOpened = true, + ) + + fun openSecondTooltip() = copy( + activeTooltip = ActiveTooltip.SECOND, + secondWasOpened = true, + ) + + fun closeFirstByButton() = copy( + activeTooltip = ActiveTooltip.NONE, + firstClosedByButton = true, + ) + + fun closeSecondByOutside() = copy( + activeTooltip = ActiveTooltip.NONE, + secondClosedByOutside = true, + ) + + fun dismissActiveTooltip() = copy(activeTooltip = ActiveTooltip.NONE) + + fun reset() = initial() + + companion object { + fun initial() = TooltipScenarioUiState( + activeTooltip = ActiveTooltip.NONE, + firstWasOpened = false, + firstClosedByButton = false, + secondWasOpened = false, + secondClosedByOutside = false, + ) + } +} diff --git a/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/testtags/CommonScenarioTags.kt b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/testtags/CommonScenarioTags.kt new file mode 100644 index 000000000..1afdf7014 --- /dev/null +++ b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/testtags/CommonScenarioTags.kt @@ -0,0 +1,12 @@ +package com.sdds.playground.integrationtest.testtags + +@Suppress( + "UndocumentedPublicClass", + "UndocumentedPublicProperty", +) +internal object CommonScenarioTags { + const val TITLE = "scenario_title" + const val DESCRIPTION = "scenario_description" + const val PROGRESS = "scenario_progress" + const val CHECKS_SECTION = "scenario_checks" +} diff --git a/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/testtags/LoginFormTags.kt b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/testtags/LoginFormTags.kt new file mode 100644 index 000000000..f730ab506 --- /dev/null +++ b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/testtags/LoginFormTags.kt @@ -0,0 +1,22 @@ +package com.sdds.playground.integrationtest.testtags + +/** + * Тэги для TextField + */ +@Suppress( + "UndocumentedPublicProperty", + "UndocumentedPublicClass", +) +internal object LoginFormTags { + const val ROOT = "login_form_scenario_root" + const val EMAIL = "login_form_email" + const val PASSWORD = "login_form_password" + const val CONTINUE = "login_form_continue" + const val USE_INVALID = "login_form_use_invalid" + const val USE_VALID = "login_form_use_valid" + const val RESET = "login_form_reset" + const val ERROR = "login_form_error" + const val LOADING_HINT = "login_form_loading_hint" + + internal fun check(index: Int): String = "login_form_check_$index" +} diff --git a/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/testtags/ModalTags.kt b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/testtags/ModalTags.kt new file mode 100644 index 000000000..1e8af0cca --- /dev/null +++ b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/testtags/ModalTags.kt @@ -0,0 +1,32 @@ +package com.sdds.playground.integrationtest.testtags + +/** + * Тэги для Modal + */ +@Suppress( + "UndocumentedPublicProperty", + "UndocumentedPublicClass", +) +internal object ModalTags { + const val ROOT = "modal_scenario_root" + + const val FIRST_OPEN_BUTTON = "modal_first_open_button" + const val FIRST_MODAL = "modal_first_content" + const val FIRST_CLOSE_BUTTON = "modal_first_close_button" + const val FIRST_SCROLL_CONTAINER = "modal_first_scroll_container" + const val FIRST_SCROLL_TARGET = "modal_first_scroll_target" + + const val SECOND_OPEN_BUTTON = "modal_second_open_button" + const val SECOND_MODAL = "modal_second_content" + const val SECOND_CLOSE_BUTTON = "modal_second_close_button" + const val SECOND_SCROLL_CONTAINER = "modal_second_scroll_container" + const val SECOND_SCROLL_TARGET = "modal_second_scroll_target" + + const val CHECK_FIRST_OPENED = "modal_check_first_opened" + const val CHECK_FIRST_CLOSED = "modal_check_first_closed" + const val CHECK_FIRST_SCROLLED = "modal_check_first_scrolled" + const val CHECK_SECOND_OPENED = "modal_check_second_opened" + const val CHECK_SECOND_CLOSED = "modal_check_second_closed" + const val CHECK_SECOND_SCROLLED = "modal_check_second_scrolled" + const val RESET = "modal_reset" +} diff --git a/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/testtags/ToastTags.kt b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/testtags/ToastTags.kt new file mode 100644 index 000000000..3df6b83c2 --- /dev/null +++ b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/testtags/ToastTags.kt @@ -0,0 +1,15 @@ +package com.sdds.playground.integrationtest.testtags + +/** + * Тэги для Toast + */ +@Suppress( + "UndocumentedPublicProperty", + "UndocumentedPublicClass", +) +internal object ToastTags { + const val ROOT = "toast_scenario_root" + const val TOAST = "toast_success_toast" + const val CHECK_TOAST_AFTER_VALID_SUBMIT = "toast_check_after_valid_submit" + const val RESET = "toast_reset" +} diff --git a/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/testtags/TooltipTags.kt b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/testtags/TooltipTags.kt new file mode 100644 index 000000000..b28887675 --- /dev/null +++ b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/testtags/TooltipTags.kt @@ -0,0 +1,22 @@ +package com.sdds.playground.integrationtest.testtags + +/** + * Тэги для Tooltip + */ +@Suppress( + "UndocumentedPublicProperty", + "UndocumentedPublicClass", +) +internal object TooltipTags { + const val ROOT = "tooltip_scenario_root" + const val FIRST_OPEN_BUTTON = "tooltip_first_open_button" + const val FIRST_TOOLTIP = "tooltip_first_content" + const val FIRST_CLOSE_BUTTON = "tooltip_first_close_button" + const val SECOND_OPEN_BUTTON = "tooltip_second_open_button" + const val SECOND_TOOLTIP = "tooltip_second_content" + const val CHECK_FIRST_OPENED = "tooltip_check_first_opened" + const val CHECK_FIRST_CLOSED_BY_BUTTON = "tooltip_check_first_closed_by_button" + const val CHECK_SECOND_OPENED = "tooltip_check_second_opened" + const val CHECK_SECOND_CLOSED_BY_OUTSIDE = "tooltip_check_second_closed_by_outside" + const val RESET = "tooltip_reset" +} diff --git a/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/theme/SddsServIntegrationSandboxTheme.kt b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/theme/SddsServIntegrationSandboxTheme.kt new file mode 100644 index 000000000..8f9cd5d30 --- /dev/null +++ b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/theme/SddsServIntegrationSandboxTheme.kt @@ -0,0 +1,52 @@ +package com.sdds.playground.integrationtest.theme + +import android.app.Activity +import android.graphics.Color +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat +import com.sdds.serv.theme.SddsServTheme +import com.sdds.serv.theme.darkSddsServColors +import com.sdds.serv.theme.darkSddsServGradients +import com.sdds.serv.theme.lightSddsServColors +import com.sdds.serv.theme.lightSddsServGradients + +private val DarkColors = darkSddsServColors() +private val LightColors = lightSddsServColors() +private val DarkGradients = darkSddsServGradients() +private val LightGradients = lightSddsServGradients() + +/** + * Тема для песочницы приложения + */ +@Composable +internal fun IntegrationSandboxTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit, +) { + val colorScheme = when { + darkTheme -> DarkColors + else -> LightColors + } + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = Color.TRANSPARENT + window.navigationBarColor = Color.TRANSPARENT + window.decorView.setBackgroundColor(colorScheme.backgroundDefaultPrimary.toArgb()) + WindowCompat.getInsetsController(window, view).apply { + isAppearanceLightStatusBars = !darkTheme + isAppearanceLightNavigationBars = !darkTheme + } + } + } + SddsServTheme( + colors = colorScheme, + gradients = if (darkTheme) DarkGradients else LightGradients, + content = content, + ) +} diff --git a/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/uistate/ButtonUiState.kt b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/uistate/ButtonUiState.kt new file mode 100644 index 000000000..0406f8be0 --- /dev/null +++ b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/uistate/ButtonUiState.kt @@ -0,0 +1,12 @@ +package com.sdds.playground.integrationtest.uistate + +/** + * UiState для BasicButton + */ +@Suppress("UndocumentedPublicProperty") +internal data class ButtonUiState( + val label: String, + val testTag: String, + val enabled: Boolean = true, + val loading: Boolean = false, +) diff --git a/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/uistate/ModalUiState.kt b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/uistate/ModalUiState.kt new file mode 100644 index 000000000..9054e03e6 --- /dev/null +++ b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/uistate/ModalUiState.kt @@ -0,0 +1,15 @@ +package com.sdds.playground.integrationtest.uistate + +import com.sdds.compose.uikit.ModalGravity + +/** + * UiState для Modal + */ +@Suppress("UndocumentedPublicProperty") +internal data class ModalUiState( + val useNativeBlackout: Boolean = true, + val hasClose: Boolean = false, + val hasDimBackground: Boolean = true, + val gravity: ModalGravity = ModalGravity.Center, + val edgeToEdge: Boolean = true, +) diff --git a/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/uistate/ScenarioCheckUiState.kt b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/uistate/ScenarioCheckUiState.kt new file mode 100644 index 000000000..2f225a8cd --- /dev/null +++ b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/uistate/ScenarioCheckUiState.kt @@ -0,0 +1,11 @@ +package com.sdds.playground.integrationtest.uistate + +/** + * UiState для проверки сценария + */ +@Suppress("UndocumentedPublicProperty") +internal data class ScenarioCheckUiState( + val title: String, + val passed: Boolean, + val testTag: String, +) diff --git a/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/uistate/TextFieldUiState.kt b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/uistate/TextFieldUiState.kt new file mode 100644 index 000000000..e590f5864 --- /dev/null +++ b/playground/sandbox-integration-test/src/main/kotlin/com/sdds/playground/integrationtest/uistate/TextFieldUiState.kt @@ -0,0 +1,13 @@ +package com.sdds.playground.integrationtest.uistate + +/** + * UiState для TextField + */ +@Suppress("UndocumentedPublicProperty") +internal data class TextFieldUiState( + val value: String, + val label: String, + val placeholder: String, + val testTag: String, + val isPassword: Boolean = false, +) diff --git a/playground/sandbox-integration-test/src/main/res/values/strings.xml b/playground/sandbox-integration-test/src/main/res/values/strings.xml new file mode 100644 index 000000000..405f63d6e --- /dev/null +++ b/playground/sandbox-integration-test/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Sandbox Integration Test + diff --git a/playground/settings.gradle.kts b/playground/settings.gradle.kts index 02117fd66..99ff4505a 100644 --- a/playground/settings.gradle.kts +++ b/playground/settings.gradle.kts @@ -21,4 +21,7 @@ dependencyResolutionManagement { rootProject.name = "playground" includeBuild("../build-system") includeBuild("../sdds-core") -include(":theme-builder",) +includeBuild("../integration-core") +includeBuild("../tokens") +include(":theme-builder") +include(":sandbox-integration-test")