├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md └── logo.png ├── .gitignore ├── .idea └── codeStyleSettings.xml ├── BUCK ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.gradle ├── core ├── build.gradle ├── gradle.properties └── src │ ├── androidTest │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── facebook │ │ │ └── testing │ │ │ └── screenshot │ │ │ ├── CustomScreenshotTestRunner.java │ │ │ ├── MyActivity.java │ │ │ ├── ScriptsFixtureTest.java │ │ │ ├── ViewHelpersTest.java │ │ │ ├── WindowAttachmentTest.java │ │ │ ├── internal │ │ │ ├── AlbumImplTest.java │ │ │ ├── OldApiBandaid.java │ │ │ ├── RecordBuilderImplTest.java │ │ │ ├── ScreenshotDirectoriesTest.java │ │ │ ├── ScreenshotImplTest.java │ │ │ ├── TestNameDetectorForJUnit4Test.java │ │ │ └── TestNameDetectorTest.java │ │ │ └── layouthierarchy │ │ │ └── LayoutHierarchyDumperTest.java │ └── res │ │ └── layout │ │ ├── testing_for_view_hierarchy.xml │ │ └── testing_simple_textview.xml │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── facebook │ └── testing │ └── screenshot │ ├── BUCK │ ├── RecordBuilder.java │ ├── Screenshot.java │ ├── ScreenshotRunner.java │ ├── ViewHelpers.java │ ├── WindowAttachment.java │ ├── internal │ ├── Album.java │ ├── AlbumImpl.java │ ├── MetadataRecorder.java │ ├── RecordBuilderImpl.java │ ├── Registry.java │ ├── ReportArtifactsManager.java │ ├── ScreenshotDirectories.java │ ├── ScreenshotImpl.java │ ├── TestNameDetector.java │ └── Tiling.java │ └── layouthierarchy │ ├── AbstractAttributePlugin.java │ ├── AccessibilityHierarchyDumper.java │ ├── AccessibilityIssuesDumper.java │ ├── AccessibilityUtil.java │ ├── AttributePlugin.java │ ├── BaseViewAttributePlugin.java │ ├── BaseViewHierarchyPlugin.java │ ├── HierarchyPlugin.java │ └── LayoutHierarchyDumper.java ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── layout-hierarchy-common ├── .gitignore ├── build.gradle ├── gradle.properties └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── facebook │ │ └── testing │ │ └── screenshot │ │ └── layouthierarchy │ │ └── common │ │ └── TextViewAttributePluginTest.java │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── facebook │ └── testing │ └── screenshot │ └── layouthierarchy │ └── common │ ├── BUCK │ └── TextViewAttributePlugin.java ├── layout-hierarchy-litho ├── .gitignore ├── build.gradle ├── gradle.properties └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── facebook │ │ └── testing │ │ └── screenshot │ │ └── layouthierarchy │ │ └── litho │ │ ├── LithoAttributePluginTest.java │ │ └── LithoHierarchyPluginTest.java │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── facebook │ └── testing │ └── screenshot │ └── layouthierarchy │ └── litho │ ├── BUCK │ ├── LithoAttributePlugin.java │ └── LithoHierarchyPlugin.java ├── lib ├── dexmaker │ └── BUCK ├── junit │ └── BUCK └── litho │ └── BUCK ├── plugin ├── build.gradle ├── gradle.properties └── src │ ├── main │ ├── kotlin │ │ └── com │ │ │ └── facebook │ │ │ └── testing │ │ │ └── screenshot │ │ │ └── build │ │ │ ├── CleanScreenshotsTask.kt │ │ │ ├── PullScreenshotsTask.kt │ │ │ ├── RecordScreenshotTestTask.kt │ │ │ ├── RunScreenshotTestTask.kt │ │ │ ├── ScreenshotTask.kt │ │ │ ├── ScreenshotsPlugin.kt │ │ │ └── VerifyScreenshotTestTask.kt │ └── resources │ │ └── META-INF │ │ └── gradle-plugins │ │ └── com.facebook.testing.screenshot.properties │ ├── py │ ├── BUCK │ ├── __init__.py │ └── android_screenshot_tests │ │ ├── __init__.py │ │ ├── aapt.py │ │ ├── adb_executor.py │ │ ├── background.png │ │ ├── background_dark.png │ │ ├── common.py │ │ ├── default.css │ │ ├── default.js │ │ ├── device_name_calculator.py │ │ ├── example.apk │ │ ├── fixtures │ │ ├── AndroidManifest.xml │ │ ├── dummy.zip │ │ └── sdcard │ │ │ └── screenshots │ │ │ └── com.foo │ │ │ └── screenshots-default │ │ │ ├── metadata.json │ │ │ ├── metadata_no_errors.json │ │ │ ├── one_dump.json │ │ │ └── unittest │ │ │ ├── com.foo.ScriptsFixtureTest_testGetTextViewScreenshot.png │ │ │ └── com.foo.ScriptsFixtureTest_testSecondScreenshot.png │ │ ├── metadata.py │ │ ├── metadata_fixture.json │ │ ├── no_op_device_name_calculator.py │ │ ├── pull_screenshots.py │ │ ├── recorder.py │ │ ├── simple_puller.py │ │ ├── test_aapt.py │ │ ├── test_common.py │ │ ├── test_device_name_calculator.py │ │ ├── test_metadata.py │ │ ├── test_pull_screenshots.py │ │ ├── test_recorder.py │ │ └── test_simple_puller.py │ └── test │ └── groovy │ └── com │ └── facebook │ └── testing │ └── screenshot │ └── build │ └── ScreenshotsPluginTest.groovy ├── sample ├── .gitignore ├── build.gradle ├── screenshots │ └── API_25_GP_XHDPI_768x1280_x86 │ │ ├── com.facebook.testing.screenshot.sample.ExampleScreenshotTest_testDefault.png │ │ ├── com.facebook.testing.screenshot.sample.ImageRowScreenshotTest_testDefault.png │ │ ├── com.facebook.testing.screenshot.sample.MainActivityTest_errorTextShouldBeRed.png │ │ ├── com.facebook.testing.screenshot.sample.MainActivityTest_mainActivityTestSettingsOpen.png │ │ ├── com.facebook.testing.screenshot.sample.MainActivityTest_okTextShouldBeGreen.png │ │ ├── com.facebook.testing.screenshot.sample.MainActivityTest_testScreenshotEntireActivity.png │ │ ├── com.facebook.testing.screenshot.sample.MainActivityTest_testScreenshotEntireActivityWithoutAccessibilityMetadata.png │ │ ├── com.facebook.testing.screenshot.sample.MainActivityTest_warningTextShouldBeYellow.png │ │ ├── com.facebook.testing.screenshot.sample.StandardAndroidViewTest_testChinese.png │ │ ├── com.facebook.testing.screenshot.sample.StandardAndroidViewTest_testLongText.png │ │ ├── com.facebook.testing.screenshot.sample.StandardAndroidViewTest_testRendering.png │ │ └── fab.png └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── facebook │ │ └── testing │ │ └── screenshot │ │ └── sample │ │ ├── ExampleScreenshotTest.java │ │ ├── ImageRowScreenshotTest.java │ │ ├── MainActivityTest.kt │ │ ├── ScreenshotTestRunner.kt │ │ ├── ScreenshotViewAction.kt │ │ └── StandardAndroidViewTest.kt │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── facebook │ │ └── testing │ │ └── screenshot │ │ └── sample │ │ ├── App.kt │ │ ├── ExampleSpec.kt │ │ ├── ImageRowSpec.kt │ │ └── MainActivity.kt │ └── res │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ ├── activity_main.xml │ ├── litho_view.xml │ └── search_bar.xml │ ├── menu │ └── menu_main.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── settings.gradle └── versions.gradle /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | 78 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to screenshot-tests-for-android 2 | We want to make contributing to this project as easy and transparent as 3 | possible. 4 | 5 | ## Our Development Process 6 | 7 | Our internal repository, which is copied to GitHub, is our source of truth, 8 | and development happens both directly in GitHub and internally. 9 | Internally, we might build tools around this framework that we might move 10 | into the GitHub repository in the future, but we won't fork for internal changes. 11 | 12 | This repository has two components: 13 | 14 | * in `core/` you'll find code that actually runs on the device along 15 | with the test 16 | 17 | * in `plugin/` you'll find code that runs on the "host" machine. 18 | 19 | The 'plugin' code is broken into Groovy code that runs as part of your 20 | Gradle build, and in `src/py` you'll find python code that actually 21 | does the heavy work of pulling images and generating HTML files. 22 | 23 | We encourage tests for any pull request, tests can be run with 24 | 25 | ./gradlew connectedCheck -i 26 | 27 | ## Pull Requests 28 | We actively welcome your pull requests. 29 | 30 | 1. Fork the repo and create your branch from `main`. 31 | 2. If you've added code that should be tested, add tests 32 | 3. If you've changed APIs, update the documentation. 33 | 4. Ensure the test suite passes. 34 | 5. Make sure your code lints. 35 | 6. If you haven't already, complete the Contributor License Agreement ("CLA"). 36 | 37 | ## Contributor License Agreement ("CLA") 38 | In order to accept your pull request, we need you to submit a CLA. You only need 39 | to do this once to work on any of Facebook's open source projects. 40 | 41 | Complete your CLA here: 42 | 43 | ## Issues 44 | We use GitHub issues to track public bugs. Please ensure your description is 45 | clear and has sufficient instructions to be able to reproduce the issue. 46 | 47 | Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe 48 | disclosure of security bugs. In those cases, please go through the process 49 | outlined on that page and do not file a public issue. 50 | 51 | ## Coding Style 52 | * We use Google's Java formatter (https://github.com/google/google-java-format) with default settings. Please use it to format your changes. 53 | 54 | ## License 55 | By contributing screenshot-tests-for-android, you agree that your contributions will be licensed 56 | under its Apache 2 license. 57 | -------------------------------------------------------------------------------- /.github/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/screenshot-tests-for-android/2c1defad8d50e6aa33b0bdb56074cef563047ad0/.github/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | */build/ 3 | .gradle 4 | /repo/ 5 | /build 6 | repo 7 | examples/**/build 8 | examples/**/.idea/* 9 | .idea/* 10 | !.idea/codeStyleSettings.xml 11 | *.iml 12 | local.properties 13 | plugin/src/generated 14 | -------------------------------------------------------------------------------- /BUCK: -------------------------------------------------------------------------------- 1 | load("//tools/build_defs/oss:screenshot_test_defs.bzl", "SCREENSHOT_TESTS_CORE_TARGET", "SCREENSHOT_TESTS_LAYOUT_HIERARCHY_COMMON_TARGET", "SCREENSHOT_TESTS_LAYOUT_HIERARCHY_COROUTINES_TARGET", "SCREENSHOT_TESTS_LAYOUT_HIERARCHY_LITHO_TARGET", "SCREENSHOT_TESTS_VISIBILITY", "fb_core_android_library") 2 | 3 | fb_core_android_library( 4 | name = "screenshot", 5 | visibility = SCREENSHOT_TESTS_VISIBILITY, 6 | deps = [ 7 | SCREENSHOT_TESTS_CORE_TARGET, 8 | SCREENSHOT_TESTS_LAYOUT_HIERARCHY_COMMON_TARGET, 9 | SCREENSHOT_TESTS_LAYOUT_HIERARCHY_COROUTINES_TARGET, 10 | ], 11 | exported_deps = [ 12 | SCREENSHOT_TESTS_CORE_TARGET, 13 | SCREENSHOT_TESTS_LAYOUT_HIERARCHY_COMMON_TARGET, 14 | SCREENSHOT_TESTS_LAYOUT_HIERARCHY_COROUTINES_TARGET, 15 | ], 16 | ) 17 | 18 | fb_core_android_library( 19 | name = "screenshot-litho", 20 | visibility = SCREENSHOT_TESTS_VISIBILITY, 21 | deps = [ 22 | SCREENSHOT_TESTS_CORE_TARGET, 23 | SCREENSHOT_TESTS_LAYOUT_HIERARCHY_COMMON_TARGET, 24 | SCREENSHOT_TESTS_LAYOUT_HIERARCHY_COROUTINES_TARGET, 25 | SCREENSHOT_TESTS_LAYOUT_HIERARCHY_LITHO_TARGET, 26 | ], 27 | exported_deps = [ 28 | SCREENSHOT_TESTS_CORE_TARGET, 29 | SCREENSHOT_TESTS_LAYOUT_HIERARCHY_COMMON_TARGET, 30 | SCREENSHOT_TESTS_LAYOUT_HIERARCHY_COROUTINES_TARGET, 31 | SCREENSHOT_TESTS_LAYOUT_HIERARCHY_LITHO_TARGET, 32 | ], 33 | ) 34 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 0.15.0 (Feb 2 2022) 2 | ------ 3 | - Python 3 support 4 | - Added option to specify pulling a `tar` bundle instead of individual files via `bundleResults`. This is useful for reducing the time it takes to pull a large amount of screenshots. It will also help if you experience ADB connection flakiness. 5 | 6 | 0.14.0 (Apr 22 2021) 7 | ------ 8 | - Added test orchestrator support 9 | - Switched away from xml to json for metadata files 10 | - Support for Gradle 7 11 | 12 | 0.13.0 (Jul 8 2020) 13 | ------ 14 | - Made accessibility node information optional 15 | - Bugfixes surrounding obtaining accessibility node information 16 | 17 | 0.12.0 (Mar 4 2020) 18 | ------ 19 | - Added the ability to generate a diff between the old version of a screenshot and the new output in the html report. Currently only works with a configured test image API 20 | - Added support for AGP 3.6 + Gradle 6.2.x 21 | 22 | 0.11.0 (Oct 17 2019) 23 | ------ 24 | - Replaced direct usages of deprecated Gradle APIs 25 | - Adopted the Contributor Covenant 26 | - Added `failureDir` which saves the expected, actual and diff images of each failing test when verification fails. 27 | 28 | 0.10.0 (Jun 11 2019) 29 | ------ 30 | - Added batch downloading of screenshot images instead of pulling individual files 31 | - Added Accessibility hierarchy information 32 | - Added ability to specify max sizes for images 33 | - Fixed addDeps functionality parameter in the plugin 34 | - Fixed referenceDir functionality in the plugin 35 | - Fixed an issue on Samsung devices where a crash would occur when faking a WindowAttachment 36 | - Migrated to AndroidX 37 | 38 | 0.9.0 (Apr 1 2019) 39 | ----- 40 | - Added a setMaxPixels method to the record builder interface to allow for really large images 41 | - Added an integration point to allow you to see a version of the given screenshot from a server provided service 42 | - Fixed an issue where onGlobalLayoutListener wasn'nt being triggered properly 43 | - Fixed an issue where a parcel file descriptor wasn't being closed 44 | - Added the ability to dump the accessibility hierarchy 45 | - Min SDK has been bumped to 14 46 | - Added the ability to run tests on all connected targets 47 | 48 | 0.8.0 (Jul 30 2018) 49 | ----- 50 | - Replaced androidTestApi with androidTestImplementation when adding in core dependency via the plugin 51 | - Fixed a bug where requesting focus prior to being attached to a Window would crash 52 | - Added the ability to customize the max pixel size restriction 53 | - Moved generated report to build/ instead of /tmp 54 | - Added language to the device name calculation for multiple devices 55 | 56 | 0.7.0 (Apr 19 2018) 57 | ----- 58 | - Added the ability to retrieve the resulting Bitmap for custom use on your RecordBuilder 59 | - Removed the runtime dependency on Dexmaker, this will resolve any issues of using frameworks such as Mockito in your screenshot tests 60 | - Added a a check to fail when resultant screenshots are extremely large 61 | - Rewrote the client plugin to provide screenshot test tasks per applicable variant 62 | - Re-license to Apache 2 63 | 64 | 0.6.0 (Feb 06 2018) 65 | ----- 66 | - Added the ability to run screenshot tests on multiple devices at once 67 | - Set `multipleDevices` to `true` in your `screenshots` config in your Gradle file to enable this. 68 | - The core module no longer depends on junit 69 | - Upgraded to Gradle 4.4.1 70 | - Removed R and BuildConfig classes from release artifacts 71 | - Added a Buck file for the Python module 72 | 73 | 0.5.0 (Nov 20 2017) 74 | ----- 75 | - Upgraded to AGP 3 76 | - Upgraded to Gradle 4.3 77 | - Added layout-hierarchy-litho module for Litho support in LayoutHierarchy dumps 78 | - Rewrote the entire Layout Hierarchy dump system 79 | - Added more TextView information in hierarchy output 80 | - Added a param for custom Python executables 81 | - Fixed WindowAttachmentTest for API 26+ 82 | - Implemented a view hierarchy overlay for screenshots 83 | - Changed the dump output to use JSON instead of XML 84 | - Spruced up the results page (#117) 85 | - Added a dark background toggle button (#116) 86 | 87 | 0.4.3 (Jul 13 2017) 88 | ----- 89 | - Added more examples 90 | - Fixed a longstanding issue where we showed a horizontal break in the screenshots every 512 pixels. 91 | 92 | 0.4.2 (Sep 28 2016) 93 | ----- 94 | - Support for Android gradle plugin 2.2.0 95 | - Make ViewHierarchy dump more useful information 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Screenshot Tests for Android 2 | 3 | 4 | 5 | screenshot-tests-for-android is a library that can generate fast 6 | deterministic screenshots while running instrumentation tests on 7 | Android. 8 | 9 | We mimic Android's measure(), layout() and draw() to generate screenshots 10 | on the test thread. By not having to do the rendering on a separate 11 | thread we have control over animations and handler callbacks which 12 | makes the screenshots extremely deterministic and reliable for catching 13 | regressions in continuous integration. 14 | 15 | We also provide utilities for using screenshot tests during the development 16 | process. With these scripts you can iterate on a view or layout and quickly 17 | see how the view renders in a real Android environment, without having to 18 | build the whole app. You can also render the view in multiple configurations 19 | at one go. 20 | 21 | ## Documentation 22 | 23 | Take a look at the documentation at http://facebook.github.io/screenshot-tests-for-android/#getting-started 24 | 25 | ## Requirements 26 | 27 | screenshot-tests-for-android is known to work with MacOS or Linux. 28 | 29 | The host tooling probably doesn't work on Windows, but can be made to 30 | work with a little effort. We'll happily accept pull requests! 31 | 32 | You need python-2.7 for the gradle plugin to work, and we also 33 | recommending installing the python-pillow library which is required 34 | for recording and verifying screenshots. 35 | 36 | ## Building screenshot-tests-for-android 37 | 38 | You don't have to build screenshot-tests-for-android from scratch if 39 | you don't plan to contribute. All artifacts are available from Maven 40 | Central. 41 | 42 | If you plan to contribute, this is the code is broken up into a few modules: 43 | 44 | * The `core` module is packaged as part of your instrumentation tests 45 | and generates screenshots on the device. 46 | 47 | * The `plugin` module adds Gradle tasks to make it easier to work 48 | with screenshot tests. 49 | 50 | * The `layout-hierarchy-common` module adds extra common `View` information to your reports' layout hierarchy viewer 51 | 52 | * The `layout-hierarchy-litho` module adds extra Litho component information to your reports' layout hierarchy viewer 53 | 54 | 55 | In addition you'll find python code inside `plugin/src/py`. This code 56 | is packaged into the gradle plugin. 57 | 58 | We have tests for the python code and the core library. Run these 59 | commands to run all the tests: 60 | 61 | ```bash 62 | $ gradle :plugin:pyTests 63 | $ gradle :core:connectedAndroidTest 64 | ``` 65 | 66 | Both need a running emulator. 67 | 68 | Python tests rely on the `mock` and `Pillow` libraries. Both can be installed via `pip install mock` 69 | and `pip install Pillow`. 70 | 71 | You can install all the artifacts to your local maven repository using 72 | 73 | ```bash 74 | $ gradle installArchives 75 | ``` 76 | 77 | ## Running With a Remote Service 78 | 79 | For usage with a remote testing service (e.g. Google Cloud Test Lab) where ADB is not available directly the plugin supports a "disconnected" 80 | workflow. Collect all screenshots into a single directory and run the plugin using the following options 81 | 82 | ### Example 83 | The location of the screenshot artifacts can be configured in the project's build.gradle: 84 | ```groovy 85 | screenshots { 86 | // Points to the directory containing all the files pulled from a device 87 | referenceDir = path/to/screenshots 88 | } 89 | ``` 90 | 91 | Then, screenshots may be verified by executing the following: 92 | ```bash 93 | $ gradle ::verifyScreenshotTest 94 | ``` 95 | 96 | To record, simply change `verify` to `record`. 97 | 98 | ## Contributing 99 | 100 | Please see the [contributing](.github/CONTRIBUTING.md) file. 101 | 102 | ## Authors 103 | 104 | screenshot-tests-for-android was originally written by Arnold Noronha (arnold@tdrhq.com) 105 | You can reach him at @tdrhq on GitHub. 106 | 107 | It is currently maintained by Hilal Alsibai (@xiphirx) 108 | 109 | ## License 110 | 111 | screenshot-tests-for-android is Apache-2-licensed. 112 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | apply plugin: 'com.github.ben-manes.versions' 18 | 19 | buildscript { 20 | apply from: rootProject.file('versions.gradle') 21 | repositories { 22 | mavenCentral() 23 | google() 24 | jcenter() 25 | } 26 | 27 | dependencies { 28 | classpath plugs.agp 29 | classpath plugs.versions 30 | classpath plugs.publish 31 | classpath plugs.dokka 32 | } 33 | } 34 | 35 | allprojects { 36 | version = property('VERSION_NAME') 37 | repositories { 38 | mavenCentral() 39 | google() 40 | jcenter() 41 | } 42 | } 43 | 44 | def taskLogOutput = new StringBuilder() 45 | gradle.addListener(new TaskExecutionListener() { 46 | def listener = new StandardOutputListener() { 47 | @Override 48 | void onOutput(CharSequence charSequence) { 49 | taskLogOutput.append(charSequence) 50 | } 51 | } 52 | 53 | @Override 54 | void beforeExecute(Task task) { 55 | task.logging.addStandardOutputListener(listener) 56 | } 57 | 58 | @Override 59 | void afterExecute(Task task, TaskState state) { 60 | task.logging.removeStandardOutputListener(listener) 61 | } 62 | }) 63 | 64 | task cleanAll(dependsOn: [ 65 | ':core:clean', 66 | ':plugin:clean', 67 | ':layout-hierarchy-common:clean', 68 | ':layout-hierarchy-litho:clean', 69 | ]) 70 | 71 | task integrationTest(dependsOn: ':sample:runDebugAndroidTestScreenshotTest') { 72 | doLast { 73 | assert taskLogOutput.toString().contains('Found 12 screenshots') 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /core/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | apply plugin: 'com.android.library' 18 | apply plugin: "com.vanniktech.maven.publish" 19 | 20 | group = 'com.facebook.testing.screenshot' 21 | 22 | android { 23 | compileSdkVersion versions.compileSdk 24 | 25 | packagingOptions { 26 | exclude 'LICENSE.txt' 27 | } 28 | 29 | defaultConfig { 30 | minSdkVersion versions.minSdk 31 | targetSdkVersion versions.targetSdk 32 | testInstrumentationRunner "com.facebook.testing.screenshot.CustomScreenshotTestRunner" 33 | } 34 | 35 | lintOptions { 36 | abortOnError false 37 | disable 'InvalidPackage' 38 | } 39 | } 40 | 41 | afterEvaluate { 42 | generateReleaseBuildConfig.enabled = false 43 | } 44 | 45 | tasks.whenTaskAdded { 46 | if (it.name == "androidJavadoc") { 47 | it.configure { 48 | exclude '**/BUCK' 49 | } 50 | } 51 | } 52 | 53 | dependencies { 54 | compileOnly deps.jsr305 55 | 56 | implementation deps.supportAppCompat 57 | implementation deps.gson 58 | 59 | androidTestImplementation deps.dexmaker 60 | androidTestImplementation deps.dexmakerDx 61 | androidTestImplementation deps.junit 62 | androidTestImplementation deps.androidTestRules 63 | androidTestImplementation deps.espresso 64 | androidTestImplementation deps.mockito 65 | androidTestImplementation deps.dexmakerMockito 66 | androidTestImplementation deps.hamcrest 67 | } 68 | -------------------------------------------------------------------------------- /core/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=Core 2 | POM_DESCRIPTION=Screenshot tests core android library 3 | POM_ARTIFACT_ID=core 4 | POM_PACKAGING=aar 5 | -------------------------------------------------------------------------------- /core/src/androidTest/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /core/src/androidTest/java/com/facebook/testing/screenshot/CustomScreenshotTestRunner.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot; 18 | 19 | import android.os.Bundle; 20 | import androidx.test.runner.AndroidJUnitRunner; 21 | 22 | public class CustomScreenshotTestRunner extends AndroidJUnitRunner { 23 | @Override 24 | public void onCreate(Bundle args) { 25 | ScreenshotRunner.onCreate(this, args); 26 | super.onCreate(args); 27 | } 28 | 29 | @Override 30 | public void finish(int resultCode, Bundle results) { 31 | ScreenshotRunner.onDestroy(); 32 | super.finish(resultCode, results); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /core/src/androidTest/java/com/facebook/testing/screenshot/MyActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot; 18 | 19 | import android.app.Activity; 20 | 21 | /** A dummy activity */ 22 | public class MyActivity extends Activity { 23 | public boolean destroyed = false; 24 | 25 | @Override 26 | public void onDestroy() { 27 | super.onDestroy(); 28 | destroyed = true; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /core/src/androidTest/java/com/facebook/testing/screenshot/ScriptsFixtureTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot; 18 | 19 | import android.view.View; 20 | import android.view.ViewGroup; 21 | import android.widget.FrameLayout; 22 | import android.widget.TextView; 23 | import androidx.test.InstrumentationRegistry; 24 | import org.junit.Before; 25 | 26 | /** 27 | * This is not really a test, this test is just a "fixture" for all the tests for the scripts 28 | * related to running tests and getting screenshots. 29 | */ 30 | public class ScriptsFixtureTest { 31 | private static final int HEIGHT = 100; 32 | private static final int WIDTH = 200; 33 | 34 | private TextView mTextView; 35 | 36 | @Before 37 | public void setUp() throws Exception { 38 | mTextView = new TextView(InstrumentationRegistry.getInstrumentation().getTargetContext()); 39 | mTextView.setText("foobar"); 40 | 41 | // Unfortunately TextView needs a LayoutParams for onDraw 42 | mTextView.setLayoutParams( 43 | new FrameLayout.LayoutParams( 44 | ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); 45 | 46 | measureAndLayout(); 47 | } 48 | 49 | public void testGetTextViewScreenshot() throws Throwable { 50 | Screenshot.snap(mTextView).record(); 51 | } 52 | 53 | public void testSecondScreenshot() throws Throwable { 54 | mTextView.setText("foobar3"); 55 | measureAndLayout(); 56 | Screenshot.snap(mTextView).record(); 57 | } 58 | 59 | private void measureAndLayout() { 60 | final Throwable[] exceptions = new Throwable[1]; 61 | InstrumentationRegistry.getInstrumentation() 62 | .runOnMainSync( 63 | new Runnable() { 64 | public void run() { 65 | try { 66 | mTextView.measure( 67 | View.MeasureSpec.makeMeasureSpec(WIDTH, View.MeasureSpec.EXACTLY), 68 | View.MeasureSpec.makeMeasureSpec(HEIGHT, View.MeasureSpec.EXACTLY)); 69 | mTextView.layout( 70 | 0, 0, mTextView.getMeasuredWidth(), mTextView.getMeasuredHeight()); 71 | } catch (Throwable throwable) { 72 | exceptions[0] = throwable; 73 | } 74 | } 75 | }); 76 | if (exceptions[0] != null) { 77 | throw new RuntimeException(exceptions[0]); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /core/src/androidTest/java/com/facebook/testing/screenshot/ViewHelpersTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot; 18 | 19 | import static org.hamcrest.MatcherAssert.assertThat; 20 | import static org.hamcrest.Matchers.*; 21 | import static org.junit.Assert.assertEquals; 22 | 23 | import android.content.Context; 24 | import android.widget.ArrayAdapter; 25 | import android.widget.ListView; 26 | import android.widget.TextView; 27 | import androidx.test.InstrumentationRegistry; 28 | import com.facebook.testing.screenshot.test.R; 29 | import org.junit.Before; 30 | 31 | /** Tests {@link ViewHelpers} */ 32 | public class ViewHelpersTest { 33 | private TextView mTextView; 34 | private Context targetContext; 35 | 36 | @Before 37 | public void setUp() throws Exception { 38 | targetContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 39 | mTextView = new TextView(targetContext); 40 | mTextView.setText("foobar"); 41 | } 42 | 43 | public void testPreconditions() throws Throwable { 44 | assertEquals(0, mTextView.getMeasuredHeight()); 45 | } 46 | 47 | public void testMeasureWithoutHeight() throws Throwable { 48 | ViewHelpers.setupView(mTextView).setExactWidthDp(100).layout(); 49 | 50 | assertThat(mTextView.getMeasuredHeight(), greaterThan(0)); 51 | } 52 | 53 | public void testMeasureWithoutHeightPx() throws Throwable { 54 | ViewHelpers.setupView(mTextView).setExactWidthPx(100).layout(); 55 | 56 | assertThat(mTextView.getMeasuredHeight(), greaterThan(0)); 57 | } 58 | 59 | public void testMeasureForOnlyWidth() throws Throwable { 60 | ViewHelpers.setupView(mTextView).setExactHeightPx(100).layout(); 61 | 62 | assertThat(mTextView.getMeasuredHeight(), equalTo(100)); 63 | assertThat(mTextView.getMeasuredWidth(), greaterThan(0)); 64 | } 65 | 66 | public void testBothWrapContent() throws Throwable { 67 | ViewHelpers.setupView(mTextView).layout(); 68 | 69 | assertThat(mTextView.getMeasuredHeight(), greaterThan(0)); 70 | assertThat(mTextView.getMeasuredWidth(), greaterThan(0)); 71 | } 72 | 73 | public void testHeightAndWidthCorrectlyPropagated() throws Throwable { 74 | ViewHelpers.setupView(mTextView).setExactHeightDp(100).setExactWidthDp(1000).layout(); 75 | 76 | assertThat(mTextView.getMeasuredWidth(), greaterThan(mTextView.getMeasuredHeight())); 77 | } 78 | 79 | public void testListViewHeight() throws Throwable { 80 | ListView view = new ListView(targetContext); 81 | view.setDividerHeight(0); 82 | ArrayAdapter adapter = 83 | new ArrayAdapter(targetContext, R.layout.testing_simple_textview); 84 | view.setAdapter(adapter); 85 | 86 | for (int i = 0; i < 20; i++) { 87 | adapter.add("foo"); 88 | } 89 | 90 | ViewHelpers.setupView(view).guessListViewHeight().setExactWidthDp(200).layout(); 91 | 92 | assertThat(view.getMeasuredHeight(), greaterThan(10)); 93 | 94 | int oneHeight = view.getChildAt(0).getMeasuredHeight(); 95 | assertThat(view.getMeasuredHeight(), equalTo(oneHeight * 20)); 96 | } 97 | 98 | public void testMaxHeightLessThanHeight() throws Throwable { 99 | ViewHelpers.setupView(mTextView).setMaxHeightPx(100).layout(); 100 | assertThat(mTextView.getMeasuredHeight(), lessThan(100)); 101 | } 102 | 103 | public void testMaxHeightUsesFullHeight() throws Throwable { 104 | ViewHelpers.setupView(mTextView).setMaxHeightPx(1).layout(); 105 | assertThat(mTextView.getMeasuredHeight(), equalTo(1)); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /core/src/androidTest/java/com/facebook/testing/screenshot/internal/OldApiBandaid.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot.internal; 18 | 19 | import java.util.regex.Matcher; 20 | import java.util.regex.Pattern; 21 | 22 | /** This exists to help alleviate some API transition pain */ 23 | public class OldApiBandaid { 24 | private OldApiBandaid() {} 25 | 26 | public static void assertMatchesRegex(String expected, String actual) { 27 | assertMatchesRegex(null, expected, actual); 28 | } 29 | 30 | public static void assertMatchesRegex(String message, String expected, String actual) { 31 | if (actual == null) { 32 | throw new IllegalStateException("Actual == null"); 33 | } 34 | Pattern pattern = Pattern.compile(expected); 35 | Matcher matcher = pattern.matcher(actual); 36 | if (!matcher.matches()) { 37 | if (message != null) { 38 | throw new IllegalStateException(message + " " + actual); 39 | } 40 | throw new IllegalStateException(actual + " does not match regex " + expected); 41 | } 42 | } 43 | 44 | public static void assertContainsRegex(String expected, String actual) { 45 | if (actual == null) { 46 | throw new IllegalStateException("Actual == null"); 47 | } 48 | Pattern pattern = Pattern.compile(expected); 49 | Matcher matcher = pattern.matcher(actual); 50 | if (!matcher.find()) { 51 | throw new IllegalStateException(actual + " does not contain regex " + expected); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /core/src/androidTest/java/com/facebook/testing/screenshot/internal/RecordBuilderImplTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot.internal; 18 | 19 | import static org.mockito.Mockito.*; 20 | 21 | import org.junit.Before; 22 | 23 | /** Tests {@link RecordBuilderImpl} */ 24 | public class RecordBuilderImplTest { 25 | private ScreenshotImpl mScreenshotImpl; 26 | 27 | @Before 28 | public void setUp() throws Exception { 29 | mScreenshotImpl = mock(ScreenshotImpl.class); 30 | } 31 | 32 | public void testIncompleteTiles() throws Throwable { 33 | RecordBuilderImpl recordBuilder = 34 | new RecordBuilderImpl(mScreenshotImpl).setTiling(new Tiling(3, 4)); 35 | 36 | try { 37 | recordBuilder.record(); 38 | throw new IllegalStateException("expected exception"); 39 | } catch (IllegalStateException e) { 40 | OldApiBandaid.assertMatchesRegex(".*tiles.*", e.getMessage()); 41 | } 42 | } 43 | 44 | public void testCompleteTiles() throws Throwable { 45 | RecordBuilderImpl recordBuilder = 46 | new RecordBuilderImpl(mScreenshotImpl).setTiling(new Tiling(3, 4)); 47 | 48 | for (int i = 0; i < 3; i++) { 49 | for (int j = 0; j < 4; j++) { 50 | recordBuilder.getTiling().setAt(i, j, "foobar"); 51 | } 52 | } 53 | 54 | recordBuilder.record(); 55 | } 56 | 57 | public void testWithErrorStillDoesntFail() throws Throwable { 58 | RecordBuilderImpl recordBuilder = new RecordBuilderImpl(mScreenshotImpl); 59 | 60 | recordBuilder.setError("foo"); 61 | recordBuilder.record(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /core/src/androidTest/java/com/facebook/testing/screenshot/internal/ScreenshotDirectoriesTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot.internal; 18 | 19 | import static org.junit.Assert.*; 20 | 21 | import android.content.Context; 22 | import androidx.test.InstrumentationRegistry; 23 | import java.io.File; 24 | import javax.annotation.Nullable; 25 | import org.junit.After; 26 | import org.junit.Test; 27 | 28 | public class ScreenshotDirectoriesTest { 29 | @Nullable File mDir; 30 | 31 | @After 32 | public void teardown() throws Exception { 33 | if (mDir != null) { 34 | mDir.delete(); 35 | } 36 | } 37 | 38 | @Test 39 | public void testUsesSdcard() { 40 | Context context = InstrumentationRegistry.getTargetContext(); 41 | ScreenshotDirectories dirs = new ScreenshotDirectories(context); 42 | 43 | mDir = dirs.get("foobar"); 44 | assertTrue(mDir.exists()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /core/src/androidTest/java/com/facebook/testing/screenshot/internal/TestNameDetectorForJUnit4Test.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot.internal; 18 | 19 | import static org.junit.Assert.*; 20 | 21 | import org.junit.Test; 22 | 23 | /** Tests {@link TestNameDetector} (for JUnit4 style tests) */ 24 | public class TestNameDetectorForJUnit4Test { 25 | @Test 26 | public void testTestNameIsDetectedOnNonUiThread() throws Throwable { 27 | assertEquals("testTestNameIsDetectedOnNonUiThread", TestNameDetector.getTestName()); 28 | assertEquals( 29 | "com.facebook.testing.screenshot.internal.TestNameDetectorForJUnit4Test", 30 | TestNameDetector.getTestClass()); 31 | } 32 | 33 | @Test 34 | public void testDelegated() throws Throwable { 35 | delegate(true); 36 | delegatePrivate(); 37 | } 38 | 39 | public void delegate(boolean foobar) { 40 | assertEquals("testDelegated", TestNameDetector.getTestName()); 41 | } 42 | 43 | private void delegatePrivate() { 44 | assertEquals("testDelegated", TestNameDetector.getTestName()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /core/src/androidTest/java/com/facebook/testing/screenshot/internal/TestNameDetectorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot.internal; 18 | 19 | import static org.junit.Assert.assertEquals; 20 | 21 | import androidx.test.annotation.UiThreadTest; 22 | import org.junit.Test; 23 | 24 | /** Tests {@link TestNameDetector} */ 25 | public class TestNameDetectorTest { 26 | @Test 27 | @UiThreadTest 28 | public void testTestNameIsDetected() throws Throwable { 29 | assertEquals("testTestNameIsDetected", TestNameDetector.getTestName()); 30 | assertEquals( 31 | "com.facebook.testing.screenshot.internal.TestNameDetectorTest", 32 | TestNameDetector.getTestClass()); 33 | } 34 | 35 | @Test 36 | public void testTestNameIsDetectedOnNonUiThread() throws Throwable { 37 | assertEquals("testTestNameIsDetectedOnNonUiThread", TestNameDetector.getTestName()); 38 | assertEquals( 39 | "com.facebook.testing.screenshot.internal.TestNameDetectorTest", 40 | TestNameDetector.getTestClass()); 41 | } 42 | 43 | @Test 44 | @UiThreadTest 45 | public void testTestNameIsDetectedThroughExtraMethod() throws Throwable { 46 | extraLayerMethod(); 47 | } 48 | 49 | private void extraLayerMethod() { 50 | assertEquals("testTestNameIsDetectedThroughExtraMethod", TestNameDetector.getTestName()); 51 | assertEquals( 52 | "com.facebook.testing.screenshot.internal.TestNameDetectorTest", 53 | TestNameDetector.getTestClass()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /core/src/androidTest/res/layout/testing_for_view_hierarchy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 15 | 16 | 21 | 22 | 27 | 32 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /core/src/androidTest/res/layout/testing_simple_textview.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /core/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/BUCK: -------------------------------------------------------------------------------- 1 | load("//tools/build_defs/oss:screenshot_test_defs.bzl", "SCREENSHOT_TESTS_VISIBILITY", "fb_core_android_library") 2 | 3 | fb_core_android_library( 4 | name = "screenshot", 5 | srcs = glob(["**/*.java"]), 6 | visibility = SCREENSHOT_TESTS_VISIBILITY, 7 | deps = [ 8 | "//fbandroid/third-party/java/infer-annotations:infer-annotations", 9 | "//third-party/java/androidx/annotation/annotation:annotation", 10 | "//third-party/java/androidx/core/core:core", 11 | "//third-party/java/com/google/code/findbugs/jsr305:jsr305", 12 | "//third-party/java/com/google/code/gson/gson:gson", 13 | "//third-party/java/com/google/guava/guava:guava", 14 | ], 15 | ) 16 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/RecordBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot; 18 | 19 | import android.graphics.Bitmap; 20 | 21 | /** Builds all the information related to a screenshot. */ 22 | public interface RecordBuilder { 23 | /** 24 | * Set a name (identifier) for the screenshot. If you skip the name a name will be generated based 25 | * on the Test class and Test method name this is being run from. That means if you have multiple 26 | * screenshots in the same test, then you have to explicitly specify names to disambiguate. 27 | */ 28 | RecordBuilder setName(String name); 29 | 30 | /** 31 | * Set a long description of the what the screenshot is about. 32 | * 33 | *

This will be shown as part of the report, and in general it can help document a screenshot 34 | * if you're using it as part of an external tooling. 35 | */ 36 | RecordBuilder setDescription(String description); 37 | 38 | /** 39 | * Add extra metadata about this screenshots. 40 | * 41 | *

There will be no semantic information associated with this metadata, but we'll try to 42 | * provide this as debugging information whenever you're viewing screenshots. 43 | */ 44 | RecordBuilder addExtra(String key, String value); 45 | 46 | /** Groups similar or identical screenshots which makes it easier to compare. */ 47 | RecordBuilder setGroup(String groupName); 48 | 49 | /** 50 | * Enables or disables extra information attached to the metadata generated related to 51 | * accessibility information. 52 | * 53 | * @param includeAccessibilityInfo 54 | */ 55 | RecordBuilder setIncludeAccessibilityInfo(boolean includeAccessibilityInfo); 56 | 57 | /** 58 | * Stops the recording and returns the generated bitmap, possibly compressed. 59 | * 60 | *

You cannot call this after record(), nor can you call record() after this call. 61 | */ 62 | Bitmap getBitmap(); 63 | 64 | /** 65 | * Set the maximum number of pixels this screenshot should produce. Producing any number higher 66 | * will throw an exception. 67 | * 68 | * @param maxPixels Maximum number of pixels this screenshot should produce. Specify zero or a 69 | * negative number for no limit. 70 | */ 71 | RecordBuilder setMaxPixels(long maxPixels); 72 | 73 | /** Finish the recording. */ 74 | void record(); 75 | } 76 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/Screenshot.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot; 18 | 19 | import android.app.Activity; 20 | import android.view.View; 21 | import com.facebook.infer.annotation.Nullsafe; 22 | import com.facebook.testing.screenshot.internal.ScreenshotImpl; 23 | 24 | /** 25 | * A testing tool for taking a screenshot during an Activity instrumentation test. This is really 26 | * useful while manually investigating how the rendering looks like after setting up some complex 27 | * set of conditions in the test. (Which might be hard to manually recreate) 28 | * 29 | *

Eventually we can use this to catch rendering changes, with very little work added to the 30 | * instrumentation test. 31 | */ 32 | @Nullsafe(Nullsafe.Mode.LOCAL) 33 | public class Screenshot { 34 | /** 35 | * Take a snapshot of an already measured and layout-ed view. See adb-logcat for how to pull the 36 | * screenshot. 37 | * 38 | *

This method is thread safe. 39 | */ 40 | public static RecordBuilder snap(View measuredView) { 41 | return ScreenshotImpl.getInstance().snap(measuredView); 42 | } 43 | 44 | /** 45 | * Take a snapshot of the activity and store it with the the testName. See the adb-logcat for how 46 | * to pull the screenshot. 47 | * 48 | *

This method is thread safe. 49 | */ 50 | public static RecordBuilder snapActivity(Activity activity) { 51 | return ScreenshotImpl.getInstance().snapActivity(activity); 52 | } 53 | 54 | /** 55 | * @return The largest amount of pixels we'll capture, otherwise an exception will be thrown. 56 | */ 57 | public static long getMaxPixels() { 58 | return ScreenshotImpl.getMaxPixels(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/ScreenshotRunner.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot; 18 | 19 | import android.app.Instrumentation; 20 | import android.os.Bundle; 21 | import com.facebook.infer.annotation.Nullsafe; 22 | import com.facebook.testing.screenshot.internal.Registry; 23 | import com.facebook.testing.screenshot.internal.ScreenshotImpl; 24 | 25 | /** 26 | * The ScreenshotRunner needs to be called from the top level Instrumentation test runner before and 27 | * after all the tests run. 28 | */ 29 | @Nullsafe(Nullsafe.Mode.LOCAL) 30 | public abstract class ScreenshotRunner { 31 | 32 | /** These strings can be used as Keys to Bundle Arguments. */ 33 | public static final String SDCARD_DIRECTORY = "sdcard_directory"; 34 | 35 | public static final String SCREENSHOT_TESTS_RUN_ID = "SCREENSHOT_TESTS_RUN_ID"; 36 | 37 | /** 38 | * Call this exactly once in your process before any screenshots are generated. 39 | * 40 | *

Typically this will be in {@code AndroidJUnitRunner#onCreate()} 41 | */ 42 | public static void onCreate(Instrumentation instrumentation, Bundle arguments) { 43 | Registry registry = Registry.getRegistry(); 44 | registry.instrumentation = instrumentation; 45 | registry.arguments = arguments; 46 | } 47 | 48 | /** 49 | * Call this exactly once after all your tests have run. 50 | * 51 | *

Typically this can be in {@code AndroidJUnitRunner#finish()} 52 | */ 53 | public static void onDestroy() { 54 | if (ScreenshotImpl.hasBeenCreated()) { 55 | ScreenshotImpl.getInstance().flush(); 56 | } 57 | 58 | Registry.clear(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/internal/Album.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot.internal; 18 | 19 | import android.graphics.Bitmap; 20 | import java.io.IOException; 21 | 22 | /** Stores metadata about an album of screenshots during an instrumentation test run. */ 23 | public interface Album { 24 | 25 | /** 26 | * Writes the bitmap corresponding to the screenshot with the name {@code name} in the {@code 27 | * (tilei, tilej)} position. 28 | */ 29 | String writeBitmap(String name, int tilei, int tilej, Bitmap bitmap) throws IOException; 30 | 31 | /** Call after all the screenshots are done. */ 32 | void flush(); 33 | 34 | /** Cleanup any disk state associated with this album. */ 35 | void cleanup(); 36 | 37 | /** 38 | * Opens a stream to dump the view hierarchy into. This should be called before addRecord() is 39 | * called for the given name. 40 | * 41 | *

It is the callers responsibility to call {@code close()} on the returned stream. 42 | */ 43 | void writeViewHierarchyFile(String name, String data) throws IOException; 44 | 45 | /** 46 | * Opens a stream to dump the accessibility issues into. This should be called before addRecord() 47 | * is called for the given name. 48 | * 49 | *

It is the callers responsibility to call {@code close()} on the returned stream. 50 | */ 51 | void writeAxIssuesFile(String name, String data) throws IOException; 52 | 53 | /** This is called after every record is finally set up. */ 54 | void addRecord(RecordBuilderImpl recordBuilder) throws IOException; 55 | } 56 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/internal/Registry.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot.internal; 18 | 19 | import android.app.Instrumentation; 20 | import android.os.Bundle; 21 | import com.facebook.infer.annotation.Nullsafe; 22 | import javax.annotation.Nullable; 23 | 24 | /** Stores some of the static state. We bundle this into a class for easy cleanup. */ 25 | @Nullsafe(Nullsafe.Mode.LOCAL) 26 | public class Registry { 27 | @Nullable private static Registry sRegistry; 28 | // NULLSAFE_FIXME[Field Not Initialized] 29 | public Instrumentation instrumentation; 30 | // NULLSAFE_FIXME[Field Not Initialized] 31 | public Bundle arguments; 32 | 33 | Registry() {} 34 | 35 | public static Registry getRegistry() { 36 | if (sRegistry == null) { 37 | sRegistry = new Registry(); 38 | } 39 | 40 | return sRegistry; 41 | } 42 | 43 | public static void clear() { 44 | sRegistry = null; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/internal/ReportArtifactsManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot.internal; 18 | 19 | import com.facebook.infer.annotation.Nullsafe; 20 | import java.io.File; 21 | import java.io.FileOutputStream; 22 | import java.io.IOException; 23 | import javax.annotation.Nullable; 24 | 25 | @Nullsafe(Nullsafe.Mode.LOCAL) 26 | class ReportArtifactsManager { 27 | 28 | private final String mTestRunId; 29 | private final File mRootDir; 30 | @Nullable private File mCurrentTestRunReportsDirectory; 31 | 32 | public ReportArtifactsManager(String testRunId, File rootDir) { 33 | mTestRunId = testRunId; 34 | mRootDir = rootDir; 35 | } 36 | 37 | public void recordFile(String fileName, byte[] content) throws IOException { 38 | File reportsDirectory = getOrCreateCurrentTestRunReportsDirectory(); 39 | try (FileOutputStream recordedFile = 40 | new FileOutputStream(new File(reportsDirectory, fileName))) { 41 | recordedFile.write(content); 42 | } 43 | } 44 | 45 | @Nullable 46 | public File readFile(String fileName) { 47 | File requestedFile = new File(getOrCreateCurrentTestRunReportsDirectory(), fileName); 48 | if (requestedFile.isFile()) { 49 | return requestedFile; 50 | } else { 51 | return null; 52 | } 53 | } 54 | 55 | private File getOrCreateCurrentTestRunReportsDirectory() { 56 | if (mCurrentTestRunReportsDirectory == null) { 57 | mCurrentTestRunReportsDirectory = new File(mRootDir, mTestRunId); 58 | if (!mCurrentTestRunReportsDirectory.mkdir() 59 | && !mCurrentTestRunReportsDirectory.isDirectory()) { 60 | throw new IllegalStateException("Unable to create a directory to store reports."); 61 | } 62 | } 63 | return mCurrentTestRunReportsDirectory; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/internal/TestNameDetector.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot.internal; 18 | 19 | import com.facebook.infer.annotation.Nullsafe; 20 | import java.lang.annotation.Annotation; 21 | import java.lang.reflect.Method; 22 | import javax.annotation.Nullable; 23 | 24 | /** Detect the test name and class that is being run currently. */ 25 | @Nullsafe(Nullsafe.Mode.LOCAL) 26 | public class TestNameDetector { 27 | private static final String JUNIT_TEST_CASE = "junit.framework.TestCase"; 28 | private static final String JUNIT_RUN_WITH = "org.junit.runner.RunWith"; 29 | private static final String JUNIT_TEST = "org.junit.Test"; 30 | private static final String UNKNOWN = "unknown"; 31 | 32 | private TestNameDetector() {} 33 | 34 | /** 35 | * Get the current test class in a standard JUnit3 or JUnit4 test, or "unknown" if we couldn't 36 | * detect it. 37 | */ 38 | public static String getTestClass() { 39 | StackTraceElement element = getFirstTestElement(new Throwable().getStackTrace()); 40 | if (element == null) { 41 | return UNKNOWN; 42 | } 43 | return element.getClassName(); 44 | } 45 | 46 | /** 47 | * Get the current test name in a standard JUnit3 or JUnit4 test, or "unknown" if we couldn't 48 | * detect it. 49 | */ 50 | public static String getTestName() { 51 | StackTraceElement[] stack = new Throwable().getStackTrace(); 52 | StackTraceElement testElement = getFirstTestElement(stack); 53 | if (testElement == null) { 54 | return UNKNOWN; 55 | } 56 | String methodName = testElement.getMethodName(); 57 | for (StackTraceElement element : stack) { 58 | if (testElement.getClassName().equals(element.getClassName())) { 59 | methodName = element.getMethodName(); 60 | } 61 | } 62 | return methodName; 63 | } 64 | 65 | private static @Nullable StackTraceElement getFirstTestElement(StackTraceElement[] stack) { 66 | for (StackTraceElement element : stack) { 67 | try { 68 | Class clazz = Class.forName(element.getClassName()); 69 | Method method = clazz.getMethod(element.getMethodName()); 70 | if (isTestClass(clazz) || isTestMethod(method)) { 71 | return element; 72 | } 73 | } catch (NoSuchMethodException ignored) { 74 | // Not actionable, move onto the next element 75 | } catch (ClassNotFoundException ignored) { 76 | // Not actionable, move onto the next element 77 | } 78 | } 79 | return null; 80 | } 81 | 82 | private static boolean isTestClass(@Nullable Class clazz) { 83 | return clazz != null 84 | && (JUNIT_TEST_CASE.equals(clazz.getCanonicalName()) 85 | || hasAnnotation(clazz.getAnnotations(), JUNIT_RUN_WITH) 86 | || isTestClass(clazz.getSuperclass())); 87 | } 88 | 89 | private static boolean isTestMethod(Method method) { 90 | return hasAnnotation(method.getAnnotations(), JUNIT_TEST); 91 | } 92 | 93 | private static boolean hasAnnotation(Annotation[] annotations, String annotationCanonicalName) { 94 | for (Annotation annotation : annotations) { 95 | if (annotationCanonicalName.equalsIgnoreCase( 96 | annotation.annotationType().getCanonicalName())) { 97 | return true; 98 | } 99 | } 100 | return false; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/internal/Tiling.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot.internal; 18 | 19 | import com.facebook.infer.annotation.Nullsafe; 20 | 21 | /** 22 | * A 2D layout of image tiles. We represent images as strings which can be looked up in an {@code 23 | * AlbumImpl} 24 | */ 25 | @Nullsafe(Nullsafe.Mode.LOCAL) 26 | public class Tiling { 27 | private int mWidth; 28 | private int mHeight; 29 | private String[][] mContents; 30 | 31 | public Tiling(int width, int height) { 32 | mWidth = width; 33 | mHeight = height; 34 | mContents = new String[width][height]; 35 | } 36 | 37 | /** Convenience factory method for tests */ 38 | public static Tiling singleTile(String name) { 39 | Tiling ret = new Tiling(1, 1); 40 | ret.setAt(0, 0, name); 41 | return ret; 42 | } 43 | 44 | public int getHeight() { 45 | return mHeight; 46 | } 47 | 48 | public int getWidth() { 49 | return mWidth; 50 | } 51 | 52 | public String getAt(int x, int y) { 53 | return mContents[x][y]; 54 | } 55 | 56 | public void setAt(int x, int y, String name) { 57 | mContents[x][y] = name; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/layouthierarchy/AbstractAttributePlugin.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot.layouthierarchy; 18 | 19 | import android.text.TextUtils; 20 | import com.facebook.infer.annotation.Nullsafe; 21 | import org.json.JSONException; 22 | import org.json.JSONObject; 23 | 24 | @Nullsafe(Nullsafe.Mode.LOCAL) 25 | public abstract class AbstractAttributePlugin implements AttributePlugin { 26 | protected String prefix(String name) { 27 | String prefix = namespace(); 28 | if (TextUtils.isEmpty(prefix)) { 29 | return name; 30 | } 31 | return prefix + ":" + name; 32 | } 33 | 34 | protected void put(JSONObject node, String key, String value) throws JSONException { 35 | node.put(prefix(key), value); 36 | } 37 | 38 | protected void putPlain(JSONObject node, String key, String value) throws JSONException { 39 | node.put(key, value); 40 | } 41 | 42 | protected void putRequired(JSONObject node, String name, int left, int top, int width, int height) 43 | throws JSONException { 44 | node.put(KEY_CLASS, name); 45 | node.put(KEY_LEFT, left); 46 | node.put(KEY_TOP, top); 47 | node.put(KEY_WIDTH, width); 48 | node.put(KEY_HEIGHT, height); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/layouthierarchy/AccessibilityIssuesDumper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot.layouthierarchy; 18 | 19 | import android.view.View; 20 | import android.view.ViewGroup; 21 | import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; 22 | import com.facebook.infer.annotation.Nullsafe; 23 | import javax.annotation.Nullable; 24 | import org.json.JSONArray; 25 | import org.json.JSONException; 26 | import org.json.JSONObject; 27 | 28 | @Nullsafe(Nullsafe.Mode.LOCAL) 29 | public final class AccessibilityIssuesDumper { 30 | 31 | AccessibilityIssuesDumper() {} 32 | 33 | public static JSONArray dumpIssues(AccessibilityUtil.AXTreeNode axTree) throws JSONException { 34 | JSONArray root = new JSONArray(); 35 | 36 | JSONObject focusableElementsWithoutFeedback = 37 | findTalkbackFocusableElementsWithoutSpokenFeedback(axTree); 38 | if (focusableElementsWithoutFeedback != null) { 39 | root.put(focusableElementsWithoutFeedback); 40 | } 41 | 42 | return root; 43 | } 44 | 45 | private static @Nullable JSONObject findTalkbackFocusableElementsWithoutSpokenFeedback( 46 | AccessibilityUtil.AXTreeNode axTree) throws JSONException { 47 | JSONObject evaluation = new JSONObject(); 48 | evaluation.put("id", "talkback_focusable_element_without_spoken_feedback"); 49 | evaluation.put("name", "Focusable Element Without Spoken Feedback"); 50 | evaluation.put( 51 | "description", 52 | "The element is focusable by screen readers such as Talkback, but has no text to " 53 | + "announce."); 54 | 55 | JSONArray elements = new JSONArray(); 56 | for (AccessibilityUtil.AXTreeNode axTreeNode : axTree.getAllNodes()) { 57 | View view = axTreeNode.getView(); 58 | AccessibilityNodeInfoCompat nodeInfo = axTreeNode.getNodeInfo(); 59 | if (AccessibilityUtil.isTalkbackFocusable(view) 60 | && !AccessibilityUtil.isSpeakingNode(nodeInfo, view)) { 61 | JSONObject element = new JSONObject(); 62 | element.put("name", view.getClass().getSimpleName()); 63 | element.put("class", view.getClass().getName()); 64 | JSONObject elementPos = new JSONObject(); 65 | elementPos.put("left", view.getLeft()); 66 | elementPos.put("top", view.getTop()); 67 | elementPos.put("width", view.getWidth()); 68 | elementPos.put("height", view.getHeight()); 69 | element.put("position", elementPos); 70 | JSONArray suggestions = new JSONArray(); 71 | suggestions.put("Add a contentDescription to the element."); 72 | if (view instanceof ViewGroup) { 73 | suggestions.put("Add a contentDescription or visible text to a child element."); 74 | } 75 | element.put("suggestions", suggestions); 76 | elements.put(element); 77 | } 78 | } 79 | 80 | if (elements.length() > 0) { 81 | evaluation.put("elements", elements); 82 | return evaluation; 83 | } 84 | 85 | return null; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/layouthierarchy/AttributePlugin.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot.layouthierarchy; 18 | 19 | import android.graphics.Point; 20 | import com.facebook.infer.annotation.Nullsafe; 21 | import org.json.JSONException; 22 | import org.json.JSONObject; 23 | 24 | /** 25 | * A plugin for a layout hierarchy that allows you to configure what attributes are added per view 26 | * type. 27 | */ 28 | @Nullsafe(Nullsafe.Mode.LOCAL) 29 | public interface AttributePlugin { 30 | String KEY_CLASS = "class"; 31 | String KEY_LEFT = "left"; 32 | String KEY_TOP = "top"; 33 | String KEY_WIDTH = "width"; 34 | String KEY_HEIGHT = "height"; 35 | 36 | /** Determines whether this plugin operates on the given type */ 37 | boolean accept(Object obj); 38 | 39 | /** Returns the namespace of the attributes this plugin inserts */ 40 | String namespace(); 41 | 42 | /** Puts all interesting attributes of the given object into the node */ 43 | void putAttributes(JSONObject node, Object obj, Point offset) throws JSONException; 44 | } 45 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/layouthierarchy/BaseViewAttributePlugin.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot.layouthierarchy; 18 | 19 | import android.graphics.Point; 20 | import android.view.View; 21 | import com.facebook.infer.annotation.Nullsafe; 22 | import org.json.JSONException; 23 | import org.json.JSONObject; 24 | 25 | /** Dumps basic information that applies to all {@link View}s, like position and class */ 26 | @Nullsafe(Nullsafe.Mode.LOCAL) 27 | public class BaseViewAttributePlugin extends AbstractAttributePlugin { 28 | private static final BaseViewAttributePlugin INSTANCE = new BaseViewAttributePlugin(); 29 | 30 | public static BaseViewAttributePlugin getInstance() { 31 | return INSTANCE; 32 | } 33 | 34 | private BaseViewAttributePlugin() { 35 | // Single instance 36 | } 37 | 38 | @Override 39 | public boolean accept(Object obj) { 40 | return obj instanceof View; 41 | } 42 | 43 | @Override 44 | public String namespace() { 45 | return ""; 46 | } 47 | 48 | @Override 49 | public void putAttributes(JSONObject node, Object obj, Point offset) throws JSONException { 50 | final View view = (View) obj; 51 | putRequired( 52 | node, 53 | // NULLSAFE_FIXME[Parameter Not Nullable] 54 | view.getClass().getCanonicalName(), 55 | offset.x + LayoutHierarchyDumper.getViewLeft(view), 56 | offset.y + LayoutHierarchyDumper.getViewTop(view), 57 | view.getWidth(), 58 | view.getHeight()); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/layouthierarchy/BaseViewHierarchyPlugin.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot.layouthierarchy; 18 | 19 | import android.graphics.Point; 20 | import android.view.View; 21 | import android.view.ViewGroup; 22 | import com.facebook.infer.annotation.Nullsafe; 23 | import org.json.JSONArray; 24 | import org.json.JSONException; 25 | import org.json.JSONObject; 26 | 27 | @Nullsafe(Nullsafe.Mode.LOCAL) 28 | public class BaseViewHierarchyPlugin implements HierarchyPlugin { 29 | private static BaseViewHierarchyPlugin INSTANCE = new BaseViewHierarchyPlugin(); 30 | 31 | public static BaseViewHierarchyPlugin getInstance() { 32 | return INSTANCE; 33 | } 34 | 35 | private BaseViewHierarchyPlugin() { 36 | // Stateless, no instances 37 | } 38 | 39 | @Override 40 | public boolean accept(Object obj) { 41 | return obj instanceof View; 42 | } 43 | 44 | @Override 45 | public void putHierarchy(LayoutHierarchyDumper dumper, JSONObject root, Object view, Point offset) 46 | throws JSONException { 47 | if (!(view instanceof ViewGroup)) { 48 | return; 49 | } 50 | 51 | ViewGroup group = (ViewGroup) view; 52 | final int offsetLeft = LayoutHierarchyDumper.getViewLeft(group); 53 | final int offsetTop = LayoutHierarchyDumper.getViewTop(group); 54 | offset.offset(offsetLeft, offsetTop); 55 | 56 | JSONArray children = new JSONArray(); 57 | for (int i = 0, size = group.getChildCount(); i < size; ++i) { 58 | View child = group.getChildAt(i); 59 | children.put(dumper.dumpHierarchy(child, offset)); 60 | } 61 | 62 | root.put(KEY_CHILDREN, children); 63 | offset.offset(-offsetLeft, -offsetTop); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/layouthierarchy/HierarchyPlugin.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot.layouthierarchy; 18 | 19 | import android.graphics.Point; 20 | import com.facebook.infer.annotation.Nullsafe; 21 | import org.json.JSONException; 22 | import org.json.JSONObject; 23 | 24 | /** 25 | * A plugin for a layout hierarchy that allows you to configure how certain hierarchies are created. 26 | * If you have a custom view group, for example, that you want to display differently than normal, 27 | * then you would create a plugin for it. 28 | */ 29 | @Nullsafe(Nullsafe.Mode.LOCAL) 30 | public interface HierarchyPlugin { 31 | String KEY_CHILDREN = "children"; 32 | 33 | /** Determines whether this plugin operates on the given type */ 34 | boolean accept(Object obj); 35 | 36 | /** Constructs the hierarchy of the given type into a {@link org.json.JSONObject} */ 37 | void putHierarchy(LayoutHierarchyDumper dumper, JSONObject node, Object obj, Point offset) 38 | throws JSONException; 39 | } 40 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | VERSION_NAME=0.16.0-SNAPSHOT 2 | GROUP=com.facebook.testing.screenshot 3 | 4 | POM_DESCRIPTION=Screenshot Testing framework for Android. 5 | POM_URL=https://github.com/facebook/screenshot-tests-for-android 6 | 7 | POM_SCM_URL=https://github.com/facebook/screenshot-tests-for-android.git 8 | POM_SCM_CONNECTION=scm:git@github.com:facebook/screenshot-tests-for-android.git 9 | POM_SCM_DEV_CONNECTION=scm:git@github.com:facebook/screenshot-tests-for-android.git 10 | 11 | POM_LICENCE_NAME=Apache-2 12 | POM_LICENCE_URL=https://github.com/facebook/screenshot-tests-for-android/blob/main/LICENSE 13 | POM_LICENCE_DIST=repo 14 | 15 | POM_DEVELOPER_ID=facebook 16 | POM_DEVELOPER_NAME=Facebook 17 | 18 | org.gradle.configureondemand=true 19 | android.useAndroidX = true 20 | android.enableJetifier = false 21 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/screenshot-tests-for-android/2c1defad8d50e6aa33b0bdb56074cef563047ad0/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | @rem Execute Gradle 88 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 89 | 90 | :end 91 | @rem End local scope for the variables with windows NT shell 92 | if "%ERRORLEVEL%"=="0" goto mainEnd 93 | 94 | :fail 95 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 96 | rem the _cmd.exe /c_ return code! 97 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 98 | exit /b 1 99 | 100 | :mainEnd 101 | if "%OS%"=="Windows_NT" endlocal 102 | 103 | :omega 104 | -------------------------------------------------------------------------------- /layout-hierarchy-common/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /layout-hierarchy-common/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | apply plugin: 'com.android.library' 18 | apply plugin: "com.vanniktech.maven.publish" 19 | 20 | group = 'com.facebook.testing.screenshot' 21 | 22 | android { 23 | compileSdkVersion versions.compileSdk 24 | 25 | packagingOptions { 26 | exclude 'LICENSE.txt' 27 | } 28 | 29 | defaultConfig { 30 | minSdkVersion versions.minSdk 31 | targetSdkVersion versions.targetSdk 32 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 33 | } 34 | } 35 | 36 | afterEvaluate { 37 | generateReleaseBuildConfig.enabled = false 38 | } 39 | 40 | tasks.whenTaskAdded { 41 | if (it.name == "androidJavadoc") { 42 | it.configure { 43 | exclude '**/BUCK' 44 | } 45 | } 46 | } 47 | 48 | dependencies { 49 | implementation project(':core') 50 | 51 | androidTestImplementation deps.espresso 52 | } 53 | 54 | -------------------------------------------------------------------------------- /layout-hierarchy-common/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=Layout Hierarchy Common Plugins 2 | POM_DESCRIPTION=Screenshot Tests Common Layout Hierarchy Plugins 3 | POM_ARTIFACT_ID=layout-hierarchy-common 4 | POM_PACKAGING=aar 5 | -------------------------------------------------------------------------------- /layout-hierarchy-common/src/androidTest/java/com/facebook/testing/screenshot/layouthierarchy/common/TextViewAttributePluginTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot.layouthierarchy.common; 18 | 19 | import static org.junit.Assert.assertEquals; 20 | 21 | import android.graphics.Color; 22 | import android.graphics.Point; 23 | import android.os.Build; 24 | import android.view.View; 25 | import android.widget.TextView; 26 | import androidx.test.InstrumentationRegistry; 27 | import androidx.test.runner.AndroidJUnit4; 28 | import org.json.JSONObject; 29 | import org.junit.Test; 30 | import org.junit.runner.RunWith; 31 | 32 | /** Tests {@link TextViewAttributePlugin} */ 33 | @RunWith(AndroidJUnit4.class) 34 | public class TextViewAttributePluginTest { 35 | @Test 36 | public void testAcceptsTextViews() { 37 | TextView textView = new TextView(InstrumentationRegistry.getTargetContext()); 38 | assertEquals(true, TextViewAttributePlugin.getInstance().accept(textView)); 39 | } 40 | 41 | @Test 42 | public void testDoesntAcceptOtherViews() { 43 | View view = new View(InstrumentationRegistry.getTargetContext()); 44 | assertEquals(false, TextViewAttributePlugin.getInstance().accept(view)); 45 | } 46 | 47 | @Test 48 | public void testPutsExpectedAttributes() throws Throwable { 49 | JSONObject node = new JSONObject(); 50 | TextView textView = 51 | new TextView(InstrumentationRegistry.getTargetContext()) { 52 | @Override 53 | public CharSequence getText() { 54 | return "foobar"; 55 | } 56 | 57 | @Override 58 | public float getTextSize() { 59 | return 1337f; 60 | } 61 | }; 62 | textView.setTextColor(Color.BLACK); 63 | 64 | TextViewAttributePlugin.getInstance().putAttributes(node, textView, new Point()); 65 | 66 | assertEquals("foobar", node.getString("TextView:text")); 67 | assertEquals("1337.0", node.getString("TextView:textSize")); 68 | assertEquals("ff000000", node.getString("TextView:textColor")); 69 | if (Build.VERSION.SDK_INT >= 17) { 70 | assertEquals("TEXT_ALIGNMENT_GRAVITY", node.getString("TextView:textAlignment")); 71 | } 72 | } 73 | 74 | @Test 75 | public void testNullDoesntKillUs() throws Throwable { 76 | JSONObject node = new JSONObject(); 77 | TextView textView = 78 | new TextView(InstrumentationRegistry.getTargetContext()) { 79 | @Override 80 | public CharSequence getText() { 81 | return null; 82 | } 83 | }; 84 | 85 | TextViewAttributePlugin.getInstance().putAttributes(node, textView, new Point()); 86 | 87 | assertEquals("null", node.getString("TextView:text")); 88 | } 89 | 90 | @Test 91 | public void testABadTextViewDoesntKillUs() throws Throwable { 92 | JSONObject node = new JSONObject(); 93 | TextView textView = 94 | new TextView(InstrumentationRegistry.getTargetContext()) { 95 | @Override 96 | public CharSequence getText() { 97 | throw new RuntimeException("Foobar"); 98 | } 99 | }; 100 | 101 | TextViewAttributePlugin.getInstance().putAttributes(node, textView, new Point()); 102 | 103 | assertEquals("Foobar", node.getString("TextView:text")); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /layout-hierarchy-common/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /layout-hierarchy-common/src/main/java/com/facebook/testing/screenshot/layouthierarchy/common/BUCK: -------------------------------------------------------------------------------- 1 | load("//tools/build_defs/oss:screenshot_test_defs.bzl", "SCREENSHOT_TESTS_CORE_TARGET", "SCREENSHOT_TESTS_VISIBILITY", "fb_core_android_library") 2 | 3 | fb_core_android_library( 4 | name = "common", 5 | srcs = glob(["**/*.java"]), 6 | visibility = SCREENSHOT_TESTS_VISIBILITY, 7 | deps = [ 8 | "//fbandroid/third-party/java/infer-annotations:infer-annotations", 9 | SCREENSHOT_TESTS_CORE_TARGET, 10 | ], 11 | ) 12 | -------------------------------------------------------------------------------- /layout-hierarchy-common/src/main/java/com/facebook/testing/screenshot/layouthierarchy/common/TextViewAttributePlugin.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot.layouthierarchy.common; 18 | 19 | import android.graphics.Point; 20 | import android.os.Build; 21 | import android.view.View; 22 | import android.widget.TextView; 23 | import com.facebook.infer.annotation.Nullsafe; 24 | import com.facebook.testing.screenshot.layouthierarchy.AbstractAttributePlugin; 25 | import org.json.JSONException; 26 | import org.json.JSONObject; 27 | 28 | /** Provides attribute details from a TextView */ 29 | @Nullsafe(Nullsafe.Mode.LOCAL) 30 | public class TextViewAttributePlugin extends AbstractAttributePlugin { 31 | private static final String NAMESPACE = "TextView"; 32 | private static final String TEXT = "text"; 33 | private static final String TEXT_SIZE = "textSize"; 34 | private static final String TEXT_COLOR = "textColor"; 35 | private static final String TEXT_ALIGNMENT = "textAlignment"; 36 | 37 | private static TextViewAttributePlugin INSTANCE = new TextViewAttributePlugin(); 38 | 39 | public static TextViewAttributePlugin getInstance() { 40 | return INSTANCE; 41 | } 42 | 43 | private TextViewAttributePlugin() { 44 | // Only one instance 45 | } 46 | 47 | @Override 48 | public boolean accept(Object obj) { 49 | return obj instanceof TextView; 50 | } 51 | 52 | @Override 53 | public String namespace() { 54 | return NAMESPACE; 55 | } 56 | 57 | @Override 58 | public void putAttributes(JSONObject node, Object obj, Point offset) throws JSONException { 59 | if (!accept(obj)) { 60 | return; 61 | } 62 | final TextView textView = (TextView) obj; 63 | 64 | try { 65 | CharSequence text = textView.getText(); 66 | if (text != null) { 67 | put(node, TEXT, text.toString()); 68 | } else { 69 | put(node, TEXT, "null"); 70 | } 71 | } catch (Exception e) { 72 | // NULLSAFE_FIXME[Parameter Not Nullable] 73 | put(node, TEXT, e.getMessage()); 74 | } 75 | 76 | put(node, TEXT_SIZE, String.valueOf(textView.getTextSize())); 77 | put(node, TEXT_COLOR, Integer.toHexString(textView.getCurrentTextColor())); 78 | if (Build.VERSION.SDK_INT >= 17) { 79 | put(node, TEXT_ALIGNMENT, nameForAlignment(textView.getTextAlignment())); 80 | } 81 | } 82 | 83 | private static String nameForAlignment(int alignment) { 84 | switch (alignment) { 85 | case View.TEXT_ALIGNMENT_CENTER: 86 | return "TEXT_ALIGNMENT_CENTER"; 87 | case View.TEXT_ALIGNMENT_GRAVITY: 88 | return "TEXT_ALIGNMENT_GRAVITY"; 89 | case View.TEXT_ALIGNMENT_INHERIT: 90 | return "TEXT_ALIGNMENT_INHERIT"; 91 | case View.TEXT_ALIGNMENT_TEXT_END: 92 | return "TEXT_ALIGNMENT_TEXT_END"; 93 | case View.TEXT_ALIGNMENT_TEXT_START: 94 | return "TEXT_ALIGNMENT_TEXT_START"; 95 | case View.TEXT_ALIGNMENT_VIEW_END: 96 | return "TEXT_ALIGNMENT_VIEW_END"; 97 | case View.TEXT_ALIGNMENT_VIEW_START: 98 | return "TEXT_ALIGNMENT_VIEW_START"; 99 | default: 100 | return "Unknown value"; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /layout-hierarchy-litho/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /layout-hierarchy-litho/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | apply plugin: 'com.android.library' 18 | apply plugin: "com.vanniktech.maven.publish" 19 | 20 | group = 'com.facebook.testing.screenshot' 21 | 22 | android { 23 | compileSdkVersion versions.compileSdk 24 | 25 | packagingOptions { 26 | exclude 'LICENSE.txt' 27 | } 28 | 29 | defaultConfig { 30 | minSdkVersion 15 31 | targetSdkVersion versions.targetSdk 32 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 33 | } 34 | } 35 | 36 | afterEvaluate { 37 | generateReleaseBuildConfig.enabled = false 38 | } 39 | 40 | tasks.whenTaskAdded { 41 | if (it.name == "androidJavadoc") { 42 | it.configure { 43 | exclude '**/BUCK' 44 | } 45 | } 46 | } 47 | 48 | dependencies { 49 | implementation project(':core') 50 | implementation deps.lithoCore 51 | implementation deps.lithoWidget 52 | 53 | androidTestImplementation deps.espresso 54 | } 55 | 56 | -------------------------------------------------------------------------------- /layout-hierarchy-litho/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=Layout Hierarchy Litho Plugins 2 | POM_DESCRIPTION=Screenshot Tests Litho Layout Hierarchy Plugins 3 | POM_ARTIFACT_ID=layout-hierarchy-litho 4 | POM_PACKAGING=aar 5 | -------------------------------------------------------------------------------- /layout-hierarchy-litho/src/androidTest/java/com/facebook/testing/screenshot/layouthierarchy/litho/LithoAttributePluginTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot.layouthierarchy.litho; 18 | 19 | import static org.junit.Assert.assertEquals; 20 | 21 | import android.widget.ImageView; 22 | import androidx.test.InstrumentationRegistry; 23 | import androidx.test.runner.AndroidJUnit4; 24 | import com.facebook.litho.LithoView; 25 | import org.junit.Test; 26 | import org.junit.runner.RunWith; 27 | 28 | /** Tests {@link LithoAttributePlugin} */ 29 | @RunWith(AndroidJUnit4.class) 30 | public class LithoAttributePluginTest { 31 | @Test 32 | public void testAcceptsLithoView() throws Exception { 33 | LithoView lithoView = new LithoView(InstrumentationRegistry.getTargetContext()); 34 | assertEquals(true, LithoAttributePlugin.getInstance().accept(lithoView)); 35 | } 36 | 37 | @Test 38 | public void testDoesntAcceptOtherViews() { 39 | ImageView imageView = new ImageView(InstrumentationRegistry.getTargetContext()); 40 | assertEquals(false, LithoAttributePlugin.getInstance().accept(imageView)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /layout-hierarchy-litho/src/androidTest/java/com/facebook/testing/screenshot/layouthierarchy/litho/LithoHierarchyPluginTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot.layouthierarchy.litho; 18 | 19 | import static org.junit.Assert.assertEquals; 20 | 21 | import android.widget.ImageView; 22 | import androidx.test.InstrumentationRegistry; 23 | import androidx.test.runner.AndroidJUnit4; 24 | import com.facebook.litho.LithoView; 25 | import org.junit.Test; 26 | import org.junit.runner.RunWith; 27 | 28 | /** Tests {@link LithoHierarchyPlugin} */ 29 | @RunWith(AndroidJUnit4.class) 30 | public class LithoHierarchyPluginTest { 31 | @Test 32 | public void testAcceptsLithoView() throws Exception { 33 | LithoView lithoView = new LithoView(InstrumentationRegistry.getTargetContext()); 34 | assertEquals(true, LithoHierarchyPlugin.getInstance().accept(lithoView)); 35 | } 36 | 37 | @Test 38 | public void testDoesntAcceptOtherViews() { 39 | ImageView imageView = new ImageView(InstrumentationRegistry.getTargetContext()); 40 | assertEquals(false, LithoHierarchyPlugin.getInstance().accept(imageView)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /layout-hierarchy-litho/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /layout-hierarchy-litho/src/main/java/com/facebook/testing/screenshot/layouthierarchy/litho/BUCK: -------------------------------------------------------------------------------- 1 | load("//tools/build_defs/oss:screenshot_test_defs.bzl", "SCREENSHOT_TESTS_CORE_TARGET", "SCREENSHOT_TESTS_LITHO_CORE_TARGET", "SCREENSHOT_TESTS_LITHO_WIDGET_TARGET", "SCREENSHOT_TESTS_VISIBILITY", "fb_core_android_library") 2 | 3 | fb_core_android_library( 4 | name = "litho", 5 | srcs = glob(["**/*.java"]), 6 | visibility = SCREENSHOT_TESTS_VISIBILITY, 7 | deps = [ 8 | "//fbandroid/third-party/java/infer-annotations:infer-annotations", 9 | SCREENSHOT_TESTS_CORE_TARGET, 10 | SCREENSHOT_TESTS_LITHO_CORE_TARGET, 11 | SCREENSHOT_TESTS_LITHO_WIDGET_TARGET, 12 | ], 13 | ) 14 | -------------------------------------------------------------------------------- /layout-hierarchy-litho/src/main/java/com/facebook/testing/screenshot/layouthierarchy/litho/LithoAttributePlugin.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot.layouthierarchy.litho; 18 | 19 | import android.graphics.Point; 20 | import android.graphics.Rect; 21 | import com.facebook.infer.annotation.Nullsafe; 22 | import com.facebook.litho.DebugComponent; 23 | import com.facebook.litho.LithoView; 24 | import com.facebook.testing.screenshot.layouthierarchy.AbstractAttributePlugin; 25 | import org.json.JSONException; 26 | import org.json.JSONObject; 27 | 28 | @Nullsafe(Nullsafe.Mode.LOCAL) 29 | public class LithoAttributePlugin extends AbstractAttributePlugin { 30 | private static final LithoAttributePlugin INSTANCE = new LithoAttributePlugin(); 31 | 32 | public static LithoAttributePlugin getInstance() { 33 | return INSTANCE; 34 | } 35 | 36 | private LithoAttributePlugin() { 37 | // Single instance 38 | } 39 | 40 | @Override 41 | public boolean accept(Object obj) { 42 | return obj instanceof LithoView || obj instanceof DebugComponent; 43 | } 44 | 45 | @Override 46 | public String namespace() { 47 | return "Litho"; 48 | } 49 | 50 | @Override 51 | public void putAttributes(JSONObject node, Object obj, Point offset) throws JSONException { 52 | final DebugComponent debugComponent; 53 | if (obj instanceof LithoView) { 54 | ((LithoView) obj).rebind(); 55 | debugComponent = DebugComponent.getRootInstance((LithoView) obj); 56 | if (debugComponent == null) { 57 | return; 58 | } 59 | } else { 60 | debugComponent = (DebugComponent) obj; 61 | 62 | // Since we're dealing with a pure component, we cant rely on the default required 63 | // attributes to be added, so we add them here 64 | Rect bounds = debugComponent.getBoundsInLithoView(); 65 | putRequired( 66 | node, 67 | debugComponent.getComponent().getClass().getName(), 68 | offset.x + bounds.left, 69 | offset.y + bounds.top, 70 | bounds.width(), 71 | bounds.height()); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /layout-hierarchy-litho/src/main/java/com/facebook/testing/screenshot/layouthierarchy/litho/LithoHierarchyPlugin.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot.layouthierarchy.litho; 18 | 19 | import android.graphics.Point; 20 | import com.facebook.infer.annotation.Nullsafe; 21 | import com.facebook.litho.DebugComponent; 22 | import com.facebook.litho.LithoView; 23 | import com.facebook.testing.screenshot.layouthierarchy.BaseViewHierarchyPlugin; 24 | import com.facebook.testing.screenshot.layouthierarchy.HierarchyPlugin; 25 | import com.facebook.testing.screenshot.layouthierarchy.LayoutHierarchyDumper; 26 | import org.json.JSONArray; 27 | import org.json.JSONException; 28 | import org.json.JSONObject; 29 | 30 | @Nullsafe(Nullsafe.Mode.LOCAL) 31 | public class LithoHierarchyPlugin implements HierarchyPlugin { 32 | private static final LithoHierarchyPlugin INSTANCE = new LithoHierarchyPlugin(); 33 | 34 | public static LithoHierarchyPlugin getInstance() { 35 | return INSTANCE; 36 | } 37 | 38 | private LithoHierarchyPlugin() { 39 | // Single instance 40 | } 41 | 42 | @Override 43 | public boolean accept(Object obj) { 44 | return obj instanceof LithoView || obj instanceof DebugComponent; 45 | } 46 | 47 | @Override 48 | public void putHierarchy(LayoutHierarchyDumper dumper, JSONObject root, Object obj, Point offset) 49 | throws JSONException { 50 | if (!accept(obj)) { 51 | return; 52 | } 53 | 54 | if (obj instanceof LithoView) { 55 | LithoView lithoView = (LithoView) obj; 56 | DebugComponent debugComponent = DebugComponent.getRootInstance(lithoView); 57 | if (debugComponent == null) { 58 | return; 59 | } 60 | final int offsetLeft = LayoutHierarchyDumper.getViewLeft(lithoView); 61 | final int offsetTop = LayoutHierarchyDumper.getViewTop(lithoView); 62 | offset.offset(offsetLeft, offsetTop); 63 | dumpHierarchy(dumper, root, debugComponent, offset); 64 | offset.offset(-offsetLeft, -offsetTop); 65 | } else { 66 | dumpHierarchy(dumper, root, (DebugComponent) obj, offset); 67 | } 68 | } 69 | 70 | private void dumpHierarchy( 71 | LayoutHierarchyDumper dumper, JSONObject root, DebugComponent component, Point offset) 72 | throws JSONException { 73 | JSONArray children = new JSONArray(); 74 | for (DebugComponent child : component.getChildComponents()) { 75 | children.put(dumper.dumpHierarchy(child, offset)); 76 | } 77 | root.put(BaseViewHierarchyPlugin.KEY_CHILDREN, children); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/dexmaker/BUCK: -------------------------------------------------------------------------------- 1 | load("@fbsource//tools/build_defs:fb_native_wrapper.bzl", "fb_native") 2 | load("@fbsource//tools/build_defs/android:fb_java_library.bzl", "fb_java_library") 3 | load("@fbsource//tools/build_defs/android:fb_prebuilt_jar.bzl", "fb_prebuilt_jar") 4 | 5 | fb_java_library( 6 | name = "dexmaker", 7 | visibility = ["PUBLIC"], 8 | exported_deps = [ 9 | ":dexmaker-dx-prebuilt", 10 | ":dexmaker-prebuilt", 11 | ], 12 | ) 13 | 14 | fb_prebuilt_jar( 15 | name = "dexmaker-prebuilt", 16 | binary_jar = ":dexmaker.jar", 17 | visibility = ["PUBLIC"], 18 | ) 19 | 20 | fb_native.remote_file( 21 | name = "dexmaker.jar", 22 | sha1 = "6d1ffc507a9b46bbc718d030a1c39b31c572d2b6", 23 | url = "mvn:com.crittercism.dexmaker:dexmaker:jar:1.4", 24 | ) 25 | 26 | fb_prebuilt_jar( 27 | name = "dexmaker-dx-prebuilt", 28 | binary_jar = ":dexmaker-dx.jar", 29 | visibility = ["PUBLIC"], 30 | ) 31 | 32 | fb_native.remote_file( 33 | name = "dexmaker-dx.jar", 34 | sha1 = "0b1146f23dc9f562f4da9a612e2f1980199c1221", 35 | url = "mvn:com.crittercism.dexmaker:dexmaker-dx:jar:1.4", 36 | ) 37 | -------------------------------------------------------------------------------- /lib/junit/BUCK: -------------------------------------------------------------------------------- 1 | load("@fbsource//tools/build_defs:fb_native_wrapper.bzl", "fb_native") 2 | load("@fbsource//tools/build_defs/android:fb_java_library.bzl", "fb_java_library") 3 | load("@fbsource//tools/build_defs/android:fb_prebuilt_jar.bzl", "fb_prebuilt_jar") 4 | 5 | fb_java_library( 6 | name = "junit", 7 | visibility = ["PUBLIC"], 8 | exported_deps = [ 9 | ":junit-prebuilt", 10 | ], 11 | ) 12 | 13 | fb_prebuilt_jar( 14 | name = "junit-prebuilt", 15 | binary_jar = ":junit.jar", 16 | visibility = ["PUBLIC"], 17 | ) 18 | 19 | fb_native.remote_file( 20 | name = "junit.jar", 21 | sha1 = "2973d150c0dc1fefe998f834810d68f278ea58ec", 22 | url = "mvn:junit:junit:jar:4.12", 23 | ) 24 | -------------------------------------------------------------------------------- /lib/litho/BUCK: -------------------------------------------------------------------------------- 1 | load("@fbsource//tools/build_defs:fb_native_wrapper.bzl", "fb_native") 2 | load("//tools/build_defs/oss:screenshot_test_defs.bzl", "fb_core_android_library") 3 | 4 | fb_core_android_library( 5 | name = "litho-core", 6 | visibility = ["PUBLIC"], 7 | exported_deps = [ 8 | ":litho-core-prebuilt", 9 | ], 10 | ) 11 | 12 | fb_native.android_prebuilt_aar( 13 | name = "litho-core-prebuilt", 14 | aar = ":litho-core.aar", 15 | visibility = ["PUBLIC"], 16 | ) 17 | 18 | fb_native.remote_file( 19 | name = "litho-core.aar", 20 | sha1 = "76ec5c2f13063cc2698b7c1c430b91af8f59fab4", 21 | url = "mvn:com.facebook.litho:litho-core:aar:0.7.0", 22 | ) 23 | 24 | fb_core_android_library( 25 | name = "litho-widget", 26 | visibility = ["PUBLIC"], 27 | exported_deps = [ 28 | ":litho-widget-prebuilt", 29 | ], 30 | ) 31 | 32 | fb_native.android_prebuilt_aar( 33 | name = "litho-widget-prebuilt", 34 | aar = ":litho-widget.aar", 35 | visibility = ["PUBLIC"], 36 | ) 37 | 38 | fb_native.remote_file( 39 | name = "litho-widget.aar", 40 | sha1 = "2b16fcb64582263fa7cb1803ffd6ced74ba7422a", 41 | url = "mvn:com.facebook.litho:litho-widget:aar:0.7.0", 42 | ) 43 | -------------------------------------------------------------------------------- /plugin/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | buildscript { 18 | repositories { 19 | mavenCentral() 20 | google() 21 | mavenLocal() 22 | jcenter() 23 | } 24 | dependencies { 25 | classpath plugs.kotlin 26 | classpath plugs.publish 27 | classpath plugs.dokka 28 | } 29 | } 30 | 31 | apply plugin: 'groovy' 32 | apply plugin: 'kotlin' 33 | apply plugin: "java-gradle-plugin" 34 | apply plugin: 'com.vanniktech.maven.publish' 35 | 36 | final GENERATED_PACKAGE_NAME = "com.facebook.testing.screenshot.generated" 37 | final GENERATED_BUILD_DIR = 38 | new File( 39 | projectDir.absolutePath, 40 | "src/generated/kotlin/${GENERATED_PACKAGE_NAME.replace('.', '/')}") 41 | 42 | task generateBuildConfig { 43 | doLast { 44 | GENERATED_BUILD_DIR.deleteDir() 45 | GENERATED_BUILD_DIR.mkdirs() 46 | 47 | final className = "ScreenshotTestBuildConfig" 48 | final configClass = new File(GENERATED_BUILD_DIR, "${className}.kt") 49 | BufferedWriter writer = configClass.newWriter() 50 | try { 51 | writer.writeLine("package $GENERATED_PACKAGE_NAME") 52 | writer.writeLine("object $className { val VERSION = \"${project.version.toString()}\" }") 53 | writer.flush() 54 | } finally { 55 | writer.close() 56 | } 57 | } 58 | } 59 | 60 | dependencies { 61 | implementation deps.kotlinStdlib 62 | compileOnly plugs.agp 63 | 64 | testImplementation deps.junit 65 | testImplementation plugs.agp 66 | testImplementation deps.mockito 67 | testImplementation deps.hamcrest 68 | } 69 | 70 | group = 'com.facebook.testing.screenshot' 71 | 72 | compileKotlin { 73 | dependsOn generateBuildConfig 74 | sourceSets { 75 | main { 76 | kotlin.srcDirs += 'src/generated' 77 | } 78 | } 79 | } 80 | 81 | compileJava { 82 | sourceSets { 83 | main { 84 | resources.srcDirs 'src/py' 85 | resources { 86 | exclude '**/*.pyc' 87 | exclude '**/test_*.py' 88 | exclude '**/fixtures/**' 89 | } 90 | } 91 | } 92 | } 93 | 94 | clean { 95 | delete GENERATED_BUILD_DIR 96 | } 97 | 98 | task pyTests(type: Exec) { 99 | workingDir file('./src/py') 100 | commandLine 'python3', '-m', 'unittest', 'discover' 101 | } 102 | 103 | task py3Tests(type: Exec) { 104 | workingDir file('./src/py') 105 | commandLine 'python3', '-m', 'unittest', 'discover' 106 | } 107 | 108 | 109 | -------------------------------------------------------------------------------- /plugin/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=Plugin 2 | POM_DESCRIPTION=Gradle build plugin for Screenshot tests for android library 3 | POM_ARTIFACT_ID=plugin 4 | POM_PACKAGING=jar 5 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/facebook/testing/screenshot/build/CleanScreenshotsTask.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot.build 18 | 19 | import com.android.build.gradle.api.TestVariant 20 | import org.gradle.api.tasks.TaskAction 21 | 22 | open class CleanScreenshotsTask : ScreenshotTask() { 23 | companion object { 24 | fun taskName(variant: TestVariant) = "clean${variant.name.capitalize()}Screenshots" 25 | } 26 | 27 | init { 28 | description = "Clean last generated screenshot report" 29 | group = ScreenshotsPlugin.GROUP 30 | } 31 | 32 | @TaskAction 33 | fun cleanScreenshots() { 34 | val outputDir = PullScreenshotsTask.getReportDir(project, variant) 35 | project.delete(outputDir) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/facebook/testing/screenshot/build/PullScreenshotsTask.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot.build 18 | 19 | import com.android.build.gradle.api.ApkVariantOutput 20 | import com.android.build.gradle.api.TestVariant 21 | import java.io.File 22 | import org.gradle.api.Project 23 | import org.gradle.api.tasks.Input 24 | import org.gradle.api.tasks.TaskAction 25 | 26 | open class PullScreenshotsTask : ScreenshotTask() { 27 | companion object { 28 | fun taskName(variant: TestVariant) = "pull${variant.name.capitalize()}Screenshots" 29 | 30 | fun getReportDir(project: Project, variant: TestVariant): File = 31 | File(project.buildDir, "screenshots" + variant.name.capitalize()) 32 | } 33 | 34 | private lateinit var apkPath: File 35 | 36 | @Input protected var verify = false 37 | 38 | @Input protected var record = false 39 | 40 | @Input protected var bundleResults = false 41 | 42 | @Input protected lateinit var testRunId: String 43 | 44 | init { 45 | description = "Pull screenshots from your device" 46 | group = ScreenshotsPlugin.GROUP 47 | } 48 | 49 | override fun init(variant: TestVariant, extension: ScreenshotsPluginExtension) { 50 | super.init(variant, extension) 51 | val output = 52 | variant.outputs.find { it is ApkVariantOutput } as? ApkVariantOutput 53 | ?: throw IllegalArgumentException("Can't find APK output") 54 | val packageTask = 55 | variant.packageApplicationProvider.orNull 56 | ?: throw IllegalArgumentException("Can't find package application provider") 57 | 58 | apkPath = File(packageTask.outputDirectory.asFile.get(), output.outputFileName) 59 | bundleResults = extension.bundleResults 60 | testRunId = extension.testRunId 61 | } 62 | 63 | @TaskAction 64 | fun pullScreenshots() { 65 | val codeSource = ScreenshotsPlugin::class.java.protectionDomain.codeSource 66 | val jarFile = File(codeSource.location.toURI().path) 67 | val isVerifyOnly = verify && extension.referenceDir != null 68 | 69 | val outputDir = 70 | if (isVerifyOnly) { 71 | File(extension.referenceDir) 72 | } else { 73 | getReportDir(project, variant) 74 | } 75 | 76 | assert(if (isVerifyOnly) outputDir.exists() else !outputDir.exists()) 77 | 78 | project.exec { 79 | it.executable = extension.pythonExecutable 80 | it.environment("PYTHONPATH", jarFile) 81 | 82 | it.args = 83 | mutableListOf( 84 | "-m", 85 | "android_screenshot_tests.pull_screenshots", 86 | "--apk", 87 | apkPath.absolutePath, 88 | "--test-run-id", 89 | testRunId, 90 | "--temp-dir", 91 | outputDir.absolutePath) 92 | .apply { 93 | if (verify) { 94 | add("--verify") 95 | } else if (record) { 96 | add("--record") 97 | } 98 | 99 | if (verify || record) { 100 | add(extension.recordDir) 101 | } 102 | 103 | if (verify && extension.failureDir != null) { 104 | add("--failure-dir") 105 | add("${extension.failureDir}") 106 | } 107 | 108 | if (extension.multipleDevices) { 109 | add("--multiple-devices") 110 | add("${extension.multipleDevices}") 111 | } 112 | 113 | if (isVerifyOnly) { 114 | add("--no-pull") 115 | } 116 | 117 | if (bundleResults) { 118 | add("--bundle-results") 119 | } 120 | } 121 | 122 | println(it.args) 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/facebook/testing/screenshot/build/RecordScreenshotTestTask.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot.build 18 | 19 | import com.android.build.gradle.api.TestVariant 20 | 21 | open class RecordScreenshotTestTask : RunScreenshotTestTask() { 22 | companion object { 23 | fun taskName(variant: TestVariant) = "record${variant.name.capitalize()}ScreenshotTest" 24 | } 25 | 26 | init { 27 | description = 28 | "Installs and runs screenshot tests, then records their output for later verification" 29 | group = ScreenshotsPlugin.GROUP 30 | } 31 | 32 | override fun init(variant: TestVariant, extension: ScreenshotsPluginExtension) { 33 | super.init(variant, extension) 34 | record = true 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/facebook/testing/screenshot/build/RunScreenshotTestTask.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot.build 18 | 19 | import com.android.build.gradle.api.TestVariant 20 | 21 | open class RunScreenshotTestTask : PullScreenshotsTask() { 22 | companion object { 23 | fun taskName(variant: TestVariant) = "run${variant.name.capitalize()}ScreenshotTest" 24 | } 25 | 26 | init { 27 | description = "Installs and runs screenshot tests, then generates a report" 28 | group = ScreenshotsPlugin.GROUP 29 | } 30 | 31 | override fun init(variant: TestVariant, extension: ScreenshotsPluginExtension) { 32 | super.init(variant, extension) 33 | 34 | if (verify && extension.referenceDir != null) { 35 | return 36 | } 37 | 38 | dependsOn(variant.connectedInstrumentTestProvider) 39 | mustRunAfter(variant.connectedInstrumentTestProvider) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/facebook/testing/screenshot/build/ScreenshotTask.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot.build 18 | 19 | import com.android.build.gradle.api.TestVariant 20 | import org.gradle.api.DefaultTask 21 | import org.gradle.api.tasks.Input 22 | 23 | open class ScreenshotTask : DefaultTask() { 24 | @Input protected lateinit var extension: ScreenshotsPluginExtension 25 | 26 | @Input protected lateinit var variant: TestVariant 27 | 28 | open fun init(variant: TestVariant, extension: ScreenshotsPluginExtension) { 29 | this.extension = extension 30 | this.variant = variant 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/facebook/testing/screenshot/build/VerifyScreenshotTestTask.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.facebook.testing.screenshot.build 18 | 19 | import com.android.build.gradle.api.TestVariant 20 | 21 | open class VerifyScreenshotTestTask : RunScreenshotTestTask() { 22 | companion object { 23 | fun taskName(variant: TestVariant) = "verify${variant.name.capitalize()}ScreenshotTest" 24 | } 25 | 26 | init { 27 | description = 28 | "Installs and runs screenshot tests, then verifies their output against previously recorded screenshots" 29 | group = ScreenshotsPlugin.GROUP 30 | verify = true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /plugin/src/main/resources/META-INF/gradle-plugins/com.facebook.testing.screenshot.properties: -------------------------------------------------------------------------------- 1 | implementation-class=com.facebook.testing.screenshot.build.ScreenshotsPlugin -------------------------------------------------------------------------------- /plugin/src/py/BUCK: -------------------------------------------------------------------------------- 1 | load("@fbsource//tools/build_defs:fb_python_library.bzl", "fb_python_library") 2 | 3 | fb_python_library( 4 | name = "android_screenshot_tests", 5 | srcs = glob(["android_screenshot_tests/**/*.py"]), 6 | base_module = "", 7 | visibility = ["PUBLIC"], 8 | deps = ["//third-party/pypi/pillow:pillow"], 9 | ) 10 | -------------------------------------------------------------------------------- /plugin/src/py/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/screenshot-tests-for-android/2c1defad8d50e6aa33b0bdb56074cef563047ad0/plugin/src/py/__init__.py -------------------------------------------------------------------------------- /plugin/src/py/android_screenshot_tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Meta Platforms, Inc. and affiliates. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from __future__ import absolute_import, division, print_function, unicode_literals 16 | -------------------------------------------------------------------------------- /plugin/src/py/android_screenshot_tests/aapt.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Meta Platforms, Inc. and affiliates. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from __future__ import absolute_import, division, print_function, unicode_literals 16 | 17 | import os 18 | import subprocess 19 | import tempfile 20 | from os.path import exists, join 21 | 22 | from . import common 23 | 24 | 25 | def _check_output(args, **kwargs): 26 | with tempfile.TemporaryFile() as f: 27 | kwargs["stderr"] = f 28 | return common.check_output(args, **kwargs) 29 | 30 | 31 | def parse_package_line(line): 32 | """The line looks like this: 33 | package: name='com.facebook.testing.tests' versionCode='1' versionName=''""" 34 | 35 | for word in line.split(): 36 | if word.startswith("name='"): 37 | return word[len("name='") : -1] 38 | 39 | 40 | def get_aapt_bin(): 41 | """Find the binary for aapt from $ANDROID_SDK""" 42 | android_sdk = common.get_android_sdk() 43 | 44 | build_tools = os.path.join(android_sdk, "build-tools") 45 | 46 | versions = os.listdir(build_tools) 47 | versions = sorted( 48 | versions, 49 | key=lambda x: "0000000" + x if x.startswith("android") else x, 50 | reverse=True, 51 | ) 52 | 53 | for v in versions: 54 | aapt = join(build_tools, v, "aapt") 55 | if exists(aapt) or exists(aapt + ".exe"): 56 | return aapt 57 | 58 | raise RuntimeError("Could not find build-tools in " + android_sdk) 59 | 60 | 61 | def get_package(apk): 62 | output = _check_output([get_aapt_bin(), "dump", "badging", apk], stderr=os.devnull) 63 | for line in output.split("\n"): 64 | if line.startswith("package:"): 65 | return parse_package_line(line) 66 | -------------------------------------------------------------------------------- /plugin/src/py/android_screenshot_tests/adb_executor.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Meta Platforms, Inc. and affiliates. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from . import common 16 | 17 | 18 | class AdbExecutor: 19 | def __init__(self): 20 | pass 21 | 22 | def execute(self, command): 23 | result = common.check_output([common.get_adb()] + command) 24 | if result is None: 25 | raise RuntimeError( 26 | "ERROR: you shouldn't see this in normal operation," 27 | "file a bug report please.\n\n " 28 | "Trying to execute adb " + " ".join(command) 29 | ) 30 | return result 31 | -------------------------------------------------------------------------------- /plugin/src/py/android_screenshot_tests/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/screenshot-tests-for-android/2c1defad8d50e6aa33b0bdb56074cef563047ad0/plugin/src/py/android_screenshot_tests/background.png -------------------------------------------------------------------------------- /plugin/src/py/android_screenshot_tests/background_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/screenshot-tests-for-android/2c1defad8d50e6aa33b0bdb56074cef563047ad0/plugin/src/py/android_screenshot_tests/background_dark.png -------------------------------------------------------------------------------- /plugin/src/py/android_screenshot_tests/common.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import os 17 | import re 18 | import subprocess 19 | import sys 20 | 21 | 22 | def get_image_file_name(name, x, y): 23 | image_file = name 24 | if x != 0 or y != 0: 25 | image_file += "_%d_%d" % (x, y) 26 | 27 | image_file += ".png" 28 | return image_file 29 | 30 | 31 | def get_android_sdk(): 32 | android_sdk = os.environ.get("ANDROID_SDK") or os.environ.get("ANDROID_HOME") 33 | 34 | if not android_sdk: 35 | raise RuntimeError("ANDROID_SDK or ANDROID_HOME needs to be set") 36 | 37 | return os.path.expanduser(android_sdk) 38 | 39 | 40 | def get_adb(): 41 | return os.path.join(get_android_sdk(), "platform-tools", "adb") 42 | 43 | 44 | # a version of subprocess.check_output that returns a utf-8 string 45 | def check_output(args, **kwargs): 46 | return subprocess.check_output(args, **kwargs).decode() 47 | 48 | 49 | # a compat version for py3, since assertRegexpMatches is deprecated 50 | def assertRegex(testcase, regex, string): 51 | if sys.version_info >= (3,): 52 | testcase.assertRegex(regex, string) 53 | else: 54 | testcase.assertRegexpMatches(regex, string) 55 | 56 | 57 | def get_connected_devices(): 58 | try: 59 | output = check_output([get_adb(), "devices"]).splitlines() 60 | target_pattern = re.compile(r"\b(device|emulator)\b") 61 | return [ 62 | line.split()[0] 63 | for line in output 64 | if target_pattern.search(line) and "offline" not in line 65 | ] 66 | except subprocess.CalledProcessError: 67 | return None 68 | -------------------------------------------------------------------------------- /plugin/src/py/android_screenshot_tests/default.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | body { 18 | background: #fafafa; 19 | font-family: 'Arial'; 20 | } 21 | 22 | img { 23 | display:block; 24 | } 25 | 26 | .flex-wrapper { 27 | display: flex; 28 | flex-direction: row; 29 | } 30 | 31 | .img-block { 32 | border-right: 1px solid #aaaaaa; 33 | margin-right: 8px; 34 | margin-top: 10px; 35 | padding-right: 8px; 36 | } 37 | 38 | .img-wrapper { 39 | background-image: url("background.png"); 40 | margin-top: 5px; 41 | position: relative; 42 | } 43 | 44 | .img-wrapper.dark { 45 | background-image: url("background_dark.png"); 46 | } 47 | 48 | .hierarchy-overlay { 49 | position: absolute; 50 | left: 0; 51 | top: 0; 52 | right: 0; 53 | bottom: 0; 54 | display: none; 55 | } 56 | 57 | .hierarchy-node { 58 | position: absolute; 59 | display: block; 60 | border: 2px solid #000; 61 | } 62 | 63 | .hierarchy-node.highlight { 64 | background-color: rgba(20, 20, 128, 0.5); 65 | border-color: #00f; 66 | } 67 | 68 | .command-wrapper { 69 | margin-top: 16px; 70 | flex-grow: 1; 71 | overflow: auto; 72 | } 73 | 74 | .view-hierarchy { 75 | background: #ddd; 76 | padding: 16px; 77 | max-height: 750px; 78 | overflow: auto; 79 | border-radius: 3px; 80 | } 81 | 82 | div.screenshot { 83 | padding: 10px; 84 | } 85 | 86 | div.alternate { 87 | 88 | } 89 | 90 | div.screenshot_error { 91 | color: red; 92 | } 93 | 94 | table { 95 | border-collapse: collapse; 96 | } 97 | 98 | table, th, tr, td, img{ 99 | padding: 0; 100 | margin: 0; 101 | border: 0; 102 | } 103 | 104 | table { 105 | border-spacing: 0; 106 | border-collapse: collapse; 107 | } 108 | 109 | .screenshot_name { 110 | font-size: 24px; 111 | } 112 | 113 | .screenshot_name .demphasize { 114 | color: #999; 115 | } 116 | 117 | .screenshot_description { 118 | color: #999; 119 | font-style: italic; 120 | } 121 | 122 | button { 123 | border: 1px solid #000; 124 | border-radius: 3px; 125 | background-color: #fff; 126 | padding: 16px; 127 | font-size: 18px; 128 | margin-right: 8px; 129 | } 130 | 131 | button:hover { 132 | background-color: #000; 133 | color: #fff; 134 | cursor: pointer; 135 | } 136 | 137 | .clearfix { 138 | display: block; 139 | clear: both; 140 | content: ""; 141 | } 142 | 143 | hr { 144 | border: 0; 145 | height: 1px; 146 | background-color: #000; 147 | margin: 16px 0; 148 | } 149 | 150 | h3 { 151 | font-weight: normal; 152 | } 153 | 154 | details { 155 | margin-left: 24px; 156 | } 157 | 158 | summary { 159 | font-size: 16px; 160 | } 161 | 162 | ul { 163 | list-style: none; 164 | margin: 0; 165 | margin-bottom: 8px; 166 | } 167 | -------------------------------------------------------------------------------- /plugin/src/py/android_screenshot_tests/default.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | $(function () { 18 | $(".extra").click(function () { 19 | var str = $(this).attr('data'); 20 | $('

').dialog({
21 |                 modal: true,
22 |                 title: "Extra Info",
23 |                 open: function () {
24 |                     $(this).html(str);
25 |                 },
26 |                 buttons: {
27 |                     Ok: function () {
28 |                         $(this).dialog("close");
29 |                     }
30 |                 },
31 |                 width:'1000px',
32 |                 position: {
33 |                     at: 'top',
34 |                 },
35 |             });
36 |         });
37 | 
38 |     $(".toggle_dark").click(function() {
39 |         $(this).closest(".screenshot").find(".img-wrapper").toggleClass("dark");
40 |     })
41 | 
42 |     $(".toggle_hierarchy").click(function() {
43 |         $(this).closest(".screenshot").find(".hierarchy-overlay").toggle();
44 |     })
45 | 
46 |     $(".view-hierarchy")
47 |         .mousemove(
48 |             function(e) {
49 |                 $(".hierarchy-node").removeClass('highlight');
50 |                 $($(e.target).closest("details").attr('target')).addClass('highlight');
51 |             })
52 |         .mouseout(
53 |             function() {
54 |                 $(".hierarchy-node").removeClass('highlight');
55 |             });
56 | });
57 | 


--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/device_name_calculator.py:
--------------------------------------------------------------------------------
  1 | # Copyright (c) Meta Platforms, Inc. and affiliates.
  2 | #
  3 | # Licensed under the Apache License, Version 2.0 (the "License");
  4 | # you may not use this file except in compliance with the License.
  5 | # You may obtain a copy of the License at
  6 | #
  7 | #     http://www.apache.org/licenses/LICENSE-2.0
  8 | #
  9 | # Unless required by applicable law or agreed to in writing, software
 10 | # distributed under the License is distributed on an "AS IS" BASIS,
 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12 | # See the License for the specific language governing permissions and
 13 | # limitations under the License.
 14 | 
 15 | import re
 16 | import subprocess
 17 | 
 18 | from .adb_executor import AdbExecutor
 19 | 
 20 | 
 21 | class DeviceNameCalculator:
 22 |     def __init__(self, executor=AdbExecutor()):
 23 |         self.executor = executor
 24 | 
 25 |     def name(self):
 26 |         api_version_text = self._api_version_text()
 27 |         play_services_text = self._play_services_text()
 28 |         screen_density_text = self._screen_density_text()
 29 |         screen_size_text = self._screen_size_text()
 30 |         architecture_text = self._architecture_text()
 31 |         locale = self._locale()
 32 | 
 33 |         device_parameters = [
 34 |             api_version_text,
 35 |             play_services_text,
 36 |             screen_density_text,
 37 |             screen_size_text,
 38 |             architecture_text,
 39 |             locale,
 40 |         ]
 41 | 
 42 |         if None in device_parameters:
 43 |             raise RuntimeError(
 44 |                 "ERROR: you shouldn't see this in normal operation,"
 45 |                 "file a bug report please.\n\n "
 46 |                 "One or more device params are None"
 47 |             )
 48 | 
 49 |         return "{0}_{1}_{2}_{3}_{4}_{5}".format(
 50 |             api_version_text,
 51 |             play_services_text,
 52 |             screen_density_text,
 53 |             screen_size_text,
 54 |             architecture_text,
 55 |             locale,
 56 |         )
 57 | 
 58 |     def _screen_density_text(self):
 59 |         density = int(self._screen_density())
 60 | 
 61 |         if density in range(0, 121):
 62 |             return "LDPI"
 63 |         elif density in range(121, 161):
 64 |             return "MDPI"
 65 |         elif density in range(161, 241):
 66 |             return "HDPI"
 67 |         elif density in range(241, 321):
 68 |             return "XHDPI"
 69 |         elif density in range(321, 481):
 70 |             return "XXHDPI"
 71 | 
 72 |         return "XXXHDPI"
 73 | 
 74 |     def _screen_density(self):
 75 |         result = self.executor.execute(["shell", "wm", "density"])
 76 |         density = re.search("[0-9]+", result)
 77 |         if density:
 78 |             return density.group(0)
 79 | 
 80 |     def _screen_size_text(self):
 81 |         result = self.executor.execute(["shell", "wm", "size"])
 82 |         density = re.search("[0-9]+x[0-9]+", result)
 83 |         if density:
 84 |             return density.group(0)
 85 | 
 86 |     def _has_play_services(self):
 87 |         try:
 88 |             output = self.executor.execute(
 89 |                 ["shell", "pm", "path", "com.google.android.gms"]
 90 |             )
 91 |             return True if output else False
 92 |         except subprocess.CalledProcessError:
 93 |             return False
 94 | 
 95 |     def _play_services_text(self):
 96 |         play_services = self._has_play_services()
 97 |         return "GP" if play_services else "NO_GP"
 98 | 
 99 |     def _api_version(self):
100 |         return self.executor.execute(["shell", "getprop", "ro.build.version.sdk"])
101 | 
102 |     def _api_version_text(self):
103 |         return "API_{0}".format(int(self._api_version()))
104 | 
105 |     def _architecture_text(self):
106 |         architecture = self.executor.execute(["shell", "getprop", "ro.product.cpu.abi"])
107 |         return architecture.rstrip()
108 | 
109 |     def _locale(self):
110 |         persist_locale = self.executor.execute(
111 |             ["shell", "getprop", "persist.sys.locale"]
112 |         )
113 |         product_locale = self.executor.execute(
114 |             ["shell", "getprop", "ro.product.locale"]
115 |         )
116 |         return persist_locale.rstrip() if persist_locale else product_locale.rstrip()
117 | 


--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/example.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/facebook/screenshot-tests-for-android/2c1defad8d50e6aa33b0bdb56074cef563047ad0/plugin/src/py/android_screenshot_tests/example.apk


--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/fixtures/AndroidManifest.xml:
--------------------------------------------------------------------------------
 1 | 
 7 | 
 8 |   
11 | 
12 | 
13 |   
14 | 
15 |   
16 |     
17 |   
18 | 
19 | 


--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/fixtures/dummy.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/facebook/screenshot-tests-for-android/2c1defad8d50e6aa33b0bdb56074cef563047ad0/plugin/src/py/android_screenshot_tests/fixtures/dummy.zip


--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/fixtures/sdcard/screenshots/com.foo/screenshots-default/metadata.json:
--------------------------------------------------------------------------------
 1 | [
 2 |   {
 3 |     "description": null,
 4 |     "name": "com.foo.ScriptsFixtureTest_testGetTextViewScreenshot",
 5 |     "testClass": "com.facebook.testing.screenshot.ScriptsFixtureTest",
 6 |     "testName": "testGetTextViewScreenshot",
 7 |     "tileWidth": 1,
 8 |     "tileHeight": 1,
 9 |     "relativeFileNames": [
10 |       "com.foo.ScriptsFixtureTest_testGetTextViewScreenshot.png"
11 |     ],
12 |     "viewHierarchy": "one_dump.json"
13 |   },
14 |   {
15 |     "description": null,
16 |     "name": "com.foo.ScriptsFixtureTest_testSecondScreenshot",
17 |     "testClass": "com.facebook.testing.screenshot.ScriptsFixtureTest",
18 |     "testName": "testSecondScreenshot",
19 |     "tileWidth": 1,
20 |     "tileHeight": 1,
21 |     "relativeFileNames": [
22 |       "com.foo.ScriptsFixtureTest_testSecondScreenshot.png"
23 |     ]
24 |   },
25 |   {
26 |     "description": null,
27 |     "_comment": " the '.' makes wkhtmlimage think this is an external url ",
28 |     "name": "com.facebook.something.FooBar2.png",
29 |     "testName": "unknown",
30 |     "testClass": "unknown",
31 |     "error": "Outofmem and such"
32 |   }
33 | ]
34 | 


--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/fixtures/sdcard/screenshots/com.foo/screenshots-default/metadata_no_errors.json:
--------------------------------------------------------------------------------
 1 | [
 2 |   {
 3 |     "description": null,
 4 |     "name": "com.foo.ScriptsFixtureTest_testGetTextViewScreenshot",
 5 |     "testClass": "com.facebook.testing.screenshot.ScriptsFixtureTest",
 6 |     "testName": "testGetTextViewScreenshot",
 7 |     "tileWidth": 1,
 8 |     "tileHeight": 1,
 9 |     "absoluteFileNames": [
10 |       "/sdcard/screenshots/com.foo/screenshots-default/com.foo.ScriptsFixtureTest_testGetTextViewScreenshot.png"
11 |     ]
12 |   },
13 |   {
14 |     "description": null,
15 |     "name": "com.foo.ScriptsFixtureTest_testSecondScreenshot",
16 |     "testClass": "com.facebook.testing.screenshot.ScriptsFixtureTest",
17 |     "testName": "testSecondScreenshot",
18 |     "tileWidth": 1,
19 |     "tileHeight": 1,
20 |     "absoluteFileNames": [
21 |       "/sdcard/screenshots/com.foo/screenshots-default/com.foo.ScriptsFixtureTest_testSecondScreenshot.png"
22 |     ]
23 |   }
24 | ]
25 | 


--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/fixtures/sdcard/screenshots/com.foo/screenshots-default/one_dump.json:
--------------------------------------------------------------------------------
1 | {
2 |   "class": "test",
3 |   "x": 0,
4 |   "y": 0,
5 |   "width": 1,
6 |   "height": 1
7 | }


--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/fixtures/sdcard/screenshots/com.foo/screenshots-default/unittest/com.foo.ScriptsFixtureTest_testGetTextViewScreenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/facebook/screenshot-tests-for-android/2c1defad8d50e6aa33b0bdb56074cef563047ad0/plugin/src/py/android_screenshot_tests/fixtures/sdcard/screenshots/com.foo/screenshots-default/unittest/com.foo.ScriptsFixtureTest_testGetTextViewScreenshot.png


--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/fixtures/sdcard/screenshots/com.foo/screenshots-default/unittest/com.foo.ScriptsFixtureTest_testSecondScreenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/facebook/screenshot-tests-for-android/2c1defad8d50e6aa33b0bdb56074cef563047ad0/plugin/src/py/android_screenshot_tests/fixtures/sdcard/screenshots/com.foo/screenshots-default/unittest/com.foo.ScriptsFixtureTest_testSecondScreenshot.png


--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/metadata.py:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env python3
 2 | # Copyright (c) Meta Platforms, Inc. and affiliates.
 3 | #
 4 | # Licensed under the Apache License, Version 2.0 (the "License");
 5 | # you may not use this file except in compliance with the License.
 6 | # You may obtain a copy of the License at
 7 | #
 8 | #     http://www.apache.org/licenses/LICENSE-2.0
 9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | 
16 | from __future__ import absolute_import, division, print_function, unicode_literals
17 | 
18 | import json
19 | import os
20 | import re
21 | import shutil
22 | import tempfile
23 | import unittest
24 | 
25 | 
26 | # Given a metadata file locally, this transforms it (in-place), to
27 | # remove any screenshot elements that don't satisfy the given filter
28 | # criteria
29 | def filter_screenshots(metadata_file, name_regex=None):
30 |     with open(metadata_file, "r") as f:
31 |         parsed = json.load(f)
32 |         to_remove = []
33 |         for s in parsed:
34 |             if name_regex and not (re.search(name_regex, s["name"])):
35 |                 to_remove.append(s)
36 | 
37 |         for s in to_remove:
38 |             parsed.remove(s)
39 | 
40 |     with open(metadata_file, "w") as f:
41 |         f.write(json.dumps(parsed))
42 | 


--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/no_op_device_name_calculator.py:
--------------------------------------------------------------------------------
 1 | # Copyright (c) Meta Platforms, Inc. and affiliates.
 2 | #
 3 | # Licensed under the Apache License, Version 2.0 (the "License");
 4 | # you may not use this file except in compliance with the License.
 5 | # You may obtain a copy of the License at
 6 | #
 7 | #     http://www.apache.org/licenses/LICENSE-2.0
 8 | #
 9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | 
15 | 
16 | class NoOpDeviceNameCalculator:
17 |     def __init__(self):
18 |         pass
19 | 
20 |     def name(self):
21 |         return ""
22 | 


--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/simple_puller.py:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env python3
 2 | # Copyright (c) Meta Platforms, Inc. and affiliates.
 3 | #
 4 | # Licensed under the Apache License, Version 2.0 (the "License");
 5 | # you may not use this file except in compliance with the License.
 6 | # You may obtain a copy of the License at
 7 | #
 8 | #     http://www.apache.org/licenses/LICENSE-2.0
 9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | 
16 | import subprocess
17 | import tarfile
18 | import tempfile
19 | 
20 | from . import common
21 | from .common import get_adb
22 | 
23 | 
24 | class SimplePuller:
25 |     """Pulls a given file from the device"""
26 | 
27 |     def __init__(self, adb_args=[]):
28 |         self._adb_args = list(adb_args)
29 | 
30 |     def remote_file_exists(self, src):
31 |         output = common.check_output(
32 |             [get_adb()]
33 |             + self._adb_args
34 |             + ["shell", "ls %s && echo EXISTS || echo DOES_NOT_EXIST" % src]
35 |         )
36 |         return "EXISTS" in output
37 | 
38 |     def pull(self, src, dest):
39 |         subprocess.check_call(
40 |             [get_adb()] + self._adb_args + ["pull", src, dest], stderr=subprocess.STDOUT
41 |         )
42 | 
43 |     @staticmethod
44 |     def _get_tar_name(src):
45 |         return "{}.tar.gz".format(src)
46 | 
47 |     def _tar(self, src):
48 |         subprocess.check_call(
49 |             [get_adb()]
50 |             + self._adb_args
51 |             + [
52 |                 "shell",
53 |                 "tar",
54 |                 "-zcvf",
55 |                 SimplePuller._get_tar_name(src),
56 |                 "-C",
57 |                 src,
58 |                 ".",
59 |             ],
60 |             stderr=subprocess.STDOUT,
61 |         )
62 | 
63 |     def _remove_temp_tar(self, src):
64 |         subprocess.check_call(
65 |             [get_adb()]
66 |             + self._adb_args
67 |             + ["shell", "rm", SimplePuller._get_tar_name(src)],
68 |             stderr=subprocess.STDOUT,
69 |         )
70 | 
71 |     def pull_folder(self, src, dest):
72 |         # Pulling a folder with lots of files is very slow, as each file transmission needs
73 |         # to reestablish the connection, slowing down the overall throughput.
74 |         # Hence taring the entire folder first.
75 |         self._tar(src)
76 |         with tempfile.NamedTemporaryFile() as f:
77 |             self.pull(SimplePuller._get_tar_name(src), f.name)
78 |             local_file = tarfile.open(f.name)
79 |             local_file.extractall(dest)
80 |             local_file.close()
81 |         self._remove_temp_tar(src)
82 | 
83 |     def get_external_data_dir(self):
84 |         output = common.check_output(
85 |             [get_adb()] + self._adb_args + ["shell", "echo", "$EXTERNAL_STORAGE"]
86 |         )
87 |         return output.strip().split()[-1]
88 | 


--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/test_aapt.py:
--------------------------------------------------------------------------------
  1 | # Copyright (c) Meta Platforms, Inc. and affiliates.
  2 | #
  3 | # Licensed under the Apache License, Version 2.0 (the "License");
  4 | # you may not use this file except in compliance with the License.
  5 | # You may obtain a copy of the License at
  6 | #
  7 | #     http://www.apache.org/licenses/LICENSE-2.0
  8 | #
  9 | # Unless required by applicable law or agreed to in writing, software
 10 | # distributed under the License is distributed on an "AS IS" BASIS,
 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12 | # See the License for the specific language governing permissions and
 13 | # limitations under the License.
 14 | 
 15 | from __future__ import absolute_import, division, print_function, unicode_literals
 16 | 
 17 | import os
 18 | import shutil
 19 | import tempfile
 20 | import unittest
 21 | from os.path import dirname, join
 22 | 
 23 | from . import aapt
 24 | from .common import assertRegex
 25 | 
 26 | CURDIR = dirname(__file__)
 27 | 
 28 | 
 29 | class TestAapt(unittest.TestCase):
 30 |     def setUp(self):
 31 |         os.oldenviron = dict(os.environ)
 32 |         self.android_sdk = tempfile.mkdtemp()
 33 |         os.mkdir(join(self.android_sdk, "build-tools"))
 34 |         os.environ["ANDROID_SDK"] = os.environ.get("ANDROID_SDK") or os.environ.get(
 35 |             "ANDROID_HOME"
 36 |         )
 37 |         os.environ.pop("ANDROID_HOME", None)
 38 | 
 39 |     def tearDown(self):
 40 |         os.environ.clear()
 41 |         os.environ.update(os.oldenviron)
 42 |         shutil.rmtree(self.android_sdk)
 43 | 
 44 |     def _use_mock(self):
 45 |         os.environ["ANDROID_SDK"] = self.android_sdk
 46 | 
 47 |     def _add_aapt(self, version):
 48 |         f = join(self.android_sdk, "build-tools", version)
 49 |         os.mkdir(f)
 50 |         open(join(f, "aapt"), "w").close()
 51 | 
 52 |     def test_finds_aapt_happy_path(self):
 53 |         self.assertTrue(aapt.get_aapt_bin().endswith("aapt"))
 54 | 
 55 |     def test_finds_an_aapt_happy_path(self):
 56 |         self._use_mock()
 57 |         self._add_aapt("21.0")
 58 |         self.assertEqual(
 59 |             join(self.android_sdk, "build-tools", "21.0", "aapt"), aapt.get_aapt_bin()
 60 |         )
 61 | 
 62 |     def test_finds_the_aapt_with_highest_version(self):
 63 |         self._use_mock()
 64 |         self._add_aapt("21.0")
 65 |         self._add_aapt("22.0")
 66 |         self.assertEqual(
 67 |             join(self.android_sdk, "build-tools", "22.0", "aapt"), aapt.get_aapt_bin()
 68 |         )
 69 | 
 70 |     def test_does_not_use_old_android_versions(self):
 71 |         self._use_mock()
 72 |         self._add_aapt("21.0")
 73 |         self._add_aapt("22.0")
 74 |         self._add_aapt("android-4.1")
 75 |         self.assertEqual(
 76 |             join(self.android_sdk, "build-tools", "22.0", "aapt"), aapt.get_aapt_bin()
 77 |         )
 78 | 
 79 |     def test_no_android_sdk(self):
 80 |         os.environ.pop("ANDROID_SDK")
 81 | 
 82 |         try:
 83 |             aapt.get_aapt_bin()
 84 |             self.fail("expected exception")
 85 |         except RuntimeError as e:
 86 |             assertRegex(self, e.args[0], ".*ANDROID_SDK.*")
 87 | 
 88 |     def test_no_build_tools(self):
 89 |         self._use_mock()
 90 | 
 91 |         try:
 92 |             aapt.get_aapt_bin()
 93 |             self.fail("expected exception")
 94 |         except RuntimeError as e:
 95 |             assertRegex(self, e.args[0], ".*Could not find build-tools.*")
 96 | 
 97 |     def test_get_package_name(self):
 98 |         self.assertEqual(
 99 |             "com.facebook.testing.screenshot.examples",
100 |             aapt.get_package(join(CURDIR, "example.apk")),
101 |         )
102 | 


--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/test_common.py:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env python3
 2 | # Copyright (c) Meta Platforms, Inc. and affiliates.
 3 | #
 4 | # Licensed under the Apache License, Version 2.0 (the "License");
 5 | # you may not use this file except in compliance with the License.
 6 | # You may obtain a copy of the License at
 7 | #
 8 | #     http://www.apache.org/licenses/LICENSE-2.0
 9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | 
16 | import os
17 | import subprocess
18 | import sys
19 | import unittest
20 | 
21 | from . import common
22 | 
23 | 
24 | class TestCommon(unittest.TestCase):
25 |     def setUp(self):
26 |         self.android_sdk = common.get_android_sdk()
27 |         self._environ = dict(os.environ)
28 |         os.environ.pop("ANDROID_SDK", None)
29 |         os.environ.pop("ANDROID_HOME", None)
30 | 
31 |     def tearDown(self):
32 |         os.environ.clear()
33 |         os.environ.update(self._environ)
34 | 
35 |     def test_get_android_sdk_happy_path(self):
36 |         os.environ["ANDROID_SDK"] = "/tmp/foo"
37 |         self.assertEqual("/tmp/foo", common.get_android_sdk())
38 | 
39 |     def test_tilde_is_expanded(self):
40 |         if sys.version_info >= (3,):
41 |             return
42 | 
43 |         os.environ["ANDROID_SDK"] = "~/foobar"
44 | 
45 |         home = os.environ["HOME"]
46 | 
47 |         self.assertEqual(os.path.join(home, "foobar"), common.get_android_sdk())
48 | 
49 |     def test_get_adb_can_run_in_subprocess(self):
50 |         os.environ["ANDROID_SDK"] = self.android_sdk
51 |         subprocess.check_call([common.get_adb(), "devices"])
52 | 


--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/test_metadata.py:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env python3
 2 | # Copyright (c) Meta Platforms, Inc. and affiliates.
 3 | #
 4 | # Licensed under the Apache License, Version 2.0 (the "License");
 5 | # you may not use this file except in compliance with the License.
 6 | # You may obtain a copy of the License at
 7 | #
 8 | #     http://www.apache.org/licenses/LICENSE-2.0
 9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | 
16 | from __future__ import absolute_import, division, print_function, unicode_literals
17 | 
18 | import json
19 | import os
20 | import shutil
21 | import tempfile
22 | import unittest
23 | 
24 | from . import metadata
25 | 
26 | 
27 | # Tests for the metadata package
28 | class TestMetadata(unittest.TestCase):
29 |     def setUp(self):
30 |         fd, self.tmp_metadata = tempfile.mkstemp(prefix="TempMetadataJson")
31 |         os.close(fd)
32 |         os.unlink(self.tmp_metadata)
33 | 
34 |         self.fixture_metadata = os.path.join(
35 |             os.path.dirname(__file__), "metadata_fixture.json"
36 |         )
37 |         shutil.copyfile(self.fixture_metadata, self.tmp_metadata)
38 | 
39 |     def tearDown(self):
40 |         if os.path.exists(self.tmp_metadata):
41 |             os.unlink(self.tmp_metadata)
42 | 
43 |     def test_nothing_removed_for_empty_filter(self):
44 |         metadata.filter_screenshots(self.tmp_metadata)
45 | 
46 |         self.assertEqual(
47 |             self.get_num_screenshots_in(self.fixture_metadata),
48 |             self.get_num_screenshots_in(self.tmp_metadata),
49 |         )
50 | 
51 |     def test_exactly_one_result(self):
52 |         metadata.filter_screenshots(
53 |             self.tmp_metadata, name_regex="testAddPlaceIsShowing"
54 |         )
55 | 
56 |         self.assertEqual(1, self.get_num_screenshots_in(self.tmp_metadata))
57 | 
58 |     def test_regex(self):
59 |         metadata.filter_screenshots(
60 |             self.tmp_metadata, name_regex=".*testAddPlaceIsShowing.*"
61 |         )
62 | 
63 |         self.assertEqual(1, self.get_num_screenshots_in(self.tmp_metadata))
64 | 
65 |     def test_regex(self):
66 |         metadata.filter_screenshots(self.tmp_metadata, name_regex=".*CheckinTitleBar.*")
67 | 
68 |         self.assertEqual(7, self.get_num_screenshots_in(self.tmp_metadata))
69 | 
70 |     def get_num_screenshots_in(self, metadata_file):
71 |         with open(metadata_file, "r") as f:
72 |             parsed = json.load(f)
73 |             """Gets the number of screenshots in the given metadata file"""
74 |             return len(parsed)
75 | 
76 | 
77 | if __name__ == "__main__":
78 |     unittest.main()
79 | 


--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/test_simple_puller.py:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env python3
 2 | # Copyright (c) Meta Platforms, Inc. and affiliates.
 3 | #
 4 | # Licensed under the Apache License, Version 2.0 (the "License");
 5 | # you may not use this file except in compliance with the License.
 6 | # You may obtain a copy of the License at
 7 | #
 8 | #     http://www.apache.org/licenses/LICENSE-2.0
 9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | 
16 | from __future__ import absolute_import, division, print_function, unicode_literals
17 | 
18 | import os
19 | import shutil
20 | import subprocess
21 | import tempfile
22 | import unittest
23 | 
24 | from . import common
25 | from .common import get_adb
26 | from .simple_puller import SimplePuller
27 | 
28 | 
29 | class TestSimplePuller(unittest.TestCase):
30 |     def setUp(self):
31 |         self.puller = SimplePuller()
32 |         self.serial = common.check_output([get_adb(), "get-serialno"]).strip()
33 | 
34 |         subprocess.check_call([get_adb(), "shell", "echo foobar > /sdcard/blah"])
35 |         self.tmpdir = tempfile.mkdtemp()
36 | 
37 |     def tearDown(self):
38 |         shutil.rmtree(self.tmpdir)
39 |         subprocess.check_call([get_adb(), "shell", "rm", "-f", "/sdcard/blah"])
40 | 
41 |     def test_pull_integration(self):
42 |         file = os.path.join(self.tmpdir, "foo")
43 |         self.puller.pull("/sdcard/blah", file)
44 | 
45 |         with open(file, "rt") as f2:
46 |             self.assertEqual("foobar\n", f2.read())
47 | 
48 |     def test_file_exists(self):
49 |         self.assertTrue(self.puller.remote_file_exists("/sdcard/blah"))
50 |         self.assertFalse(self.puller.remote_file_exists("/sdcard/sdfdsfdf"))
51 | 
52 |     def test_pull_with_filter(self):
53 |         self.puller = SimplePuller(["-s", self.serial])
54 |         self.test_pull_integration()
55 | 
56 |     def test_get_external_data_dir(self):
57 |         accepted_dirs = [
58 |             "/mnt/sdcard",
59 |             "/sdcard",
60 |             "/storage/sdcard",
61 |             "/storage/emulated/legacy",
62 |         ]
63 |         self.assertIn(self.puller.get_external_data_dir(), accepted_dirs)
64 | 
65 |     def test_pull_folder(self):
66 |         target_remote_folder = "/sdcard/folder"
67 |         target_remote_sub_folders = [".", "a", "b"]
68 |         subprocess.check_call([get_adb(), "shell", f"mkdir -p {target_remote_folder}"])
69 |         for sub_folder in target_remote_sub_folders:
70 |             subprocess.check_call(
71 |                 [get_adb(), "shell", f"mkdir -p {target_remote_folder}/{sub_folder}"]
72 |             )
73 |             for i in range(10):
74 |                 subprocess.check_call(
75 |                     [
76 |                         get_adb(),
77 |                         "shell",
78 |                         f"echo foobar{i} > {target_remote_folder}/{sub_folder}/pic{i}.png",
79 |                     ]
80 |                 )
81 |         self.puller.pull_folder(target_remote_folder, self.tmpdir)
82 | 
83 |         for sub_folder in target_remote_sub_folders:
84 |             for i in range(10):
85 |                 file = os.path.join(self.tmpdir, sub_folder, f"pic{i}.png")
86 |                 with open(file, "rt") as f2:
87 |                     self.assertEqual(f"foobar{i}\n", f2.read())
88 | 
89 | 
90 | if __name__ == "__main__":
91 |     unittest.main()
92 | 


--------------------------------------------------------------------------------
/plugin/src/test/groovy/com/facebook/testing/screenshot/build/ScreenshotsPluginTest.groovy:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * Copyright (c) Meta Platforms, Inc. and affiliates.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  *     http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | package com.facebook.testing.screenshot.build
18 | 
19 | import org.gradle.api.Project
20 | import org.gradle.api.Task
21 | import org.gradle.testfixtures.ProjectBuilder
22 | import org.junit.Before
23 | import org.junit.Test
24 | 
25 | import static org.junit.Assert.assertTrue
26 | import static org.junit.Assert.fail
27 | 
28 | class ScreenshotsPluginTest {
29 |   Project project
30 | 
31 |   @Before
32 |   void "setup"() {
33 |     final appId = "com.facebook.testing.screenshot.integration"
34 |     project = ProjectBuilder.builder().build()
35 | 
36 |     File manifest = new File(project.projectDir, "src/main/AndroidManifest.xml")
37 |     manifest.parentFile.mkdirs()
38 |     manifest.write("""
39 |       
40 |         
41 |       """)
42 | 
43 |     project.getPluginManager().apply 'com.android.application'
44 |     project.getPluginManager().apply ScreenshotsPlugin
45 | 
46 |     project.repositories {
47 |       mavenCentral()
48 |     }
49 | 
50 |     project.android {
51 |       compileSdkVersion 22
52 | 
53 |       defaultConfig {
54 |         applicationId appId
55 |       }
56 |     }
57 |   }
58 | 
59 |   @Test
60 |   void "Ensure core dependency added"() {
61 |     project.evaluate()
62 | 
63 |     def depSet = project.getConfigurations().getByName('androidTestImplementation').getAllDependencies()
64 |     for (dep in depSet) {
65 |       if (dep.name == "core" && dep.group == 'com.facebook.testing.screenshot') {
66 |         return
67 |       }
68 |     }
69 |     fail()
70 |   }
71 | 
72 |   @Test
73 |   void "Ensure core dependency not added when requested"() {
74 |     project.screenshots {
75 |       addDeps = false
76 |     }
77 |     project.evaluate()
78 | 
79 |     def depSet = project.getConfigurations().getByName('androidTestImplementation').getAllDependencies()
80 |     for (dep in depSet) {
81 |       if (dep.name == "core" && dep.group == 'com.facebook.testing.screenshot') {
82 |         fail()
83 |       }
84 |     }
85 |   }
86 | 
87 |   @Test
88 |   void "Ensure tasks added"() {
89 |     project.evaluate()
90 | 
91 |     assertTrue(project.tasks.pullDebugAndroidTestScreenshots instanceof Task)
92 |     assertTrue(project.tasks.runDebugAndroidTestScreenshotTest instanceof Task)
93 |     assertTrue(project.tasks.recordDebugAndroidTestScreenshotTest instanceof Task)
94 |     assertTrue(project.tasks.verifyDebugAndroidTestScreenshotTest instanceof Task)
95 |   }
96 | }
97 | 


--------------------------------------------------------------------------------
/sample/.gitignore:
--------------------------------------------------------------------------------
 1 | *.iml
 2 | .gradle
 3 | /local.properties
 4 | /.idea/workspace.xml
 5 | /.idea/libraries
 6 | .DS_Store
 7 | /build
 8 | /captures
 9 | .externalNativeBuild
10 | 


--------------------------------------------------------------------------------
/sample/build.gradle:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * Copyright (c) Meta Platforms, Inc. and affiliates.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  *     http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | buildscript {
18 |   apply from: rootProject.file('versions.gradle')
19 |   repositories {
20 |     mavenLocal()
21 |     jcenter()
22 |     google()
23 |   }
24 | 
25 |   dependencies {
26 |     classpath plugs.agp
27 |     classpath plugs.screenshot
28 |     classpath plugs.kotlin
29 |   }
30 | }
31 | 
32 | allprojects {
33 |   repositories {
34 |     mavenLocal()
35 |     jcenter()
36 |     google()
37 |   }
38 | }
39 | 
40 | apply plugin: 'com.android.application'
41 | apply plugin: 'kotlin-android'
42 | apply plugin: 'kotlin-android-extensions'
43 | apply plugin: 'kotlin-kapt'
44 | apply plugin: 'com.facebook.testing.screenshot'
45 | 
46 | android {
47 |   compileSdkVersion versions.compileSdk
48 |   defaultConfig {
49 |     applicationId "com.facebook.testing.screenshot.example"
50 |     minSdkVersion 15
51 |     targetSdkVersion versions.targetSdk
52 |     versionCode 1
53 |     versionName "1.0"
54 |     testInstrumentationRunner "com.facebook.testing.screenshot.sample.ScreenshotTestRunner"
55 |     testInstrumentationRunnerArguments clearPackageData: 'true'
56 |   }
57 |   testOptions {
58 |     execution 'ANDROIDX_TEST_ORCHESTRATOR'
59 |   }
60 | }
61 | 
62 | dependencies {
63 |   implementation deps.kotlinStdlib
64 | 
65 |   implementation deps.supportAppCompat
66 |   implementation deps.supportDesign
67 | 
68 |   implementation deps.lithoCore
69 |   implementation deps.lithoWidget
70 |   implementation deps.lithoAnnotations
71 |   kapt deps.lithoProcessor
72 | 
73 |   implementation deps.soLoader
74 | 
75 |   androidTestImplementation deps.screenshotTestCommon
76 |   androidTestImplementation deps.screenshotTestLitho
77 |   androidTestImplementation deps.junit
78 |   androidTestImplementation deps.testRunner
79 |   androidTestUtil deps.orchestrator
80 |   androidTestImplementation(deps.espresso) {
81 |     exclude group: 'com.android.support', module: 'support-annotations'
82 |     exclude group: 'com.google.code.findbugs', module: 'jsr305'
83 |   }
84 |   androidTestImplementation deps.androidTestRules
85 | }
86 | 
87 | screenshots {
88 |   multipleDevices true
89 |   pythonExecutable 'python3'
90 | }
91 | 


--------------------------------------------------------------------------------
/sample/screenshots/API_25_GP_XHDPI_768x1280_x86/com.facebook.testing.screenshot.sample.ExampleScreenshotTest_testDefault.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/facebook/screenshot-tests-for-android/2c1defad8d50e6aa33b0bdb56074cef563047ad0/sample/screenshots/API_25_GP_XHDPI_768x1280_x86/com.facebook.testing.screenshot.sample.ExampleScreenshotTest_testDefault.png


--------------------------------------------------------------------------------
/sample/screenshots/API_25_GP_XHDPI_768x1280_x86/com.facebook.testing.screenshot.sample.ImageRowScreenshotTest_testDefault.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/facebook/screenshot-tests-for-android/2c1defad8d50e6aa33b0bdb56074cef563047ad0/sample/screenshots/API_25_GP_XHDPI_768x1280_x86/com.facebook.testing.screenshot.sample.ImageRowScreenshotTest_testDefault.png


--------------------------------------------------------------------------------
/sample/screenshots/API_25_GP_XHDPI_768x1280_x86/com.facebook.testing.screenshot.sample.MainActivityTest_errorTextShouldBeRed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/facebook/screenshot-tests-for-android/2c1defad8d50e6aa33b0bdb56074cef563047ad0/sample/screenshots/API_25_GP_XHDPI_768x1280_x86/com.facebook.testing.screenshot.sample.MainActivityTest_errorTextShouldBeRed.png


--------------------------------------------------------------------------------
/sample/screenshots/API_25_GP_XHDPI_768x1280_x86/com.facebook.testing.screenshot.sample.MainActivityTest_mainActivityTestSettingsOpen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/facebook/screenshot-tests-for-android/2c1defad8d50e6aa33b0bdb56074cef563047ad0/sample/screenshots/API_25_GP_XHDPI_768x1280_x86/com.facebook.testing.screenshot.sample.MainActivityTest_mainActivityTestSettingsOpen.png


--------------------------------------------------------------------------------
/sample/screenshots/API_25_GP_XHDPI_768x1280_x86/com.facebook.testing.screenshot.sample.MainActivityTest_okTextShouldBeGreen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/facebook/screenshot-tests-for-android/2c1defad8d50e6aa33b0bdb56074cef563047ad0/sample/screenshots/API_25_GP_XHDPI_768x1280_x86/com.facebook.testing.screenshot.sample.MainActivityTest_okTextShouldBeGreen.png


--------------------------------------------------------------------------------
/sample/screenshots/API_25_GP_XHDPI_768x1280_x86/com.facebook.testing.screenshot.sample.MainActivityTest_testScreenshotEntireActivity.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/facebook/screenshot-tests-for-android/2c1defad8d50e6aa33b0bdb56074cef563047ad0/sample/screenshots/API_25_GP_XHDPI_768x1280_x86/com.facebook.testing.screenshot.sample.MainActivityTest_testScreenshotEntireActivity.png


--------------------------------------------------------------------------------
/sample/screenshots/API_25_GP_XHDPI_768x1280_x86/com.facebook.testing.screenshot.sample.MainActivityTest_testScreenshotEntireActivityWithoutAccessibilityMetadata.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/facebook/screenshot-tests-for-android/2c1defad8d50e6aa33b0bdb56074cef563047ad0/sample/screenshots/API_25_GP_XHDPI_768x1280_x86/com.facebook.testing.screenshot.sample.MainActivityTest_testScreenshotEntireActivityWithoutAccessibilityMetadata.png


--------------------------------------------------------------------------------
/sample/screenshots/API_25_GP_XHDPI_768x1280_x86/com.facebook.testing.screenshot.sample.MainActivityTest_warningTextShouldBeYellow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/facebook/screenshot-tests-for-android/2c1defad8d50e6aa33b0bdb56074cef563047ad0/sample/screenshots/API_25_GP_XHDPI_768x1280_x86/com.facebook.testing.screenshot.sample.MainActivityTest_warningTextShouldBeYellow.png


--------------------------------------------------------------------------------
/sample/screenshots/API_25_GP_XHDPI_768x1280_x86/com.facebook.testing.screenshot.sample.StandardAndroidViewTest_testChinese.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/facebook/screenshot-tests-for-android/2c1defad8d50e6aa33b0bdb56074cef563047ad0/sample/screenshots/API_25_GP_XHDPI_768x1280_x86/com.facebook.testing.screenshot.sample.StandardAndroidViewTest_testChinese.png


--------------------------------------------------------------------------------
/sample/screenshots/API_25_GP_XHDPI_768x1280_x86/com.facebook.testing.screenshot.sample.StandardAndroidViewTest_testLongText.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/facebook/screenshot-tests-for-android/2c1defad8d50e6aa33b0bdb56074cef563047ad0/sample/screenshots/API_25_GP_XHDPI_768x1280_x86/com.facebook.testing.screenshot.sample.StandardAndroidViewTest_testLongText.png


--------------------------------------------------------------------------------
/sample/screenshots/API_25_GP_XHDPI_768x1280_x86/com.facebook.testing.screenshot.sample.StandardAndroidViewTest_testRendering.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/facebook/screenshot-tests-for-android/2c1defad8d50e6aa33b0bdb56074cef563047ad0/sample/screenshots/API_25_GP_XHDPI_768x1280_x86/com.facebook.testing.screenshot.sample.StandardAndroidViewTest_testRendering.png


--------------------------------------------------------------------------------
/sample/screenshots/API_25_GP_XHDPI_768x1280_x86/fab.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/facebook/screenshot-tests-for-android/2c1defad8d50e6aa33b0bdb56074cef563047ad0/sample/screenshots/API_25_GP_XHDPI_768x1280_x86/fab.png


--------------------------------------------------------------------------------
/sample/src/androidTest/java/com/facebook/testing/screenshot/sample/ExampleScreenshotTest.java:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * Copyright (c) Meta Platforms, Inc. and affiliates.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  *     http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | package com.facebook.testing.screenshot.sample;
18 | 
19 | import android.content.Context;
20 | import android.view.LayoutInflater;
21 | import androidx.test.annotation.UiThreadTest;
22 | import androidx.test.platform.app.InstrumentationRegistry;
23 | import com.facebook.litho.LithoView;
24 | import com.facebook.testing.screenshot.Screenshot;
25 | import com.facebook.testing.screenshot.ViewHelpers;
26 | import org.junit.Before;
27 | import org.junit.Test;
28 | 
29 | public class ExampleScreenshotTest {
30 |   @Before
31 |   public void before() {
32 |     InstrumentationRegistry.getInstrumentation().getUiAutomation();
33 |   }
34 | 
35 |   @Test
36 |   @UiThreadTest
37 |   public void testDefault() {
38 |     Context targetContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
39 |     LayoutInflater inflater = LayoutInflater.from(targetContext);
40 |     LithoView view = (LithoView) inflater.inflate(R.layout.litho_view, null, false);
41 | 
42 |     view.setComponent(Example.create(view.getComponentContext()).build());
43 | 
44 |     ViewHelpers.setupView(view).setExactWidthDp(300).setExactHeightDp(300).layout();
45 |     Screenshot.snap(view).record();
46 |   }
47 | }
48 | 


--------------------------------------------------------------------------------
/sample/src/androidTest/java/com/facebook/testing/screenshot/sample/ImageRowScreenshotTest.java:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * Copyright (c) Meta Platforms, Inc. and affiliates.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  *     http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | package com.facebook.testing.screenshot.sample;
18 | 
19 | import android.content.Context;
20 | import android.view.LayoutInflater;
21 | import androidx.test.annotation.UiThreadTest;
22 | import androidx.test.platform.app.InstrumentationRegistry;
23 | import com.facebook.litho.LithoView;
24 | import com.facebook.testing.screenshot.Screenshot;
25 | import com.facebook.testing.screenshot.ViewHelpers;
26 | import org.junit.Before;
27 | import org.junit.Test;
28 | 
29 | public class ImageRowScreenshotTest {
30 |   @Before
31 |   public void before() {
32 |     InstrumentationRegistry.getInstrumentation().getUiAutomation();
33 |   }
34 | 
35 |   @Test
36 |   @UiThreadTest
37 |   public void testDefault() {
38 |     Context targetContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
39 |     LayoutInflater inflater = LayoutInflater.from(targetContext);
40 |     LithoView view = (LithoView) inflater.inflate(R.layout.litho_view, null, false);
41 | 
42 |     view.setComponent(ImageRow.create(view.getComponentContext()).build());
43 | 
44 |     ViewHelpers.setupView(view).setExactWidthDp(300).layout();
45 |     Screenshot.snap(view).record();
46 |   }
47 | }
48 | 


--------------------------------------------------------------------------------
/sample/src/androidTest/java/com/facebook/testing/screenshot/sample/MainActivityTest.kt:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * Copyright (c) Meta Platforms, Inc. and affiliates.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  *     http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | package com.facebook.testing.screenshot.sample
18 | 
19 | import android.view.View
20 | import androidx.test.espresso.Espresso.onView
21 | import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
22 | import androidx.test.espresso.action.ViewActions.click
23 | import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
24 | import androidx.test.espresso.matcher.ViewMatchers.withId
25 | import androidx.test.platform.app.InstrumentationRegistry
26 | import androidx.test.rule.ActivityTestRule
27 | import com.facebook.testing.screenshot.Screenshot
28 | import org.hamcrest.core.AllOf.allOf
29 | import org.junit.Before
30 | import org.junit.Rule
31 | import org.junit.Test
32 | 
33 | class MainActivityTest {
34 |   @get:Rule
35 |   var activityTestRule = ActivityTestRule(MainActivity::class.java, false, false)
36 | 
37 |   @Before
38 |   fun before() {
39 |     InstrumentationRegistry.getInstrumentation().getUiAutomation()
40 |   }
41 | 
42 |   @Test
43 |   fun testScreenshotEntireActivity() {
44 |     val activity = activityTestRule.launchActivity(null)
45 |     Screenshot.snapActivity(activity).record()
46 |   }
47 | 
48 |   @Test
49 |   fun mainActivityTestSettingsOpen() {
50 |     val activity = activityTestRule.launchActivity(null)
51 |     val floatingActionButton = onView(allOf(withId(R.id.fab), isDisplayed()))
52 |     floatingActionButton.perform(click())
53 | 
54 |     openActionBarOverflowOrOptionsMenu(activity)
55 |     Screenshot.snapActivity(activity).record()
56 |   }
57 | 
58 |   @Test
59 |   fun mainActivityTestFabWithEspresso() {
60 |     activityTestRule.launchActivity(null)
61 |     onView(withId(R.id.fab)).perform(screenshot("fab"))
62 |   }
63 | 
64 |   @Test
65 |   fun errorTextShouldBeRed() {
66 |     val intent = MainActivity.intent(MainActivity.Status.ERROR)
67 |     val activity = activityTestRule.launchActivity(intent)
68 | 
69 |     Screenshot.snapActivity(activity).record()
70 |   }
71 | 
72 |   @Test
73 |   fun warningTextShouldBeYellow() {
74 |     val intent = MainActivity.intent(MainActivity.Status.WARNING)
75 |     val activity = activityTestRule.launchActivity(intent)
76 | 
77 |     Screenshot.snapActivity(activity).record()
78 |   }
79 | 
80 |   @Test
81 |   fun okTextShouldBeGreen() {
82 |     val intent = MainActivity.intent(MainActivity.Status.OK)
83 |     val activity = activityTestRule.launchActivity(intent)
84 | 
85 |     Screenshot.snapActivity(activity).record()
86 |   }
87 | 
88 |   @Test
89 |   fun testScreenshotEntireActivityWithoutAccessibilityMetadata() {
90 |     val activity = activityTestRule.launchActivity(null)
91 |     Screenshot.snapActivity(activity).setIncludeAccessibilityInfo(false).record()
92 |   }
93 | }
94 | 


--------------------------------------------------------------------------------
/sample/src/androidTest/java/com/facebook/testing/screenshot/sample/ScreenshotTestRunner.kt:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * Copyright (c) Meta Platforms, Inc. and affiliates.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  *     http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | package com.facebook.testing.screenshot.sample
18 | 
19 | import android.os.Bundle
20 | import androidx.test.runner.AndroidJUnitRunner
21 | import com.facebook.litho.config.LithoDebugConfigurations
22 | import com.facebook.testing.screenshot.ScreenshotRunner
23 | import com.facebook.testing.screenshot.layouthierarchy.LayoutHierarchyDumper
24 | import com.facebook.testing.screenshot.layouthierarchy.litho.LithoAttributePlugin
25 | import com.facebook.testing.screenshot.layouthierarchy.litho.LithoHierarchyPlugin
26 | 
27 | class ScreenshotTestRunner : AndroidJUnitRunner() {
28 |   companion object {
29 |     init {
30 |       LithoDebugConfigurations.isDebugModeEnabled = true
31 |       LayoutHierarchyDumper.addGlobalHierarchyPlugin(LithoHierarchyPlugin.getInstance())
32 |       LayoutHierarchyDumper.addGlobalAttributePlugin(LithoAttributePlugin.getInstance())
33 |     }
34 |   }
35 | 
36 |   override fun onCreate(arguments: Bundle) {
37 |     ScreenshotRunner.onCreate(this, arguments)
38 |     super.onCreate(arguments)
39 |   }
40 | 
41 |   override fun finish(resultCode: Int, results: Bundle) {
42 |     ScreenshotRunner.onDestroy()
43 |     super.finish(resultCode, results)
44 |   }
45 | }
46 | 


--------------------------------------------------------------------------------
/sample/src/androidTest/java/com/facebook/testing/screenshot/sample/ScreenshotViewAction.kt:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * Copyright (c) Meta Platforms, Inc. and affiliates.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  *     http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | package com.facebook.testing.screenshot.sample
18 | 
19 | import android.view.View
20 | import androidx.test.espresso.UiController
21 | import androidx.test.espresso.ViewAction
22 | import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
23 | import com.facebook.testing.screenshot.Screenshot
24 | import org.hamcrest.Matcher
25 | 
26 | fun screenshot(name: String): ViewAction {
27 |   return ScreenshotViewAction(name)
28 | }
29 | 
30 | class ScreenshotViewAction internal constructor(private val name: String) : ViewAction {
31 |   override fun getConstraints(): Matcher {
32 |     return isDisplayed()
33 |   }
34 | 
35 |   override fun getDescription(): String {
36 |     return name
37 |   }
38 | 
39 |   override fun perform(uiController: UiController, view: View) {
40 |     Screenshot.snap(view).setName(name).record()
41 |   }
42 | }
43 | 


--------------------------------------------------------------------------------
/sample/src/androidTest/java/com/facebook/testing/screenshot/sample/StandardAndroidViewTest.kt:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * Copyright (c) Meta Platforms, Inc. and affiliates.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  *     http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | package com.facebook.testing.screenshot.sample
18 | 
19 | import android.view.LayoutInflater
20 | import android.view.View
21 | import android.widget.TextView
22 | import androidx.test.platform.app.InstrumentationRegistry
23 | import com.facebook.testing.screenshot.Screenshot
24 | import com.facebook.testing.screenshot.ViewHelpers
25 | import org.junit.Test
26 | 
27 | class StandardAndroidViewTest {
28 |   @Test
29 |   @Throws(Throwable::class)
30 |   fun testRendering() {
31 |     val targetContext = InstrumentationRegistry.getInstrumentation().targetContext
32 |     val inflater = LayoutInflater.from(targetContext)
33 |     val view = inflater.inflate(R.layout.search_bar, null, false)
34 | 
35 |     ViewHelpers.setupView(view).setExactWidthDp(300).layout()
36 |     Screenshot.snap(view).record()
37 |   }
38 | 
39 |   @Test
40 |   @Throws(Throwable::class)
41 |   fun testLongText() {
42 |     val targetContext = InstrumentationRegistry.getInstrumentation().targetContext
43 |     val inflater = LayoutInflater.from(targetContext)
44 |     val view = inflater.inflate(R.layout.search_bar, null, false)
45 | 
46 |     val tv = view.findViewById(R.id.search_box) as TextView
47 | 
48 |     tv.text = "This is a really long text and should overflow"
49 |     ViewHelpers.setupView(view).setExactWidthDp(300).layout()
50 | 
51 |     Screenshot.snap(view).record()
52 |   }
53 | 
54 |   @Test
55 |   @Throws(Throwable::class)
56 |   fun testChinese() {
57 |     val targetContext = InstrumentationRegistry.getInstrumentation().targetContext
58 |     val inflater = LayoutInflater.from(targetContext)
59 |     val view = inflater.inflate(R.layout.search_bar, null, false)
60 | 
61 |     val tv = view.findViewById(R.id.search_box) as TextView
62 |     val btn = view.findViewById(R.id.button) as TextView
63 | 
64 |     tv.hint = "搜索世界"
65 |     btn.text = "搜"
66 | 
67 |     ViewHelpers.setupView(view).setExactWidthDp(300).layout()
68 | 
69 |     Screenshot.snap(view).record()
70 |   }
71 | }
72 | 


--------------------------------------------------------------------------------
/sample/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 4 | 
 5 |     
 6 |     
14 |         
18 |             
19 |                 
20 | 
21 |                 
22 |             
23 |         
24 |     
25 | 
26 | 
27 | 


--------------------------------------------------------------------------------
/sample/src/main/java/com/facebook/testing/screenshot/sample/App.kt:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * Copyright (c) Meta Platforms, Inc. and affiliates.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  *     http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | package com.facebook.testing.screenshot.sample
18 | 
19 | import android.app.Application
20 | import com.facebook.soloader.SoLoader
21 | 
22 | class App : Application() {
23 |   override fun onCreate() {
24 |     super.onCreate()
25 |     SoLoader.init(this, false)
26 |   }
27 | }
28 | 


--------------------------------------------------------------------------------
/sample/src/main/java/com/facebook/testing/screenshot/sample/ExampleSpec.kt:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * Copyright (c) Meta Platforms, Inc. and affiliates.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  *     http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | package com.facebook.testing.screenshot.sample
18 | 
19 | import com.facebook.litho.Border
20 | import com.facebook.litho.Column
21 | import com.facebook.litho.Component
22 | import com.facebook.litho.ComponentContext
23 | import com.facebook.litho.annotations.LayoutSpec
24 | import com.facebook.litho.annotations.OnCreateLayout
25 | import com.facebook.litho.widget.Text
26 | import com.facebook.yoga.YogaEdge
27 | 
28 | @LayoutSpec
29 | object ExampleSpec {
30 |   @OnCreateLayout
31 |   fun onCreateLayout(c: ComponentContext): Component =
32 |       Column.create(c)
33 |           .child(ImageRow.create(c))
34 |           .child(Text.create(c).textRes(R.string.large_text))
35 |           .paddingDip(YogaEdge.ALL, 16f)
36 |           .border(
37 |               Border.create(c)
38 |                   .colorRes(YogaEdge.ALL, R.color.colorPrimary)
39 |                   .widthDip(YogaEdge.ALL, 8f)
40 |                   .build())
41 |           .build()
42 | }
43 | 


--------------------------------------------------------------------------------
/sample/src/main/java/com/facebook/testing/screenshot/sample/ImageRowSpec.kt:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * Copyright (c) Meta Platforms, Inc. and affiliates.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  *     http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | package com.facebook.testing.screenshot.sample
18 | 
19 | import com.facebook.litho.Component
20 | import com.facebook.litho.ComponentContext
21 | import com.facebook.litho.Row
22 | import com.facebook.litho.annotations.LayoutSpec
23 | import com.facebook.litho.annotations.OnCreateLayout
24 | import com.facebook.litho.widget.Image
25 | import com.facebook.yoga.YogaEdge
26 | 
27 | @LayoutSpec
28 | object ImageRowSpec {
29 |   @OnCreateLayout
30 |   fun onCreateLayout(c: ComponentContext): Component =
31 |       Row.create(c)
32 |           .child(
33 |               Image.create(c)
34 |                   .drawableRes(R.drawable.ic_launcher_background)
35 |                   .widthDip(64f)
36 |                   .heightDip(64f)
37 |                   .paddingDip(YogaEdge.ALL, 4f))
38 |           .child(
39 |               Image.create(c)
40 |                   .drawableRes(R.drawable.ic_launcher_background)
41 |                   .widthDip(128f)
42 |                   .heightDip(128f)
43 |                   .paddingDip(YogaEdge.ALL, 4f))
44 |           .child(
45 |               Image.create(c)
46 |                   .drawableRes(R.drawable.ic_launcher_background)
47 |                   .widthDip(32f)
48 |                   .heightDip(32f)
49 |                   .paddingDip(YogaEdge.ALL, 4f))
50 |           .build()
51 | }
52 | 


--------------------------------------------------------------------------------
/sample/src/main/java/com/facebook/testing/screenshot/sample/MainActivity.kt:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * Copyright (c) Meta Platforms, Inc. and affiliates.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  *     http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | package com.facebook.testing.screenshot.sample
18 | 
19 | import android.content.Context
20 | import android.content.Intent
21 | import android.os.Bundle
22 | import android.view.Menu
23 | import android.widget.TextView
24 | import androidx.annotation.ColorRes
25 | import androidx.appcompat.app.AppCompatActivity
26 | import androidx.core.content.ContextCompat
27 | import com.google.android.material.floatingactionbutton.FloatingActionButton
28 | import com.google.android.material.snackbar.Snackbar
29 | 
30 | class MainActivity : AppCompatActivity() {
31 |   companion object {
32 |     private const val STATUS = "status"
33 | 
34 |     fun intent(status: Status) = Intent().apply { putExtra(STATUS, status.name) }
35 |   }
36 | 
37 |   enum class Status {
38 |     OK,
39 |     WARNING,
40 |     ERROR
41 |   }
42 | 
43 |   override fun onCreate(savedInstanceState: Bundle?) {
44 |     super.onCreate(savedInstanceState)
45 |     setContentView(R.layout.activity_main)
46 |     setSupportActionBar(findViewById(R.id.toolbar))
47 | 
48 |     val textView = findViewById(R.id.text_view)
49 |     val status = Status.valueOf(intent.string(STATUS, Status.OK.name))
50 |     when (status) {
51 |       Status.OK ->
52 |           textView.run {
53 |             setTextColor(context.color(R.color.ok))
54 |             text = "Status is OK"
55 |           }
56 |       Status.WARNING ->
57 |           textView.run {
58 |             setTextColor(context.color(R.color.warning))
59 |             text = "Status is WARNING"
60 |           }
61 |       Status.ERROR ->
62 |           textView.run {
63 |             setTextColor(context.color(R.color.error))
64 |             text = "Status is ERROR"
65 |           }
66 |     }
67 | 
68 |     findViewById(R.id.fab).setOnClickListener { view ->
69 |       Snackbar.make(view, "This is a snackbar", Snackbar.LENGTH_LONG)
70 |           .setAction("Action", null)
71 |           .show()
72 |     }
73 |   }
74 | 
75 |   override fun onCreateOptionsMenu(menu: Menu): Boolean {
76 |     menuInflater.inflate(R.menu.menu_main, menu)
77 |     return true
78 |   }
79 | 
80 |   private fun Intent.string(name: String, defValue: String): String {
81 |     if (hasExtra(name)) {
82 |       return getStringExtra(name)
83 |     }
84 |     return defValue
85 |   }
86 | 
87 |   private fun Context.color(@ColorRes color: Int): Int {
88 |     return ContextCompat.getColor(this, color)
89 |   }
90 | }
91 | 


--------------------------------------------------------------------------------
/sample/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
  1 | 
  2 | 
  8 |     
 13 |     
 18 |     
 23 |     
 28 |     
 33 |     
 38 |     
 43 |     
 48 |     
 53 |     
 58 |     
 63 |     
 68 |     
 73 |     
 78 |     
 83 |     
 88 |     
 93 |     
 98 |     
103 |     
108 |     
113 | 
114 | 


--------------------------------------------------------------------------------
/sample/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 8 | 
 9 |   
13 | 
14 |     
20 | 
21 |   
22 | 
23 |   
27 | 
28 |     
34 | 
35 |   
36 | 
37 |   
45 | 
46 | 
47 | 


--------------------------------------------------------------------------------
/sample/src/main/res/layout/litho_view.xml:
--------------------------------------------------------------------------------
1 | 
2 | 
7 | 


--------------------------------------------------------------------------------
/sample/src/main/res/layout/search_bar.xml:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 6 |   
14 |