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

Support includePrivatePreviews of ComposablePreviewScanner #445

Merged
Show file tree
Hide file tree
Changes from 5 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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -951,7 +951,7 @@ If you are having trouble debugging your test, try Dump mode as follows.
# Experimental Compose Preview Support

Roborazzi provides support for generating screenshot tests and easy setup for Jetpack Compose Preview.
This support uses [ComposePreviewScanner](https://github.com/sergio-sastre/ComposablePreviewScanner) to scan the Composable Previews in your project.
This support uses [ComposablePreviewScanner](https://github.com/sergio-sastre/ComposablePreviewScanner) to scan the Composable Previews in your project.

## Generate Compose Preview screenshot tests

Expand Down Expand Up @@ -994,13 +994,13 @@ roborazzi {

## Manually adding Compose Preview screenshot tests

Roborazzi provides a helper function for ComposePreviewScanner.
Roborazzi provides a helper function for ComposablePreviewScanner.
You can add the following dependency to your project to use the helper function:

`testImplementation("io.github.takahirom.roborazzi:roborazzi-compose-preview-scanner-support:[version]")`

Then you can use the `ComposablePreview<AndroidPreviewInfo>.captureRoboImage()` function to capture the Composable Preview using the settings in Preview annotations.
To obtain the `ComposablePreview` object, please refer to [ComposePreviewScanner](https://github.com/sergio-sastre/ComposablePreviewScanner).
To obtain the `ComposablePreview` object, please refer to [ComposablePreviewScanner](https://github.com/sergio-sastre/ComposablePreviewScanner).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I named it like that and I had my own doubts.
Google is also inconsistent on whether to call them "Compose Previews", like in "Compose Preview Screenshot Testing tool" or "Composable Previews" like here

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I know it should not be Compose Composable Preview. 😊

"A good name is long enough to fully communicate what the item is or does, without being so long that it becomes hard to read." https://google.github.io/eng-practices/review/reviewer/looking-for.html

I personally think we can use ComposePreview in an Android context, and also I think we can use ComposablePreview in a Jetpack Compose context. This is because Composable is an annotation name, so it's a minor distinction, while Compose is the framework name.


```kotlin
fun ComposablePreview<AndroidPreviewInfo>.captureRoboImage(
Expand Down
6 changes: 3 additions & 3 deletions docs/topics/preview_support.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Experimental Compose Preview Support

Roborazzi provides support for generating screenshot tests and easy setup for Jetpack Compose Preview.
This support uses [ComposePreviewScanner](https://github.com/sergio-sastre/ComposablePreviewScanner) to scan the Composable Previews in your project.
This support uses [ComposablePreviewScanner](https://github.com/sergio-sastre/ComposablePreviewScanner) to scan the Composable Previews in your project.

## Generate Compose Preview screenshot tests

Expand Down Expand Up @@ -44,13 +44,13 @@ roborazzi {

## Manually adding Compose Preview screenshot tests

Roborazzi provides a helper function for ComposePreviewScanner.
Roborazzi provides a helper function for ComposablePreviewScanner.
You can add the following dependency to your project to use the helper function:

`testImplementation("io.github.takahirom.roborazzi:roborazzi-compose-preview-scanner-support:[version]")`

Then you can use the `ComposablePreview<AndroidPreviewInfo>.captureRoboImage()` function to capture the Composable Preview using the settings in Preview annotations.
To obtain the `ComposablePreview` object, please refer to [ComposePreviewScanner](https://github.com/sergio-sastre/ComposablePreviewScanner).
To obtain the `ComposablePreview` object, please refer to [ComposablePreviewScanner](https://github.com/sergio-sastre/ComposablePreviewScanner).

```kotlin
fun ComposablePreview<AndroidPreviewInfo>.captureRoboImage(
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ webjar-material-design-icons = "4.0.0"
webjar-materialize = "1.0.0"
webjars-locator-lite = "0.0.4"

composable-preview-scanner = "0.1.2"
composable-preview-scanner = "0.1.3"

[libraries]
roborazzi = { module = "io.github.takahirom.roborazzi:roborazzi", version.ref = "roborazzi-for-replacing-by-include-build" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class GeneratePreviewTestTest {
checkHasImages()
}
}

@Test
fun whenKmpModuleAndRecordRunImagesShouldBeRecorded() {
RoborazziGradleRootProject(testProjectDir).previewModule.apply {
Expand All @@ -26,6 +27,16 @@ class GeneratePreviewTestTest {
checkHasImages()
}
}

@Test
fun whenIncludePrivatePreviewsAndRecordRunImagesShouldBeRecorded() {
RoborazziGradleRootProject(testProjectDir).previewModule.apply {
buildGradle.isIncludePrivatePreviews = true
record()

checkHasPrivatePreviewImages()
}
}
}

class PreviewModule(
Expand All @@ -35,20 +46,28 @@ class PreviewModule(
companion object {
val moduleName = "sample-generate-preview-tests"
}

val buildGradle = BuildGradle(testProjectDir)

class BuildGradle(private val projectFolder: TemporaryFolder) {
private val PATH = moduleName + "/build.gradle.kts"
var isKmp = false
var isIncludePrivatePreviews = false
fun write() {
val file =
projectFolder.root.resolve(PATH)
file.parentFile.mkdirs()
val includePrivatePreviewsExpr = if (isIncludePrivatePreviews) {
"""includePrivatePreviews = $isIncludePrivatePreviews"""
} else {
""
}
val roborazziExtension = """
roborazzi {
generateComposePreviewRobolectricTests {
enable = true
packages = listOf("com.github.takahirom.preview.tests")
$includePrivatePreviewsExpr
}
}
""".trimIndent()
Expand Down Expand Up @@ -89,7 +108,7 @@ class PreviewModule(
}

""".trimIndent()
val buildGradleText = if(isKmp)
val buildGradleText = if (isKmp)
"""
plugins {
kotlin("multiplatform")
Expand Down Expand Up @@ -144,7 +163,7 @@ class PreviewModule(
maven { url = uri("https://jitpack.io") }
}
""".trimIndent()
else """
else """
plugins {
id("com.android.application")
// id("com.android.library")
Expand Down Expand Up @@ -205,4 +224,13 @@ class PreviewModule(
println("images:" + images?.toList())
assert(images?.isNotEmpty() == true)
}

fun checkHasPrivatePreviewImages() {
val privateImages =
testProjectDir.root.resolve("$moduleName/build/outputs/roborazzi/").listFiles()
.orEmpty()
.filter { it.name.contains("PreviewWithPrivate") }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not to name it "WithPrivatePreviews" instead?

println("images:" + privateImages.toList())
assert(privateImages.isNotEmpty() == true)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,23 @@ fun PreviewDarkMode() {
}
}

@Preview
@Composable
private fun PreviewWithPrivate() {
val isSystemInDarkTheme = isSystemInDarkTheme()
MaterialTheme {
Card(
Modifier
.width(180.dp)
) {
Text(
modifier = Modifier.padding(8.dp),
text = "This is private preview"
)
}
}
}

@Preview(
name = "Preview Name",
// These properties are not supported by Roborazzi yet.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import org.gradle.api.tasks.testing.Test
import org.gradle.invocation.DefaultGradle
import java.io.File
import java.net.URLEncoder
import java.util.Locale
import javax.inject.Inject

open class GenerateComposePreviewRobolectricTestsExtension @Inject constructor(objects: ObjectFactory) {
Expand All @@ -31,10 +32,10 @@ open class GenerateComposePreviewRobolectricTestsExtension @Inject constructor(o
val packages: ListProperty<String> = objects.listProperty(String::class.java)

/**
* The fully qualified class name of the custom test class that implements [com.github.takahirom.roborazzi.ComposePreviewTester].
* If true, the private previews will be included in the test.
*/
val testerQualifiedClassName: Property<String> = objects.property(String::class.java)
.convention("com.github.takahirom.roborazzi.AndroidComposePreviewTester")
val includePrivatePreviews: Property<Boolean> = objects.property(Boolean::class.java)
.convention(false)

/**
* [robolectricConfig] will be passed to the Robolectric's @Config annotation in the generated test class.
Expand All @@ -48,6 +49,14 @@ open class GenerateComposePreviewRobolectricTestsExtension @Inject constructor(o
"qualifiers" to "RobolectricDeviceQualifiers.Pixel4a",
)
)

/**
* The fully qualified class name of the custom test class that implements [com.github.takahirom.roborazzi.ComposePreviewTester].
* This is advanced usage. You can implement your own test class that implements [com.github.takahirom.roborazzi.ComposePreviewTester].
*/
val testerQualifiedClassName: Property<String> = objects.property(String::class.java)
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've simply changed the order because testerQualifiedClassName is an advanced option.

.convention("com.github.takahirom.roborazzi.AndroidComposePreviewTester")

}

fun generateComposePreviewRobolectricTestsIfNeeded(
Expand All @@ -63,7 +72,7 @@ fun generateComposePreviewRobolectricTestsIfNeeded(
setupGenerateComposePreviewRobolectricTestsTask(
project = project,
variant = variant,
scanPackages = extension.packages,
extension = extension,
testerQualifiedClassName = extension.testerQualifiedClassName,
robolectricConfig = extension.robolectricConfig
)
Expand All @@ -79,23 +88,24 @@ fun generateComposePreviewRobolectricTestsIfNeeded(
private fun setupGenerateComposePreviewRobolectricTestsTask(
project: Project,
variant: Variant,
scanPackages: ListProperty<String>,
extension: GenerateComposePreviewRobolectricTestsExtension,
testerQualifiedClassName: Property<String>,
robolectricConfig: MapProperty<String, String>,
) {
check(scanPackages.get().orEmpty().isNotEmpty()) {
check(extension.packages.get().orEmpty().isNotEmpty()) {
"Please set roborazzi.generateRobolectricPreviewTests.packages in the generatePreviewTests extension or set roborazzi.generateRobolectricPreviewTests.enable = false." +
"See https://github.com/sergio-sastre/ComposablePreviewScanner?tab=readme-ov-file#how-to-use for more information."
}

val generateTestsTask = project.tasks.register(
"generate${variant.name.capitalize()}ComposePreviewRobolectricTests",
"generate${variant.name.capitalize(Locale.ROOT)}ComposePreviewRobolectricTests",
GenerateComposePreviewRobolectricTestsTask::class.java
) {
// It seems that this directory path is overridden by addGeneratedSourceDirectory.
// The generated tests will be located in build/JAVA/generate[VariantName]ComposePreviewRobolectricTests.
it.outputDir.set(project.layout.buildDirectory.dir("generated/roborazzi/preview-screenshot"))
it.scanPackageTrees.set(scanPackages)
it.scanPackageTrees.set(extension.packages)
it.includePrivatePreviews.set(extension.includePrivatePreviews)
it.testerQualifiedClassName.set(testerQualifiedClassName)
it.robolectricConfig.set(robolectricConfig)
}
Expand All @@ -114,6 +124,9 @@ abstract class GenerateComposePreviewRobolectricTestsTask : DefaultTask() {
@get:Input
var scanPackageTrees: ListProperty<String> = project.objects.listProperty(String::class.java)

@get:Input
abstract val includePrivatePreviews: Property<Boolean>

@get:Input
abstract val testerQualifiedClassName: Property<String>

Expand All @@ -126,6 +139,7 @@ abstract class GenerateComposePreviewRobolectricTestsTask : DefaultTask() {
testDir.mkdirs()

val packagesExpr = scanPackageTrees.get().joinToString(", ") { "\"$it\"" }
val includePrivatePreviewsExpr = includePrivatePreviews.get()

val generatedClassFQDN = "com.github.takahirom.roborazzi.RoborazziPreviewParameterizedTests"
val packageName = generatedClassFQDN.substringBeforeLast(".")
Expand All @@ -140,8 +154,11 @@ abstract class GenerateComposePreviewRobolectricTestsTask : DefaultTask() {
File(directory, "$className.kt").writeText(
"""
package $packageName
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.rules.TestWatcher
import org.junit.rules.RuleChain
import org.robolectric.ParameterizedRobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.annotation.GraphicsMode
Expand All @@ -158,26 +175,42 @@ abstract class GenerateComposePreviewRobolectricTestsTask : DefaultTask() {
class $className(
private val preview: ComposablePreview<Any>,
) {

private val testLifecycleOptions = tester.options().testLifecycleOptions
@get:Rule
val rule = RuleChain.outerRule(
if(testLifecycleOptions is ComposePreviewTester.Options.JUnit4TestLifecycleOptions) {
(testLifecycleOptions as ComposePreviewTester.Options.JUnit4TestLifecycleOptions).testRule
} else {
object : TestWatcher() {}
}
)
companion object {
val tester = getComposePreviewTester("$testerQualifiedClassNameString")
// lazy for performance
val previews: List<ComposablePreview<Any>> by lazy {
getComposePreviewTester("$testerQualifiedClassNameString").previews(
$packagesExpr
)
setupDefaultOptions()
tester.previews()
}
@JvmStatic
@ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
fun values(): List<ComposablePreview<Any>> =
previews
fun values(): List<ComposablePreview<Any>> = previews

fun setupDefaultOptions() {
ComposePreviewTester.defaultOptionsFromPlugin = ComposePreviewTester.Options(
scanOptions = ComposePreviewTester.Options.ScanOptions(
packages = listOf($packagesExpr),
includePrivatePreviews = $includePrivatePreviewsExpr,
)
)
}
}


@GraphicsMode(GraphicsMode.Mode.NATIVE)
$robolectricConfigString
@Test
fun test() {
getComposePreviewTester("$testerQualifiedClassNameString").test(preview)
tester.test(preview)
}

}
Expand Down Expand Up @@ -289,7 +322,8 @@ private fun verifyLibraryDependencies(
val dependencies = this
val libNameArray = libraryName.split(":")
if (!dependencies.contains(libNameArray[0] to libNameArray[1])) {
val configurationNames = "'testImplementation'(For Android Project) or 'kotlin.sourceSets.androidUnitTest.dependencies.implementation'(For KMP)"
val configurationNames =
"'testImplementation'(For Android Project) or 'kotlin.sourceSets.androidUnitTest.dependencies.implementation'(For KMP)"
error(
"Roborazzi: Please add the following $configurationNames dependency to the 'dependencies' block in the 'build.gradle' file: '$libraryName' for the $configurationNames configuration.\n" +
"For your convenience, visit https://www.google.com/search?q=" + URLEncoder.encode(
Expand Down
Loading
Loading