├── compose-tests ├── src │ ├── main │ │ └── AndroidManifest.xml │ └── androidTest │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── radiography │ │ └── test │ │ └── compose │ │ ├── ComposeTestRules.kt │ │ └── ComposeViewTest.kt ├── README.md └── build.gradle.kts ├── sample ├── src │ ├── androidTest │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── squareup │ │ │ └── radiography │ │ │ └── sample │ │ │ └── SampleSmokeTest.kt │ └── main │ │ ├── res │ │ ├── drawable │ │ │ └── logo.png │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ └── layout │ │ │ └── main.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── squareup │ │ └── radiography │ │ └── sample │ │ └── MainActivity.kt └── build.gradle.kts ├── compose-unsupported-tests ├── src │ ├── main │ │ └── AndroidManifest.xml │ └── androidTest │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── radiography │ │ └── test │ │ └── compose │ │ └── ComposeUnsupportedTest.kt ├── README.md └── build.gradle.kts ├── assets ├── logo.psd ├── icon_32.png ├── icon_512.png ├── icon_64.png ├── logo_512.png ├── social_preview.png ├── sample_screenshot.png └── compose_sample_screenshot.png ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── sample-compose ├── src │ ├── main │ │ ├── res │ │ │ ├── drawable │ │ │ │ ├── logo_inner.webp │ │ │ │ └── logo_outer.webp │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ └── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ ├── java │ │ │ └── com │ │ │ │ └── squareup │ │ │ │ └── radiography │ │ │ │ └── sample │ │ │ │ └── compose │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── RadiographyLogo.kt │ │ │ │ └── ComposeSampleApp.kt │ │ └── AndroidManifest.xml │ └── androidTest │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── squareup │ │ └── radiography │ │ └── sample │ │ └── compose │ │ └── ComposeSampleUiTest.kt └── build.gradle.kts ├── .github ├── CODEOWNERS └── workflows │ ├── publish-release.yml │ └── android.yml ├── .editorconfig ├── radiography ├── src │ ├── main │ │ ├── java │ │ │ └── radiography │ │ │ │ ├── internal │ │ │ │ ├── Strings.kt │ │ │ │ ├── Semantics.kt │ │ │ │ ├── CompositionContexts.kt │ │ │ │ ├── RenderTreeString.kt │ │ │ │ ├── ComposeViews.kt │ │ │ │ └── ComposeLayoutInfo.kt │ │ │ │ ├── ExperimentalRadiographyComposeApi.kt │ │ │ │ ├── ViewFilter.kt │ │ │ │ ├── AttributeAppendable.kt │ │ │ │ ├── ScanScope.kt │ │ │ │ ├── ViewStateRenderer.kt │ │ │ │ ├── Views.kt │ │ │ │ ├── ScanScopes.kt │ │ │ │ ├── ViewFilters.kt │ │ │ │ ├── Radiography.kt │ │ │ │ ├── ScannableView.kt │ │ │ │ └── ViewStateRenderers.kt │ │ └── AndroidManifest.xml │ ├── androidTest │ │ ├── res │ │ │ └── layout │ │ │ │ └── test.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── radiography │ │ │ └── test │ │ │ ├── utilities │ │ │ └── TestActivity.kt │ │ │ └── RadiographyUiTest.kt │ └── test │ │ └── java │ │ └── radiography │ │ ├── ViewFiltersTest.kt │ │ ├── JavaInteropTest.java │ │ ├── ViewStateRenderersTest.kt │ │ ├── AndroidViewTest.kt │ │ ├── RenderTreeStringTest.kt │ │ └── RadiographyTest.kt ├── gradle.properties ├── build.gradle.kts └── api │ └── radiography.api ├── .gitignore ├── CONTRIBUTING.md ├── settings.gradle.kts ├── gradle.properties ├── RELEASING.md ├── gradlew.bat ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── gradlew ├── LICENSE └── README.md /compose-tests/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sample/src/androidTest/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /compose-unsupported-tests/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block/radiography/HEAD/assets/logo.psd -------------------------------------------------------------------------------- /assets/icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block/radiography/HEAD/assets/icon_32.png -------------------------------------------------------------------------------- /assets/icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block/radiography/HEAD/assets/icon_512.png -------------------------------------------------------------------------------- /assets/icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block/radiography/HEAD/assets/icon_64.png -------------------------------------------------------------------------------- /assets/logo_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block/radiography/HEAD/assets/logo_512.png -------------------------------------------------------------------------------- /assets/social_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block/radiography/HEAD/assets/social_preview.png -------------------------------------------------------------------------------- /assets/sample_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block/radiography/HEAD/assets/sample_screenshot.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block/radiography/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /assets/compose_sample_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block/radiography/HEAD/assets/compose_sample_screenshot.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block/radiography/HEAD/sample/src/main/res/drawable/logo.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block/radiography/HEAD/sample/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block/radiography/HEAD/sample/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block/radiography/HEAD/sample/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block/radiography/HEAD/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block/radiography/HEAD/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample-compose/src/main/res/drawable/logo_inner.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block/radiography/HEAD/sample-compose/src/main/res/drawable/logo_inner.webp -------------------------------------------------------------------------------- /sample-compose/src/main/res/drawable/logo_outer.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block/radiography/HEAD/sample-compose/src/main/res/drawable/logo_outer.webp -------------------------------------------------------------------------------- /sample-compose/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block/radiography/HEAD/sample-compose/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample-compose/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block/radiography/HEAD/sample-compose/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample-compose/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block/radiography/HEAD/sample-compose/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample-compose/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block/radiography/HEAD/sample-compose/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample-compose/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block/radiography/HEAD/sample-compose/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /compose-unsupported-tests/README.md: -------------------------------------------------------------------------------- 1 | # compose-unsupported-tests 2 | 3 | Tests that the library degrades gracefully in an app that's using an unsupported Compose version. 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in 2 | # the repo, unless a later match takes precedence. 3 | * @square/mobile-foundation-android @pyricau 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Make ktlint respect our style guide's indentation. 2 | [*.{kt,kts}] 3 | # See https://github.com/square/radiography/issues/67. 4 | # indent_size = 2 5 | # continuation_indent_size = 4 6 | indent_size = unset 7 | -------------------------------------------------------------------------------- /compose-tests/src/androidTest/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /sample-compose/src/androidTest/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /compose-tests/README.md: -------------------------------------------------------------------------------- 1 | # compose-tests 2 | 3 | Contains the UI tests for Compose support. These can't live in the main radiography module since 4 | they require the Compose compiler to be turned on. No non-test source code should be placed in this 5 | module. 6 | -------------------------------------------------------------------------------- /compose-unsupported-tests/src/androidTest/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /radiography/src/main/java/radiography/internal/Strings.kt: -------------------------------------------------------------------------------- 1 | package radiography.internal 2 | 3 | internal fun CharSequence.ellipsize(maxLength: Int): CharSequence = 4 | if (length > maxLength) "${subSequence(0, maxLength - 1)}…" else this 5 | 6 | internal fun formatPixelDimensions( 7 | width: Int, 8 | height: Int 9 | ): String = "$width×${height}px" 10 | -------------------------------------------------------------------------------- /radiography/src/main/java/radiography/ExperimentalRadiographyComposeApi.kt: -------------------------------------------------------------------------------- 1 | package radiography 2 | 3 | @RequiresOptIn( 4 | message = "This API is experimental, may only work with a specific version of Compose, " + 5 | "and may change or break at any time. Use with caution." 6 | ) 7 | @Retention(AnnotationRetention.BINARY) 8 | public annotation class ExperimentalRadiographyComposeApi 9 | -------------------------------------------------------------------------------- /radiography/src/main/java/radiography/ViewFilter.kt: -------------------------------------------------------------------------------- 1 | package radiography 2 | 3 | /** 4 | * Used to filter out views from the output of [Radiography.scan]. 5 | */ 6 | public fun interface ViewFilter { 7 | /** 8 | * @return true to keep the view in the output of [Radiography.scan], false to filter it out. 9 | */ 10 | public fun matches(view: ScannableView): Boolean 11 | } 12 | -------------------------------------------------------------------------------- /radiography/src/main/java/radiography/AttributeAppendable.kt: -------------------------------------------------------------------------------- 1 | package radiography 2 | 3 | public class AttributeAppendable( 4 | private val stringBuilder: StringBuilder 5 | ) { 6 | 7 | private var first = true 8 | 9 | public fun append(attribute: CharSequence?) { 10 | if (attribute == null) { 11 | return 12 | } 13 | if (first) { 14 | first = false 15 | } else { 16 | stringBuilder.append(", ") 17 | } 18 | stringBuilder.append(attribute) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /radiography/src/androidTest/res/layout/test.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /radiography/src/main/java/radiography/ScanScope.kt: -------------------------------------------------------------------------------- 1 | package radiography 2 | 3 | /** 4 | * Defines the scope of the scan output by returning a list of root [ScannableView]s that will to 5 | * scan. 6 | * 7 | * Some commons scopes are: 8 | * - [ScanScopes.AllWindowsScope] 9 | * - [ScanScopes.FocusedWindowScope] 10 | * - [ScanScopes.singleViewScope] 11 | */ 12 | public fun interface ScanScope { 13 | 14 | /** Returns the [ScannableView]s that scanning should start from. */ 15 | public fun findRoots(): List 16 | } 17 | -------------------------------------------------------------------------------- /radiography/src/androidTest/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /sample-compose/src/main/java/com/squareup/radiography/sample/compose/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.radiography.sample.compose 2 | 3 | import android.os.Bundle 4 | import androidx.activity.compose.setContent 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.compose.runtime.currentComposer 7 | 8 | class MainActivity : AppCompatActivity() { 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | setContent { 12 | currentComposer.collectParameterInformation() 13 | ComposeSampleApp() 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | 3 | # macOS 4 | .DS_Store 5 | 6 | # Compiled class file 7 | *.class 8 | 9 | # Log file 10 | *.log 11 | 12 | # BlueJ files 13 | *.ctxt 14 | 15 | # Mobile Tools for Java (J2ME) 16 | .mtj.tmp/ 17 | 18 | # Package Files # 19 | *.jar 20 | *.war 21 | *.nar 22 | *.ear 23 | *.zip 24 | *.tar.gz 25 | *.rar 26 | 27 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 28 | hs_err_pid* 29 | 30 | # Gradle 31 | out/ 32 | .gradle/ 33 | build/ 34 | local.properties 35 | .gradletasknamecache 36 | 37 | 38 | # Intellij 39 | *.iml 40 | .idea/ 41 | captures/ 42 | -------------------------------------------------------------------------------- /radiography/src/main/java/radiography/ViewStateRenderer.kt: -------------------------------------------------------------------------------- 1 | package radiography 2 | 3 | /** 4 | * Renders extra attributes for specifics types in the output of [Radiography.scan]. 5 | * 6 | * Call [androidViewStateRendererFor][ViewStateRenderers.androidViewStateRendererFor] to create 7 | * instances for specific [View][android.view.View] types: 8 | * ``` 9 | * val myRenderer: StateRenderer = androidViewStateRendererFor { 10 | * append(it.customAttributeValue) 11 | * } 12 | * ``` 13 | */ 14 | public fun interface ViewStateRenderer { 15 | public fun AttributeAppendable.render(view: ScannableView) 16 | } 17 | -------------------------------------------------------------------------------- /sample-compose/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /radiography/src/main/java/radiography/internal/Semantics.kt: -------------------------------------------------------------------------------- 1 | package radiography.internal 2 | 3 | import androidx.compose.ui.semantics.SemanticsProperties 4 | import radiography.ScannableView.ComposeView 5 | import radiography.ExperimentalRadiographyComposeApi 6 | 7 | /** Returns all tag strings set on the composable via `Modifier.testTag`. */ 8 | @OptIn(ExperimentalRadiographyComposeApi::class) 9 | internal fun ComposeView.findTestTags(): Sequence { 10 | return semanticsConfigurations 11 | .asSequence() 12 | .flatMap { semantics -> 13 | semantics.filter { it.key == SemanticsProperties.TestTag } 14 | } 15 | .mapNotNull { it.value as? String } 16 | } 17 | -------------------------------------------------------------------------------- /radiography/src/main/java/radiography/Views.kt: -------------------------------------------------------------------------------- 1 | package radiography 2 | 3 | import android.view.View 4 | import radiography.ScanScopes.singleViewScope 5 | import radiography.ViewStateRenderers.DefaultsNoPii 6 | 7 | /** 8 | * Extension function for [Radiography.scan] when scanning starts from a specific view. 9 | * @see Radiography.scan 10 | */ 11 | @JvmSynthetic 12 | public fun View?.scan( 13 | viewStateRenderers: List = DefaultsNoPii, 14 | viewFilter: ViewFilter = ViewFilters.NoFilter 15 | ): String { 16 | return if (this == null) { 17 | "null" 18 | } else { 19 | Radiography.scan(singleViewScope(this), viewStateRenderers, viewFilter) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /radiography/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /radiography/gradle.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2020 Square Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | POM_ARTIFACT_ID=radiography 17 | POM_NAME=Radiography 18 | POM_PACKAGING=aar 19 | -------------------------------------------------------------------------------- /sample/src/androidTest/java/com/squareup/radiography/sample/SampleSmokeTest.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.radiography.sample 2 | 3 | import androidx.test.espresso.Espresso.onView 4 | import androidx.test.espresso.assertion.ViewAssertions.matches 5 | import androidx.test.espresso.matcher.ViewMatchers.isDisplayed 6 | import androidx.test.espresso.matcher.ViewMatchers.withText 7 | import androidx.test.ext.junit.rules.ActivityScenarioRule 8 | import org.junit.Rule 9 | import org.junit.Test 10 | 11 | class SampleSmokeTest { 12 | 13 | @get:Rule 14 | val activityRule = ActivityScenarioRule(MainActivity::class.java) 15 | 16 | @Test fun displaysInitialScreen() { 17 | onView(withText("Remember me")).check(matches(isDisplayed())) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | If you would like to contribute code to Radiography you can do so through GitHub by 5 | forking the repository and sending a pull request. 6 | 7 | When submitting code, please make every effort to follow existing conventions 8 | and style in order to keep the code as readable as possible. Please also make 9 | sure your code compiles by running `./gradlew clean build`. If you're using IntelliJ IDEA, 10 | we use [Square's code style definitions][2]. 11 | 12 | Before your code can be accepted into the project you must also sign the 13 | [Individual Contributor License Agreement (CLA)][1]. 14 | 15 | [1]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1 16 | [2]: https://github.com/square/java-code-styles 17 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | rootProject.name = "radiography" 17 | 18 | include( 19 | ":compose-tests", 20 | ":compose-unsupported-tests", 21 | ":radiography", 22 | ":sample", 23 | ":sample-compose" 24 | ) 25 | -------------------------------------------------------------------------------- /radiography/src/androidTest/java/radiography/test/utilities/TestActivity.kt: -------------------------------------------------------------------------------- 1 | package radiography.test.utilities 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.widget.TextView 7 | import androidx.test.platform.app.InstrumentationRegistry 8 | import com.squareup.radiography.test.R 9 | 10 | class TestActivity : Activity() { 11 | 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | setContentView(R.layout.test) 15 | 16 | intent.getStringExtra("textViewText")?.let { text -> 17 | findViewById(R.id.intent_text).text = text 18 | } 19 | } 20 | 21 | companion object { 22 | val intent = Intent( 23 | InstrumentationRegistry.getInstrumentation().targetContext, 24 | TestActivity::class.java 25 | ) 26 | 27 | fun Intent.withTextViewText(text: String): Intent = apply { 28 | putExtra("textViewText", text) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /compose-tests/src/androidTest/java/radiography/test/compose/ComposeTestRules.kt: -------------------------------------------------------------------------------- 1 | package radiography.test.compose 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.currentComposer 6 | import androidx.compose.ui.test.junit4.ComposeContentTestRule 7 | 8 | /** 9 | * Calls [ComposeContentTestRule.setContent] but wraps [content] with a [Box] to emulate how real 10 | * apps usually have one or more root-level composables. Compose UI tests should use this instead 11 | * of `setContent` to insulate them from the implementation details of the internal composable graph 12 | * that `setContent` creates behind the scenes and might change between releases. 13 | * 14 | * E.g. If you use `setContent` directly, root children will all show up as `Providers` in alpha04. 15 | */ 16 | fun ComposeContentTestRule.setContentWithExplicitRoot(content: @Composable () -> Unit) { 17 | setContent { 18 | currentComposer.collectParameterInformation() 19 | Box { 20 | content() 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /compose-unsupported-tests/src/androidTest/java/radiography/test/compose/ComposeUnsupportedTest.kt: -------------------------------------------------------------------------------- 1 | package radiography.test.compose 2 | 3 | import androidx.compose.foundation.text.BasicText 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import com.google.common.truth.Truth.assertThat 6 | import org.junit.Rule 7 | import org.junit.Test 8 | import radiography.Radiography 9 | import radiography.ViewStateRenderers.DefaultsIncludingPii 10 | 11 | class ComposeUnsupportedTest { 12 | 13 | @get:Rule 14 | val composeRule = createComposeRule() 15 | 16 | @Test fun when_composeVersionNotSupported_then_failsGracefully() { 17 | composeRule.setContent { 18 | BasicText("FooBar") 19 | } 20 | 21 | composeRule.runOnIdle { 22 | val hierarchy = Radiography.scan(viewStateRenderers = DefaultsIncludingPii) 23 | assertThat(hierarchy).doesNotContain("FooBar") 24 | assertThat(hierarchy).contains( 25 | "Composition was found, but either Compose Tooling artifact is missing or the Compose " + 26 | "version is not supported. Please ensure you have a dependency on " + 27 | "androidx.ui:ui-tooling or check https://github.com/square/radiography for a new " + 28 | "release." 29 | ) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /sample-compose/src/androidTest/java/com/squareup/radiography/sample/compose/ComposeSampleUiTest.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.radiography.sample.compose 2 | 3 | import androidx.compose.ui.test.assert 4 | import androidx.compose.ui.test.assertIsDisplayed 5 | import androidx.compose.ui.test.hasText 6 | import androidx.compose.ui.test.junit4.createAndroidComposeRule 7 | import androidx.compose.ui.test.onNodeWithTag 8 | import androidx.compose.ui.test.onNodeWithText 9 | import androidx.compose.ui.test.performTextReplacement 10 | import org.junit.Rule 11 | import org.junit.Test 12 | 13 | class ComposeSampleUiTest { 14 | 15 | @get:Rule 16 | val composeRule = createAndroidComposeRule() 17 | 18 | @Test fun launches() { 19 | composeRule.onNodeWithText("Remember me").assertIsDisplayed() 20 | } 21 | 22 | @Test fun displaysHierarchyInline() { 23 | composeRule.onNodeWithTag(LIVE_HIERARCHY_TEST_TAG) 24 | .assert(hasText("Remember me", substring = true)) 25 | .assert(hasText("toggle-state:Off", substring = true)) 26 | 27 | composeRule.onNodeWithTag(TEXT_FIELD_TEST_TAG) 28 | .performTextReplacement("foobar") 29 | 30 | composeRule.onNodeWithTag(LIVE_HIERARCHY_TEST_TAG) 31 | .assert(hasText("Remember me", substring = true)) 32 | .assert(hasText("foobar", substring = true)) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /radiography/src/test/java/radiography/ViewFiltersTest.kt: -------------------------------------------------------------------------------- 1 | package radiography 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import android.widget.EditText 6 | import android.widget.TextView 7 | import com.google.common.truth.Truth.assertThat 8 | import org.junit.Test 9 | import org.junit.runner.RunWith 10 | import org.robolectric.RobolectricTestRunner 11 | import org.robolectric.RuntimeEnvironment 12 | import org.robolectric.annotation.Config 13 | import radiography.ScannableView.AndroidView 14 | import radiography.ViewFilters.androidViewFilterFor 15 | 16 | @RunWith(RobolectricTestRunner::class) 17 | @Config(manifest = Config.NONE) 18 | internal class ViewFiltersTest { 19 | @Suppress("DEPRECATION") 20 | private val context: Context = RuntimeEnvironment.application 21 | 22 | @Test fun `androidViewFilterFor ignores different view types`() { 23 | var filterRan = false 24 | val filter = androidViewFilterFor { 25 | filterRan = true 26 | false 27 | } 28 | 29 | filter.matches(AndroidView(View(context))) 30 | 31 | assertThat(filterRan).isFalse() 32 | } 33 | 34 | @Test fun `androidViewFilterFor accepts view subtypes`() { 35 | var filterRan = false 36 | val filter = androidViewFilterFor { 37 | filterRan = true 38 | false 39 | } 40 | 41 | filter.matches(AndroidView(EditText(context))) 42 | 43 | assertThat(filterRan).isTrue() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2020 Square Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | org.gradle.jvmargs='-Dfile.encoding=UTF-8' 17 | android.useAndroidX=true 18 | 19 | # Required to publish to Nexus (see https://github.com/gradle/gradle/issues/11308) 20 | systemProp.org.gradle.internal.publish.checksums.insecure=true 21 | 22 | GROUP=com.squareup.radiography 23 | VERSION_NAME=2.8-SNAPSHOT 24 | 25 | POM_DESCRIPTION=Pretty printing of view hierarchies 26 | 27 | POM_URL=https://github.com/square/radiography/ 28 | POM_SCM_URL=https://github.com/square/radiography/ 29 | POM_SCM_CONNECTION=scm:git:git://github.com/square/radiography.git 30 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/square/radiography.git 31 | 32 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 33 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 34 | POM_LICENCE_DIST=repo 35 | 36 | POM_DEVELOPER_ID=square 37 | POM_DEVELOPER_NAME=Square, Inc. 38 | -------------------------------------------------------------------------------- /compose-tests/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | id("com.android.library") 5 | kotlin("android") 6 | } 7 | 8 | android { 9 | compileSdk = 34 10 | 11 | compileOptions { 12 | sourceCompatibility = JavaVersion.VERSION_1_8 13 | targetCompatibility = JavaVersion.VERSION_1_8 14 | } 15 | 16 | defaultConfig { 17 | minSdk = 21 18 | targetSdk = 34 19 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 20 | } 21 | 22 | buildFeatures { 23 | buildConfig = false 24 | compose = true 25 | } 26 | 27 | composeOptions { 28 | kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() 29 | } 30 | 31 | packaging { 32 | resources.excludes += listOf( 33 | "META-INF/AL2.0", 34 | "META-INF/LGPL2.1", 35 | ) 36 | } 37 | namespace = "com.squareup.radiography.test.compose.empty" 38 | testNamespace = "com.squareup.radiography.test.compose" 39 | } 40 | 41 | tasks.withType { 42 | kotlinOptions { 43 | jvmTarget = "1.8" 44 | freeCompilerArgs += listOf( 45 | "-Xopt-in=kotlin.RequiresOptIn" 46 | ) 47 | } 48 | } 49 | 50 | dependencies { 51 | androidTestImplementation(project(":radiography")) 52 | androidTestImplementation(libs.appCompat) 53 | androidTestImplementation(libs.compose.material) 54 | androidTestImplementation(libs.compose.testing) 55 | androidTestImplementation(libs.compose.tooling) 56 | androidTestImplementation(libs.test.androidx.rules) 57 | androidTestImplementation(libs.test.androidx.runner) 58 | androidTestImplementation(libs.test.truth) 59 | } 60 | -------------------------------------------------------------------------------- /sample-compose/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | id("com.android.application") 5 | kotlin("android") 6 | } 7 | 8 | android { 9 | compileSdk = 34 10 | 11 | compileOptions { 12 | sourceCompatibility = JavaVersion.VERSION_1_8 13 | targetCompatibility = JavaVersion.VERSION_1_8 14 | } 15 | 16 | defaultConfig { 17 | minSdk = 21 18 | targetSdk = 34 19 | applicationId = "com.squareup.radiography.sample.compose" 20 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 21 | } 22 | 23 | buildFeatures { 24 | compose = true 25 | } 26 | 27 | composeOptions { 28 | kotlinCompilerExtensionVersion = libs.versions.compose.sample.compiler.get() 29 | } 30 | 31 | packaging { 32 | resources.excludes += listOf( 33 | "META-INF/AL2.0", 34 | "META-INF/LGPL2.1" 35 | ) 36 | } 37 | namespace = "com.squareup.radiography.sample.compose" 38 | testNamespace = "com.squareup.radiography.sample.compose.test" 39 | } 40 | 41 | tasks.withType { 42 | kotlinOptions { 43 | jvmTarget = "1.8" 44 | freeCompilerArgs += listOf( 45 | "-Xopt-in=kotlin.RequiresOptIn" 46 | ) 47 | } 48 | } 49 | 50 | dependencies { 51 | implementation(project(":radiography")) 52 | implementation(libs.appCompat) 53 | implementation(libs.compose.sample.activity) 54 | implementation(libs.compose.sample.material) 55 | implementation(libs.compose.sample.tooling) 56 | 57 | androidTestImplementation(libs.compose.sample.testing) 58 | androidTestImplementation(libs.test.androidx.rules) 59 | androidTestImplementation(libs.test.androidx.runner) 60 | } 61 | -------------------------------------------------------------------------------- /compose-unsupported-tests/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | id("com.android.library") 5 | kotlin("android") 6 | } 7 | 8 | android { 9 | compileSdk = 34 10 | 11 | compileOptions { 12 | sourceCompatibility = JavaVersion.VERSION_1_8 13 | targetCompatibility = JavaVersion.VERSION_1_8 14 | } 15 | 16 | defaultConfig { 17 | minSdk = 21 18 | targetSdk = 34 19 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 20 | } 21 | 22 | buildFeatures { 23 | buildConfig = false 24 | compose = true 25 | } 26 | 27 | composeOptions { 28 | kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() 29 | } 30 | 31 | packaging { 32 | resources.excludes += listOf( 33 | "META-INF/AL2.0", 34 | "META-INF/LGPL2.1" 35 | ) 36 | } 37 | namespace = "com.squareup.radiography.test.compose.unsupported.empty" 38 | testNamespace = "com.squareup.radiography.test.compose.unsupported" 39 | } 40 | 41 | tasks.withType { 42 | kotlinOptions { 43 | jvmTarget = "1.8" 44 | freeCompilerArgs += listOf( 45 | "-Xopt-in=kotlin.RequiresOptIn" 46 | ) 47 | } 48 | } 49 | 50 | dependencies { 51 | androidTestImplementation(project(":radiography")) 52 | androidTestImplementation(libs.appCompat) 53 | androidTestImplementation(libs.compose.old.activity) 54 | androidTestImplementation(libs.compose.old.material) 55 | androidTestImplementation(libs.compose.old.testing) 56 | androidTestImplementation(libs.test.androidx.rules) 57 | androidTestImplementation(libs.test.junit) 58 | androidTestImplementation(libs.test.androidx.runner) 59 | androidTestImplementation(libs.test.truth) 60 | } 61 | -------------------------------------------------------------------------------- /sample/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id("com.android.application") 19 | kotlin("android") 20 | } 21 | 22 | android { 23 | compileSdk = 34 24 | 25 | compileOptions { 26 | sourceCompatibility = JavaVersion.VERSION_1_8 27 | targetCompatibility = JavaVersion.VERSION_1_8 28 | } 29 | 30 | defaultConfig { 31 | minSdk = 21 32 | targetSdk = 34 33 | applicationId = "com.squareup.radiography.sample" 34 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 35 | } 36 | namespace = "com.squareup.radiography.sample" 37 | testNamespace = "com.squareup.radiography.sample.test" 38 | } 39 | 40 | dependencies { 41 | implementation(project(":radiography")) 42 | implementation(libs.appCompat) 43 | implementation(libs.constraintLayout) 44 | 45 | androidTestImplementation(libs.test.androidx.core) 46 | androidTestImplementation(libs.test.androidx.espresso) 47 | androidTestImplementation(libs.test.androidx.rules) 48 | androidTestImplementation(libs.test.androidx.junit) 49 | androidTestImplementation(libs.test.androidx.runner) 50 | androidTestImplementation(libs.test.truth) 51 | } 52 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | ## Set up GitHub CLI 4 | 5 | Install GitHub CLI 6 | 7 | ```bash 8 | brew install gh 9 | ``` 10 | 11 | ## Creating the release 12 | 13 | * Update the changelog 14 | ```bash 15 | mate CHANGELOG.md 16 | ``` 17 | 18 | * Create a local release branch from `main` and update `VERSION_NAME` in `gradle.properties` (removing `-SNAPSHOT`) and the README, then run the publish workflow and finish the release: 19 | 20 | ```bash 21 | git checkout main && \ 22 | git pull && \ 23 | git checkout -b release_{NEW_VERSION} && \ 24 | sed -i '' 's/VERSION_NAME=.*-SNAPSHOT/VERSION_NAME={NEW_VERSION}/' gradle.properties 25 | sed -i '' "s/com.squareup.radiography:radiography:.*'/com.squareup.radiography:radiography:{NEW_VERSION}'/" README.md && \ 26 | git commit -am "Prepare {NEW_VERSION} release" && \ 27 | ./gradlew clean && \ 28 | ./gradlew build && \ 29 | ./gradlew connectedCheck && \ 30 | git tag v{NEW_VERSION} && \ 31 | git push origin v{NEW_VERSION} && \ 32 | gh workflow run publish-release.yml --ref v{NEW_VERSION} && \ 33 | gh run list --workflow=publish-release.yml --branch v{NEW_VERSION} --json databaseId --jq ".[].databaseId" | xargs -I{} gh run watch {} --exit-status && \ 34 | git checkout main && \ 35 | git pull && \ 36 | git merge --no-ff --no-edit release_{NEW_VERSION} && \ 37 | sed -i '' 's/VERSION_NAME={NEW_VERSION}/VERSION_NAME={NEXT_VERSION}-SNAPSHOT/' gradle.properties && \ 38 | git commit -am "Prepare for next development iteration" && \ 39 | git push && \ 40 | gh release create v{NEW_VERSION} --title v{NEW_VERSION} --notes 'See [Change Log](https://github.com/square/radiography/blob/main/CHANGELOG.md)' 41 | ``` 42 | 43 | * Wait for the release to be available [on Maven Central](https://repo1.maven.org/maven2/com/squareup/radiography/radiography/). 44 | * Tell your friends, update all of your apps, and tweet the new release. 45 | As a nice extra touch, mention external contributions. 46 | -------------------------------------------------------------------------------- /radiography/src/main/java/radiography/internal/CompositionContexts.kt: -------------------------------------------------------------------------------- 1 | package radiography.internal 2 | 3 | import androidx.compose.runtime.Composer 4 | import androidx.compose.runtime.CompositionContext 5 | import androidx.compose.ui.tooling.data.Group 6 | import androidx.compose.ui.tooling.data.UiToolingDataApi 7 | import kotlin.LazyThreadSafetyMode.PUBLICATION 8 | 9 | private val REFLECTION_CONSTANTS by lazy(PUBLICATION) { 10 | try { 11 | object { 12 | val CompositionContextHolderClass = 13 | Class.forName("androidx.compose.runtime.ComposerImpl\$CompositionContextHolder") 14 | val CompositionContextImplClass = 15 | Class.forName("androidx.compose.runtime.ComposerImpl\$CompositionContextImpl") 16 | val CompositionContextHolderRefField = 17 | CompositionContextHolderClass.getDeclaredField("ref") 18 | .apply { isAccessible = true } 19 | val CompositionContextImplComposersField = 20 | CompositionContextImplClass.getDeclaredField("composers") 21 | .apply { isAccessible = true } 22 | } 23 | } catch (e: Throwable) { 24 | null 25 | } 26 | } 27 | 28 | @OptIn(UiToolingDataApi::class) 29 | internal fun Group.getCompositionContexts(): Sequence { 30 | return REFLECTION_CONSTANTS?.run { 31 | data.asSequence() 32 | .filter { it != null && it::class.java == CompositionContextHolderClass } 33 | .mapNotNull { holder -> holder.tryGetCompositionContext() } 34 | } ?: emptySequence() 35 | } 36 | 37 | @Suppress("UNCHECKED_CAST") 38 | internal fun CompositionContext.tryGetComposers(): Iterable { 39 | return REFLECTION_CONSTANTS?.let { 40 | if (!it.CompositionContextImplClass.isInstance(this)) return emptyList() 41 | it.CompositionContextImplComposersField.get(this) as? Iterable 42 | } ?: emptyList() 43 | } 44 | 45 | private fun Any?.tryGetCompositionContext() = REFLECTION_CONSTANTS?.let { 46 | it.CompositionContextHolderRefField.get(this) as? CompositionContext 47 | } 48 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | publish-release: 8 | runs-on: macos-latest 9 | if: github.repository == 'square/radiography' 10 | timeout-minutes: 35 11 | 12 | steps: 13 | - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 14 | - uses: gradle/wrapper-validation-action@699bb18358f12c5b78b37bb0111d3a0e2276e0e2 # v2 15 | 16 | # checkVersionIsNotSnapshot doesn't exist in Rick's horde. Pretty trivial to re-implement, 17 | # but also not crucial while this is not part of CI (unlikely ever to happen). And in the meantime, 18 | # quite handy to leave this check out so that we can use this workflow to publish 19 | # SNAPSHOTs. 20 | # 21 | # OTOH, if we get around to making a Publish Snapshot alternative to this workflow 22 | # and tie it to CI, having a checkVersionIsSnapshot task will be very important for that. 23 | # - name: Ensure this isn't a -SNAPSHOT version 24 | # uses: rickbusarow/actions/gradle-task@bf0940965387f10bcb8b6699a79499d18167dfbe # v1 25 | # with: 26 | # task: checkVersionIsNotSnapshot 27 | 28 | - name: Assemble 29 | uses: rickbusarow/actions/gradle-task@bf0940965387f10bcb8b6699a79499d18167dfbe # v1 30 | with: 31 | task: assemble 32 | 33 | # artifactsCheck doesn't exist in Rick's horde, so we continue to rely on manual discipline 34 | # to run tests locally before publishing. Could copy this from workflow, but it's tied 35 | # to the build-logic system there so doing so would be Actual Work. 36 | # - name: Check 37 | # uses: rickbusarow/actions/gradle-task@bf0940965387f10bcb8b6699a79499d18167dfbe # v1 38 | # with: 39 | # task: check -x artifactsCheck 40 | 41 | - name: Publish Release 42 | uses: rickbusarow/actions/gradle-task@bf0940965387f10bcb8b6699a79499d18167dfbe # v1 43 | with: 44 | task: publish 45 | env: 46 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} 47 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} 48 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.ARTIFACT_SIGNING_PRIVATE_KEY }} 49 | -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on : 4 | pull_request : 5 | merge_group : 6 | 7 | jobs: 8 | build: 9 | strategy: 10 | fail-fast: false 11 | runs-on : workflow-kotlin-test-runner-ubuntu-4core 12 | steps: 13 | - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 14 | - name: set up JDK 17 15 | uses: actions/setup-java@v1 16 | with: 17 | java-version: 17 18 | - name: Build with Gradle 19 | run: ./gradlew build 20 | 21 | instrumentation-tests: 22 | name: Instrumentation tests 23 | runs-on : workflow-kotlin-test-runner-ubuntu-4core 24 | timeout-minutes: 30 25 | strategy: 26 | # Allow tests to continue on other devices if they fail on one device. 27 | fail-fast: false 28 | matrix: 29 | api-level: 30 | - 21 31 | - 23 32 | - 26 33 | - 29 34 | - 30 35 | steps: 36 | # Setup the runner in the KVM group to enable HW Accleration for the emulator. 37 | # see https://github.blog/changelog/2023-02-23-hardware-accelerated-android-virtualization-on-actions-windows-and-linux-larger-hosted-runners/ 38 | - name: Enable KVM group perms 39 | shell: bash 40 | run: | 41 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules 42 | sudo udevadm control --reload-rules 43 | sudo udevadm trigger --name-match=kvm 44 | 45 | - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 46 | - name: Set up JDK 17 47 | uses: actions/setup-java@v1 48 | with: 49 | java-version: 17 50 | 51 | - name: Instrumentation Tests 52 | uses: reactivecircus/android-emulator-runner@v2 53 | with: 54 | api-level: ${{ matrix.api-level }} 55 | target: google_apis 56 | arch: x86_64 57 | script: ./gradlew connectedCheck --no-build-cache --no-daemon --stacktrace 58 | 59 | - name: Upload results 60 | if: ${{ always() }} 61 | uses: actions/upload-artifact@v2 62 | with: 63 | name: instrumentation-test-results ${{ matrix.api-level }} 64 | path: ./**/build/reports/androidTests/connected/** 65 | -------------------------------------------------------------------------------- /radiography/src/test/java/radiography/JavaInteropTest.java: -------------------------------------------------------------------------------- 1 | package radiography; 2 | 3 | import android.widget.TextView; 4 | import kotlin.Unit; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.robolectric.RobolectricTestRunner; 8 | import org.robolectric.annotation.Config; 9 | import radiography.ScannableView.AndroidView; 10 | 11 | import static java.util.Arrays.asList; 12 | import static java.util.Collections.singletonList; 13 | import static radiography.ScanScopes.AllWindowsScope; 14 | import static radiography.ScanScopes.FocusedWindowScope; 15 | import static radiography.ViewFilters.NoFilter; 16 | import static radiography.ViewFilters.androidViewFilterFor; 17 | import static radiography.ViewStateRenderers.DefaultsIncludingPii; 18 | import static radiography.ViewStateRenderers.ViewRenderer; 19 | import static radiography.ViewStateRenderers.androidViewStateRendererFor; 20 | import static radiography.ViewStateRenderers.textViewRenderer; 21 | 22 | /** 23 | * Doesn't actually test anything at runtime, just lets us validate how the API looks from Java 24 | * consumers. 25 | */ 26 | @SuppressWarnings("unused") 27 | @RunWith(RobolectricTestRunner.class) 28 | @Config(manifest = Config.NONE) 29 | public class JavaInteropTest { 30 | 31 | @Test public void createScanScopeFromJava() { 32 | @SuppressWarnings("ConstantConditions") 33 | ScanScope javaScanScope = () -> singletonList(new AndroidView(null)); 34 | } 35 | 36 | @Test public void createViewFilterFromJava() { 37 | ViewFilter javaViewFilter = androidViewFilterFor(TextView.class, 38 | view -> view.getText() != null); 39 | } 40 | 41 | @Test public void createViewRendererFromJava() { 42 | ViewStateRenderer javaViewRenderer = androidViewStateRendererFor(TextView.class, 43 | (appendable, textView) -> { 44 | CharSequence error = textView.getError(); 45 | if (error != null) { 46 | appendable.append(error); 47 | } 48 | return Unit.INSTANCE; 49 | }); 50 | } 51 | 52 | @Test public void scanFromJava() { 53 | Radiography.scan(); 54 | Radiography.scan(FocusedWindowScope); 55 | Radiography.scan( 56 | AllWindowsScope, 57 | asList( 58 | ViewRenderer, 59 | textViewRenderer() 60 | )); 61 | Radiography.scan( 62 | AllWindowsScope, 63 | DefaultsIncludingPii, 64 | NoFilter 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /radiography/src/main/java/radiography/internal/RenderTreeString.kt: -------------------------------------------------------------------------------- 1 | package radiography.internal 2 | 3 | import java.util.BitSet 4 | 5 | /** 6 | * Renders [rootNode] as a [String] by recursively rendering it and all its children as an ASCII- 7 | * art tree. 8 | * 9 | * @param renderNode A function which write the description of a node to a [StringBuilder], and 10 | * returns the node's children. 11 | */ 12 | internal fun renderTreeString( 13 | builder: StringBuilder, 14 | rootNode: N, 15 | renderNode: StringBuilder.(N) -> List 16 | ) { 17 | renderRecursively(builder, rootNode, renderNode, depth = 0, lastChildMask = BitSet()) 18 | } 19 | 20 | private fun renderRecursively( 21 | builder: StringBuilder, 22 | node: N, 23 | renderNode: StringBuilder.(N) -> List, 24 | depth: Int, 25 | lastChildMask: BitSet 26 | ) { 27 | // Render node into a separate buffer so we can append a prefix to every line. 28 | val nodeDescription = StringBuilder() 29 | val children = nodeDescription.renderNode(node) 30 | 31 | nodeDescription.lineSequence().forEachIndexed { index, line -> 32 | builder.appendLinePrefix(depth, continuePreviousLine = index > 0, lastChildMask = lastChildMask) 33 | @Suppress("DEPRECATION") 34 | (builder.appendln(line)) 35 | } 36 | 37 | val lastChildIndex = children.size - 1 38 | children.forEachIndexed { index, childNode -> 39 | val isLastChild = (index == lastChildIndex) 40 | // Set bit before recursing, will be unset again before returning. 41 | if (isLastChild) { 42 | lastChildMask.set(depth) 43 | } 44 | 45 | childNode?.let { 46 | renderRecursively(builder, childNode, renderNode, depth + 1, lastChildMask) 47 | } 48 | } 49 | 50 | // Unset the bit we set above before returning. 51 | lastChildMask.clear(depth) 52 | } 53 | 54 | private fun StringBuilder.appendLinePrefix( 55 | depth: Int, 56 | continuePreviousLine: Boolean, 57 | lastChildMask: BitSet 58 | ) { 59 | val lastDepth = depth - 1 60 | // Add a non-breaking space at the beginning of the line because Logcat eats normal spaces. 61 | append('\u00a0') 62 | for (parentDepth in 0..lastDepth) { 63 | if (parentDepth > 0) { 64 | append(' ') 65 | } 66 | val lastChild = lastChildMask[parentDepth] 67 | if (lastChild) { 68 | if (parentDepth == lastDepth && !continuePreviousLine) { 69 | append('╰') 70 | } else { 71 | append(' ') 72 | } 73 | } else { 74 | if (parentDepth == lastDepth && !continuePreviousLine) { 75 | append('├') 76 | } else { 77 | append('│') 78 | } 79 | } 80 | } 81 | if (depth > 0) { 82 | if (continuePreviousLine) { 83 | append(" ") 84 | } else { 85 | append("─") 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /radiography/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 17 | 18 | plugins { 19 | id("com.android.library") 20 | kotlin("android") 21 | id("com.vanniktech.maven.publish.base") 22 | } 23 | 24 | android { 25 | compileSdk = 34 26 | 27 | compileOptions { 28 | sourceCompatibility = JavaVersion.VERSION_1_8 29 | targetCompatibility = JavaVersion.VERSION_1_8 30 | } 31 | 32 | defaultConfig { 33 | minSdk = 17 34 | targetSdk = 34 35 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 36 | } 37 | 38 | buildFeatures { 39 | buildConfig = false 40 | } 41 | 42 | testOptions { 43 | execution = "ANDROIDX_TEST_ORCHESTRATOR" 44 | } 45 | namespace = "com.squareup.radiography" 46 | testNamespace = "com.squareup.radiography.test" 47 | } 48 | 49 | tasks.withType { 50 | kotlinOptions { 51 | freeCompilerArgs += listOfNotNull( 52 | "-Xopt-in=kotlin.RequiresOptIn", 53 | 54 | // Require explicit public modifiers and types. 55 | // TODO this should be moved to a top-level `kotlin { explicitApi() }` once that's working 56 | // for android projects, see https://youtrack.jetbrains.com/issue/KT-37652. 57 | "-Xexplicit-api=strict".takeUnless { 58 | // Tests aren't part of the public API, don't turn explicit API mode on for them. 59 | name.contains("test", ignoreCase = true) 60 | } 61 | ) 62 | } 63 | } 64 | 65 | dependencies { 66 | implementation(libs.curtains) 67 | // We don't want to bring any Compose dependencies in unless the consumer of this library is 68 | // bringing them in itself. 69 | compileOnly(libs.compose.toolingData) 70 | 71 | testImplementation(libs.test.junit) 72 | testImplementation(libs.test.mockito) 73 | testImplementation(libs.test.robolectric) 74 | testImplementation(libs.test.truth) 75 | 76 | androidTestImplementation(libs.test.androidx.core) 77 | androidTestImplementation(libs.test.androidx.espresso) 78 | androidTestImplementation(libs.test.androidx.rules) 79 | androidTestImplementation(libs.test.androidx.runner) 80 | androidTestImplementation(libs.test.truth) 81 | androidTestUtil(libs.test.androidx.orchestrator) 82 | } 83 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 20 | 30 | 40 | 47 | 48 | 53 | 54 |