Skip to content

Commit

Permalink
KOA-4919 Add compose BpkDialog (#1043)
Browse files Browse the repository at this point in the history
* KOA-4919 Add compose BpkDialog

* KOA-4919 Rename alert to warning

alert dialog is a loaded term on Android

* KOA-4919 Use material IconShape

* KOA-4919 Add none dialog type for story

* KOA-4919 Add tests and docs
  • Loading branch information
marianeum committed May 24, 2022
1 parent 2960305 commit e385523
Show file tree
Hide file tree
Showing 30 changed files with 786 additions and 28 deletions.
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ dependencies {
androidTestImplementation rootProject.ext.espressoContrib
androidTestImplementation rootProject.ext.testRules
androidTestImplementation rootProject.ext.coroutinesTest
androidTestImplementation rootProject.ext.composeTestJUnit
implementation project(':Backpack')
implementation project(':backpack-compose')
}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/**
* Backpack for Android - Skyscanner's Design System
*
* Copyright 2018 Skyscanner Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.skyscanner.backpack.compose.dialog

import android.view.ViewGroup
import android.view.ViewParent
import android.widget.FrameLayout
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.isDialog
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.ActivityTestRule
import net.skyscanner.backpack.BpkSnapshotTest
import net.skyscanner.backpack.BpkTestVariant
import net.skyscanner.backpack.demo.R
import net.skyscanner.backpack.demo.compose.BackpackPreview
import net.skyscanner.backpack.demo.compose.DestructiveDialogExample
import net.skyscanner.backpack.demo.compose.NoIconDialogExample
import net.skyscanner.backpack.demo.compose.SuccessOneButtonDialogExample
import net.skyscanner.backpack.demo.compose.SuccessThreeButtonsDialogExample
import net.skyscanner.backpack.demo.compose.SuccessTwoButtonsDialogExample
import net.skyscanner.backpack.demo.compose.WarningDialogExample
import org.junit.Assume
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.lang.reflect.Field

@RunWith(AndroidJUnit4::class)
class BpkDialogTest : BpkSnapshotTest() {

@get:Rule
var activityRule = ActivityTestRule(AppCompatActivity::class.java, true, false)

@get:Rule
val composeTestRule = AndroidComposeTestRule(activityRule) { it.activity }

@Before
fun setup() {
setDimensions(height = 600, width = 420)
}

@Test
fun successOneButton() = record {
SuccessOneButtonDialogExample()
}

@Test
fun successTwoButtons() {
assumeVariant(BpkTestVariant.Default, BpkTestVariant.DarkMode)
record {
SuccessTwoButtonsDialogExample()
}
}

@Test
fun successThreeButtons() {
assumeVariant(BpkTestVariant.Default, BpkTestVariant.DarkMode)
record {
SuccessThreeButtonsDialogExample()
}
}

@Test
fun warning() {
assumeVariant(BpkTestVariant.Default, BpkTestVariant.DarkMode)
record {
WarningDialogExample()
}
}

@Test
fun destructive() {
assumeVariant(BpkTestVariant.Default, BpkTestVariant.DarkMode)
record {
DestructiveDialogExample()
}
}

@Test
fun noIcon() {
assumeVariant(BpkTestVariant.Default, BpkTestVariant.DarkMode)
record {
NoIconDialogExample()
}
}

private fun record(content: @Composable () -> Unit) {
// we don't run Compose tests in Themed variant – Compose uses it own theming engine
Assume.assumeFalse(BpkTestVariant.current == BpkTestVariant.Themed)

val asyncScreenshot = prepareForAsyncTest()
with(activityRule.launchActivity(null)) {
runOnUiThread {
setContent {
BackpackPreview(
content = content,
)
}
}

composeTestRule.onNode(isDialog()).assertIsDisplayed()

runOnUiThread {
// This is not ideal but we need to see the background contrast as well
val viewRoot = getViewRoots().first { it.hasWindowFocus() }
val view = viewRoot.getChildAt(0)

viewRoot.removeView(view)

val wrapper = FrameLayout(this)
wrapper.layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT
)
wrapper.setPadding(20, 20, 20, 20)
wrapper.setBackgroundColor(getColor(R.color.bpkTextSecondary))
wrapper.addView(view)

setupView(wrapper)
asyncScreenshot.record(wrapper)
}
}
}

// we need this to be able to get the dialog root, rather than the window root
private fun getViewRoots(): List<ViewGroup> {
val viewRoots: MutableList<ViewGroup> = ArrayList()
try {
val windowManager: Any = Class.forName("android.view.WindowManagerGlobal")
.getMethod("getInstance").invoke(null) as Any
val rootsField: Field = windowManager.javaClass.getDeclaredField("mRoots")
rootsField.isAccessible = true
val stoppedField: Field = Class.forName("android.view.ViewRootImpl")
.getDeclaredField("mStopped")
stoppedField.isAccessible = true

val viewField: Field = Class.forName("android.view.ViewRootImpl")
.getDeclaredField("mView")
viewField.isAccessible = true
val viewParents = rootsField.get(windowManager) as List<ViewParent>
// Filter out inactive view roots
for (viewParent in viewParents) {
val stopped = stoppedField.get(viewParent) as Boolean
val view = viewField.get(viewParent) as ViewGroup?
if (!stopped && view != null) {
viewRoots.add(view)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
return viewRoots
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@

package net.skyscanner.backpack.docs

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.espresso.Espresso
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.matcher.ViewMatchers
Expand All @@ -28,6 +32,7 @@ import net.skyscanner.backpack.calendar.model.CalendarRange
import net.skyscanner.backpack.calendar2.CalendarSelection
import net.skyscanner.backpack.calendar2.data.CalendarDispatchers
import net.skyscanner.backpack.demo.R
import net.skyscanner.backpack.demo.compose.ShownDialog
import net.skyscanner.backpack.util.InternalBackpackApi
import org.threeten.bp.LocalDate

Expand All @@ -44,12 +49,12 @@ object DocsRegistry {
ComposeScreenshot("Button - Compose - Default", "default"),
ComposeScreenshot("Button - Compose - Large", "large"),
ComposeScreenshot("Button - Compose - Link", "link"),
ViewScreenshot("Calendar - Default", "range", ::setupCalendar),
ViewScreenshot("Calendar - Colored", "colored", ::setupCalendar),
ViewScreenshot("Calendar - Labeled", "labeled", ::setupCalendar),
ViewScreenshot("Calendar 2 - Pre-selected range", "range", ::setupCalendar2),
ViewScreenshot("Calendar 2 - Day colours", "colored", ::setupCalendar2),
ViewScreenshot("Calendar 2 - Day labels", "labeled", ::setupCalendar2),
ViewScreenshot("Calendar - Default", "range") { setupCalendar() },
ViewScreenshot("Calendar - Colored", "colored") { setupCalendar() },
ViewScreenshot("Calendar - Labeled", "labeled") { setupCalendar() },
ViewScreenshot("Calendar 2 - Pre-selected range", "range") { setupCalendar2() },
ViewScreenshot("Calendar 2 - Day colours", "colored") { setupCalendar2() },
ViewScreenshot("Calendar 2 - Day labels", "labeled") { setupCalendar2() },
ViewScreenshot("Card - View - Default", "default"),
ViewScreenshot("Card - View - Without padding", "without-padding"),
ViewScreenshot("Card - View - Selected", "selected"),
Expand All @@ -65,9 +70,12 @@ object DocsRegistry {
ViewScreenshot("Chip - With icon", "with-icon"),
ViewScreenshot("Checkbox - View", "default"),
ComposeScreenshot("Checkbox - Compose", "default"),
ViewScreenshot("Dialog - With call to action", "with-cta", ::setupDialog),
ViewScreenshot("Dialog - Delete confirmation", "delete-confirmation", ::setupDialog),
ViewScreenshot("Dialog - Flare", "with-flare", ::setupDialog),
ViewScreenshot("Dialog - View - With call to action", "with-cta") { setupDialog() },
ViewScreenshot("Dialog - View - Delete confirmation", "delete-confirmation") { setupDialog() },
ViewScreenshot("Dialog - View - Flare", "with-flare") { setupDialog() },
ComposeScreenshot("Dialog - Compose", "success") { setupComposeDialog(it, ShownDialog.SuccessThreeButtons) },
ComposeScreenshot("Dialog - Compose", "warning") { setupComposeDialog(it, ShownDialog.Warning) },
ComposeScreenshot("Dialog - Compose", "destructive") { setupComposeDialog(it, ShownDialog.Destructive) },
ViewScreenshot("Flare - Default", "default"),
ViewScreenshot("Flare - Pointing up", "pointing-up"),
ViewScreenshot("Flare - Pointer offset", "pointer-offset"),
Expand All @@ -76,8 +84,8 @@ object DocsRegistry {
ViewScreenshot("Horizontal Nav", "default"),
ViewScreenshot("Floating Action Button", "default"),
ViewScreenshot("Nav Bar - Default", "expanded"),
ViewScreenshot("Nav Bar - Default", "collapsed", ::setupNavBarCollapsed),
ViewScreenshot("Nav Bar - With Menu", "navigation", ::setupNavBarCollapsed),
ViewScreenshot("Nav Bar - Default", "collapsed") { setupNavBarCollapsed() },
ViewScreenshot("Nav Bar - With Menu", "navigation") { setupNavBarCollapsed() },
ViewScreenshot("Nudger", "all"),
ViewScreenshot("Overlay", "all"),
ViewScreenshot("Panel - View", "all"),
Expand All @@ -89,8 +97,8 @@ object DocsRegistry {
ViewScreenshot("Rating - Vertical", "vertical"),
ViewScreenshot("Rating - Pill", "pill"),
ViewScreenshot("Slider", "all"),
ViewScreenshot("Snackbar", "default", ::setupSnackbar),
ViewScreenshot("Snackbar", "icon", ::setupSnackbarIconAction),
ViewScreenshot("Snackbar", "default") { setupSnackbar() },
ViewScreenshot("Snackbar", "icon") { setupSnackbarIconAction() },
ViewScreenshot("Star Rating - Default", "default"),
ViewScreenshot("Star Rating Interactive", "default"),
ViewScreenshot("Switch - View", "default"),
Expand All @@ -107,7 +115,7 @@ object DocsRegistry {
ViewScreenshot("Spinner - Default", "default"),
ViewScreenshot("Spinner - Small", "small"),
// Leave toast last as it stays visible in the screen for a while
ViewScreenshot("Toast", "default", ::setupToast)
ViewScreenshot("Toast", "default") { setupToast() }
)

init {
Expand All @@ -118,14 +126,14 @@ object DocsRegistry {
fun ComposeScreenshot(
name: String,
screenshotName: String,
setup: (() -> Unit)? = null,
setup: ((ComposeTestRule) -> Unit)? = null,
): Array<Any?> =
arrayOf(name, screenshotName, "docs/compose", setup)

fun ViewScreenshot(
name: String,
screenshotName: String,
setup: (() -> Unit)? = null,
setup: ((ComposeTestRule) -> Unit)? = null,
): Array<Any?> =
arrayOf(name, screenshotName, "docs/view", setup)

Expand Down Expand Up @@ -170,6 +178,10 @@ private fun setupDialog() {
Thread.sleep(50)
}

private fun setupComposeDialog(testRule: ComposeTestRule, dialog: ShownDialog) {
testRule.onNodeWithText(dialog.buttonText).performClick().assertIsDisplayed()
}

private fun setupSnackbar() {
Espresso.onView(ViewMatchers.withText("Message (Duration Indefinite)"))
.perform(ViewActions.click())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ package net.skyscanner.backpack.docs

import android.content.Intent
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import net.skyscanner.backpack.demo.ComponentDetailActivity
Expand All @@ -34,7 +36,7 @@ open class GenerateScreenshots(
private val componentPath: String,
private val screenshotName: String,
private val path: String,
private val setup: (() -> Unit)?
private val setup: ((ComposeTestRule) -> Unit)?
) {

companion object {
Expand All @@ -46,6 +48,9 @@ open class GenerateScreenshots(
@get:Rule
var activityRule = ActivityTestRule(ComponentDetailActivity::class.java, true, false)

@get:Rule
val composeTestRule = AndroidComposeTestRule(activityRule) { it.activity }

private val screenshotFullName: String
get() {
val componentName = componentPath.split(" - ").first()
Expand Down Expand Up @@ -76,7 +81,7 @@ open class GenerateScreenshots(
intent.putExtra(ComponentDetailFragment.ARG_ITEM_ID, componentPath)
intent.putExtra(ComponentDetailFragment.AUTOMATION_MODE, true)
activityRule.launchActivity(intent)
setup?.invoke()
setup?.invoke(composeTestRule)
takeScreenshot(suffix)
activityRule.finishActivity()
}
Expand Down
Loading

0 comments on commit e385523

Please sign in to comment.