Giter Club home page Giter Club logo

paparazzi's Introduction

Paparazzi

An Android library to render your application screens without a physical device or emulator.

class LaunchViewTest {
  @get:Rule
  val paparazzi = Paparazzi(
    deviceConfig = PIXEL_5,
    theme = "android:Theme.Material.Light.NoActionBar"
    // ...see docs for more options
  )

  @Test
  fun launchView() {
    val view = paparazzi.inflate<LaunchView>(R.layout.launch)
    // or...
    // val view = LaunchView(paparazzi.context)

    view.setModel(LaunchModel(title = "paparazzi"))
    paparazzi.snapshot(view)
  }

  @Test
  fun launchComposable() {
    paparazzi.snapshot {
      MyComposable()
    }
  }
}

See the project website for documentation and APIs.

Tasks

./gradlew sample:testDebug

Runs tests and generates an HTML report at sample/build/reports/paparazzi/ showing all test runs and snapshots.

./gradlew sample:recordPaparazziDebug

Saves snapshots as golden values to a predefined source-controlled location (defaults to src/test/snapshots).

./gradlew sample:verifyPaparazziDebug

Runs tests and verifies against previously-recorded golden values. Failures generate diffs at sample/build/paparazzi/failures.

For more examples, check out the sample project.

Git LFS

It is recommended you use Git LFS to store your snapshots. Here's a quick setup:

brew install git-lfs
git config core.hooksPath  # optional, confirm where your git hooks will be installed
git lfs install --local
git lfs track "**/snapshots/**/*.png"
git add .gitattributes

On CI, you might set up something like:

$HOOKS_DIR/pre-receive

# compares files that match .gitattributes filter to those actually tracked by git-lfs
diff <(git ls-files ':(attr:filter=lfs)' | sort) <(git lfs ls-files -n | sort) >/dev/null

ret=$?
if [[ $ret -ne 0 ]]; then
  echo >&2 "This remote has detected files committed without using Git LFS. Run 'brew install git-lfs && git lfs install' to install it and re-commit your files.";
  exit 1;
fi

your_build_script.sh

if [[ is running snapshot tests ]]; then
  # fail fast if files not checked in using git lfs
  "$HOOKS_DIR"/pre-receive
  git lfs install --local
  git lfs pull
fi

Jetifier

If using Jetifier to migrate off Support libraries, add the following to your gradle.properties to exclude bundled Android dependencies.

android.jetifier.ignorelist=android-base-common,common

Lottie

When taking screenshots of Lottie animations, you need to force Lottie to not run on a background thread, otherwise Paparazzi can throw exceptions #494, #630.

@Before
fun setup() {
    LottieTask.EXECUTOR = Executor(Runnable::run)
}

LocalInspectionMode

Some Composables -- such as GoogleMap() -- check for LocalInspectionMode to short-circuit to a @Preview-safe Composable.

However, Paparazzi does not set LocalInspectionMode globally to ensure that the snapshot represents the true production output, similar to how it overrides View.isInEditMode for legacy views.

As a workaround, we recommend wrapping such a Composable in a custom Composable with a CompositionLocalProvider and setting LocalInspectionMode there.

 @Test
  fun inspectionModeView() {
    paparazzi.snapshot(
      CompositionLocalProvider(LocalInspectionMode provides true) {
        YourComposable()
      }
    )
  }

Releases

Our change log has release history.

Using plugin application:

buildscript {
  repositories {
    mavenCentral()
    google()
  }
  dependencies {
    classpath 'app.cash.paparazzi:paparazzi-gradle-plugin:1.3.4'
  }
}

apply plugin: 'app.cash.paparazzi'

Using the plugins DSL:

plugins {
  id 'app.cash.paparazzi' version '1.3.4'
}

Snapshots of the development version are available in Sonatype's snapshots repository.

repositories {
  // ...
  maven {
    url 'https://oss.sonatype.org/content/repositories/snapshots/'
  }
}

License

Copyright 2019 Square, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

paparazzi's People

Contributors

adamalyyan avatar alexvanyo avatar briangardneratl avatar colinmarsch avatar dependabot[bot] avatar dnihze avatar egorand avatar fcduarte avatar gabrielittner avatar gamepro65 avatar geoff-powell avatar jakewharton avatar jrodbx avatar kevinzheng-ap avatar liutikas avatar luis-cortes avatar luk1709 avatar mattprecious avatar menny avatar nak5ive avatar oldergod avatar renovate[bot] avatar rharter avatar rojanthomas avatar saket avatar sebastienrouif avatar simonmarquis avatar swankjesse avatar twisterrob avatar yschimke avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

paparazzi's Issues

Not possible to open assets

Environment:

  • Paparazzi: 0.6.0
  • Android API: less than 30 (e.g.: 29)
  • OpenJDK: 11.0.9
  • Mac OS: 10.15.6

Steps:

  • assertNotNull(paparazzi.context.assets.open("asset_name")) fails

Description:

Right now I'm using detectEnvironment().assetsDir (because Paparazzi.environment is private) to get the assets directory.

Support for updating and checking golden files

So far we’ve been thinking mostly about interactive development, but there are really 3 modes that Paparazzi can run in:

  1. Development – emit screenshots for manual checking
  2. Updating/creating golden files – generate screenshots intended for checking in
  3. Checking golden files – confirm screenshots match golden files

I think the programmatic API should be the same in each mode: call snapshot().

But we should have mechanisms to create and check golden files to defend against regressions. Maybe a system property?
-DpaparazziMode=dev
-DpaparazziMode=update
-DpaparazziMode=check

The update mode should be opinionated about where golden files go. Perhaps something like this:

src/test/paparazzi/com/squareup/paparazzi/sample/KeypadViewTest.testViews.zero_dollars.png
src/test/paparazzi/com/squareup/paparazzi/sample/KeypadViewTest.testViews.five_bucks.png
src/test/paparazzi/com/squareup/paparazzi/sample/KeypadViewTest.testViews.grey.png
src/test/paparazzi/com/squareup/paparazzi/sample/KeypadViewTest.testViews.bolt.png

I’d love it if we didn’t actually need to write files to the filesystem in check mode. We already have a bitmap in memory.

When the check mode fails we should write a bitmap with actual image.

I’d also like it if we defer failure until after the test completes. That way we get all the failing screenshots.

Support application modules

Currently, the plugin can only parse merged resources from library modules since they're in XML format, while application modules output merged resources to a binary format. This limits Paparazzi to library modules for now.

Parameterized Paparazzi tests

It would be nice to have a way to run every test multiple times with different configurations, e.g. dark vs light mode, different accessibility settings, portrait vs landscape etc. This can theoretically be achieved with a custom test runner (similar to Parameterized), but this would make it hard to use any other runner with Paparazzi tests. Perhaps the Paparazzi test rule can accept a list of customizable configurations, each of which will affect paparazzi.context, and the rule will ensure that every test is ran with every configuration?

Gradle skips Paparazzi tasks with no actions

Environment:

  • Paparazzi: 0.6.0
  • Android API: less than 30 (e.g.: 29)
  • OpenJDK: 11.0.8
  • Mac OS: 10.15.4

Steps:

  • Clone paparazzi repository
  • Collect snapshots: ./gradlew sample:recordPaparazziDebug --info
  • Verify snapshots: ./gradlew sample:verifyPaparazziDebug --info
  • Remove snapshot images
  • Collect snapshots: ./gradlew sample:recordPaparazziDebug --info

Actual result:

  • Gradle skips paparazzi task:
Skipping task ':sample:recordPaparazziDebug' as it has no actions

Additional info:
It's useful to note that this is not a permanent bug for me and it can be tricky to reproduce. Execution of paparazzi gradle task with an --rerun-tasks argument is kind of a workaround: ./gradlew sample:recordPaparazziDebug --rerun-tasks --info. But I would like to keep gradle cache and avoid using this argument if it's possible. Thanks!

Test outputs are verbose.

It would be nice to be able to define the logs as to print less info. That's quite a lof of log right now.

com.squareup.cash.payments.views.RecipientInternationalDialogTest > default[Theme: MooncakeLight] STANDARD_OUT
    Objects still linked from the DelegateManager:
    [4850] FloatPropertySetter
    [343] NativeAllocationRegistry_Delegate
    [3927] FloatPropertySetter
    [6764] Region_Delegate
    [4364] PathInterpolator
    [3926] PathInterpolator
    [4842] FloatPropertySetter
.....

Better Documentation

Better user documentation once it matures. Right now it's a little hard to get how to use it from the sample app.

Max percent difference causes false positives

Environment:

  • Paparazzi: 0.6.0
  • Android API: less than 30 (e.g.: 29)
  • OpenJDK: 11.0.9
  • Mac OS: 10.15.6

Description:

The MAX_PERCENT_DIFFERENCE value may be too high (currently 0.1) and small changes in big snapshots (like a lighter textColor) don't fail the test.

Also, this constant could be a parameter of the Paparazzi configuration, falling back to the current value as default:

@get:Rule
internal var paparazzi = Paparazzi(maxPercentDifference = 0.01)

Steps:

  1. Given the golden image
    ca skipthedishes customer ui_MenuItemOptionRadioGroupComponentTests_screenshot_ThemeOrange_idle

  2. And the new change

ca skipthedishes customer ui_MenuItemOptionRadioGroupComponentTests_screenshot_ThemeOrange_idle

  1. With MAX_PERCENT_DIFFERENCE = 0.1 the test still pass

  2. With MAX_PERCENT_DIFFERENCE = 0.01 the test fails with the expected delta

delta-ca skipthedishes customer ui_MenuItemOptionRadioGroupComponentTests_screenshot_ThemeOrange_idle

One HTML page per Test suite

Environment:

  • Paparazzi: 0.6.0
  • Android API: less than 30 (e.g.: 29)
  • OpenJDK: 11.0.9
  • Mac OS: 10.15.6

Description:

Having only one HTML page for the report gets cluttered really fast, considering that for every component (Android view with a well-defined state) we take snapshots of as many state combinations as possible on every theme (Dark and Light mode), creating dozens of snapshots. One improvement over this downside would be to generate an HTML page per Test suite, and link them all together in the main index.html

Hide debugging information behind a flag

All of the DelegateManager stuff looks to be debugging information that I'm assuming isn't useful to the general consumer. It makes it really difficult to see what's going on when a record fails since there's so much noise to scroll back through. Could all this extra information be moved behind a flag?

Build.VERSION.SDK_INT takes a value of zero

Some logic that depends on Build.VERSION.SDK_INT will become abnormal because of it takes a value of zero. For example, ContextCompat.getDrawable() is a compatible method for getting Drawables in all Android versions, provided by Google in support-compat package.

872A672F-EE87-4C5F-8410-D0AB6D219F8A

As shown in the figure above, the program will enter the wrong branch when calling this method, which will cause Drawable resource to be parsed into a String resource, the resolvedId variable will eventually be 0, unable to get the correct Drawable object.

Is it possible to provide a way to set the runtime SDK version? In addition, it is better if project parameters such as minSdkVersion and targetSdkVersion can be obtained from the Gradle Plugin.

Sources jar for layoutlib is empty

I've got a view inflation failure originating from an assert in layoutlib and it's very hard to track down as I can't easily place breakpoints into layoutlib due to missing sources.

It would be nice if the published sources jar wasn't empty:

$ unzip -lv /Users/madis/.gradle/caches/modules-2/files-2.1/app.cash.paparazzi/layoutlib-jdk11/4.0.0-alpha02-b169460/2d4f038e18d128a9de2786c4288d6034c7a0807d/layoutlib-jdk11-4.0.0-alpha02-b169460-sources.jar
 Length   Method    Size  Cmpr    Date    Time   CRC-32   Name
--------  ------  ------- ---- ---------- ----- --------  ----
       0  Defl:N        2   0% 10-02-2020 11:46 00000000  META-INF/
      25  Defl:N       27  -8% 10-02-2020 11:46 ee027fb2  META-INF/MANIFEST.MF
--------          -------  ---                            -------
      25               29 -16%                            2 files

Currently used snapshot version 1.0.0-square08 is not published on public repository

Can't build locally if don't have access to Square repository:

A problem occurred configuring root project 'paparazzi-parent'.
> Could not resolve all artifacts for configuration ':classpath'.
   > Could not find app.cash.paparazzi:paparazzi-gradle-plugin:1.0.0-square08.
     Searched in the following locations:
       - https://dl.google.com/dl/android/maven2/app/cash/paparazzi/paparazzi-gradle-plugin/1.0.0-square08/paparazzi-gradle-plugin-1.0.0-square08.pom
       - https://dl.google.com/dl/android/maven2/app/cash/paparazzi/paparazzi-gradle-plugin/1.0.0-square08/paparazzi-gradle-plugin-1.0.0-square08.jar
       - https://repo.maven.apache.org/maven2/app/cash/paparazzi/paparazzi-gradle-plugin/1.0.0-square08/paparazzi-gradle-plugin-1.0.0-square08.pom
       - https://repo.maven.apache.org/maven2/app/cash/paparazzi/paparazzi-gradle-plugin/1.0.0-square08/paparazzi-gradle-plugin-1.0.0-square08.jar
       - https://jcenter.bintray.com/app/cash/paparazzi/paparazzi-gradle-plugin/1.0.0-square08/paparazzi-gradle-plugin-1.0.0-square08.pom
       - https://jcenter.bintray.com/app/cash/paparazzi/paparazzi-gradle-plugin/1.0.0-square08/paparazzi-gradle-plugin-1.0.0-square08.jar
       - file:/Users/zhukov/.m2/repository/app/cash/paparazzi/paparazzi-gradle-plugin/1.0.0-square08/paparazzi-gradle-plugin-1.0.0-square08.pom
       - file:/Users/zhukov/.m2/repository/app/cash/paparazzi/paparazzi-gradle-plugin/1.0.0-square08/paparazzi-gradle-plugin-1.0.0-square08.jar

