Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Keeping your Pixels Perfect: Paparazzi 1.0

Keeping your Pixels Perfect: Paparazziย 1.0

Unit tests allow you refactor your code with confidence and -- if architected correctly -- run blazingly fast!

But how do you ensure that your UI looks as expected? Espresso tests are hard to set up, prone to flakiness and require APKs, emulators, dexing, ADB...ugh. Snapshot tests get you closer but also rely on instrumentation tests.

Come find out how we're improving the UI testing loop on Cash App with Paparazzi -- an Android testing library to render your application screens without a physical device or emulator!

Video: https://player.vimeo.com/video/646415398

John Rodriguez

October 21, 2021
Tweet

More Decks by John Rodriguez

Other Decks in Programming

Transcript

  1. class LaunchViewTest { @get:Rul e val paparazzi = Paparazzi( )

    @Tes t fun launch() { val view = paparazzi.inflate<LinearLayout>(R.layout.launch ) paparazzi.snapshot(view ) } }
  2. class LaunchViewTest { @get:Rul e val paparazzi = Paparazzi( )

    @Tes t fun launch() { val view = paparazzi.inflate<LinearLayout>(R.layout.launch ) paparazzi.snapshot(view ) } }
  3. class LaunchViewTest { @get:Rul e val paparazzi = Paparazzi( )

    @Tes t fun launch() { val view = paparazzi.inflate<LinearLayout>(R.layout.launch ) paparazzi.snapshot(view ) } }
  4. class LaunchViewTest { @get:Rul e val paparazzi = Paparazzi( )

    @Tes t fun launch() { val view = LaunchView(paparazzi.context ) paparazzi.snapshot(view ) } }
  5. class LaunchViewTest { @get:Rul e val paparazzi = Paparazzi( )

    @Tes t fun launch() { val view = LaunchView(paparazzi.context ) paparazzi.snapshot(view ) } }
  6. @RunWith(TestParameterInjector::class ) class TestParameterInjectorTest ( @TestParameter config: Confi g )

    { enum class Config(val deviceConfig: DeviceConfig) {โ€ฆ}a enum class Theme(val themeName: String) {โ€ฆ}a @get:Rul e val paparazzi = Paparazzi(deviceConfig = config.deviceConfig ) @Tes t fun simpleWithTheme(@TestParameter theme: Theme) { val launch = paparazzi.inflate<LinearLayout>(R.layout.launch ) paparazzi.snapshot(launch, theme = theme.themeName ) } }
  7. @RunWith(TestParameterInjector::class ) class TestParameterInjectorTest ( @TestParameter config: Confi g )

    { enum class Config(val deviceConfig: DeviceConfig) { PIXEL_4(deviceConfig =aDeviceConfig.PIXEL_4) , PIXEL_5(deviceConfig =aDeviceConfig.PIXEL_5) , }a enum class Theme(val themeName: String) { LIGHT("android:Theme.Material.Light") , LIGHT_NO_ACTION_BAR("android:Theme.Material.Light.NoActionBar" ) }a @get:Rul e val paparazzi = Paparazzi(deviceConfig = config.deviceConfig ) @Tes t fun simpleWithTheme(@TestParameter theme: Theme) { val launch = paparazzi.inflate<LinearLayout>(R.layout.launch ) )
  8. @RunWith(TestParameterInjector::class ) class TestParameterInjectorTest ( @TestParameter config: Confi g )

    { enum class Config(val deviceConfig: DeviceConfig) {โ€ฆ}a enum class Theme(val themeName: String) {โ€ฆ}a @get:Rul e val paparazzi = Paparazzi(deviceConfig = config.deviceConfig ) @Tes t fun simpleWithTheme(@TestParameter theme: Theme) { val launch = paparazzi.inflate<LinearLayout>(R.layout.launch ) paparazzi.snapshot(launch, theme = theme.themeName ) } }
  9. @RunWith(TestParameterInjector::class ) class TestParameterInjectorTest ( @TestParameter config: Confi g )

    { enum class Config(val deviceConfig: DeviceConfig) {โ€ฆ}a enum class Theme(val themeName: String) {โ€ฆ}a @get:Rul e val paparazzi = Paparazzi(deviceConfig = config.deviceConfig ) @Tes t fun simpleWithTheme(@TestParameter theme: Theme) { val launch = paparazzi.inflate<LinearLayout>(R.layout.launch ) paparazzi.snapshot(launch, theme = theme.themeName ) } }
  10. class CardPreviewView ( context: Context , attrs: AttributeSet? = null

    , ) : FrameLayout(context, attrs) , Ui<CardPreviewViewModel, CardPreviewViewEvent> { override fun setModel(model: CardPreviewViewModel) { title.text = model.titl e description.text = model.descriptio n orderButton.text = model.orde r โ€ฆ cardView.render(model.cardViewModel) } }
  11. class CardPreviewView ( context: Context , attrs: AttributeSet? = null

    , ) : FrameLayout(context, attrs) , Ui<CardPreviewViewModel, CardPreviewViewEvent> { override fun setModel(model: CardPreviewViewModel) { title.text = model.titl e description.text = model.descriptio n orderButton.text = model.orde r โ€ฆ cardView.render(model.cardViewModel) } }
  12. class CardPreviewViewTest { @get:Rul e val paparazzi = Paparazzi (

    theme = "Theme.Cash.Default.Accent" , deviceConfig = DeviceConfig.PIXEL_ 5 ) @Tes t fun cardPreview() { val view = paparazzi.inflate(R.layout.card_preview_view ) view.setModel(CardPreviewViewModel(โ€ฆ) ) paparazzi.snapshot(view ) }
  13. /* * * Entry point of the Layout Library .

    * / public abstract class Bridge { public boolean init ( Map<String, String> platformProperties , File fontLocation , String nativeLibDirPath , String icuDataPath , Map<String, Map<String, Integer>> enumValueMap , ILayoutLog log) { โ€ฆ } public RenderSession createSession(SessionParams params) { โ€ฆ } }
  14. publicaResultainit(longatimeout)a { Result result = super.init(timeout) ; if (!result.isSuccess()) {

    return result ; } else { SessionParams params = (SessionParams)this.getParams() ; BridgeContext context = this.getContext() ; this.mIsAlphaChannelImage = โ€ฆ this.mLayoutBuilder = new Builder(params, context) ; this.mInflater = new BridgeInflater(context, params.getLayoutlibCallback()) ; context.setBridgeInflater(this.mInflater) ; ILayoutPullParser layoutParser = params.getLayoutDescription() ; this.mBlockParser = new BridgeXmlBlockParser ( layoutParser, context, layoutParser.getLayoutNamespace( ) ) ; return Status.SUCCESS.createResult() ; } }a
  15. public Result init(long timeout) { Result result = super.init(timeout) ;

    if (!result.isSuccess()) { return result ; } else { SessionParams params = (SessionParams)this.getParams() ; BridgeContext context = this.getContext() ; this.mIsAlphaChannelImage = โ€ฆ this.mLayoutBuilder = new Builder(params, context) ; this.mInflater = new BridgeInflater(context, params.getLayoutlibCallback()) ; context.setBridgeInflater(this.mInflater) ; ILayoutPullParser layoutParser = params.getLayoutDescription() ; this.mBlockParser = new BridgeXmlBlockParser ( layoutParser, context, layoutParser.getLayoutNamespace( ) ) ; return Status.SUCCESS.createResult() ; } }
  16. public Result init(long timeout) { Result result = super.init(timeout) ;

    if (!result.isSuccess()) { return result ; } else { SessionParams params = (SessionParams)this.getParams() ; BridgeContext context = this.getContext() ; this.mIsAlphaChannelImage = โ€ฆ this.mLayoutBuilder = new Builder(params, context) ; this.mInflater = new BridgeInflater(context, params.getLayoutlibCallback()) ; context.setBridgeInflater(this.mInflater) ; ILayoutPullParser layoutParser = params.getLayoutDescription() ; this.mBlockParser = new BridgeXmlBlockParser ( layoutParser, context, layoutParser.getLayoutNamespace( ) ) ; return Status.SUCCESS.createResult() ; } }
  17. public Result init(long timeout) { Result result = super.init(timeout) ;

    if (!result.isSuccess()) { return result ; } else { SessionParams params = (SessionParams)this.getParams() ; BridgeContext context = this.getContext() ; this.mIsAlphaChannelImage = โ€ฆ this.mLayoutBuilder = new Builder(params, context) ; this.mInflater = new BridgeInflater(context, params.getLayoutlibCallback()) ; context.setBridgeInflater(this.mInflater) ; ILayoutPullParser layoutParser = params.getLayoutDescription() ; this.mBlockParser = new BridgeXmlBlockParser ( layoutParser, context, layoutParser.getLayoutNamespace( ) ) ; return Status.SUCCESS.createResult() ; } }
  18. $ unzip cash-master-internal-debug-11892.apk Archive: cash-master-internal-debug-11892.apk inflating: AndroidManifest.xml inflating: DebugProbesKt.bin inflating:

    LICENSE_OFL inflating: LICENSE_UNICODE inflating: META-INF/services/kotlin.reflect.jvm.internal.impl.builtins.BuiltInsLoader inflating: META-INF/services/kotlin.reflect.jvm.internal.impl.resolve.ExternalOverridabilityCondition inflating: META-INF/services/kotlinx.coroutines.CoroutineExceptionHandler inflating: META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory extracting: assets/appmessage/activity-card-promo-fallback.png extracting: assets/appmessage/activity-card-promo-placeholder.png extracting: assets/appmessage/activity-card-promo.mp4 extracting: assets/appmessage/activity-fullscreen-ad.png extracting: assets/appmessage/bitcoin-activity-fallback.png extracting: assets/appmessage/bitcoin-activity-placeholder.png extracting: assets/appmessage/bitcoin-activity.mp4 extracting: assets/appmessage/bitcoin-full-screen.mp4 extracting: assets/appmessage/cage-banner.jpeg extracting: assets/appmessage/cage-ceo.jpg extracting: assets/appmessage/cage-face-off.jpg extracting: assets/appmessage/cage-feature1.jpg extracting: assets/appmessage/cage-feature2.jpg extracting: assets/appmessage/cage-feature3.jpg extracting: assets/appmessage/cage-handsome.jpg
  19. $ cd res/layout && ls -1 abc_action_bar_title_item.xml abc_action_bar_up_container.xml abc_action_menu_item_layout.xml abc_action_menu_layout.xml

    abc_action_mode_bar.xml abc_action_mode_close_item_material.xml abc_activity_chooser_view.xml abc_activity_chooser_view_list_item.xml abc_alert_dialog_button_bar_material.xml abc_alert_dialog_material.xml abc_alert_dialog_title_material.xml abc_cascading_menu_item_layout.xml abc_dialog_title_material.xml abc_expanded_menu_layout.xml abc_list_menu_item_checkbox.xml abc_list_menu_item_icon.xml abc_list_menu_item_layout.xml abc_list_menu_item_radio.xml abc_popup_menu_header_item_layout.xml abc_popup_menu_item_layout.xml abc_screen_content_include.xml abc_screen_simple.xml abc_screen_simple_overlay_action_mode.xml abc_screen_toolbar.xml
  20. $ less blockers_selection_detail_row.xml ^C^@^H^@ ^D^@^@^A^@^\^@t^A^@^@^P^@^@^@^@^@^@^@^@^A^@^@\^@^@^@^@^@^@^@^@^@^@^@^Q^@^@^@^]^@^@^@'^@^@^@5^@^@^@:^@^@^@G^@^@^@W^@^ @^@f^@^@^@v^@^@^@<86>^@^@^@<91>^@^@^@<9B>^@^@^@<A3>^@^@^@ <E2>^@^@^@^O^A^@^@^N^NtextAppearance^@ textColor^@^G^Ggravity^@^K^Korientation^@^B^Bid^@ paddingTop^@^MpaddingBottom^@^L^Llayout_width^@^Mlayout_height^@^Mlayout_weight^@^H^HTextView^@^G^Gandroid^@^E^Eclas s^@<<com.squareup.cash.blockers.views.SelectionView$DetailRowView^@**http://schemas.android.com/apk/res/

    android^@^D^Dview^@^@^@<80>^A^H^@0^@^@^@4^@^A^A<98>^@^A^A<AF>^@^A^A<C4>^@^A^A<D0>^@^A^A<D7>^@^A^A<D9>^@^A^A<F4>^@^A^ A<F5>^@^A^A<81>^A^A^A^@^A^P^@^X ^@^@^@^B^@^@^@<FF><FF><FF><FF>^K^@^@^@^N^@^@^@^B^A^P^@<9C>^@^@^@^B^@^@^@<FF><FF><FF><FF><FF><FF><FF><FF>^O^@^@^@^T^@ ^T^@^F^@^@^@^F^@^@^@^N^@^@^@^C^@^@^@<FF><FF><FF><FF>^H^@^@^P^@^@^@^@^N^@^@^@^E^@^@^@<FF><FF><FF><FF>^H^@^@^E^A^D^@^@ ^N^@^@^@^F^@^@^@<FF><FF><FF><FF>^H^@^@^E^A^D^@^@^N^@^@^@^G^@^@^@<FF><FF><FF><FF>^H^@^@^P<FF><FF><FF><FF>^N^@^@^@^H^@ ^@^@<FF><FF><FF><FF>^H^@^@^P<FF><FF><FF><FF><FF><FF><FF><FF>^L^@^@^@^M^@^@^@^H^@^@^C^M^@^@^@^B^A^P^@<B0>^@^@^@^K^@^@ ^@<FF><FF><FF><FF><FF><FF><FF><FF> ^@^@^@^T^@^T^@^G^@^D^@^@^@^@^@^N^@^@^@^@^@^@^@<FF><FF><FF><FF>^H^@^@^A<81>^A^S^? ^N^@^@^@^A^@^@^@<FF><FF><FF><FF>^H^@^@^A<CE>^B^F^? ^N^@^@^@^B^@^@^@<FF><FF><FF><FF>^H^@^@^Q^C^@<80>^@^N^@^@^@^D^@^@^@<FF><FF><FF><FF>^H^@^@^A<CE>^B ^?^N^@^@^@^G^@^@^@<FF><FF><FF><FF>^H^@^@^E^A^@^@^@^N^@^@^@^H^@^@^@<FF><FF><FF><FF>^H^@^@^P<FE><FF><FF><FF>^N^@^@^@ ^@^@^@<FF><FF><FF><FF>^H^@^@^D^@^@<80>?^C^A^P^@^X^@^@^@^K^@^@^@ <FF><FF><FF><FF><FF><FF><FF><FF> ^@^@^@^B^A^P^@<B0>^@^@^@^U^@^@^@<FF><FF><FF><FF><FF><FF><FF><FF> ^@^@^@^T^@^T^@^G^@^D^@^@^@^@^@^N^@^@^@^@^@^@^@<FF><FF><FF><FF>^H^@^@^A<81>^A^S^? ^N^@^@^@^A^@^@^@<FF><FF><FF><FF>^H^@^@^A<CE>^B^F^? ^N^@^@^@^B^@^@^@<FF><FF><FF><FF>^H^@^@^Q^E^@<80>^@^N^@^@^@^D^@^@^@<FF><FF><FF><FF>^H^@^@^A<FC>^D
  21. $ less blockers_selection_detail_row.xml ^C^@^H^@ ^D^@^@^A^@^\^@t^A^@^@^P^@^@^@^@^@^@^@^@^A^@^@\^@^@^@^@^@^@^@^@^@^@^@^Q^@^@^@^]^@^@^@'^@^@^@5^@^@^@:^@^@^@G^@^@^@W^@^ @^@f^@^@^@v^@^@^@<86>^@^@^@<91>^@^@^@<9B>^@^@^@<A3>^@^@^@ <E2>^@^@^@^O^A^@^@^N^NtextAppearance^@ textColor^@^G^Ggravity^@^K^Korientation^@^B^Bid^@ paddingTop^@^MpaddingBottom^@^L^Llayout_width^@^Mlayout_height^@^Mlayout_weight^@^H^HTextView^@^G^Gandroid^@^E^Eclas s^@<<com.squareup.cash.blockers.views.SelectionView$DetailRowView^@**http://schemas.android.com/apk/res/

    android^@^D^Dview^@^@^@<80>^A^H^@0^@^@^@4^@^A^A<98>^@^A^A<AF>^@^A^A<C4>^@^A^A<D0>^@^A^A<D7>^@^A^A<D9>^@^A^A<F4>^@^A^ A<F5>^@^A^A<81>^A^A^A^@^A^P^@^X ^@^@^@^B^@^@^@<FF><FF><FF><FF>^K^@^@^@^N^@^@^@^B^A^P^@<9C>^@^@^@^B^@^@^@<FF><FF><FF><FF><FF><FF><FF><FF>^O^@^@^@^T^@ ^T^@^F^@^@^@^F^@^@^@^N^@^@^@^C^@^@^@<FF><FF><FF><FF>^H^@^@^P^@^@^@^@^N^@^@^@^E^@^@^@<FF><FF><FF><FF>^H^@^@^E^A^D^@^@ ^N^@^@^@^F^@^@^@<FF><FF><FF><FF>^H^@^@^E^A^D^@^@^N^@^@^@^G^@^@^@<FF><FF><FF><FF>^H^@^@^P<FF><FF><FF><FF>^N^@^@^@^H^@ ^@^@<FF><FF><FF><FF>^H^@^@^P<FF><FF><FF><FF><FF><FF><FF><FF>^L^@^@^@^M^@^@^@^H^@^@^C^M^@^@^@^B^A^P^@<B0>^@^@^@^K^@^@ ^@<FF><FF><FF><FF><FF><FF><FF><FF> ^@^@^@^T^@^T^@^G^@^D^@^@^@^@^@^N^@^@^@^@^@^@^@<FF><FF><FF><FF>^H^@^@^A<81>^A^S^? ^N^@^@^@^A^@^@^@<FF><FF><FF><FF>^H^@^@^A<CE>^B^F^? ^N^@^@^@^B^@^@^@<FF><FF><FF><FF>^H^@^@^Q^C^@<80>^@^N^@^@^@^D^@^@^@<FF><FF><FF><FF>^H^@^@^A<CE>^B ^?^N^@^@^@^G^@^@^@<FF><FF><FF><FF>^H^@^@^E^A^@^@^@^N^@^@^@^H^@^@^@<FF><FF><FF><FF>^H^@^@^P<FE><FF><FF><FF>^N^@^@^@ ^@^@^@<FF><FF><FF><FF>^H^@^@^D^@^@<80>?^C^A^P^@^X^@^@^@^K^@^@^@ <FF><FF><FF><FF><FF><FF><FF><FF> ^@^@^@^B^A^P^@<B0>^@^@^@^U^@^@^@<FF><FF><FF><FF><FF><FF><FF><FF> ^@^@^@^T^@^T^@^G^@^D^@^@^@^@^@^N^@^@^@^@^@^@^@<FF><FF><FF><FF>^H^@^@^A<81>^A^S^? ^N^@^@^@^A^@^@^@<FF><FF><FF><FF>^H^@^@^A<CE>^B^F^? ^N^@^@^@^B^@^@^@<FF><FF><FF><FF>^H^@^@^Q^E^@<80>^@^N^@^@^@^D^@^@^@<FF><FF><FF><FF>^H^@^@^A<FC>^D
  22. Drawable getDrawable_Original(@DrawableRes int id, Theme theme) { return this.getDrawableForDensity(id, 0,

    theme) ; } @LayoutlibDelegat e Drawable getDrawable(@DrawableRes int id, Theme theme) { return Resources_Delegate.getDrawable(this, id, theme) ; } android.content.res.Resources (layoutlib)
  23. @LayoutlibDelegat e static Drawable getDrawable(Resources resources, int id, Theme theme)

    { Pair<String, ResourceValue> value = getResourceValue(resources, id) ; โ€ฆ return ResourceHelper.getDrawable ( value.getSecond(), getContext(resources), them e ); } android.content.res.Resources_Delegate (layoutlib)
  24. $ find ~/.gradle/caches -name layoutlib.jar | xargs jar tf |

    grep _Delegate android/animation/PropertyValuesHolder_Delegate.class android/app/Fragment_Delegate.class android/content/res/AssetManager_Delegate.class android/content/res/Resources_Delegate.class android/content/res/Resources_Theme_Delegate.class android/content/res/TypedArray_Delegate.class android/graphics/BaseCanvas_Delegate$1.class android/graphics/BaseCanvas_Delegate.class android/graphics/BitmapFactory_Delegate.class android/graphics/BitmapShader_Delegate$1.class android/graphics/BitmapShader_Delegate$BitmapShaderPaint$BitmapShaderContext.class android/graphics/BitmapShader_Delegate$BitmapShaderPaint.class android/graphics/BitmapShader_Delegate.class android/graphics/Bitmap_Delegate$BitmapCreateFlags.class android/graphics/Bitmap_Delegate.class android/graphics/BlendModeColorFilter_Delegate.class android/graphics/BlurMaskFilter_Delegate.class android/graphics/Canvas_Delegate.class android/graphics/ColorFilter_Delegate.class android/graphics/ColorMatrixColorFilter_Delegate.class android/graphics/ColorSpace_Rgb_Delegate.class android/graphics/Color_Delegate.class
  25. class Paparazzi ( private val environment: Environment , private val

    deviceConfig: DeviceConfig , private val theme: String , private val appCompatEnabled: Boolean , private val maxPercentDifference: Double , private val snapshotHandler: SnapshotHandler , private val renderExtensions: Set<RenderExtension > ) : TestRule {
  26. class Paparazzi ( private val environment: Environment(= detectEnvironment() , private

    val deviceConfig: DeviceConfig = DeviceConfig.NEXUS_5 , private val theme: String = "android:Theme.Material.NoActionBar.Fullscreen" , private val appCompatEnabled: Boolean = true , private val maxPercentDifference: Double = 0.1 , private val snapshotHandler: SnapshotHandler = determineHandler(maxPercentDifference) , private val renderExtensions: Set<RenderExtension> = setOf( ) ) : TestRule {โ€ฆ}
  27. class Paparazzi ( private val environment: Environment(= detectEnvironment() , private

    val deviceConfig: DeviceConfig = DeviceConfig.NEXUS_5 , private val theme: String = "android:Theme.Material.NoActionBar.Fullscreen" , private val appCompatEnabled: Boolean = true , private val maxPercentDifference: Double = 0.1 , private val snapshotHandler: SnapshotHandler = determineHandler(maxPercentDifference) , private val renderExtensions: Set<RenderExtension> = setOf( ) ) : TestRule {โ€ฆ}
  28. private fun determineHandler ( maxPercentDifference: Doubl e ): SnapshotHandler =

    if (isVerifying) { SnapshotVerifier(maxPercentDifference ) } else { HtmlReportWriter( ) }
  29. class Paparazzi ( private val environment: Environment(= detectEnvironment() , private

    val deviceConfig: DeviceConfig = DeviceConfig.NEXUS_5 , private val theme: String = "android:Theme.Material.NoActionBar.Fullscreen" , private val appCompatEnabled: Boolean = true , private val maxPercentDifference: Double = 0.1 , private val snapshotHandler: SnapshotHandler = determineHandler(maxPercentDifference) , private val renderExtensions: Set<RenderExtension> = setOf( ) ) : TestRule {โ€ฆ}
  30. class Paparazzi ( private val environment: Environment(= detectEnvironment() , private

    val deviceConfig: DeviceConfig = DeviceConfig.NEXUS_5 , private val theme: String = "android:Theme.Material.NoActionBar.Fullscreen" , private val appCompatEnabled: Boolean = true , private val maxPercentDifference: Double = 0.1 , private val snapshotHandler: SnapshotHandler = determineHandler(maxPercentDifference) , private val renderExtensions: Set<RenderExtension> = setOf( ) ) : TestRule {โ€ฆ}
  31. data class Environment ( val platformDir: String , val appTestDir:

    String , val resDir: String , val assetsDir: String , val packageName: String , val compileSdkVersion: Int , val platformDataDir: String , )
  32. fun detectEnvironment(): Environment { val resourcesFile = File(getProperty("paparazzi.test.resources") ) val

    configLines = resourcesFile.readLines( ) val appTestDir = Paths.get(System.getProperty("user.dir") ) val androidHome = Paths.get(androidHome() ) return Environment ( platformDir = androidHome.resolve(configLines[3]).toString() , appTestDir = appTestDir.toString() , resDir = appTestDir.resolve(configLines[1]).toString() , โ€ฆ ) }
  33. fun detectEnvironment(): Environment { val resourcesFile = File(getProperty("paparazzi.test.resources") ) val

    configLines = resourcesFile.readLines( ) val appTestDir = Paths.get(System.getProperty("user.dir") ) val androidHome = Paths.get(androidHome() ) return Environment ( platformDir = androidHome.resolve(configLines[3]).toString() , appTestDir = appTestDir.toString() , resDir = appTestDir.resolve(configLines[1]).toString() , โ€ฆ ) }
  34. $ tree prebuilts/studio/layoutlib/data/ prebuilts/studio/layoutlib/data/ โ”œโ”€โ”€ layoutlib_native.jar โ”œโ”€โ”€ linux โ”‚ โ””โ”€โ”€

    lib64 โ”‚ โ”œโ”€โ”€ libandroid_runtime.so โ”‚ โ”œโ”€โ”€ libbinder.so โ”‚ โ””โ”€โ”€ libhidlbase.so โ”œโ”€โ”€ mac โ”‚ โ””โ”€โ”€ lib64 โ”‚ โ””โ”€โ”€ libandroid_runtime.dylib โ”œโ”€โ”€ win โ”‚ โ””โ”€โ”€ lib64 โ”‚ โ”œโ”€โ”€ libandroid_runtime.dll โ”‚ โ”œโ”€โ”€ libicuuc-host.dll โ”‚ โ””โ”€โ”€ libicuuc_stubdata.dll โ€ฆ
  35. $ tree prebuilts/studio/layoutlib/data/ prebuilts/studio/layoutlib/data/ โ”œโ”€โ”€ layoutlib_native.jar โ”œโ”€โ”€ linux โ”‚ โ””โ”€โ”€

    lib64 โ”‚ โ”œโ”€โ”€ libandroid_runtime.so โ”‚ โ”œโ”€โ”€ libbinder.so โ”‚ โ””โ”€โ”€ libhidlbase.so โ”œโ”€โ”€ mac โ”‚ โ””โ”€โ”€ lib64 โ”‚ โ””โ”€โ”€ libandroid_runtime.dylib โ”œโ”€โ”€ win โ”‚ โ””โ”€โ”€ lib64 โ”‚ โ”œโ”€โ”€ libandroid_runtime.dll โ”‚ โ”œโ”€โ”€ libicuuc-host.dll โ”‚ โ””โ”€โ”€ libicuuc_stubdata.dll โ€ฆ
  36. $ tree prebuilts/studio/layoutlib/data/ prebuilts/studio/layoutlib/data/ โ”œโ”€โ”€ layoutlib_native.jar โ”œโ”€โ”€ linux โ”‚ โ””โ”€โ”€

    lib64 โ”‚ โ”œโ”€โ”€ libandroid_runtime.so โ”‚ โ”œโ”€โ”€ libbinder.so โ”‚ โ””โ”€โ”€ libhidlbase.so โ”œโ”€โ”€ mac โ”‚ โ””โ”€โ”€ lib64 โ”‚ โ””โ”€โ”€ libandroid_runtime.dylib โ”œโ”€โ”€ win โ”‚ โ””โ”€โ”€ lib64 โ”‚ โ”œโ”€โ”€ libandroid_runtime.dll โ”‚ โ”œโ”€โ”€ libicuuc-host.dll โ”‚ โ””โ”€โ”€ libicuuc_stubdata.dll โ€ฆ layoutlib-native-jdk11.jar layoutlib-native-linux.jar layoutlib-native-mac.jar layoutlib-native-win.jar
  37. $ tree ~/.gradle/caches/modules-2/files-2.1/app.cash.paparazzi/ ~/.gradle/caches/modules-2/files-2.1/app.cash.paparazzi/ โ”œโ”€โ”€ layoutlib-native-jdk11 โ”‚ โ””โ”€โ”€ 4.1.0-ae77d65 โ”‚

    โ””โ”€โ”€ 7f954f19f39672bd8802ef030d47bb269ab2ffd2 โ”‚ โ””โ”€โ”€ layoutlib-native-jdk11-4.1.0-ae77d65.jar โ”œโ”€โ”€ layoutlib-native-macosx โ”‚ โ”œโ”€โ”€ 4.1.0-ae77d65 โ”‚ โ”‚ โ””โ”€โ”€ c893f66979493e8f4249de4e825fca5975e906e8 โ”‚ โ”‚ โ””โ”€โ”€ layoutlib-native-macosx-4.1.0-ae77d65.jar โ”œโ”€โ”€ paparazzi โ”‚ โ”œโ”€โ”€ 0.8.0 โ”‚ โ”‚ โ”œโ”€โ”€ d6b8ec9f82ad120e34d7e07d3be3c909f5dd0a4 โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ paparazzi-0.8.0.jar โ”œโ”€โ”€ paparazzi-agent โ”‚ โ”œโ”€โ”€ 0.8.0 โ”‚ โ”‚ โ”œโ”€โ”€ ee13c27af0028005e42e221d576e76fdac6a6ba5 โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ paparazzi-agent-0.8.0.jar โ””โ”€โ”€ paparazzi-gradle-plugin โ”œโ”€โ”€ 0.8.0 โ”‚ โ”œโ”€โ”€ 6f4e990290c5f1c1e190f3c38ab9059785e1432d โ”‚ โ”‚ โ””โ”€โ”€ paparazzi-gradle-plugin-0.8.0.jar
  38. /* * * Entry point of the Layout Library .

    * / public abstract class Bridge { public boolean init ( Map<String, String> platformProperties , File fontLocation , String nativeLibDirPath , String icuDataPath , Map<String, Map<String, Integer>> enumValueMap , ILayoutLog log) { โ€ฆ } public RenderSession createSession(SessionParams params) { โ€ฆ } }
  39. /* * * Entry point of the Layout Library .

    * / public abstract class Bridge { public boolean init ( Map<String, String> platformProperties , File fontLocation , String nativeLibDirPath , String icuDataPath , Map<String, Map<String, Integer>> enumValueMap , ILayoutLog log) { โ€ฆ } public RenderSession createSession(SessionParams params) { โ€ฆ } }
  40. $ tree ~/.gradle/caches/transforms-3/4a38ac62d09a6fd091f06e54d5f69fe1/ transformed/layoutlib-native-macosx-4.1.0-ae77d65 ~/.gradle/caches/transforms-3/4a38ac62d09a6fd091f06e54d5f69fe1/transformed/ layoutlib-native-macosx-4.1.0-ae77d65 โ””โ”€โ”€ data โ”œโ”€โ”€ fonts

    โ”‚ โ”œโ”€โ”€ AndroidClock.ttf โ”‚ โ”œโ”€โ”€ CarroisGothicSC-Regular.ttf โ”‚ โ”œโ”€โ”€ โ€ฆ โ”œโ”€โ”€ icu โ”‚ โ””โ”€โ”€ icudt63l.dat โ””โ”€โ”€ mac โ””โ”€โ”€ lib64 โ”œโ”€โ”€ libandroid_runtime.dylib โ”œโ”€โ”€ libc++.dylib โ””โ”€โ”€ liblog.dylib
  41. ?

  42. @RunWith(TestParameterInjector::class ) class DirectDepositSetupViewTest ( @TestParameter private val theme: DesignTheme

    , @TestParameter private val a11yTextSize: AccessibilityTextSize , ) { @get:Rule val paparazzi = Paparazzi(โ€ฆ ) @Tes t fun default() { val context = paparazzi.contex t .wrapWithTheme(theme.provide ) .scaledFontSizeBy(accessibilityTextSize.scale ) val view = DirectDepositSetupView(context ) view.setModel(โ€ฆ ) paparazzi.scrollingSnapshot(view ) } }
  43. @RunWith(TestParameterInjector::class ) class DirectDepositSetupViewTest ( @TestParameter private val theme: DesignTheme

    , @TestParameter private val a11yTextSize: AccessibilityTextSize , ) { @get:Rule val paparazzi = Paparazzi(โ€ฆ ) @Tes t fun default() { val context = paparazzi.contex t .wrapWithTheme(theme.provide ) .scaledFontSizeBy(accessibilityTextSize.scale ) val view = DirectDepositSetupView(context ) view.setModel(โ€ฆ ) paparazzi.scrollingSnapshot(view ) } } enum class DesignTheme(val provide: Context.() -> ThemeInfo) { Light({ moonCakeLight() }) , Dark({ moonCakeDark() } ) } enum class AccessibilityTextSize(val scale: Float) { NORMAL(scale = 1f) , LARGE(scale = 2f) , }
  44. @RunWith(TestParameterInjector::class ) class DirectDepositSetupViewTest ( @TestParameter private val theme: DesignTheme

    , @TestParameter private val a11yTextSize: AccessibilityTextSize , ) { @get:Rule val paparazzi = Paparazzi(โ€ฆ ) @Tes t fun default() { val context = paparazzi.contex t .wrapWithTheme(theme.provide ) .scaledFontSizeBy(accessibilityTextSize.scale ) val view = DirectDepositSetupView(context ) view.setModel(โ€ฆ ) paparazzi.scrollingSnapshot(view ) } }
  45. @RunWith(TestParameterInjector::class ) class DirectDepositSetupViewTest ( @TestParameter private val theme: DesignTheme

    , @TestParameter private val a11yTextSize: AccessibilityTextSize , ) { @get:Rule val paparazzi = Paparazzi(โ€ฆ ) @Tes t fun default() { val context = paparazzi.contex t .wrapWithTheme(theme.provide ) .scaledFontSizeBy(accessibilityTextSize.scale ) val view = DirectDepositSetupView(context ) view.setModel(โ€ฆ ) paparazzi.scrollingSnapshot(view ) } }
  46. fun Paparazzi.scrollingSnapshot ( view: View , scrollableView: View = vie

    w ) { var scrollCount = 0 snapshot(view, scrollCount.toString() ) while (scrollableView.canScrollVertically(1)) { scrollableView.scrollBy(0, scrollableView.height ) scrollCount+ + check(scrollCount < 10) { "This view has been scrolled 10 times! Bad input? " } snapshot(view, name = scrollCount.toString() ) } }
  47. class Paparazzi ( private val environment: Environment(= detectEnvironment() , private

    val deviceConfig: DeviceConfig , private val theme: String , private val appCompatEnabled: Boolean , private val maxPercentDifference: Double , private val snapshotHandler: SnapshotHandler , private val renderExtensions: Set<RenderExtension > ) : TestRule {โ€ฆ}
  48. class Paparazzi ( private val environment: Environment(= detectEnvironment() , private

    val deviceConfig: DeviceConfig , private val theme: String , private val appCompatEnabled: Boolean , private val maxPercentDifference: Double , private val snapshotHandler: SnapshotHandler , private val renderExtensions: Set <โ€‹ RenderExtensio nโ€‹ > ) : TestRulea{โ€ฆ}a
  49. /* * * An extension for overlaying additional information on

    top of * each rendered frame . * / interface RenderExtensio nโ€‹ { fun render ( snapshot: Snapshot, view: View, image: BufferedImag e ): BufferedImag e }
  50. /* * * An extension for overlaying additional information on

    top of * each rendered frame . * / interface RenderExtension { fun render ( snapshot: Snapshot, view: View, image: BufferedImag e ): BufferedImag e } frameHandler.handle(scaleImage(bridgeRenderSession.image))
  51. /* * * An extension for overlaying additional information on

    top of * each rendered frame . * / interface RenderExtension { fun render ( snapshot: Snapshot, view: View, image: BufferedImag e ): BufferedImag e } var image = bridgeRenderSession.imag e renderExtensions.forEach { image = it.render(snapshot, view, image ) } frameHandler.handle(scaleImage(image))
  52. class AccessibilityTest { @get:Rul e val paparazzi = Paparazzi()a @Tes

    t fun test() { val view = buildView(paparazzi.context ) paparazzi.snapshot(view ) } }
  53. class AccessibilityTest { @get:Rul e val paparazzi = Paparazzi (

    renderExtensions = setOf(AccessibilityRenderExtension() ) )a @Tes t fun test() { val view = buildView(paparazzi.context ) paparazzi.snapshot(view ) } }
  54. fun registerMatrixMultiplyInterception() { val matrixClass = Class.forName("android.opengl.Matrix" ) InterceptorRegistrar.addMethodInterceptors (

    matrixClass , setOf ( "multiplyMM" to MatrixMatrixMultiplicationInterceptor::class.java , "multiplyMV" to MatrixVectorMultiplicationInterceptor::class.jav a ) ) }
  55. fun registerMatrixMultiplyInterception() { val matrixClass = Class.forName("android.opengl.Matrix" ) InterceptorRegistrar.addMethodInterceptors (

    matrixClass , setOf ( "multiplyMM" to MatrixMatrixMultiplicationInterceptor::class.java , "multiplyMV" to MatrixVectorMultiplicationInterceptor::class.jav a ) ) }
  56. fun registerMatrixMultiplyInterception() { val matrixClass = Class.forName("android.opengl.Matrix" ) InterceptorRegistrar.addMethodInterceptors (

    matrixClass , setOf ( "multiplyMM" to MatrixMatrixMultiplicationIntercepto rโ€‹ ::class.java , "multiplyMV" to MatrixVectorMultiplicationInterceptor::class.jav a ) ) }
  57. object MatrixMatrixMultiplicationIntercepto rโ€‹ { fun intercept ( result: FloatArray, resultOffset:

    Int , lhs: FloatArray, lhsOffset: Int , rhs: FloatArray, rhsOffset: In t ) { for (i in 0..3) { val rhs_i0 = rhs[I(i, 0, rhsOffset) ] var ri0 = lhs[I(0, 0, lhsOffset)] * rhs_i 0 โ€ฆ for (j in 1..3) { val rhs_ij = rhs[I(i, j, rhsOffset) ] ri0 += lhs[I(j, 0, lhsOffset)] * rhs_i j โ€ฆ } result[I(i, 0, resultOffset)] = ri 0 โ€ฆ } } }
  58. /* * * See : * https://github.com/cashapp/paparazzi/issues/11 9 * https://issuetracker.google.com/issues/15606547

    2 * / private fun registerFontLookupInterceptionIfResourceCompatDetected() { val clz = Class.forName("androidx.core.content.res.ResourcesCompat" ) InterceptorRegistrar.addMethodInterceptor ( clz, "getFont", ResourcesInterceptor::class.jav a ) }
  59. /* * * See : * https://github.com/cashapp/paparazzi/issues/11 9 * https://issuetracker.google.com/issues/15606547

    2 * / private fun registerFontLookupInterceptionIfResourceCompatDetected() { val clz = Class.forName("androidx.core.content.res.ResourcesCompat" ) InterceptorRegistrar.addMethodInterceptor ( clz, "getFont", ResourcesInterceptor::class.jav a ) } private fun registerViewEditModeInterception() { val clz = Class.forName("android.view.View" ) InterceptorRegistrar.addMethodInterceptor ( clz, "isInEditMode", EditModeInterceptor::class.jav a ) }