Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce support for rendering Compose hierarchies. #33

Merged
merged 1 commit into from
Aug 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ extensions.configure<ApiValidationExtension> {
// Only leaf project name is valid configuration, and every project must be individually ignored.
// See https://github.com/Kotlin/binary-compatibility-validator/issues/3
ignoredProjects = mutableSetOf(
"compose-tests",
"compose-unsupported-tests",
"sample",
"sample-compose"
)
Expand Down
13 changes: 10 additions & 3 deletions buildSrc/src/main/java/Dependencies.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
object Versions {
/**
* To change this in the IDE, use `systemProp.square.kotlinVersion=x.y.z` in your
* `~/.gradle/gradle.properties` file.
*/
* To change this in the IDE, use `systemProp.square.kotlinVersion=x.y.z` in your
* `~/.gradle/gradle.properties` file.
*/
val KotlinCompiler = System.getProperty("square.kotlinVersion") ?: "1.4.0"

/** Use a lower version of the stdlib so the library can be consumed by lower kotlin versions. */
val KotlinStdlib = System.getProperty("square.kotlinStdlibVersion") ?: "1.3.72"

const val Compose = "0.1.0-dev17"

// Allows using a different version of Compose to validate that we degrade gracefully on apps
// built with unsupported Compose versions.
const val OldCompose = "0.1.0-dev12"
const val OldComposeCompiler = "1.3.70-dev-withExperimentalGoogleExtensions-20200424"
}

object Dependencies {
Expand All @@ -29,6 +34,8 @@ object Dependencies {
object Compose {
const val Material = "androidx.compose.material:material:${Versions.Compose}"
const val Testing = "androidx.ui:ui-test:${Versions.Compose}"
const val OldMaterial = "androidx.ui:ui-material:${Versions.OldCompose}"
const val OldTesting = "androidx.ui:ui-test:${Versions.OldCompose}"
const val Tooling = "androidx.ui:ui-tooling:${Versions.Compose}"
}

Expand Down
5 changes: 5 additions & 0 deletions compose-tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# compose-tests

Contains the UI tests for Compose support. These can't live in the main radiography module since
they require the Compose compiler to be turned on. No non-test source code should be placed in this
module.
58 changes: 58 additions & 0 deletions compose-tests/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
id("com.android.library")
kotlin("android")
}

android {
compileSdkVersion(30)

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

defaultConfig {
minSdkVersion(21)
targetSdkVersion(28)
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

buildFeatures {
buildConfig = false
compose = true
}

composeOptions {
kotlinCompilerVersion = Versions.KotlinCompiler
kotlinCompilerExtensionVersion = Versions.Compose
}
}

tasks.withType<KotlinCompile> {
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs = listOf(
"-Xallow-jvm-ir-dependencies",
"-Xskip-prerelease-check",
"-Xopt-in=kotlin.RequiresOptIn"
)
}
}

dependencies {
// Don't use Versions.KotlinStdlib for Kotlin stdlib, since this module actually uses the Compose
// compiler and needs the latest stdlib.

androidTestImplementation(project(":radiography"))
androidTestImplementation(Dependencies.AppCompat)
androidTestImplementation(Dependencies.Compose.Material)
androidTestImplementation(Dependencies.Compose.Testing)
androidTestImplementation(Dependencies.Compose.Tooling)
androidTestImplementation(Dependencies.InstrumentationTests.Rules)
androidTestImplementation(Dependencies.InstrumentationTests.Runner)
androidTestImplementation(Dependencies.Truth)
}
1 change: 1 addition & 0 deletions compose-tests/src/androidTest/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<manifest package="com.squareup.radiography.test.compose" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
package radiography.test.compose

import android.view.ViewGroup.LayoutParams
import android.widget.TextView
import androidx.compose.foundation.Box
import androidx.compose.foundation.Text
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Stack
import androidx.compose.foundation.layout.size
import androidx.compose.material.Button
import androidx.compose.material.Checkbox
import androidx.compose.runtime.SlotTable
import androidx.compose.runtime.currentComposer
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.node.Ref
import androidx.compose.ui.platform.DensityAmbient
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.SemanticsProperties.AccessibilityLabel
import androidx.compose.ui.semantics.SemanticsProperties.AccessibilityValue
import androidx.compose.ui.semantics.SemanticsProperties.Disabled
import androidx.compose.ui.semantics.SemanticsProperties.Focused
import androidx.compose.ui.semantics.SemanticsProperties.Hidden
import androidx.compose.ui.semantics.SemanticsProperties.IsDialog
import androidx.compose.ui.semantics.SemanticsProperties.IsPopup
import androidx.compose.ui.semantics.SemanticsProperties.TestTag
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.viewinterop.AndroidView
import androidx.ui.test.createComposeRule
import androidx.ui.test.runOnIdle
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import radiography.Radiography
import radiography.ViewStateRenderers.DefaultsIncludingPii
import radiography.ViewStateRenderers.DefaultsNoPii
import radiography.compose.ComposeLayoutFilters.skipLayoutIdsFilter
import radiography.compose.ComposeLayoutFilters.skipTestTagsFilter
import radiography.compose.ComposeLayoutRenderers.StandardSemanticsRenderer
import radiography.compose.ComposeLayoutRenderers.composeTextRenderer
import radiography.compose.ExperimentalRadiographyComposeApi
import radiography.compose.scan

@OptIn(ExperimentalRadiographyComposeApi::class)
class ComposeUiTest {

@get:Rule
val composeRule = createComposeRule()

@Test fun when_includingPii_then_hierarchyContainsText() {
composeRule.setContent {
Text("FooBar")
}

runOnIdle {
val hierarchy = Radiography.scan(viewStateRenderers = DefaultsIncludingPii)
assertThat(hierarchy).contains("FooBar")
assertThat(hierarchy).contains("text-length:6")
}
}

@Test fun when_noPii_then_hierarchyExcludesText() {
composeRule.setContent {
Text("FooBar")
}

runOnIdle {
val hierarchy = Radiography.scan(viewStateRenderers = DefaultsNoPii)
assertThat(hierarchy).doesNotContain("FooBar")
assertThat(hierarchy).contains("text-length:6")
}
}

@Test fun viewSizeReported() {
composeRule.setContent {
val (width, height) = with(DensityAmbient.current) {
Pair(30.toDp(), 40.toDp())
}
Box(modifier = Modifier.size(width, height))
}

val hierarchy = runOnIdle {
Radiography.scan(viewStateRenderers = emptyList())
}

assertThat(hierarchy).contains("Box { 30×40px }")
}

@Test fun zeroSizeViewReported() {
composeRule.setContent {
Box()
}

val hierarchy = runOnIdle {
Radiography.scan(viewStateRenderers = emptyList())
}

assertThat(hierarchy).contains("Box { }")
}

@Test fun semanticsAreReported() {
composeRule.setContent {
Box(Modifier.semantics { set(TestTag, "test tag") })
Box(Modifier.semantics { set(AccessibilityLabel, "acc label") })
Box(Modifier.semantics { set(AccessibilityValue, "acc value") })
Box(Modifier.semantics { set(Disabled, Unit) })
Box(Modifier.semantics { set(Focused, true) })
Box(Modifier.semantics { set(Focused, false) })
Box(Modifier.semantics { set(Hidden, Unit) })
Box(Modifier.semantics { set(IsDialog, Unit) })
Box(Modifier.semantics { set(IsPopup, Unit) })
}

val hierarchy = runOnIdle {
Radiography.scan()
}

assertThat(hierarchy).contains("Box { test-tag:\"test tag\" }")
assertThat(hierarchy).contains("Box { label:\"acc label\" }")
assertThat(hierarchy).contains("Box { value:\"acc value\" }")
assertThat(hierarchy).contains("Box { DISABLED }")
assertThat(hierarchy).contains("Box { FOCUSED }")
assertThat(hierarchy).contains("Box { }")
assertThat(hierarchy).contains("Box { HIDDEN }")
assertThat(hierarchy).contains("Box { DIALOG }")
assertThat(hierarchy).contains("Box { POPUP }")
}

@Test fun checkableChecked() {
composeRule.setContent {
Checkbox(checked = true, onCheckedChange = {})
}

val hierarchy = runOnIdle {
Radiography.scan(viewStateRenderers = listOf(StandardSemanticsRenderer))
}

assertThat(hierarchy).contains("Checkbox")
assertThat(hierarchy).contains("Checked")
}

@Test fun textViewContents() {
composeRule.setContent {
Text("Baguette Avec Fromage")
}

val hierarchy = runOnIdle {
Radiography.scan(viewStateRenderers = listOf(composeTextRenderer(includeText = true)))
}

assertThat(hierarchy).contains("text-length:21")
assertThat(hierarchy).contains("text:\"Baguette Avec Fromage\"")
zach-klippenstein marked this conversation as resolved.
Show resolved Hide resolved
}

@Test fun textViewContentsEllipsized() {
composeRule.setContent {
Text("Baguette Avec Fromage")
}

val hierarchy = runOnIdle {
Radiography.scan(
viewStateRenderers = listOf(
composeTextRenderer(
includeText = true,
maxTextLength = 11
)
)
)
}

assertThat(hierarchy).contains("text-length:21")
assertThat(hierarchy).contains("text:\"Baguette A…\"")
}

@Test fun skipTestTags() {
composeRule.setContent {
Box {
Button(modifier = Modifier.testTag("42"), onClick = {}, content = {})
}
}

val hierarchy = runOnIdle {
Radiography.scan(viewFilter = skipTestTagsFilter("42"))
}

assertThat(hierarchy).contains("Box")
assertThat(hierarchy).doesNotContain("Button")
}

@Test fun skipLayoutIds() {
val layoutId = Any()

composeRule.setContent {
Box {
Button(modifier = Modifier.layoutId(layoutId), onClick = {}, content = {})
}
}

val hierarchy = runOnIdle {
Radiography.scan(viewFilter = skipLayoutIdsFilter { it === layoutId })
}

assertThat(hierarchy).contains("Box")
assertThat(hierarchy).doesNotContain("Button")
}

@Test fun nestedLayouts() {
val slotTable = Ref<SlotTable>()
composeRule.setContent {
slotTable.value = currentComposer.slotTable

Stack {
Box()
Column {
Box()
Box()
}
Row {
Box()
Box()
}
}
}

val hierarchy = runOnIdle {
slotTable.value!!.scan()
}

assertThat(hierarchy).contains(
"""
 Stack { }
 $BLANK +-Box { }
 $BLANK +-Column { }
 $BLANK | +-Box { }
 $BLANK | `-Box { }
 $BLANK `-Row { }
 $BLANK +-Box { }
 $BLANK `-Box { }
""".trimIndent()
)
}

@Test fun nestedViewsInsideLayouts() {
val slotTable = Ref<SlotTable>()
composeRule.setContent {
slotTable.value = currentComposer.slotTable

Box {
AndroidView(::TextView) {
it.layoutParams = LayoutParams(0, 0)
}
}
}

val hierarchy = runOnIdle {
slotTable.value!!.scan()
}

// The stuff below the AndroidView is implementation details, testing it is brittle and
// pointless.
assertThat(hierarchy).contains(
"""
 Box { }
 $BLANK `-AndroidView { }
""".trimIndent()
)
// But this view description should show up at some point.
assertThat(hierarchy).contains("`-TextView { 0×0px, text-length:0 }")
}

companion object {
private const val BLANK = '\u00a0'
}
}
1 change: 1 addition & 0 deletions compose-tests/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<manifest package="com.squareup.radiography.test.compose.empty" />
3 changes: 3 additions & 0 deletions compose-unsupported-tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# compose-unsupported-tests

Tests that the library degrades gracefully in an app that's using an unsupported Compose version.
Loading