Skip to content
This repository was archived by the owner on Aug 8, 2022. It is now read-only.

Commit 6a04294

Browse files
authored
Merge pull request #254 from Shopify/201-compose-ext
201: Initial version of Compose Extension companion library
2 parents 4e2706d + 5d80613 commit 6a04294

20 files changed

Lines changed: 576 additions & 8 deletions

File tree

.github/workflows/central_release.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,11 @@ jobs:
3030
ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.OSSRH_GPG_SECRET_KEY_ID }}
3131
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.OSSRH_GPG_SECRET_KEY }}
3232
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }}
33+
- name: Publish Testify Compose Library
34+
run: ./gradlew ComposeExtensions:publish
35+
env:
36+
OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }}
37+
OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
38+
ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.OSSRH_GPG_SECRET_KEY_ID }}
39+
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.OSSRH_GPG_SECRET_KEY }}
40+
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }}

Ext/Compose/README.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Testify — Android Screenshot Testing — Jetpack Compose Extensions
2+
3+
Easily create screenshot tests for `@Composable` functions.
4+
5+
<a href="https://search.maven.org/artifact/com.shopify.testify/testify-compose"><img alt="Maven Central" src="https://img.shields.io/maven-central/v/com.shopify.testify/testify-compose?color=%236e40ed&label=com.shopify.testify%3Atestify-compose"/></a>
6+
7+
---
8+
9+
# Set up testify-compose
10+
11+
**Root build.gradle**
12+
```groovy
13+
buildscript {
14+
repositories {
15+
mavenCentral()
16+
}
17+
dependencies {
18+
classpath "com.shopify.testify:plugin:1.2.0-alpha01"
19+
}
20+
}
21+
```
22+
23+
**Application build.gradle**
24+
```groovy
25+
dependencies {
26+
androidTestImplementation "com.shopify.testify:testify-compose:1.2.0-alpha01"
27+
}
28+
```
29+
30+
# Write a test
31+
32+
In order to test a `@Composable` function, you must first declare an instance variable of the `ComposableScreenshotRule` class. Then, you can invoke the `setCompose()` method on the `rule` instance and declare any Compose UI functions you wish to test.
33+
34+
Testify will capture only the bounds of the `@Composable`.
35+
36+
```kotlin
37+
class ComposableScreenshotTest {
38+
39+
@get:Rule val rule = ComposableScreenshotRule()
40+
41+
@ScreenshotInstrumentation
42+
@Test
43+
fun default() {
44+
rule
45+
.setCompose {
46+
Text(text = "Hello, Testify!")
47+
}
48+
.assertSame()
49+
}
50+
}
51+
52+
```
53+
54+
# License
55+
56+
MIT License
57+
58+
Copyright (c) 2021 Shopify
59+
60+
Permission is hereby granted, free of charge, to any person obtaining a copy
61+
of this software and associated documentation files (the "Software"), to deal
62+
in the Software without restriction, including without limitation the rights
63+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
64+
copies of the Software, and to permit persons to whom the Software is
65+
furnished to do so, subject to the following conditions:
66+
67+
The above copyright notice and this permission notice shall be included in all
68+
copies or substantial portions of the Software.
69+
70+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
71+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
72+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
73+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
74+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
75+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
76+
SOFTWARE.

Ext/Compose/build.gradle

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
plugins {
2+
id 'com.android.library'
3+
id 'kotlin-android'
4+
id 'org.jetbrains.dokka'
5+
}
6+
7+
ext {
8+
pom = [
9+
publishedGroupId : 'com.shopify.testify',
10+
artifact : 'testify-compose',
11+
libraryName : 'testify-compose',
12+
libraryDescription: 'Jetpack Compose extensions for Android Testify',
13+
siteUrl : 'https://github.com/Shopify/android-testify',
14+
gitUrl : 'https://github.com/Shopify/android-testify.git',
15+
licenseName : 'The MIT License',
16+
licenseUrl : 'https://opensource.org/licenses/MIT',
17+
author : 'Shopify Inc.'
18+
]
19+
}
20+
21+
version = "$project.versions.testify"
22+
group = pom.publishedGroupId
23+
archivesBaseName = pom.artifact
24+
25+
android {
26+
compileSdkVersion coreVersions.compileSdk
27+
28+
lintOptions {
29+
abortOnError true
30+
warningsAsErrors true
31+
textOutput 'stdout'
32+
textReport true
33+
xmlReport false
34+
}
35+
36+
defaultConfig {
37+
minSdkVersion coreVersions.minSdk
38+
targetSdkVersion coreVersions.targetSdk
39+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
40+
multiDexEnabled = true
41+
kotlinOptions {
42+
allWarningsAsErrors = true
43+
}
44+
}
45+
46+
libraryVariants.all { variant ->
47+
variant.outputs.all {
48+
outputFileName = "${archivesBaseName}-${version}.aar"
49+
}
50+
}
51+
52+
testOptions {
53+
unitTests.returnDefaultValues = true
54+
unitTests.all {
55+
testLogging {
56+
events "passed", "skipped", "failed", "standardOut", "standardError"
57+
outputs.upToDateWhen { false }
58+
showStandardStreams = true
59+
}
60+
}
61+
}
62+
63+
buildFeatures {
64+
viewBinding true
65+
compose true
66+
}
67+
68+
kotlinOptions {
69+
jvmTarget = "1.8"
70+
useIR = true
71+
}
72+
composeOptions {
73+
kotlinCompilerExtensionVersion versions.compose
74+
kotlinCompilerVersion versions.kotlin
75+
}
76+
77+
dependencies {
78+
implementation project(":Library")
79+
80+
implementation "androidx.activity:activity-compose:${versions.androidx.activityCompose}"
81+
implementation "androidx.appcompat:appcompat:${versions.androidx.appCompat}"
82+
implementation "androidx.compose.material:material:${versions.compose}"
83+
implementation "androidx.compose.ui:ui-tooling-preview:${versions.compose}"
84+
implementation "androidx.compose.ui:ui:${versions.compose}"
85+
implementation "androidx.lifecycle:lifecycle-runtime-ktx:${versions.androidx.lifecycleKtx}"
86+
implementation "androidx.test.espresso:espresso-core:${versions.androidx.test.espresso}"
87+
implementation "androidx.test:rules:${versions.androidx.test.rules}"
88+
implementation "androidx.test:runner:${versions.androidx.test.runner}"
89+
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${versions.kotlin}"
90+
}
91+
}
92+
93+
task sourcesJar(type: Jar) {
94+
from android.sourceSets.main.java.srcDirs
95+
classifier = 'sources'
96+
}
97+
98+
afterEvaluate {
99+
apply from: "../../publish.build.gradle"
100+
}
101+
102+
apply from: '../../ktlint.gradle'
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
package="com.shopify.testify.compose">
4+
5+
<application>
6+
<activity
7+
android:name="com.shopify.testify.ComposableTestActivity"
8+
android:exported="false" />
9+
</application>
10+
</manifest>
11+
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* The MIT License (MIT)
3+
*
4+
* Copyright (c) 2021 Shopify Inc.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
package com.shopify.testify
25+
26+
import android.app.Activity
27+
import android.graphics.Bitmap
28+
import androidx.compose.runtime.Composable
29+
import androidx.compose.ui.platform.ComposeView
30+
import androidx.test.espresso.Espresso
31+
import com.shopify.testify.compose.R
32+
import com.shopify.testify.internal.disposeComposition
33+
import org.junit.Assert.assertTrue
34+
import java.util.concurrent.CountDownLatch
35+
import java.util.concurrent.TimeUnit
36+
37+
/**
38+
* Helper extension of [ScreenshotRule] which simplifies testing [Composable] functions.
39+
*/
40+
open class ComposableScreenshotRule(
41+
var exactness: Float = 0.9f
42+
) : ScreenshotRule<ComposableTestActivity>(
43+
ComposableTestActivity::class.java,
44+
launchActivity = false,
45+
) {
46+
lateinit var composeFunction: @Composable () -> Unit
47+
48+
open fun onCleanUp(activity: Activity) {
49+
activity.disposeComposition()
50+
}
51+
52+
/**
53+
* Set a screenshot view provider to capture only the @Composable bounds
54+
*/
55+
override fun beforeAssertSame() {
56+
super.beforeAssertSame()
57+
TestifyFeatures.PixelCopyCapture.setEnabled(true)
58+
setExactness(exactness)
59+
setScreenshotViewProvider {
60+
it.getChildAt(0)
61+
}
62+
}
63+
64+
/**
65+
* Render the composable function after the activity has loaded.
66+
*/
67+
override fun afterActivityLaunched() {
68+
Espresso.onIdle()
69+
val latch = CountDownLatch(1)
70+
activity.runOnUiThread {
71+
val composeView = activity.findViewById<ComposeView>(R.id.compose_container)
72+
composeView.setContent {
73+
composeFunction()
74+
latch.countDown()
75+
}
76+
}
77+
assertTrue(latch.await(COMPOSE_TIMEOUT_SECONDS, TimeUnit.SECONDS))
78+
Espresso.onIdle()
79+
super.afterActivityLaunched()
80+
}
81+
82+
/**
83+
* Proactively dispose of any compositions after the screenshot has been taken.
84+
*/
85+
override fun afterScreenshot(activity: Activity, currentBitmap: Bitmap?) {
86+
super.afterScreenshot(activity, currentBitmap)
87+
onCleanUp(activity)
88+
}
89+
90+
/**
91+
* Used to provide a @Composable function to be rendered in the screenshot.
92+
*/
93+
fun setCompose(composable: @Composable () -> Unit): ComposableScreenshotRule {
94+
composeFunction = composable
95+
return this
96+
}
97+
98+
companion object {
99+
private const val COMPOSE_TIMEOUT_SECONDS: Long = 15
100+
}
101+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* The MIT License (MIT)
3+
*
4+
* Copyright (c) 2021 Shopify Inc.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
package com.shopify.testify
25+
26+
import android.graphics.Color
27+
import android.os.Bundle
28+
import android.view.ViewGroup
29+
import androidx.appcompat.app.AppCompatActivity
30+
import androidx.compose.ui.platform.ComposeView
31+
import com.shopify.testify.compose.R
32+
33+
open class ComposableTestActivity : AppCompatActivity() {
34+
35+
override fun onCreate(savedInstanceState: Bundle?) {
36+
super.onCreate(savedInstanceState)
37+
38+
setContentView(ComposeView(this).apply {
39+
layoutParams = ViewGroup.LayoutParams(
40+
ViewGroup.LayoutParams.WRAP_CONTENT,
41+
ViewGroup.LayoutParams.WRAP_CONTENT
42+
)
43+
setBackgroundColor(Color.WHITE)
44+
id = R.id.compose_container
45+
})
46+
}
47+
}

0 commit comments

Comments
 (0)