Support for text appearance

android:textAppearance in XML and setTextAppearance() in code are both not recognized in a paparazzi test.

Would be nice to have this working!

View inflation error

I’m trying to work with the application module and Spek, so this is destined to fail, but anyways. Since #105 blocks me from using the Paparazzi Gradle plugin, I’m doing what the plugin does on my own.

view_basic.xml

<?xml version="1.0" encoding="utf-8"?>
<View
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    />

PaparazziSpec.kt

describe("paparazzi") {

    val paparazzi by memoized { Paparazzi() }

    beforeEachTest {
        val resourcesFile = File("REDACTED_MODULE_PATH/build/intermediates/paparazzi/resources.txt")
        val resources =
                """
                REDACTED_MODULE_PACKAGE_NAME
                REDACTED_MODULE_PATH/build/intermediates/res/merged/debug
                """

        resourcesFile.apply {
            parentFile?.mkdirs()
            createNewFile()
            writeText(resources.trimIndent())
        }

        paparazzi.prepare(Description.createTestDescription(PaparazziSpec::class.java, "paparazzi"))
    }

    it("should work") {
        paparazzi.snapshot(paparazzi.inflate(R.layout.view_basic), "basic")
    }

    afterEachTest {
        paparazzi.close()
    }
}

It actually compiles but fails to proceed.

kotlin.TypeCastException: null cannot be cast to non-null type V
    at app.cash.paparazzi.Paparazzi.inflate(Paparazzi.kt:128)
    at PaparazziSpec$1$1$2.invoke(PaparazziSpec.kt:42)

I’ve explored with a debugger a bit and seems like BridgeInflater#inflate returns null

  • since bridgeContext.getRenderResources().getResolvedResource(layoutInfo) returns null
  • since RenderResources#getResolvedResource returns null
  • since RenderResources#getUnresolvedResource returns null
  • since ResourceResolver#getUnresolvedResource returns null
  • since getResourceValueMap(reference.getNamespace(), reference.getResourceType()) returns empty map

java.lang.NoSuchFieldException: modifiers

Environment:
AdoptOpenJDK 12 12.0.2, x86_64
or
OpenJDK 14.0.1

Steps:

  • checkout sample project
  • ./gradlew sample:testDebug

Crash:
app.cash.paparazzi.sample.LaunchViewTest > nexus5_differentOrientations FAILED
java.lang.NoSuchFieldException: modifiers
at java.base/java.lang.Class.getDeclaredField(Class.java:2416)
at app.cash.paparazzi.Paparazzi.forcePlatformSdkVersion(Paparazzi.kt:286)
at app.cash.paparazzi.Paparazzi.prepare(Paparazzi.kt:111)
at app.cash.paparazzi.Paparazzi$apply$statement$1.evaluate(Paparazzi.kt:94)
at app.cash.paparazzi.agent.AgentTestRule$apply$1.evaluate(AgentTestRule.kt:17)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.runTestClass(JUnitTestClassExecutor.java:110)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:58)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:38)
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestClassProcessor.processTestClass(AbstractJUnitTestClassProcessor.java:62)
at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.processTestClass(SuiteTestClassProcessor.java:51)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:567)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
at com.sun.proxy.$Proxy2.processTestClass(Unknown Source)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.processTestClass(TestWorker.java:119)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:567)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:182)
at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:164)
at org.gradle.internal.remote.internal.hub.MessageHub$Handler.run(MessageHub.java:414)
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:48)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:56)
at java.base/java.lang.Thread.run(Thread.java:835)

