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

Keeping your Pixels perfect ๐Ÿ“ธ

Keeping your Pixels perfectย ๐Ÿ“ธ

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!

Video: https://www.droidcon.com/media-detail?video=362742507

6c8b509fe5422470d148c2c4cf2eb4b0?s=128

John Rodriguez

August 27, 2019
Tweet

Transcript

  1. None
  2. None
  3. None
  4. None
  5. None
  6. None
  7. None
  8. None
  9. None
  10. None
  11. None
  12. None
  13. None
  14. None
  15. None
  16. None
  17. None
  18. None
  19. None
  20. None
  21. None
  22. None
  23. None
  24. None
  25. None
  26. None
  27. None
  28. None
  29. None
  30. None
  31. None
  32. None
  33. None
  34. None
  35. None
  36. <LinearLayout android:orientation="vertical" android:gravity="center" android:background="@color/launchBackground" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:layout_width="200dp" android:layout_height="200dp" android:src="@drawable/camera"

    /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:textColor="@color/cameraBody" android:text="paparazzi" android:textSize="70sp" android:textFontWeight="100" />
  37. <LinearLayout android:orientation="vertical" android:gravity="center" android:background="@color/launchBackground" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:layout_width="200dp" android:layout_height="200dp" android:src="@drawable/camera"

    /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:textColor="@color/cameraBody" android:text="paparazzi" android:textSize="70sp" android:textFontWeight="100" />
  38. dependencies { implementation deps.kotlin.stdlib.jdk8 }a

  39. dependencies { implementation deps.kotlin.stdlib.jdk8 api project(':paparazzi') }a

  40. class LaunchViewTest { @get:Rule var paparazzi = Paparazzi("app.cash.paparazzi.sample") @Test fun

    testViews() { val launch = paparazzi.inflate<LinearLayout>(R.layout.launch) paparazzi.snapshot(launch, "launch") } }
  41. sample/ โ”œโ”€โ”€ build.gradle โ””โ”€โ”€ src โ”œโ”€โ”€ main โ”‚ โ”œโ”€โ”€ AndroidManifest.xml

    โ”‚ โ””โ”€โ”€ res โ”‚ โ”œโ”€โ”€ drawable โ”‚ โ”‚ โ””โ”€โ”€ camera.png โ”‚ โ”œโ”€โ”€ layout โ”‚ โ”‚ โ””โ”€โ”€ launch.xml โ”‚ โ””โ”€โ”€ values โ”‚ โ””โ”€โ”€ colors.xml โ””โ”€โ”€ test โ””โ”€โ”€ java โ””โ”€โ”€ app โ””โ”€โ”€ cash โ””โ”€โ”€ paparazzi โ””โ”€โ”€ sample โ””โ”€โ”€ LaunchViewTest.kt
  42. sample/ โ””โ”€โ”€ build โ””โ”€โ”€ reports โ””โ”€โ”€ paparazzi โ”œโ”€โ”€ images โ”‚

    โ””โ”€โ”€ 215bfbc973787f8c7e63650970fefc19fdd3399d.png โ”œโ”€โ”€ index.html โ”œโ”€โ”€ index.js โ”œโ”€โ”€ paparazzi.js โ””โ”€โ”€ runs โ””โ”€โ”€ 20190826232155_06ba49.js
  43. sample/ โ””โ”€โ”€ build โ””โ”€โ”€ reports โ””โ”€โ”€ paparazzi โ”œโ”€โ”€ images โ”‚

    โ””โ”€โ”€ 215bfbc973787f8c7e63650970fefc19fdd3399d.png โ”œโ”€โ”€ index.html โ”œโ”€โ”€ index.js โ”œโ”€โ”€ paparazzi.js โ””โ”€โ”€ runs โ””โ”€โ”€ 20190826232155_06ba49.js
  44. None
  45. None
  46. None
  47. None
  48. None
  49. None
  50. None
  51. None
  52. public Drawable getDrawable(@DrawableRes int id, Theme t) { return getDrawableForDensity(id,

    0, t); } android.content.res.Resources (android-28)
  53. public Drawable getDrawable_Original(@DrawableRes int id, Theme t) { return getDrawableForDensity(id,

    0, t); } @LayoutlibDelegate public Drawable getDrawable(@DrawableRes int id, Theme t) { return Resources_Delegate.getDrawable(id, 0, t); } android.content.res.Resources (layoutlib)
  54. @LayoutlibDelegate static Drawable getDrawable(Resources resources, int id, Theme theme) {

    Pair<String, ResourceValue> value = getResourceValue(resources, id); if (value != null) { return ResourceHelper.getDrawable(value.second, getContext(resources), theme); } throwException(resources, id); } android.content.res.Resources_Delegate (layoutlib)
  55. $ find $HOME/.gradle/caches/ -name layoutlib-3.4.0.jar | xargs jar tf |

    grep _Delegate 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/BitmapFactory_Delegate.class android/graphics/Bitmap_Delegate.class android/graphics/Canvas_Delegate.class android/graphics/Color_Delegate.class android/graphics/FontFamily_Delegate.class android/graphics/Matrix_Delegate.class android/graphics/Paint_Delegate.class android/graphics/Typeface_Delegate.class android/graphics/drawable/AnimatedVectorDrawable_Delegate.class android/graphics/drawable/VectorDrawable_Delegate.class android/os/Binder_Delegate.class android/os/Handler_Delegate.class android/os/SystemClock_Delegate.class android/os/SystemProperties_Delegate.class
  56. android/graphics/drawable/AnimatedVectorDrawable_Delegate.class android/graphics/drawable/VectorDrawable_Delegate.class android/os/Binder_Delegate.class android/os/Handler_Delegate.class android/os/SystemClock_Delegate.class android/os/SystemProperties_Delegate.class android/text/format/DateFormat_Delegate.class android/util/Log_Delegate.class android/view/Choreographer_Delegate.class android/view/Display_Delegate.class

    android/view/LayoutInflater_Delegate.class android/view/MenuInflater_Delegate.class android/view/ViewGroup_Delegate.class android/view/ViewRootImpl_Delegate.class android/view/View_Delegate.class android/view/WindowManagerGlobal_Delegate.class com/android/tools/layoutlib/java/LinkedHashMap_Delegate.class com/android/tools/layoutlib/java/System_Delegate.class dalvik/system/VMRuntime_Delegate.class libcore/icu/ICU_Delegate.class libcore/io/MemoryMappedFile_Delegate.class libcore/util/NativeAllocationRegistry_Delegate.class
  57. public final class Bridge extends com.android.ide.common.rendering.api.Bridge { public boolean init(

    Map<String, String> platformProperties, File fontLocation, String icuDataPath, Map<String, Map<String, Integer>> enumValueMap, LayoutLog log ) {โ€ฆ} public RenderSession createSession(SessionParams params) {โ€ฆ} } layoutlib
  58. public RenderSession createSession(SessionParams params) { RenderSessionImpl scene = new RenderSessionImpl(params);

    Result lastResult; try { prepareThread(); lastResult = scene.init(params.getTimeout()); if (lastResult.isSuccess()) { lastResult = scene.inflate(); boolean doNotRenderOnCreate = โ€ฆ if (lastResult.isSuccess() && !doNotRenderOnCreate) { lastResult = scene.render(true); } } } finally { scene.release(); cleanupThread(); } return new BridgeRenderSession(scene, lastResult); }
  59. public RenderSession createSession(SessionParams params) { RenderSessionImpl scene = new RenderSessionImpl(params);

    Result lastResult; try { prepareThread(); lastResult = scene.init(params.getTimeout()); if (lastResult.isSuccess()) { lastResult = scene.inflate(); boolean doNotRenderOnCreate = โ€ฆ if (lastResult.isSuccess() && !doNotRenderOnCreate) { lastResult = scene.render(true); } } } finally { scene.release(); cleanupThread(); } return new BridgeRenderSession(scene, lastResult); }
  60. 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(); mInflater = BridgeInflater(context, params.layoutlibCallback); context.setBridgeInflater(this.mInflater); ILayoutPullParser layoutParser = params.getLayoutDescription(); mBlockParser = BridgeXmlBlockParser(layoutParser, context, โ€ฆ); return Status.SUCCESS.createResult(); } }
  61. 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(); mInflater = BridgeInflater(context, params.layoutlibCallback); context.setBridgeInflater(this.mInflater); ILayoutPullParser layoutParser = params.getLayoutDescription(); mBlockParser = BridgeXmlBlockParser(layoutParser, context, โ€ฆ); return Status.SUCCESS.createResult(); } }
  62. public abstract class RenderAction<T extends RenderParams> { public Result init(long

    timeout) { HardwareConfig hardwareConfig = mParams.getHardwareConfig(); DisplayMetrics metrics = new DisplayMetrics(); metrics.densityDpi = metrics.noncompatDensityDpi = โ€ฆ metrics.density = metrics.noncompatDensity = โ€ฆ RenderResources resources = this.mParams.getResources(); this.mContext = new BridgeContext(this.mParams); this.setUp(); return Status.SUCCESS.createResult(); } }
  63. public abstract class RenderAction<T extends RenderParams> { public Result init(long

    timeout) { HardwareConfig hardwareConfig = mParams.getHardwareConfig(); DisplayMetrics metrics = new DisplayMetrics(); metrics.densityDpi = metrics.noncompatDensityDpi = โ€ฆ metrics.density = metrics.noncompatDensity = โ€ฆ RenderResources resources = this.mParams.getResources(); this.mContext = new BridgeContext(this.mParams); this.setUp(); return Status.SUCCESS.createResult(); } }
  64. public abstract class RenderAction<T extends RenderParams> { public Result init(long

    timeout) { HardwareConfig hardwareConfig = mParams.getHardwareConfig(); DisplayMetrics metrics = new DisplayMetrics(); metrics.densityDpi = metrics.noncompatDensityDpi = โ€ฆ metrics.density = metrics.noncompatDensity = โ€ฆ RenderResources resources = this.mParams.getResources(); this.mContext = new BridgeContext(this.mParams); this.setUp(); return Status.SUCCESS.createResult(); } }
  65. 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(); mInflater = BridgeInflater(context, params.layoutlibCallback); context.setBridgeInflater(this.mInflater); ILayoutPullParser layoutParser = params.getLayoutDescription(); mBlockParser = BridgeXmlBlockParser(layoutParser, context, โ€ฆ); return Status.SUCCESS.createResult(); } }
  66. 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(); mInflater = BridgeInflater(context, params.layoutlibCallback); context.setBridgeInflater(this.mInflater); ILayoutPullParser layoutParser = params.getLayoutDescription(); mBlockParser = BridgeXmlBlockParser(layoutParser, context, โ€ฆ); return Status.SUCCESS.createResult(); } }
  67. $ unzip cash-master-internal-debug-6765.apk Archive: cash-master-internal-debug-6765.apk inflating: AndroidManifest.xml inflating: LICENSE_OFL inflating:

    LICENSE_UNICODE inflating: META-INF/CERT.RSA inflating: META-INF/CERT.SF inflating: META-INF/MANIFEST.MF 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/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 โ€ฆ
  68. $ cd res/layout $ ls -1 activity_app_message_video_view.xml activity_check_payment_status_dialog.xml activity_contact_empty_view.xml activity_contact_empty_view_header_view.xml

    activity_contact_header.xml activity_contact_item.xml activity_header_view.xml activity_header_view_tab.xml activity_invite.xml activity_invite_item.xml activity_item.xml activity_payment_view.xml activity_receipt_detail_button.xml activity_receipt_detail_item.xml activity_receipt_details_sheet.xml โ€ฆ
  69. $ less blockers_selection_detail_row.xml ^C^@^H^@^\^D^@^@^A^@^\^@p^A^@^@^P^@^@^@^@^@^@^@^@^A^@^@\^@^@^@^@^@^@^@ ^@^@^@^@^Q^@^@^@^]^@^@^@'^@^@^@5^@^@^@:^@^@^@G^@^@^@W^@^@^@f^@^@^@v^@^ @^@<86>^@^@^@<91>^@^@^@<9B>^@^@^@<A3>^@^@^@ <DF>^@^@^@^L^A^@^@^N^NtextAppearance^@ textColor^@^G^Ggravity^@^K^Korientation^@^B^Bid^@ paddingTop^@^MpaddingBottom^@^L^Llayout_width^@^Mlayout_height^@^Mlayo ut_weight^@^H^HTextView^@^G^Gandroid^@^E^Eclass^@99com.squareup.cash.u

    i.blockers.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>B^F^? ^N^@^@^@^B^@^@^@<FF><FF><FF><FF>^H^@^@^Q^C^@<80>^@^N^@^@^@^D^@^@^@<FF>
  70. $ less blockers_selection_detail_row.xml ^C^@^H^@^\^D^@^@^A^@^\^@p^A^@^@^P^@^@^@^@^@^@^@^@^A^@^@\^@^@^@^@^@^@^@ ^@^@^@^@^Q^@^@^@^]^@^@^@'^@^@^@5^@^@^@:^@^@^@G^@^@^@W^@^@^@f^@^@^@v^@^ @^@<86>^@^@^@<91>^@^@^@<9B>^@^@^@<A3>^@^@^@ <DF>^@^@^@^L^A^@^@^N^NtextAppearance^@ textColor^@^G^Ggravity^@^K^Korientation^@^B^Bid^@ paddingTop^@^MpaddingBottom^@^L^Llayout_width^@^Mlayout_height^@^Mlayo ut_weight^@^H^HTextView^@^G^Gandroid^@^E^Eclass^@99com.squareup.cash.u

    i.blockers.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>B^F^? ^N^@^@^@^B^@^@^@<FF><FF><FF><FF>^H^@^@^Q^C^@<80>^@^N^@^@^@^D^@^@^@<FF>
  71. $ less blockers_selection_detail_row.xml ^C^@^H^@^\^D^@^@^A^@^\^@p^A^@^@^P^@^@^@^@^@^@^@^@^A^@^@\^@^@^@^@^@^@^@ ^@^@^@^@^Q^@^@^@^]^@^@^@'^@^@^@5^@^@^@:^@^@^@G^@^@^@W^@^@^@f^@^@^@v^@^ @^@<86>^@^@^@<91>^@^@^@<9B>^@^@^@<A3>^@^@^@ <DF>^@^@^@^L^A^@^@^N^NtextAppearance^@ textColor^@^G^Ggravity^@^K^Korientation^@^B^Bid^@ paddingTop^@^MpaddingBottom^@^L^Llayout_width^@^Mlayout_height^@^Mlayo ut_weight^@^H^HTextView^@^G^Gandroid^@^E^Eclass^@99com.squareup.cash.u

    i.blockers.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>B^F^? ^N^@^@^@^B^@^@^@<FF><FF><FF><FF>^H^@^@^Q^C^@<80>^@^N^@^@^@^D^@^@^@<FF>
  72. $ less blockers_selection_detail_row.xml ^C^@^H^@^\^D^@^@^A^@^\^@p^A^@^@^P^@^@^@^@^@^@^@^@^A^@^@\^@^@^@^@^@^@^@ ^@^@^@^@^Q^@^@^@^]^@^@^@'^@^@^@5^@^@^@:^@^@^@G^@^@^@W^@^@^@f^@^@^@v^@^ @^@<86>^@^@^@<91>^@^@^@<9B>^@^@^@<A3>^@^@^@ <DF>^@^@^@^L^A^@^@^N^NtextAppearance^@ textColor^@^G^Ggravity^@^K^Korientation^@^B^Bid^@ paddingTop^@^MpaddingBottom^@^L^Llayout_width^@^Mlayout_height^@^Mlayo ut_weight^@^H^HTextView^@^G^Gandroid^@^E^Eclass^@99com.squareup.cash.u

    i.blockers.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>B^F^? ^N^@^@^@^B^@^@^@<FF><FF><FF><FF>^H^@^@^Q^C^@<80>^@^N^@^@^@^D^@^@^@<FF>
  73. 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(); mInflater = BridgeInflater(context, params.layoutlibCallback); context.setBridgeInflater(this.mInflater); ILayoutPullParser layoutParser = params.getLayoutDescription(); mBlockParser = BridgeXmlBlockParser(layoutParser, context, โ€ฆ); return Status.SUCCESS.createResult(); } }
  74. public RenderSession createSession(SessionParams params) { RenderSessionImpl scene = new RenderSessionImpl(params);

    Result lastResult; try { prepareThread(); lastResult = scene.init(params.getTimeout()); if (lastResult.isSuccess()) { lastResult = scene.inflate(); boolean doNotRenderOnCreate = โ€ฆ if (lastResult.isSuccess() && !doNotRenderOnCreate) { lastResult = scene.render(true); } } } finally { scene.release(); cleanupThread(); } return new BridgeRenderSession(scene, lastResult); }
  75. public RenderSession createSession(SessionParams params) { RenderSessionImpl scene = new RenderSessionImpl(params);

    Result lastResult; try { prepareThread(); lastResult = scene.init(params.getTimeout()); if (lastResult.isSuccess()) { lastResult = scene.inflate(); boolean doNotRenderOnCreate = โ€ฆ if (lastResult.isSuccess() && !doNotRenderOnCreate) { lastResult = scene.render(true); } } } finally { scene.release(); cleanupThread(); } return new BridgeRenderSession(scene, lastResult); }
  76. public RenderSession createSession(SessionParams params) { RenderSessionImpl scene = new RenderSessionImpl(params);

    Result lastResult; try { prepareThread(); lastResult = scene.init(params.getTimeout()); if (lastResult.isSuccess()) { lastResult = scene.inflate(); boolean doNotRenderOnCreate = โ€ฆ if (lastResult.isSuccess() && !doNotRenderOnCreate) { lastResult = scene.render(true); } } } finally { scene.release(); cleanupThread(); } return new BridgeRenderSession(scene, lastResult); }
  77. Recap: layoutlib

  78. โ€ข layoutlib_create -> android1.jar android2.jar โ€ฆ Recap: layoutlib

  79. โ€ข layoutlib_create -> android1.jar android2.jar โ€ฆ โ€ข *_Delegate Recap: layoutlib

  80. โ€ข layoutlib_create -> android1.jar android2.jar โ€ฆ โ€ข *_Delegate โ€ข Bridge

    Recap: layoutlib
  81. โ€ข layoutlib_create -> android1.jar android2.jar โ€ฆ โ€ข *_Delegate โ€ข Bridge

    โ€ข RenderSession Recap: layoutlib
  82. None
  83. None
  84. None
  85. None
  86. None
  87. $ ./gradlew paparazzi:dependencies --configuration compileClasspath > Task :paparazzi:dependencies ------------------------------------------------------------ Project

    :paparazzi โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€” compileClasspath - Compile classpath for compilation 'main'. +--- app.cash.paparazzi:layoutlib:3.4.0 +--- com.android.tools:sdk-common:26.4.2 | +--- com.android.tools:sdklib:26.4.2 | | +--- com.android.tools.layoutlib:layoutlib-api:26.4.2 (*) | | +--- com.android.tools:common:26.4.2 (*) | | +--- net.sf.kxml:kxml2:2.3.0 โ€ฆ โ€ฆ โ€ฆ
  88. $ ./gradlew paparazzi:dependencies --configuration compileClasspath > Task :paparazzi:dependencies ------------------------------------------------------------ Project

    :paparazzi โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€” compileClasspath - Compile classpath for compilation 'main'. +--- app.cash.paparazzi:layoutlib:3.4.0 +--- com.android.tools:sdk-common:26.4.2 | +--- com.android.tools:sdklib:26.4.2 | | +--- com.android.tools.layoutlib:layoutlib-api:26.4.2 (*) | | +--- com.android.tools:common:26.4.2 (*) | | +--- net.sf.kxml:kxml2:2.3.0 โ€ฆ โ€ฆ โ€ฆ
  89. abstract class LayoutlibCallback { abstract Object loadView( String name, Class[]

    ctorSignature, Object[] ctorArgs); abstract ResourceReference resolveResourceId(int id); โ€ฆ } com.android.tools.layoutlib:layoutlib-api
  90. /** Common interface for all Android resource repositories. */ public

    interface ResourceRepository { List<ResourceItem> getResources( ResourceNamespace namespace, ResourceType resourceType, String resourceName); โ€ฆ } com.android.tools:sdk-common
  91. public class ResourceResolver { ResourceResolver create(Map<ResourceNamespace, Map<โ€ฆ>> res, โ€ฆ) {

    โ€ฆ ResourceResolver resolver = new ResourceResolver(res, theme); resolver.preProcessStyles(); return resolver; } ResourceValue findResource(ResourceReference ref) { ResourceValueMap resourceValueMap = getResourceValueMap(ref.getNamespace(), ref.getResourceType()); if (resourceValueMap != null) { return resourceValueMap.get(reference.getName()); } return null; } } com.android.tools:sdk-common
  92. class Paparazzi( private val packageName: String, private val environment: Environment,

    private val snapshotHandler: SnapshotHandler ) : TestRule
  93. class Paparazzi( private val packageName: String, private val environment: Environment,

    private val snapshotHandler: SnapshotHandler ) : TestRule data class Environment( val platformDir: String, val testDir: String ) { val testResDir = "$testDir/app/build/intermediates/classes/โ€ฆ" val assetsDir = "$testDir/src/main/assets" val resDir = "$testDir/src/main/res" }
  94. class Paparazzi( private val packageName: String, private val environment: Environment

    = detectEnvironment(), private val snapshotHandler: SnapshotHandler ) : TestRule data class Environment( val platformDir: String, val testDir: String ) {โ€ฆ} fun detectEnvironment(): Environment { val userDir = System.getProperty("user.dir") val userHome = System.getProperty("user.home") val androidHome = System.getenv("ANDROID_HOME") ?: "$userHome/Library/Android/sdk" val platformDir = "$androidHome/platforms/android-โ€ฆ/" return Environment(platformDir, userDir) }
  95. class Paparazzi( private val packageName: String, private val environment: Environment

    = detectEnvironment(), private val snapshotHandler: SnapshotHandler ) : TestRule {โ€ฆ}
  96. class Paparazzi( private val packageName: String, private val environment: Environment

    = detectEnvironment(), private val snapshotHandler: SnapshotHandler = HtmlReportWriter() ) : TestRule {โ€ฆ}
  97. class Paparazzi( private val packageName: String, private val environment: Environment,

    private val snapshotHandler: SnapshotHandler ) : TestRule { fun prepare(description: Description) {โ€ฆ} }
  98. fun prepare(description: Description) {โ€ฆ}

  99. fun prepare(description: Description) { val layoutlibCallback = PaparazziCallback(logger, packageName) layoutlibCallback.initResources()

    }
  100. $ find sample/ -name "R*" sample/build/intermediates/โ€ฆ/debug/generateDebugRFile/R.jar sample/build/intermediates/symbols/debug/R.txt $ JAR=sample/build/intermediates/โ€ฆ/debug/generateDebugRFile/R.jar $

    javap -classpath $JAR $(jar -tf $JAR | grep "class$" | sed s/ \.class$//) public final class app.cash.paparazzi.sample.R$color { public static int cameraBody; public static int launchBackground; } public final class app.cash.paparazzi.sample.R$drawable { public static int camera; }
  101. fun prepare(description: Description) { val layoutlibCallback = PaparazziCallback(logger, packageName) layoutlibCallback.initResources()

    }
  102. fun prepare(description: Description) { val layoutlibCallback = PaparazziCallback(logger, packageName) layoutlibCallback.initResources()

    renderer = Renderer(environment, layoutlibCallback, logger) val sessionParamsBuilder = renderer.prepare() }
  103. fun prepare(description: Description) { val layoutlibCallback = PaparazziCallback(logger, packageName) layoutlibCallback.initResources()

    renderer = Renderer(environment, layoutlibCallback, logger) val sessionParamsBuilder = renderer.prepare() val root = "<FrameLayoutโ€ฆ/>" val sessionParams = sessionParamsBuilder .copy( layoutPullParser = LayoutPullParser.createFromString(root), renderingMode = SessionParams.RenderingMode.V_SCROLL ) .withTheme("Theme.Material.Light.NoActionBar.Fullscreen", false) .build() }
  104. fun prepare(description: Description) { val layoutlibCallback = PaparazziCallback(logger, packageName) layoutlibCallback.initResources()

    renderer = Renderer(environment, layoutlibCallback, logger) val sessionParamsBuilder = renderer.prepare() val root = "<FrameLayoutโ€ฆ/>" val sessionParams = sessionParamsBuilder .copy( layoutPullParser = LayoutPullParser.createFromString(root), renderingMode = SessionParams.RenderingMode.V_SCROLL ) .withTheme("Theme.Material.Light.NoActionBar.Fullscreen", false) .build() renderSession = RenderSessionImpl(sessionParams) prepareThread() renderSession.init(sessionParams.timeout) bridgeRenderSession = createBridgeSession(renderSession, renderSession.inflate()) }
  105. class LaunchViewTest { @get:Rule var paparazzi = Paparazzi("app.cash.paparazzi.sample") }

  106. class LaunchViewTest { @get:Rule var paparazzi = Paparazzi("app.cash.paparazzi.sample") @Test fun

    testViews() { val launch = paparazzi.inflate<LinearLayout>(R.layout.launch) paparazzi.snapshot(launch, "launch") } }
  107. fun snapshot(view: View, name: String) { val snapshot = Snapshot(name,

    testName, Date()) val frameHandler = snapshotHandler.newFrameHandler(snapshot) frameHandler.use { val viewGroup = bridgeRenderSession.rootViews[0].viewObject viewGroup.addView(view) try { withTime(0) { renderSession.render(true) frameHandler.handle(scaleImage(bridgeRenderSession.image)) } } finally { viewGroup.removeView(view) } } }
  108. None
  109. sample/ โ”œโ”€โ”€ build.gradle โ””โ”€โ”€ src โ”œโ”€โ”€ main โ”‚ โ”œโ”€โ”€ AndroidManifest.xml

    โ”‚ โ””โ”€โ”€ res โ”‚ โ”œโ”€โ”€ drawable โ”‚ โ”‚ โ””โ”€โ”€ camera.png โ”‚ โ”œโ”€โ”€ layout โ”‚ โ”‚ โ””โ”€โ”€ launch.xml โ”‚ โ””โ”€โ”€ values โ”‚ โ””โ”€โ”€ colors.xml โ””โ”€โ”€ test โ””โ”€โ”€ java โ””โ”€โ”€ app โ””โ”€โ”€ cash โ””โ”€โ”€ paparazzi โ””โ”€โ”€ sample โ””โ”€โ”€ LaunchViewTest.kt
  110. โ”œโ”€โ”€ resources โ”‚ โ”œโ”€โ”€ build.gradle โ”‚ โ””โ”€โ”€ src โ”‚ โ””โ”€โ”€

    main โ”‚ โ”œโ”€โ”€ AndroidManifest.xml โ”‚ โ””โ”€โ”€ res โ”‚ โ”œโ”€โ”€ drawable โ”‚ โ”‚ โ””โ”€โ”€ camera.png โ”‚ โ””โ”€โ”€ values โ”‚ โ””โ”€โ”€ colors.xml โ””โ”€โ”€ sample โ””โ”€โ”€ โ€ฆ
  111. <LinearLayout android:orientation="vertical" android:gravity="center" android:background="@color/launchBackground" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:layout_width="200dp" android:layout_height="200dp" android:src="@drawable/camera"

    /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:textColor="@color/cameraBody" android:text="paparazzi" android:textSize="70sp" android:textFontWeight="100" />
  112. <LinearLayout android:orientation="vertical" android:gravity="center" android:background="@color/launchBackground" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:layout_width="200dp" android:layout_height="200dp" android:src="@drawable/camera"

    /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:textColor="@color/cameraBody" android:text="paparazzi" android:textSize="70sp" android:textFontWeight="100" />
  113. <LinearLayout android:orientation="vertical" android:gravity="center" android:background="@color/launchBackground" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:layout_width="200dp" android:layout_height="200dp" android:src="@drawable/camera"

    /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:textColor="@color/cameraBody" android:text="paparazzi" android:textSize="70sp" android:textFontWeight="100" />
  114. None
  115. class Paparazzi( private val packageName: String, private val environment: Environment,

    private val snapshotHandler: SnapshotHandler ) : TestRule data class Environment( val platformDir: String, val testDir: String ) { val testResDir = "$testDir/app/build/intermediates/classes/โ€ฆ" val assetsDir = "$testDir/src/main/assets" val resDir = "$testDir/src/main/res" }
  116. class Paparazzi( private val packageName: String, private val environment: Environment,

    private val snapshotHandler: SnapshotHandler ) : TestRule data class Environment( val platformDir: String, val testDir: String ) { val testResDir = "$testDir/app/build/intermediates/classes/โ€ฆ" val assetsDir = "$testDir/src/main/assets" val resDir = "$testDir/src/main/res" }
  117. internal class Renderer( private val env: Environment, โ€ฆ ) :

    Closeable { โ€ฆ fun prepare(): SessionParamsBuilder { โ€ฆ val projectRes = object : ResourceRepository(env.resDir, false) { override fun createResourceItem(name: String): ResourceItem { return ResourceItem(name) } }
  118. None
  119. None
  120. None
  121. AGP mergeResources PP preparePaparazziResources AGP testDebugUnitTest

  122. AGP mergeResources PP preparePaparazziResources AGP testDebugUnitTest "build/intermediates/res/merged/debug"

  123. AGP mergeResources PP preparePaparazziResources AGP testDebugUnitTest "build/intermediates/paparazzi/resources.txt"

  124. AGP mergeResources PP preparePaparazziResources AGP testDebugUnitTest

  125. dependencies { implementation project('app.cash.paparazzi :paparazzi:TODO') } /PROJECT/build.gradle

  126. buildscript { dependencies { classpath 'app.cash.paparazzi:paparazzi-gradle-plugin:TODO' } } /build.gradle apply

    plugin: 'app.cash.paparazzi ' /PROJECT/build.gradle
  127. None
  128. None
  129. class BitcoinUiTest { @get:Rule var paparazzi = Paparazzi("com.squareup.cash.investing.sample") @Test fun

    investingGraphSample() { val sample = paparazzi.inflate<LinearLayout>(R.layout.graph_sample) val graphView = sample.findViewById<InvestingGraphView>(R.id.graph) graphView.render(DEFAULT_MODEL) paparazzi.snapshot(graphSample, "graph sample") } }
  130. class InvestingGraphView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null,

    defStyleAttr: Int = 0 ) : LinearLayout(context, attrs, defStyleAttr) { private val sparkView: SparkView by bindView(R.id.spark_view) private val eventLabel: TextView by bindView(R.id.spark_event_label) private val styler = InvestingGraphStyler( ContextCompat.getColor(context, R.color.investing_graph_line_color) ) private val graphAdapter = InvestingGraphAdapter(styler) var scrubListener: ((point: Point?) -> Unit)? = null var isSmoothed: Boolean get() = graphAdapter.smoothData set(value) { graphAdapter.smoothData = value } init { orientation = VERTICAL inflate(context, R.layout.investing_graph, this)
  131. fun render(viewModel: InvestingGraphContentModel) { when (viewModel) { is Error ->

    TODO() is Loading -> { sparkView.isScrubEnabled = false graphAdapter.content = viewModel } is Loaded -> { sparkView.isScrubEnabled = true graphAdapter.content = viewModel } } }
  132. private val WIDTH = 360 val DEFAULT_MODEL = InvestingGraphContentModel.Loaded( graphWidth

    = WIDTH.toFloat(), heightBoundPercentage = 0.20f, points = (1 until 80).map { x -> Point( x = x.toFloat(), y = Math.sin(x.toDouble() / 45).toFloat() + random(), effectFromPreviousPoint = NONE, treatment = PointTreatment.NONE, scrubText = null ) } )
  133. investing/ โ”œโ”€โ”€ components โ”‚ โ”œโ”€โ”€ build.gradle โ”‚ โ””โ”€โ”€ src โ”‚

    โ””โ”€โ”€ main โ”‚ โ”œโ”€โ”€ java โ”‚ โ”‚ โ””โ”€โ”€ com โ”‚ โ”‚ โ””โ”€โ”€ squareup โ”‚ โ”‚ โ””โ”€โ”€ cash โ”‚ โ”‚ โ””โ”€โ”€ investing โ”‚ โ”‚ โ””โ”€โ”€ components โ”‚ โ”‚ โ”œโ”€โ”€ InvestingGraphView.kt โ”‚ โ”‚ โ”œโ”€โ”€ graph โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ CashSparkView.kt โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ InvestingGraphAdapter.kt โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ InvestingGraphPathType.kt โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ InvestingGraphStyler.kt โ”‚ โ”‚ โ””โ”€โ”€ investingAdapters.kt โ”‚ โ””โ”€โ”€ res โ”‚ โ”œโ”€โ”€ layout โ”‚ โ”‚ โ””โ”€โ”€ investing_graph.xml โ”‚ โ””โ”€โ”€ values
  134. โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ InvestingGraphPathType.kt โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ InvestingGraphStyler.kt

    โ”‚ โ”‚ โ””โ”€โ”€ investingAdapters.kt โ”‚ โ””โ”€โ”€ res โ”‚ โ”œโ”€โ”€ layout โ”‚ โ”‚ โ””โ”€โ”€ investing_graph.xml โ”‚ โ””โ”€โ”€ values โ”‚ โ”œโ”€โ”€ colors.xml โ”‚ โ”œโ”€โ”€ dimens.xml โ”‚ โ”œโ”€โ”€ strings.xml โ”‚ โ””โ”€โ”€ styles.xml โ”œโ”€โ”€ sample โ”‚ โ”œโ”€โ”€ build.gradle โ”‚ โ””โ”€โ”€ src โ”‚ โ”œโ”€โ”€ main โ”‚ โ”‚ โ””โ”€โ”€ res โ”‚ โ”‚ โ”œโ”€โ”€ layout โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ graph_sample.xml โ”‚ โ”‚ โ””โ”€โ”€ values โ”‚ โ”‚ โ”œโ”€โ”€ strings.xml โ”‚ โ”‚ โ””โ”€โ”€ themes.xml โ”‚ โ””โ”€โ”€ test โ”‚ โ””โ”€โ”€ java โ”‚ โ””โ”€โ”€ com
  135. โ”‚ โ”‚ โ””โ”€โ”€ values โ”‚ โ”‚ โ”œโ”€โ”€ strings.xml โ”‚ โ”‚

    โ””โ”€โ”€ themes.xml โ”‚ โ””โ”€โ”€ test โ”‚ โ””โ”€โ”€ java โ”‚ โ””โ”€โ”€ com โ”‚ โ””โ”€โ”€ squareup โ”‚ โ””โ”€โ”€ cash โ”‚ โ””โ”€โ”€ investing โ”‚ โ””โ”€โ”€ sample โ”‚ โ””โ”€โ”€ InvestingTest.kt โ””โ”€โ”€ viewmodels โ”œโ”€โ”€ build.gradle โ””โ”€โ”€ src โ””โ”€โ”€ main โ””โ”€โ”€ java โ””โ”€โ”€ com โ””โ”€โ”€ squareup โ””โ”€โ”€ cash โ””โ”€โ”€ investing โ””โ”€โ”€ viewmodels โ””โ”€โ”€ InvestingGraphContentModel.kt
  136. class BitcoinUiTest { @get:Rule var paparazzi = Paparazzi("com.squareup.cash.investing.sample") @Test fun

    investingGraphSample() { val sample = paparazzi.inflate<LinearLayout>(R.layout.graph_sample) val graphView = sample.findViewById<InvestingGraphView>(R.id.graph) graphView.render(DEFAULT_MODEL) paparazzi.snapshot(graphSample, "graph sample") } }
  137. None
  138. None
  139. class CardDesignViewTest { @get:Rule var paparazzi = Paparazzi("com.squareup.cash.ui.blockers") @Test fun

    cardDesignPreview() { val cardDesign = paparazzi.inflate<ConstraintLayout>(R.layout.card_design_view) renderPreviewModel(cardDesign) paparazzi.snapshot(cardDesign, "card design preview") } @Test fun cardDesignCustomization() {โ€ฆ} }
  140. @Test fun cardDesignCustomization() { val cardDesign = paparazzi.inflate<ConstraintLayout>(R.layout.card_design_view) val signatureView

    = renderCustomizationView(cardDesign) signatureView.signature.startGlyph() var glyphCounter = 0 val signAnimator = ValueAnimator.ofFloat(0.0f, 1.0f) signAnimator.duration = 3000 signAnimator.addUpdateListener { if (glyphCounter >= glyphs.size) return@addUpdateListener val e = glyphs[glyphCounter++] signatureView.signature.extendGlyph(e[0], e[1], e[2]) signatureView.signature.invalidateCurrentGlyph(signatureView) } signAnimator.addListener(object : AnimatorListener { override fun onAnimationEnd(animation: Animator?) { signatureView.signature.finishGlyph() }
  141. if (glyphCounter >= glyphs.size) return@addUpdateListener val e = glyphs[glyphCounter++] signatureView.signature.extendGlyph(e[0],

    e[1], e[2]) signatureView.signature.invalidateCurrentGlyph(signatureView) } signAnimator.addListener(object : AnimatorListener { override fun onAnimationEnd(animation: Animator?) { signatureView.signature.finishGlyph() } }) signAnimator.start() paparazzi.gif( cardDesign, "card design customization", start = 0, end = signAnimator.duration, fps = 60 ) }
  142. if (glyphCounter >= glyphs.size) return@addUpdateListener val e = glyphs[glyphCounter++] signatureView.signature.extendGlyph(e[0],

    e[1], e[2]) signatureView.signature.invalidateCurrentGlyph(signatureView) } signAnimator.addListener(object : AnimatorListener { override fun onAnimationEnd(animation: Animator?) { signatureView.signature.finishGlyph() } }) signAnimator.start() paparazzi.gif( cardDesign, "card design customization", start = 0, end = signAnimator.duration, fps = 60 ) }
  143. None
  144. None
  145. @jrodbx @CashApp