Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for full screen snapshots. #59

Merged
merged 16 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 79 additions & 10 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,43 @@ jobs:
path: unit-test-build-reports.zip

instrumentationTests:
runs-on: macos-latest
runs-on: ubuntu-latest
timeout-minutes: 55
needs: [build]

permissions:
contents: write
pull-requests: write

steps:
- name: Delete unnecessary tools 🔧
run: |
echo Remote tool cache
sudo rm -rf "$AGENT_TOOLSDIRECTORY" || true

echo Remove dotnet runtime
sudo rm -rf /usr/share/dotnet || true

echo Remove haskell runtime
sudo rm -rf /opt/ghc || true
sudo rm -rf /usr/local/.ghcup || true

echo Remove swap storage
sudo swapoff -a || true
sudo rm -f /mnt/swapfile || true
free -h

- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm

- name: Checkout
uses: actions/checkout@v2

- name: Gradle Wrapper Validation
uses: gradle/wrapper-validation-action@v1
uses: gradle/wrapper-validation-action@v2

- uses: actions/cache@v3
with:
Expand All @@ -93,35 +122,37 @@ jobs:
${{ runner.os }}-gradle

- name: Install JDK 11
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: 11

# Retrieve the cached emulator snapshot.
- uses: actions/cache@v3
- name: AVD cache
uses: actions/cache@v4
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: ${{ runner.os }}-avd-x86_64-pixel_5-31

- name: Create AVD snapshot
- name: Create AVD snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 31
arch: x86_64
profile: pixel_5
disable-animations: false
force-avd-creation: false
disable-animations: false
ram-size: 4096M
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
script: echo "Generated AVD snapshot."
script: echo "Generated AVD snapshot for caching."

- name: Run instrumentation tests
id: instrumentation-tests
id: screenshotsverify
continue-on-error: true
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 31
Expand All @@ -130,10 +161,48 @@ jobs:
disable-animations: true
force-avd-creation: false
ram-size: 4096M
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -no-snapshot-save
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
# Workaround for https://github.com/ReactiveCircus/android-emulator-runner/issues/319
script: adb uninstall com.dropbox.dropshots.test; ./gradlew connectedCheck --stacktrace

- name: Prevent pushing new screenshots if this is a fork
id: checkfork_screenshots
continue-on-error: false
if: steps.screenshotsverify.outcome == 'failure' && github.event.pull_request.head.repo.full_name != github.repository
run: |
echo "::error::Screenshot tests failed, please create a PR in your fork first." && exit 1

- name: Record new screenshots
id: screenshotsrecord
if: steps.screenshotsverify.outcome == 'failure' && github.event_name == 'pull_request'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 31
arch: x86_64
profile: pixel_5
disable-animations: true
force-avd-creation: false
ram-size: 4096M
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
# Workaround for https://github.com/ReactiveCircus/android-emulator-runner/issues/319
script: adb uninstall com.dropbox.dropshots.test; ./gradlew connectedCheck -Pdropshots.record --stacktrace

- name: Pull screenshots
id: screenshotspull
continue-on-error: true
if: steps.screenshotsrecord.outcome == 'success' && github.event_name == 'pull_request'
run: |
rm dropshots/src/androidTest/assets/*.png || true
cp dropshots/build/reports/androidTests/dropshots/*.png dropshots/src/androidTest/assets/

- name: Push new screenshots if available
uses: stefanzweifel/git-auto-commit-action@4b8a201e31cadd9829df349894b28c54e6c19fe6
if: steps.screenshotspull.outcome == 'success'
with:
file_pattern: '*/*.png'
disable_globbing: true
commit_message: "🤖 Updates screenshots"

