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

Conversation

vinaygaba
Copy link
Collaborator

@vinaygaba vinaygaba commented Dec 24, 2022

This PR implements the showkase-screenshot-testing-paparazzi artifact that enables complete automation of screenshot testing for your codebase using Showkase + Paparazzi.

Assuming you were already using Showkase and had previews of your @Composable functions (by using either @Preview or @ShowkaseComposable), this is all the extra setup you will need to add screenshot testing to your codebase using Showkase + Paparazzi

@ShowkaseScreenshot(rootShowkaseClass = YourShowkaseRootModuleClass::class)
abstract class MyPaparazziShowkaseScreenshotTest: PaparazziShowkaseScreenshotTest {
    companion object: PaparazziShowkaseScreenshotTest.CompanionObject
}

With just 3 lines of code, all the previews from your codebase will be screenshot tested using Paparazzi with appropriate defaults 🤯💥

You might have a use case where you also want to screenshot test your components in dark mode or in RTL. Doing so is equally trivial. Just implement the appropriate functions to override the default behavior.

@ShowkaseScreenshot(rootShowkaseClass = YourShowkaseRootModuleClass::class)
abstract class MyPaparazziShowkaseScreenshotTest: PaparazziShowkaseScreenshotTest {
    companion object: PaparazziShowkaseScreenshotTest.CompanionObject {
        override fun layoutDirections(): List<LayoutDirection> = listOf(LayoutDirection.Ltr, LayoutDirection.Rtl)

        override fun uiModes(): List<PaparazziShowkaseUIMode> = listOf(PaparazziShowkaseUIMode.DEFAULT, PaparazziShowkaseUIMode.DARK)
    }
}

This will now ensure that all the previews are also screenshot tested in RTL and dark mode configurations. This creates a test matrix so all permutations are screenshot tested, just like you'd expect.

Generated File

// This is an auto-generated file. Please do not edit/modify this file.
package com.airbnb.android.showkase.screenshot.testing.paparazzi.sample

import androidx.compose.ui.unit.LayoutDirection
import app.cash.paparazzi.Paparazzi
import com.airbnb.android.showkase.models.Showkase
import com.airbnb.android.showkase.screenshot.testing.paparazzi.ColorPaparazziShowkaseTestPreview
import com.airbnb.android.showkase.screenshot.testing.paparazzi.ComponentPaparazziShowkaseTestPreview
import com.airbnb.android.showkase.screenshot.testing.paparazzi.PaparazziShowkaseDeviceConfig
import com.airbnb.android.showkase.screenshot.testing.paparazzi.PaparazziShowkaseTestPreview
import com.airbnb.android.showkase.screenshot.testing.paparazzi.PaparazziShowkaseUIMode
import com.airbnb.android.showkase.screenshot.testing.paparazzi.TypographyPaparazziShowkaseTestPreview
import com.airbnb.android.showkase.screenshot.testing.paparazzi.sample.getMetadata
import com.google.testing.junit.testparameterinjector.TestParameter
import com.google.testing.junit.testparameterinjector.TestParameter.TestParameterValuesProvider
import com.google.testing.junit.testparameterinjector.TestParameterInjector
import kotlin.Unit
import kotlin.collections.List
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(TestParameterInjector::class)
public class MyPaparazziShowkaseScreenshotTest_PaparazziShowkaseTest :
    MyPaparazziShowkaseScreenshotTest() {
  @get:Rule
  public val paparazzi: Paparazzi = providePaparazzi()

  @Test
  public fun test_previews(
    @TestParameter(valuesProvider = PaparazziShowkasePreviewProvider::class)
        elementPreview: PaparazziShowkaseTestPreview,
    @TestParameter(valuesProvider = PaparazziShowkaseDeviceConfigProvider::class)
        config: PaparazziShowkaseDeviceConfig,
    @TestParameter(valuesProvider = PaparazziShowkaseLayoutDirectionProvider::class)
        direction: LayoutDirection,
    @TestParameter(valuesProvider = PaparazziShowkaseUIModeProvider::class)
        uiMode: PaparazziShowkaseUIMode,
  ): Unit {
    paparazzi.unsafeUpdateConfig(config.deviceConfig.copy(softButtons = false))
    takePaparazziSnapshot(paparazzi, elementPreview, direction, uiMode)
  }

  private object PaparazziShowkasePreviewProvider : TestParameter.TestParameterValuesProvider {
    public override fun provideValues(): List<PaparazziShowkaseTestPreview> {
      val metadata = Showkase.getMetadata()
      val components = metadata.componentList.map(::ComponentPaparazziShowkaseTestPreview)
      val colors = metadata.colorList.map(::ColorPaparazziShowkaseTestPreview)
      val typography = metadata.typographyList.map(::TypographyPaparazziShowkaseTestPreview)
      return components + colors + typography
    }
  }

  private object PaparazziShowkaseDeviceConfigProvider : TestParameter.TestParameterValuesProvider {
    public override fun provideValues(): List<PaparazziShowkaseDeviceConfig> = deviceConfigs()
  }

  private object PaparazziShowkaseLayoutDirectionProvider :
      TestParameter.TestParameterValuesProvider {
    public override fun provideValues(): List<LayoutDirection> = layoutDirections()
  }

  private object PaparazziShowkaseUIModeProvider : TestParameter.TestParameterValuesProvider {
    public override fun provideValues(): List<PaparazziShowkaseUIMode> = uiModes()
  }
}

@airbnb/showkase-maintainers

Copy link
Contributor

@oas004 oas004 left a comment

Choose a reason for hiding this comment

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

Wow this is cool! Can't wait to test it out! 😍

@@ -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? :)


android {
namespace 'com.airbnb.android.showkase.screenshot.testing.paparazzi'
compileSdk 32
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe it should be part of a separate PR or something, but should we bump the compile sdk and target sdk to 33 now? :)

@vinaygaba vinaygaba marked this pull request as ready for review December 29, 2022 06:21
Copy link
Contributor

@BenSchwab BenSchwab left a comment

Choose a reason for hiding this comment

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

Neat! Generally looks good to me, only question is if some of the code-gen can be moved to the base interface / abstract class to simplify maintenence

fileBuilder.build().writeTo(environment.filer, mode = XFiler.Mode.Aggregating)
}

private fun TypeSpec.Builder.addPreviewProvider() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it necessary to code gen this (and some of the other objects?) They seem purely static from what I can see

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@BenSchwab That was necessary so that users could override the behavior of some of the methods declared inside the CompanionObject interface. If I were to move these objects directly inside the interface, I'd need to change the interface to an abstract class, but at that point, I won't be able to allow overriding the behavior which is vital. Is there another way to structure this? 🤔

@vinaygaba vinaygaba merged commit 9832c54 into master Jan 24, 2023
@@ -16,7 +16,7 @@ import java.io.File
* Temporarily set this to true to have the test runner update test resource file expected outputs
* instead of failing tests on mismatch. Use this to easily update expected outputs.
*/
const val UPDATE_TEST_OUTPUTS = false
const val UPDATE_TEST_OUTPUTS = true
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not 100% sure about this but should we set this to false in the master branch? Thinking since now, it will generate the test output always and not verify that the output is actually correct 🤔
Wdyt? @vinaygaba :)

Copy link
Contributor

Choose a reason for hiding this comment

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

Made #300 to address this :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants