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

Implemented Showkase + Paparazzi artifact to automate screenshot testing #294

Merged
merged 19 commits into from
Jan 24, 2023
Merged
Show file tree
Hide file tree
Changes from 14 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: 4 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ buildscript {
'detekt' : '1.7.4',
'espresso' : '3.2.0',
'gradle' : '7.2.1',
'junit' : '4.13',
'junit' : '4.13.2',
'junitImplementation' : '1.1.2',
'kotlin' : '1.7.10',
'kotlinCompilerVersion' : '1.7.0',
Expand All @@ -23,6 +23,7 @@ buildscript {
'ksp' : "$KSP_VERSION",
'ktx' : '1.1.0',
'lifecycle' : '2.2.0',
'paparazzi' : '1.1.0',
'picasso' : '2.8',
'appcompat' : '1.4.0',
'testRunner' : '1.4.0',
Expand Down Expand Up @@ -77,7 +78,8 @@ buildscript {
'androidxTestRunner' : "androidx.test:runner:${versions.testRunner}",
'strikt' : "io.strikt:strikt-core:${versions.strikt}",
'shotAndroid' : "com.karumi:shot-android:${versions.shot}",
'testParameterInjector': "com.google.testparameterinjector:test-parameter-injector:${versions.testParameterInjector}"
'testParameterInjector': "com.google.testparameterinjector:test-parameter-injector:${versions.testParameterInjector}",
'paparazzi' : "app.cash.paparazzi:paparazzi:${versions.paparazzi}"
],
'material' : [
'material' : "com.google.android.material:material:${versions.material}",
Expand Down
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ include ':showkase-browser-testing'
include ':showkase-browser-testing-submodule'
include ':showkase-screenshot-testing-shot'
include ':showkase-screenshot-testing-paparazzi-sample'
include ':showkase-screenshot-testing-paparazzi'
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ class ShowkaseProcessorTest : BaseProcessorTest() {
@Test
fun `open class with no interface but ShowkaseScreenshoTest annotation throws compilation error`() {
assertCompilationFails(
"Only an implementation of com.airbnb.android.showkase.screenshot.testing.ShowkaseScreenshotTest can be annotated with @ShowkaseScreenshot"
"Only an implementation of com.airbnb.android.showkase.screenshot.testing.ShowkaseScreenshotTest or com.airbnb.android.showkase.screenshot.testing.paparazzi.PaparazziShowkaseScreenshotTest can be annotated with @ShowkaseScreenshot"
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.airbnb.android.showkase.processor.models.getShowkaseColorMetadata
import com.airbnb.android.showkase.processor.models.getShowkaseMetadata
import com.airbnb.android.showkase.processor.models.getShowkaseMetadataFromPreview
import com.airbnb.android.showkase.processor.models.getShowkaseTypographyMetadata
import com.airbnb.android.showkase.processor.writer.PaparazziShowkaseScreenshotTestWriter
import com.airbnb.android.showkase.processor.writer.ShowkaseBrowserProperties
import com.airbnb.android.showkase.processor.writer.ShowkaseBrowserWriter
import com.airbnb.android.showkase.processor.writer.ShowkaseBrowserWriter.Companion.CODEGEN_AUTOGEN_CLASS_NAME
Expand Down Expand Up @@ -212,7 +213,7 @@ class ShowkaseProcessor @JvmOverloads constructor(
val rootElement = getShowkaseRootElement(roundEnvironment, environment)

// Showkase test annotation
val screenshotTestElement = getShowkaseScreenshotTestElement(roundEnvironment)
val (screenshotTestElement, screenshotTestType) = getShowkaseScreenshotTestElement(roundEnvironment)

var showkaseBrowserProperties = ShowkaseBrowserProperties()

Expand All @@ -231,9 +232,10 @@ class ShowkaseProcessor @JvmOverloads constructor(
)
}

if (screenshotTestElement != null) {
if (screenshotTestElement != null && screenshotTestType != null) {
// Generate screenshot test file if ShowkaseScreenshotTest is present in the root module
writeScreenshotTestFiles(screenshotTestElement, rootElement, showkaseBrowserProperties)
writeScreenshotTestFiles(screenshotTestElement, screenshotTestType, rootElement,
showkaseBrowserProperties)
}
}

Expand All @@ -246,11 +248,13 @@ class ShowkaseProcessor @JvmOverloads constructor(
return showkaseRootElements.singleOrNull() as XTypeElement?
}

private fun getShowkaseScreenshotTestElement(roundEnvironment: XRoundEnv): XTypeElement? {
private fun getShowkaseScreenshotTestElement(
roundEnvironment: XRoundEnv
): Pair<XTypeElement?, ScreenshotTestType?> {
val testElements = roundEnvironment.getElementsAnnotatedWith(ShowkaseScreenshot::class)
.filterIsInstance<XTypeElement>()
showkaseValidator.validateShowkaseTestElement(testElements, environment)
return testElements.singleOrNull()
val screenshotTestType = showkaseValidator.validateShowkaseTestElement(testElements, environment)
return testElements.singleOrNull() to screenshotTestType
}

private fun writeShowkaseFiles(
Expand Down Expand Up @@ -288,6 +292,7 @@ class ShowkaseProcessor @JvmOverloads constructor(

private fun writeScreenshotTestFiles(
screenshotTestElement: XTypeElement,
screenshotTestType: ScreenshotTestType,
rootElement: XTypeElement?,
showkaseBrowserProperties: ShowkaseBrowserProperties,
) {
Expand Down Expand Up @@ -329,11 +334,7 @@ class ShowkaseProcessor @JvmOverloads constructor(
}

writeShowkaseScreenshotTestFile(
// We only handle composables without preview parameter for screenshots. This is because
// there's no way to get information about how many previews are dynamically generated using
// preview parameter as it happens on run time and our codegen doesn't get enough information
// to be able to predict how many extra composables the preview parameters extrapolate to.
// TODO(vinaygaba): Add screenshot testing support for composabable with preview parameters as well
screenshotTestType,
showkaseTestMetadata.componentsSize,
showkaseTestMetadata.colorsSize,
showkaseTestMetadata.typographySize,
Expand Down Expand Up @@ -434,22 +435,42 @@ class ShowkaseProcessor @JvmOverloads constructor(

@Suppress("LongParameterList")
private fun writeShowkaseScreenshotTestFile(
screenshotTestType: ScreenshotTestType,
componentsSize: Int,
colorsSize: Int,
typographySize: Int,
screenshotTestPackageName: String,
rootModulePackageName: String,
testClassName: String,
) {
ShowkaseScreenshotTestWriter(environment).apply {
generateScreenshotTests(
componentsSize,
colorsSize,
typographySize,
screenshotTestPackageName,
rootModulePackageName,
testClassName
)
when(screenshotTestType) {
// We only handle composables without preview parameter for screenshots. This is because
// there's no way to get information about how many previews are dynamically generated using
// preview parameter as it happens on run time and our codegen doesn't get enough information
// to be able to predict how many extra composables the preview parameters extrapolate to.
// TODO(vinaygaba): Add screenshot testing support for composabable with preview
// parameters as well
ScreenshotTestType.SHOWKASE -> {
ShowkaseScreenshotTestWriter(environment).apply {
generateScreenshotTests(
componentsSize,
colorsSize,
typographySize,
screenshotTestPackageName,
rootModulePackageName,
testClassName
)
}
}
ScreenshotTestType.PAPARAZZI_SHOWKASE -> {
PaparazziShowkaseScreenshotTestWriter(environment).apply {
generateScreenshotTests(
screenshotTestPackageName,
rootModulePackageName,
testClassName
)
}
}
}
}

Expand All @@ -459,15 +480,12 @@ class ShowkaseProcessor @JvmOverloads constructor(
val typographySize: Int,
)
companion object {
const val COMPOSABLE_CLASS_NAME = "androidx.compose.runtime.Composable"
const val COMPOSABLE_SIMPLE_NAME = "Composable"
const val PREVIEW_CLASS_NAME = "androidx.compose.ui.tooling.preview.Preview"
const val PREVIEW_SIMPLE_NAME = "Preview"
const val PREVIEW_PARAMETER_CLASS_NAME =
"androidx.compose.ui.tooling.preview.PreviewParameter"
const val PREVIEW_PARAMETER_SIMPLE_NAME = "PreviewParameter"
const val TYPE_STYLE_CLASS_NAME = "androidx.compose.ui.text.TextStyle"
const val CODEGEN_PACKAGE_NAME = "com.airbnb.android.showkase"
internal const val COMPOSABLE_SIMPLE_NAME = "Composable"
internal const val PREVIEW_CLASS_NAME = "androidx.compose.ui.tooling.preview.Preview"
internal const val PREVIEW_SIMPLE_NAME = "Preview"
internal const val PREVIEW_PARAMETER_SIMPLE_NAME = "PreviewParameter"
internal const val TYPE_STYLE_CLASS_NAME = "androidx.compose.ui.text.TextStyle"
internal const val CODEGEN_PACKAGE_NAME = "com.airbnb.android.showkase"
}
}

Expand All @@ -489,3 +507,8 @@ internal enum class ShowkaseGeneratedMetadataType {
TYPOGRAPHY
}

internal enum class ScreenshotTestType {
SHOWKASE,
Copy link
Contributor

Choose a reason for hiding this comment

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

This is the shot one right? Would it make sense to name this SHOT_SHOWKASE maybe? :)

PAPARAZZI_SHOWKASE
}

Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.airbnb.android.showkase.annotation.ShowkaseComposable
import com.airbnb.android.showkase.annotation.ShowkaseRoot
import com.airbnb.android.showkase.annotation.ShowkaseRootModule
import com.airbnb.android.showkase.annotation.ShowkaseScreenshot
import com.airbnb.android.showkase.processor.ScreenshotTestType
import com.airbnb.android.showkase.processor.ShowkaseProcessor.Companion.COMPOSABLE_SIMPLE_NAME
import com.airbnb.android.showkase.processor.ShowkaseProcessor.Companion.PREVIEW_PARAMETER_SIMPLE_NAME
import com.airbnb.android.showkase.processor.exceptions.ShowkaseProcessorException
Expand Down Expand Up @@ -306,8 +307,8 @@ internal class ShowkaseValidator {
internal fun validateShowkaseTestElement(
elements: Collection<XTypeElement>,
environment: XProcessingEnv,
) {
if (elements.isEmpty()) return
): ScreenshotTestType? {
if (elements.isEmpty()) return null

val showkaseScreenshotAnnotationName = ShowkaseScreenshot::class.java.simpleName

Expand All @@ -330,18 +331,68 @@ internal class ShowkaseValidator {

// Validate that the class annotated with @ShowkaseScreenshot extends the
// ShowkaseScreenshotTest interface
requireInterface(
element = element,
interfaceType = showkaseScreenshotTestTypeMirror,
annotationName = showkaseScreenshotAnnotationName,
)
val isShowkaseScreenshotTest =
showkaseScreenshotTestTypeMirror.isAssignableFrom(element.type)

return if (isShowkaseScreenshotTest) {
ScreenshotTestType.SHOWKASE
} else if (
environment.findType(PAPARAZZI_SHOWKASE_SCREENSHOT_TEST_CLASS_NAME)
?.isAssignableFrom(element.type) == true
) {
val paparazziShowkaseScreenshotTestTypeMirror = environment
.requireType(PAPARAZZI_SHOWKASE_SCREENSHOT_TEST_CLASS_NAME)
validatePaparazziShowkaseScreenshotTest(environment, element,
paparazziShowkaseScreenshotTestTypeMirror)

ScreenshotTestType.PAPARAZZI_SHOWKASE
} else {
throw ShowkaseProcessorException(
"Only an implementation of com.airbnb.android.showkase.screenshot.testing" +
".ShowkaseScreenshotTest or com.airbnb.android.showkase.screenshot" +
".testing.paparazzi.PaparazziShowkaseScreenshotTest can be annotated " +
"with @$showkaseScreenshotAnnotationName",
element
)
}

// TODO(vinaygaba): Validate that the passed root class is annotated with @ShowkaseRoot
// and implements [ShowkaseRootModule]
}
}
}

private fun validatePaparazziShowkaseScreenshotTest(
environment: XProcessingEnv,
element: XTypeElement,
paparazziShowkaseScreenshotTestTypeMirror: XType
) {
val paparazziShowkaseScreenshotTestCompanionType = environment
.requireType(PAPARAZZI_SHOWKASE_SCREENSHOT_TEST_COMPANION_CLASS_NAME)

val companionObjectTypeElements = element.getEnclosedTypeElements().filter {
it.isCompanionObject()
}
if (companionObjectTypeElements.isEmpty()) {
throw ShowkaseProcessorException(
"Classes implementing the ${paparazziShowkaseScreenshotTestTypeMirror.typeName} interface " +
"should have a companion object that implements the " +
"${paparazziShowkaseScreenshotTestCompanionType.typeName} interface.",
element
)
}

if (!paparazziShowkaseScreenshotTestCompanionType
.isAssignableFrom(companionObjectTypeElements[0].type)) {
throw ShowkaseProcessorException(
"Classes implementing the ${paparazziShowkaseScreenshotTestTypeMirror.typeName} interface " +
"should have a companion object that implements the " +
"${paparazziShowkaseScreenshotTestCompanionType.typeName} interface.",
element
)
}
}

private fun requireOpenClass(
element: XTypeElement,
annotationName: String,
Expand Down Expand Up @@ -377,5 +428,9 @@ internal class ShowkaseValidator {
companion object {
private const val SHOWKASE_SCREENSHOT_TEST_CLASS_NAME =
"com.airbnb.android.showkase.screenshot.testing.ShowkaseScreenshotTest"
private const val PAPARAZZI_SHOWKASE_SCREENSHOT_TEST_CLASS_NAME =
"com.airbnb.android.showkase.screenshot.testing.paparazzi.PaparazziShowkaseScreenshotTest"
private const val PAPARAZZI_SHOWKASE_SCREENSHOT_TEST_COMPANION_CLASS_NAME =
"com.airbnb.android.showkase.screenshot.testing.paparazzi.PaparazziShowkaseScreenshotTest.CompanionObject"
}
}
Loading