Support AppCompat subclasses created in code

Libraries like Contour promote creating views in XML vs code. We've encountered a couple of issues around vector drawable resources and custom fonts with this approach, due to how AppCompat handles these during layout inflation vs direct view instantiation.

Paparazzi.Config

Goals:

  • make it easy and fast to re-run an entire set of screenshots after a style change
  • centralize control without Gradle hacks or weird IntelliJ configuration

Implementation:
Put configuration in code! Rather than system properties, etc.
Anyone who wants to can create a Paparazzi.Config from system properties but that’s their problem.

Sketch of possible API:

val PAPARAZZI_CONFIG = Paparazzi.Config(
  mode = Mode.RECORD, // could be verify
  screenshotsPath = "src/test/screenshots",
)
class MyTextViewTest {
  val paparazzi = Paparazzi(PAPARAZZI_CONFIG)

  @Test test() { ... }
}

`Paparazzi.snapshot()` does not load resources of its `deviceConfig` parameter

To reproduce:

  1. Check out this commit
  2. Record sample screenshots: ./gradlew :sample:recPapaDeb
  3. Note how background in app.cash.paparazzi.sample_LaunchViewTest_mix_nexus7.png is not red #f00 as it should be.
  4. Swap lines 29 and 30 to set the starting config to NEXUS_7 and re-run the tests
  5. Note how the nexus7 background is red but the pixel3 screenshot is now also red.

Unable to add snapshot tests for views that depend on Android animation

Sample error message when running a snapshot test on AmountView:

java.lang.RuntimeException: Method setTimeListener in android.animation.TimeAnimator not mocked. See http://g.co/androidstudio/not-mocked for details.
at android.animation.TimeAnimator.setTimeListener(TimeAnimator.java)
at com.squareup.cash.ui.widget.amount.AnimationContext.<init>(AnimationTools.kt:19)
at com.squareup.cash.ui.widget.amount.AmountView.<init>(AmountView.kt:42)
at com.squareup.cash.ui.widget.amount.AmountView.<init>(AmountView.kt:31)
at com.squareup.cash.bitcoin.views.BitcoinPaymentPadView.<init>(BitcoinPaymentPadView.kt:26)

Animations and Gifs

If we have a view that animates or transitions, we should be able to capture all of the frames of the animation as a .gif or similar.

This example captures frames at 300 ms thru 1300 ms and produces a 1-second animation.

    paparazzi.snapshot(keypad, "zero dollars", start = 300, end = 1300, fps = 30)

AGP library plugin requirement

The current version of the PaparazziPlugin requires to have com.android.library declared. Is it intentional? I’m asking because I’ve tried to use it with a usual application module and saw the plugin complaint.

The Android Gradle library plugin must be applied before the Paparazzi plugin.

Attempting to run without configured environment SDK installed throws confusing error

The first stacktrace we end up seeing looks something like

com.package.SomeTest > someTest FAILED
    java.lang.NullPointerException
        at com.android.layoutlib.bridge.impl.RenderSessionImpl.init(RenderSessionImpl.java:181)
        at app.cash.paparazzi.Paparazzi.prepare(Paparazzi.kt:134)
        at app.cash.paparazzi.Paparazzi$apply$statement$1.evaluate(Paparazzi.kt:96)
        at app.cash.paparazzi.agent.AgentTestRule$apply$1.evaluate(AgentTestRule.kt:17)
        at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
        ...

There are however multiple stacktraces thrown, and we can work out what the issue is if we keep scrolling and find...

app.cash.paparazzi.internal.PaparazziLogger error
    SEVERE: broken: Unable to load the list of fonts. Try re-installing the SDK Platform from the SDK Manager.
    java.io.FileNotFoundException: /Users/user/Library/Android/sdk/platforms/android-29/data/fonts/fontsInSdk.txt (No such file or directory)

So not the end of the world, but might be nice to throw a descriptive error if API 29 isn't installed.

Could not find app.cash.paparazzi:paparazzi-gradle-plugin:1.0.0-square08.

Repro:

git clone https://github.com/cashapp/paparazzi
cd paparazzi
gradlew tasks

Error:

$ gradlew tasks
Configuration on demand is an incubating feature.

FAILURE: Build failed with an exception.