- name: (Fail-only) Bundle test reports
if: failure()
run: find . -type d '(' -name 'reports' -o -name 'androidTest-results' ')' | zip -@ -r instrumentation-test-build-reports.zip
Expand All @@ -148,7 +217,7 @@ jobs:
publish:
runs-on: ubuntu-latest
if: github.repository == 'dropbox/dropshots' && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
needs: [unitTests]
needs: [unitTests, instrumentationTests]
steps:
- name: Checkout
uses: actions/checkout@v2
Expand Down
2 changes: 2 additions & 0 deletions dropshots/api/dropshots.api
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ public final class com/dropbox/dropshots/Dropshots : org/junit/rules/TestRule {
public final fun assertSnapshot (Landroid/app/Activity;Ljava/lang/String;Ljava/lang/String;)V
public final fun assertSnapshot (Landroid/graphics/Bitmap;Ljava/lang/String;Ljava/lang/String;)V
public final fun assertSnapshot (Landroid/view/View;Ljava/lang/String;Ljava/lang/String;)V
public final fun assertSnapshot (Ljava/lang/String;Ljava/lang/String;)V
public static synthetic fun assertSnapshot$default (Lcom/dropbox/dropshots/Dropshots;Landroid/app/Activity;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V
public static synthetic fun assertSnapshot$default (Lcom/dropbox/dropshots/Dropshots;Landroid/graphics/Bitmap;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V
public static synthetic fun assertSnapshot$default (Lcom/dropbox/dropshots/Dropshots;Landroid/view/View;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V
public static synthetic fun assertSnapshot$default (Lcom/dropbox/dropshots/Dropshots;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V
}

public final class com/dropbox/dropshots/ResultValidatorKt {
Expand Down
5 changes: 5 additions & 0 deletions dropshots/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ android {
kotlinOptions {
jvmTarget = "1.8"
}

val isRecordingScreenshots = hasProperty("dropshots.record")
buildTypes.getByName("debug") {
resValue("bool", "is_recording_screenshots", isRecordingScreenshots.toString())
}
}

kotlin {
Expand Down
Binary file modified dropshots/src/androidTest/assets/MatchesActivityScreenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified dropshots/src/androidTest/assets/MatchesViewScreenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import com.dropbox.differ.Image
import com.dropbox.differ.ImageComparator
import com.dropbox.differ.ImageComparator.ComparisonResult
import com.dropbox.differ.Mask
import org.junit.Assume.assumeFalse
import org.junit.Before
import org.junit.Rule
import org.junit.Test

class CustomImageComparatorTest {
private val isRecordingScreenshots = isRecordingScreenshots()

@get:Rule
val activityScenarioRule = ActivityScenarioRule(TestActivity::class.java)
Expand All @@ -31,6 +33,8 @@ class CustomImageComparatorTest {

@Test
fun imageComparatorIsConfigurable() {
assumeFalse(isRecordingScreenshots)

val calls = mutableListOf<Triple<Image, Image, Mask?>>()
comparator.compareFunc = { left, right, mask ->
calls.add(Triple(left, right, mask))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,22 @@ import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.test.ext.junit.rules.ActivityScenarioRule
import com.dropbox.differ.SimpleImageComparator
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.fail
import org.junit.Assert.*
import org.junit.Assume.assumeFalse
import org.junit.Before
import org.junit.Rule
import org.junit.Test

class DropshotsTest {

private val fakeValidator = FakeResultValidator()
private val isRecordingScreenshots = isRecordingScreenshots()

@get:Rule
val activityScenarioRule = ActivityScenarioRule(TestActivity::class.java)

@get:Rule
val dropshots = Dropshots(
recordScreenshots = false,
resultValidator = fakeValidator,
imageComparator = SimpleImageComparator(
maxDistance = 0.004f,
Expand Down Expand Up @@ -57,6 +56,13 @@ class DropshotsTest {
}
}

@Test
fun testMatchesFullScreenshot() {
activityScenarioRule.scenario.onActivity {
dropshots.assertSnapshot("MatchesFullScreenshot")
}
}

@Test
fun testMatchesActivityScreenshot() {
activityScenarioRule.scenario.onActivity {
Expand All @@ -76,13 +82,16 @@ class DropshotsTest {

@Test
fun testFailsForDifferences() {
assumeFalse(isRecordingScreenshots)

var failed = false
activityScenarioRule.scenario.onActivity {
try {
Log.d("!!! TEST !!!", "Asserting snapshot...")
dropshots.assertSnapshot(
view = it.findViewById(android.R.id.content),
name = "MatchesViewScreenshotBad"
name = "MatchesViewScreenshotBad",
filePath = "static"
)
Log.d("!!! TEST !!!", "Snapshot asserted")
failed = true
Expand All @@ -100,25 +109,31 @@ class DropshotsTest {

@Test
fun testPassesWhenValidatorPasses() {
assumeFalse(isRecordingScreenshots)

fakeValidator.validator = { true }
activityScenarioRule.scenario.onActivity {
dropshots.assertSnapshot(
view = it.findViewById(android.R.id.content),
name = "MatchesViewScreenshotBad"
name = "MatchesViewScreenshotBad",
filePath = "static"
)
}
}

@Test
fun testFailsWhenValidatorFails() {
assumeFalse(isRecordingScreenshots)

fakeValidator.validator = { false }

var caughtError: AssertionError? = null
activityScenarioRule.scenario.onActivity {
try {
dropshots.assertSnapshot(
view = it.findViewById(android.R.id.content),
name = "MatchesViewScreenshotBad"
name = "MatchesViewScreenshotBad",
filePath = "static"
)
} catch (e: AssertionError) {
caughtError = e
Expand All @@ -130,12 +145,15 @@ class DropshotsTest {

@Test
fun fastFailsForMismatchedSize() {
assumeFalse(isRecordingScreenshots)

var failed = false
activityScenarioRule.scenario.onActivity {
try {
dropshots.assertSnapshot(
view = it.findViewById(android.R.id.content),
name = "MatchesViewScreenshotBadSize"
name = "MatchesViewScreenshotBadSize",
filePath = "static"
)
failed = true
} catch (e: Throwable) {
Expand Down
4 changes: 0 additions & 4 deletions dropshots/src/androidTest/res/values/bools.xml

This file was deleted.

17 changes: 15 additions & 2 deletions dropshots/src/main/java/com/dropbox/dropshots/Dropshots.kt
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public class Dropshots(
}

/**
* Compares a screenshot of the view to a references screenshot from the test application's assets.
* Compares a screenshot of the view to a reference screenshot from the test application's assets.
*
* If `BuildConfig.IS_RECORD_SCREENSHOTS` is set to `true`, then the screenshot will simply be written
* to disk to be pulled to the host machine to update the reference images.
Expand All @@ -80,7 +80,7 @@ public class Dropshots(
) = assertSnapshot(Screenshot.capture(view).bitmap, name, filePath)

/**
* Compares a screenshot of the activity to a references screenshot from the test application's assets.
* Compares a screenshot of the activity to a reference screenshot from the test application's assets.
*
* If `BuildConfig.IS_RECORD_SCREENSHOTS` is set to `true`, then the screenshot will simply be written
* to disk to be pulled to the host machine to update the reference images.
Expand All @@ -93,6 +93,19 @@ public class Dropshots(
filePath: String? = null,
) = assertSnapshot(Screenshot.capture(activity).bitmap, name, filePath)

/**
* Compares a screenshot of the visible screen content to a reference screenshot from the test application's assets.
*
* If `BuildConfig.IS_RECORD_SCREENSHOTS` is set to `true`, then the screenshot will simply be written
* to disk to be pulled to the host machine to update the reference images.
*
* @param filePath where the screenshots should be store in project eg. "views/colors"
*/
public fun assertSnapshot(
name: String = snapshotName,
filePath: String? = null,
) = assertSnapshot(Screenshot.capture().bitmap, name, filePath)

@Suppress("LongMethod")
public fun assertSnapshot(
bitmap: Bitmap,
Expand Down
10 changes: 4 additions & 6 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
kotlin = "1.7.22"
agp = "7.4.0"
androidx-core = "1.7.0"
androidx-test = "1.4.0"
androidx-test-ext = "1.1.3"

[libraries]
android = { module = "com.android.tools.build:gradle", version.ref = "agp" }
Expand All @@ -13,10 +11,10 @@ androidx-appcompat = { module = "androidx.appcompat:appcompat", version = "1.4.1
androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version = "2.1.4" }
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
androidx-fragment = { module = "androidx.fragment:fragment-ktx", version = "1.3.6" }
androidx-test-core = { module = "androidx.test:core-ktx", version.ref = "androidx-test" }
androidx-test-ext-junit = { module = "androidx.test.ext:junit-ktx", version.ref = "androidx-test-ext" }
androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-test" }
androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test" }
androidx-test-core = { module = "androidx.test:core-ktx", version = "1.5.0" }
androidx-test-ext-junit = { module = "androidx.test.ext:junit-ktx", version = "1.1.5" }
androidx-test-rules = { module = "androidx.test:rules", version = "1.5.0" }
androidx-test-runner = { module = "androidx.test:runner", version = "1.5.2" }
differ = "com.dropbox.differ:differ:0.0.1-alpha1"
junit = "junit:junit:4.12"
truth = "com.google.truth:truth:1.1.3"
Expand Down