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

John Rodriguez

August 27, 2019
Tweet

More Decks by John Rodriguez

Other Decks in Technology

Transcript

  1. <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" />
  2. <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" />
  3. 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") } }
  4. 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
  5. sample/ โ””โ”€โ”€ build โ””โ”€โ”€ reports โ””โ”€โ”€ paparazzi โ”œโ”€โ”€ images โ”‚

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

    โ””โ”€โ”€ 215bfbc973787f8c7e63650970fefc19fdd3399d.png โ”œโ”€โ”€ index.html โ”œโ”€โ”€ index.js โ”œโ”€โ”€ paparazzi.js โ””โ”€โ”€ runs โ””โ”€โ”€ 20190826232155_06ba49.js
  7. 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)
  8. @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)
  9. $ 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
  10. 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
  11. 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
  12. 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); }
  13. 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); }
  14. 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(); } }
  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(); mInflater = BridgeInflater(context, params.layoutlibCallback); context.setBridgeInflater(this.mInflater); ILayoutPullParser layoutParser = params.getLayoutDescription(); mBlockParser = BridgeXmlBlockParser(layoutParser, context, โ€ฆ); return Status.SUCCESS.createResult(); } }
  16. 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(); } }
  17. 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(); } }
  18. 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(); } }
  19. 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(); } }
  20. 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(); } }
  21. $ 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 โ€ฆ
  22. $ 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 โ€ฆ
  23. $ 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>
  24. $ 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>
  25. $ 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>
  26. $ 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>
  27. 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(); } }
  28. 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); }
  29. 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); }
  30. 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); }
  31. $ ./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 โ€ฆ โ€ฆ โ€ฆ
  32. $ ./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 โ€ฆ โ€ฆ โ€ฆ
  33. abstract class LayoutlibCallback { abstract Object loadView( String name, Class[]

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

    interface ResourceRepository { List<ResourceItem> getResources( ResourceNamespace namespace, ResourceType resourceType, String resourceName); โ€ฆ } com.android.tools:sdk-common
  35. 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
  36. class Paparazzi( private val packageName: String, private val environment: Environment,

    private val snapshotHandler: SnapshotHandler ) : TestRule
  37. 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" }
  38. 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) }
  39. class Paparazzi( private val packageName: String, private val environment: Environment

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

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

    private val snapshotHandler: SnapshotHandler ) : TestRule { fun prepare(description: Description) {โ€ฆ} }
  42. $ 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; }
  43. fun prepare(description: Description) { val layoutlibCallback = PaparazziCallback(logger, packageName) layoutlibCallback.initResources()

    renderer = Renderer(environment, layoutlibCallback, logger) val sessionParamsBuilder = renderer.prepare() }
  44. 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() }
  45. 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()) }
  46. 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") } }
  47. 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) } } }
  48. 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
  49. โ”œโ”€โ”€ resources โ”‚ โ”œโ”€โ”€ build.gradle โ”‚ โ””โ”€โ”€ src โ”‚ โ””โ”€โ”€

    main โ”‚ โ”œโ”€โ”€ AndroidManifest.xml โ”‚ โ””โ”€โ”€ res โ”‚ โ”œโ”€โ”€ drawable โ”‚ โ”‚ โ””โ”€โ”€ camera.png โ”‚ โ””โ”€โ”€ values โ”‚ โ””โ”€โ”€ colors.xml โ””โ”€โ”€ sample โ””โ”€โ”€ โ€ฆ
  50. <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" />
  51. <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" />
  52. <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" />
  53. 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" }
  54. 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" }
  55. 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) } }
  56. 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") } }
  57. 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)
  58. 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 } } }
  59. 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 ) } )
  60. 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
  61. โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ 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
  62. โ”‚ โ”‚ โ””โ”€โ”€ 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
  63. 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") } }
  64. 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() {โ€ฆ} }
  65. @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() }
  66. 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 ) }
  67. 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 ) }