* What went wrong:
A problem occurred configuring root project 'paparazzi-parent'.
> Could not resolve all artifacts for configuration ':classpath'.
   > Could not find app.cash.paparazzi:paparazzi-gradle-plugin:1.0.0-square08.
     Searched in the following locations:
       - https://dl.google.com/dl/android/maven2/app/cash/paparazzi/paparazzi-gradle-plugin/1.0.0-square08/paparazzi-gradle-plugin-1.0.0-square08.pom
       - https://dl.google.com/dl/android/maven2/app/cash/paparazzi/paparazzi-gradle-plugin/1.0.0-square08/paparazzi-gradle-plugin-1.0.0-square08.jar
       - https://repo.maven.apache.org/maven2/app/cash/paparazzi/paparazzi-gradle-plugin/1.0.0-square08/paparazzi-gradle-plugin-1.0.0-square08.pom
       - https://repo.maven.apache.org/maven2/app/cash/paparazzi/paparazzi-gradle-plugin/1.0.0-square08/paparazzi-gradle-plugin-1.0.0-square08.jar
       - https://jcenter.bintray.com/app/cash/paparazzi/paparazzi-gradle-plugin/1.0.0-square08/paparazzi-gradle-plugin-1.0.0-square08.pom
       - https://jcenter.bintray.com/app/cash/paparazzi/paparazzi-gradle-plugin/1.0.0-square08/paparazzi-gradle-plugin-1.0.0-square08.jar
       - file:/Users/<>/.m2/repository/app/cash/paparazzi/paparazzi-gradle-plugin/1.0.0-square08/paparazzi-gradle-plugin-1.0.0-square08.pom
       - file:/Users/<>/.m2/repository/app/cash/paparazzi/paparazzi-gradle-plugin/1.0.0-square08/paparazzi-gradle-plugin-1.0.0-square08.jar
     Required by:
         project :

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 1s

Jetifier fails to transform com.android.tools/common

Not sure if there's anything we can do to fix the problem but I figured it would be worth filing anyway. Basically getting this Jetifier error:

> Could not resolve all artifacts for configuration ':features:notification-center:impl:debugUnitTestCompileClasspath'.
   > Failed to transform artifact 'common.jar (com.android.tools:common:26.5.0)' to match attributes {artifactType=android-classes, org.gradle.libraryelements=jar, org.gradle.usage=java-runtime}.
      > Execution failed for JetifyTransform: /Users/zhukov/.gradle/caches/modules-2/files-2.1/com.android.tools/common/26.5.0/6ac94244acf356790ac8d6fa41c3158636422e67/common-26.5.0.jar.
         > Failed to transform '/Users/zhukov/.gradle/caches/modules-2/files-2.1/com.android.tools/common/26.5.0/6ac94244acf356790ac8d6fa41c3158636422e67/common-26.5.0.jar' using Jetifier. Reason: The given artifact contains a string literal with a package reference 'android.support.design.widget' that cannot be safely rewritten. Libraries using reflection such as annotation processors need to be updated manually to add support for androidx.. (Run with --stacktrace for more details.)

As a workaround blacklisting the library seems to work: android.jetifier.blacklist=com.android.tools.common in your gradle properties.

Paparazzi respects `tools:` attribute overrides

I think this is a side-effect of using layoutlib, but I'm not sure whether it is intended or not? E.g. if a view has tools:background="#000" then the screenshot will have a black background.

Support custom fonts when constructing views in code

  // fails
  @Test
  fun textViewInCode() {
    val textView = AppCompatTextView(paparazzi.context).apply {
      typeface = ResourcesCompat.getFont(context, R.font.cashmarket_medium)
      text = "Hello, world!"
    }

    paparazzi.snapshot(textView, "text in code")
  }

  // passes
  @Test
  fun textViewInXml() {
    val textView = paparazzi.inflate<TextView>(R.layout.textview)
    paparazzi.snapshot(textView, "text in xml")
  }

related xml for the latter:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:fontFamily="@font/cashmarket_medium"
    android:text="Hello, world!"
    tools:ignore="HardcodedText"
    />

Support different API levels?

Right now, we use whatever API level we pulled from Android Studio's layout lib at the time. A possible improvement would be to account for different API levels which can have different rendering behaviors.

Compile error with AGP 3.6

With AGP 3.6 we get a build error with this stacktrace.

Caused by: java.lang.NoSuchMethodError: com.android.build.gradle.tasks.MergeResources.getOutputDir()Ljava/io/File;
	at app.cash.paparazzi.gradle.PrepareResourcesTask.writeResourcesFile(PrepareResourcesTask.kt:45)

Support inline complex XML resources

example1.xml:

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:aapt="http://schemas.android.com/aapt"
    ...>
  <path android:pathData="...">
    <aapt:attr name="android:fillColor">
      <gradient ... />
    </aapt:attr>
  </path>
</vector>

workaround: extract the inlined resource

example2.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:aapt="http://schemas.android.com/aapt"
    ...>
  <path android:pathData="..." android:fillColor="@color/other" />
</vector>

other.xml

 <gradient ... />

NullPointerException when executing Paparazzi tests via Android Studio

Environment:

  • Paparazzi: 0.6.0
  • Android API: less than 30 (e.g.: 29)
  • OpenJDK: 11.0.8
  • Mac OS: 10.15.4
  • Android Studio: 4.0.2

Steps:

  • Clone paparazzi repository
  • Open paparazzi project via Android Studio
  • Open any sample test class file (e.g.: LaunchViewTest.kt)
  • Run any test individually, tapping on run button (e.g.: pixel3)

Actual result:

  • NullPointerException:
java.lang.NullPointerException
	at java.base/java.io.File.<init>(File.java:278)
	at app.cash.paparazzi.EnvironmentKt.detectEnvironment(Environment.kt:36)
	at app.cash.paparazzi.Paparazzi.<init>(Paparazzi.kt:57)
	at app.cash.paparazzi.sample.LaunchViewTest.<init>(LaunchViewTest.kt:29)
	at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:490)
	at org.junit.runners.BlockJUnit4ClassRunner.createTest(BlockJUnit4ClassRunner.java:250)
	at org.junit.runners.BlockJUnit4ClassRunner.createTest(BlockJUnit4ClassRunner.java:260)
	at org.junit.runners.BlockJUnit4ClassRunner$2.runReflectiveCall(BlockJUnit4ClassRunner.java:309)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.BlockJUnit4ClassRunner.methodBlock(BlockJUnit4ClassRunner.java:306)
	at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
	at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
	at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)

Test Paparazzi with Android R

Tests broke when I ran locally, due to detectEnvironment picking up the android-R platform and not being able to classload android.graphics.Typeface.

$ ./gradlew paparazzi:test --tests=app.cash.paparazzi.PaparazziTest

> Task :paparazzi:test

app.cash.paparazzi.PaparazziTest > drawCalls FAILED
    java.lang.NullPointerException
        at app.cash.paparazzi.Paparazzi.takeSnapshots(Paparazzi.kt:199)
        at app.cash.paparazzi.Paparazzi.snapshot(Paparazzi.kt:140)
        at app.cash.paparazzi.Paparazzi.snapshot$default(Paparazzi.kt:138)
        at app.cash.paparazzi.PaparazziTest.drawCalls(PaparazziTest.kt:44)

app.cash.paparazzi.PaparazziTest > animationEvents FAILED
    java.lang.NullPointerException
        at app.cash.paparazzi.Paparazzi.takeSnapshots(Paparazzi.kt:199)
        at app.cash.paparazzi.Paparazzi.gif(Paparazzi.kt:158)
        at app.cash.paparazzi.Paparazzi.gif$default(Paparazzi.kt:150)
        at app.cash.paparazzi.PaparazziTest.animationEvents(PaparazziTest.kt:78)

Paparazzi fails on CircleCI

We're trying to use Paparazzi as part of our CI build & PR checks but it fails on CircleCI for some reason. I reproduced the issue by forking the repo and running the sample in CircleCI: https://app.circleci.com/pipelines/github/madisp/paparazzi/2/workflows/d62a07b5-8333-45e9-b249-410995ba9bc6/jobs/2/artifacts

It looks like some part of Typeface class initialization fails.

I tried to reproduce locally but had no success, is this somehow related to the environment like maybe CircleCI is running in headless or it's broken because of docker?

Stacktrace:

    java.lang.NoClassDefFoundError: Could not initialize class android.graphics.Typeface
        at android.graphics.Paint_Delegate.reset(Paint_Delegate.java:1262)
        at android.graphics.Paint_Delegate.<init>(Paint_Delegate.java:1216)
        at android.graphics.Paint_Delegate.nInit(Paint_Delegate.java:689)
        at android.graphics.Paint.nInit(Paint.java)
        at android.graphics.Paint.<init>(Paint.java:558)
        at android.graphics.drawable.ColorDrawable.<init>(ColorDrawable.java:56)
        at com.android.layoutlib.bridge.impl.ResourceHelper.getDrawable(ResourceHelper.java:318)
        at android.content.res.BridgeTypedArray.getDrawable(BridgeTypedArray.java:673)
        at android.view.View.<init>(View.java:5254)
        at android.view.ViewGroup.<init>(ViewGroup.java:675)
        at android.widget.FrameLayout.<init>(FrameLayout.java:99)
        at com.android.layoutlib.bridge.MockView.<init>(MockView.java:55)
        at com.android.layoutlib.bridge.MockView.<init>(MockView.java:51)
        at com.android.layoutlib.bridge.MockView.<init>(MockView.java:47)
        at android.view.BridgeInflater.createViewFromTag(BridgeInflater.java:324)
        at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:961)
        at android.view.LayoutInflater.inflate(LayoutInflater.java:659)
        at android.view.LayoutInflater.inflate(LayoutInflater.java:501)
        at android.view.BridgeInflater.inflate(BridgeInflater.java:383)
        at app.cash.paparazzi.Paparazzi.inflate(Paparazzi.kt:153)
        at app.cash.paparazzi.sample.LaunchViewTest.pixel3_differentThemes(LaunchViewTest.kt:38)

Stdout & stderr are empty.

Enable code completion for test projects

Adding new test projects to /paparazzi-gradle-plugin/src/test is slightly painful right now because code completion doesn't work. Is this something that can be enabled?

Use Skia Renderer

Android Studio 4.0 switched to Skia for its layout rendering (from java2d) which means it should support more rendering features. I think it's missing some things like PathMeasure.getSegment if I remember correctly.

If Paparazzi is able to use skia, I would consider switching the Lottie regression tests to it.

FileNotFoundException: /usr/local/share/android-sdk/platforms/android-Q/build.prop

When running paparazzi getting this error:

   java.io.FileNotFoundException: /usr/local/share/android-sdk/platforms/android-Q/build.prop (No such file or directory)
        at java.io.FileInputStream.open0(Native Method)
        at java.io.FileInputStream.open(FileInputStream.java:195)
        at java.io.FileInputStream.<init>(FileInputStream.java:138)
        at app.cash.paparazzi.DeviceConfig$Companion.loadProperties(DeviceConfig.kt:202)
        at app.cash.paparazzi.internal.Renderer.prepare(Renderer.kt:76)
        at app.cash.paparazzi.Paparazzi.prepare(Paparazzi.kt:106)
        at app.cash.paparazzi.Paparazzi$apply$1.evaluate(Paparazzi.kt:89)
        at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:305)
        at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
        at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:365)
        at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
        at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
        at org.junit.runners.ParentRunner$4.run(ParentRunner.java:330)
        at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:78)
        at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:328)
        at org.junit.runners.ParentRunner.access$100(ParentRunner.java:65)
        at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:292)
        at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:305)
        at org.junit.runners.ParentRunner.run(ParentRunner.java:412)
        at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.runTestClass(JUnitTestClassExecutor.java:110)
        at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:58)
        at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:38)
        at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestClassProcessor.processTestClass(AbstractJUnitTestClassProcessor.java:62)
        at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.processTestClass(SuiteTestClassProcessor.java:51)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
        at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
        at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
        at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
        at com.sun.proxy.$Proxy2.processTestClass(Unknown Source)
        at org.gradle.api.internal.tasks.testing.worker.TestWorker.processTestClass(TestWorker.java:118)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
        at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
        at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:182)
        at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:164)
        at org.gradle.internal.remote.internal.hub.MessageHub$Handler.run(MessageHub.java:412)
        at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
        at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:48)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:56)
        at java.lang.Thread.run(Thread.java:748)

Turns out I had empty (possibly leftover) directory /usr/local/share/android-sdk/platforms/android-Q which is why it fails. We could make Environment smarter by filtering out platform directories that don't have build.prop which is what we ultimately care about.

Move supporting code out of libs/

We should write a script to build these from version control. Probably in supporting modules like paparazzi-android-base-common/ and paparazzi-layoutlib/.

android-base-common.jar
layoutlib.jar

Properly handle TextView.breakStrategy

This is currently a bug in the old layoutlib and appears to be fixed in the new experimental skia-based renderer.

The likely fix is upgrading to the Skia Renderer, which will be the main priority after the first release. However, a workaround could be to method intercept/replace with the logic used by LineBreaker.nComputeLineBreaks: https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/jni/android/graphics/text/LineBreaker.cpp;l=95?q=nComputeLineBreaks

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.