├── .gitignore ├── CHANGELOG ├── CONTRIBUTING.md ├── LICENSE ├── LICENSE-examples ├── PATENTS ├── 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 │ │ │ ├── HostFileSenderTest.java │ │ │ ├── RecordBuilderImplTest.java │ │ │ ├── ScreenshotDirectoriesTest.java │ │ │ ├── ScreenshotImplTest.java │ │ │ ├── TestNameDetectorForJUnit4Test.java │ │ │ ├── TestNameDetectorTest.java │ │ │ └── ViewHierarchyTest.java │ │ │ └── plugin │ │ │ └── TextViewDumperTest.java │ └── res │ │ └── layout │ │ ├── testing_for_view_hierarchy.xml │ │ └── testing_simple_textview.xml │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── facebook │ └── testing │ └── screenshot │ ├── RecordBuilder.java │ ├── Screenshot.java │ ├── ScreenshotRunner.java │ ├── ScreenshotTestRunner.java │ ├── ViewHelpers.java │ ├── WindowAttachment.java │ ├── internal │ ├── Album.java │ ├── AlbumImpl.java │ ├── HostFileSender.java │ ├── RecordBuilderImpl.java │ ├── Registry.java │ ├── ScreenshotDirectories.java │ ├── ScreenshotImpl.java │ ├── TestNameDetector.java │ ├── Tiling.java │ └── ViewHierarchy.java │ └── plugin │ ├── PluginRegistry.java │ ├── TextViewDumper.java │ └── ViewDumpPlugin.java ├── examples ├── app-example-androidjunitrunner │ ├── .gitignore │ ├── app │ │ ├── .gitignore │ │ ├── build.gradle │ │ ├── gradle │ │ │ └── wrapper │ │ │ │ ├── gradle-wrapper.jar │ │ │ │ └── gradle-wrapper.properties │ │ ├── gradlew │ │ ├── gradlew.bat │ │ ├── local.properties │ │ ├── proguard-rules.pro │ │ ├── screenshots │ │ │ ├── com.example.screenshots.MainActivityTest_mainActivityTest.png │ │ │ └── com.example.screenshots.MainActivityTest_mainActivityTestSettingsOpen.png │ │ └── src │ │ │ ├── androidTest │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── screenshots │ │ │ │ ├── CustomTestRunner.java │ │ │ │ └── MainActivityTest.java │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── screenshots │ │ │ │ │ └── MainActivity.java │ │ │ └── res │ │ │ │ ├── layout │ │ │ │ ├── activity_main.xml │ │ │ │ └── content_main.xml │ │ │ │ ├── menu │ │ │ │ └── menu_main.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values-v21 │ │ │ │ └── styles.xml │ │ │ │ ├── values-w820dp │ │ │ │ └── dimens.xml │ │ │ │ └── values │ │ │ │ ├── colors.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── strings.xml │ │ │ │ └── styles.xml │ │ │ └── test │ │ │ └── java │ │ │ └── com │ │ │ └── example │ │ │ └── screenshots │ │ │ └── ExampleUnitTest.java │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ └── settings.gradle └── app-example │ ├── build.gradle │ ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ ├── screenshots │ ├── com.facebook.testing.screenshot.SearchBarTest_testChinese.png │ ├── com.facebook.testing.screenshot.SearchBarTest_testLongText.png │ └── com.facebook.testing.screenshot.SearchBarTest_testRendering.png │ └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── facebook │ │ └── testing │ │ └── screenshot │ │ └── SearchBarTest.java │ └── main │ ├── AndroidManifest.xml │ └── res │ └── layout │ └── search_bar.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── integration_test.sh ├── plugin ├── build.gradle ├── gradle.properties └── src │ ├── main │ ├── groovy │ │ └── com │ │ │ └── facebook │ │ │ └── testing │ │ │ └── screenshot │ │ │ └── build │ │ │ └── ScreenshotsPlugin.groovy │ └── resources │ │ └── META-INF │ │ └── gradle-plugins │ │ └── com.facebook.testing.screenshot.properties │ ├── py │ ├── __init__.py │ └── android_screenshot_tests │ │ ├── __init__.py │ │ ├── aapt.py │ │ ├── background.png │ │ ├── common.py │ │ ├── default.css │ │ ├── default.js │ │ ├── example.apk │ │ ├── fixtures │ │ ├── AndroidManifest.xml │ │ ├── dummy.zip │ │ └── sdcard │ │ │ └── screenshots │ │ │ └── com.foo │ │ │ └── screenshots-default │ │ │ ├── com.foo.ScriptsFixtureTest_testGetTextViewScreenshot.png │ │ │ ├── com.foo.ScriptsFixtureTest_testSecondScreenshot.png │ │ │ ├── metadata.xml │ │ │ ├── metadata_no_errors.xml │ │ │ └── one_dump.xml │ │ ├── metadata.py │ │ ├── metadata_fixture.xml │ │ ├── pull_screenshots.py │ │ ├── recorder.py │ │ ├── simple_puller.py │ │ ├── test_aapt.py │ │ ├── test_common.py │ │ ├── test_metadata.py │ │ ├── test_pull_screenshots.py │ │ ├── test_recorder.py │ │ └── test_simple_puller.py │ └── test │ └── groovy │ └── com │ └── facebook │ └── testing │ └── screenshot │ └── build │ └── ScreenshotsPluginTest.groovy ├── pull_screenshots ├── release.gradle └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | */build/ 3 | .gradle 4 | /repo/ 5 | /build 6 | repo 7 | examples/**/build 8 | .idea 9 | *.iml 10 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 2 | * 0.4.2 3 | 4 | - Support for Android gradle plugin 2.2.0 5 | - Make ViewHierarchy dump more useful information 6 | -------------------------------------------------------------------------------- /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 GitHub repository is our source of truth, and all development 8 | happens directly in GitHub. Internally, we might build tools around 9 | this framework that we might move into the GitHub repository in the 10 | future, but we won't fork for internal changes. 11 | 12 | This repository has two components: 13 | 14 | * in `src/` 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 `master`. 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 | * 2 spaces for indentation rather than tabs 53 | * 80 character line length 54 | * ... 55 | 56 | ## License 57 | By contributing screenshot-tests-for-android, you agree that your contributions will be licensed 58 | under its BSD license. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | For screenshot-tests-for-android software 4 | 5 | Copyright (c) 2014-present, Facebook, Inc. All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without modification, 8 | are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name Facebook nor the names of its contributors may be used to 18 | endorse or promote products derived from this software without specific 19 | prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 22 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 25 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 28 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /LICENSE-examples: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-present, Facebook, Inc. All rights reserved. 2 | 3 | The examples provided by Facebook are for non-commercial testing and evaluation 4 | purposes only. Facebook reserves all rights not expressly granted. 5 | 6 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 7 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 8 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 9 | FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 10 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 11 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | -------------------------------------------------------------------------------- /PATENTS: -------------------------------------------------------------------------------- 1 | Additional Grant of Patent Rights Version 2 2 | 3 | "Software" means the screenshot-tests-for-android software distributed 4 | by Facebook, Inc. 5 | 6 | Facebook, Inc. ("Facebook") hereby grants to each recipient of the Software 7 | ("you") a perpetual, worldwide, royalty-free, non-exclusive, irrevocable 8 | (subject to the termination provision below) license under any Necessary 9 | Claims, to make, have made, use, sell, offer to sell, import, and otherwise 10 | transfer the Software. For avoidance of doubt, no license is granted under 11 | Facebook's rights in any patent claims that are infringed by (i) modifications 12 | to the Software made by you or any third party or (ii) the Software in 13 | combination with any software or other technology. 14 | 15 | The license granted hereunder will terminate, automatically and without notice, 16 | if you (or any of your subsidiaries, corporate affiliates or agents) initiate 17 | directly or indirectly, or take a direct financial interest in, any Patent 18 | Assertion: (i) against Facebook or any of its subsidiaries or corporate 19 | affiliates, (ii) against any party if such Patent Assertion arises in whole or 20 | in part from any software, technology, product or service of Facebook or any of 21 | its subsidiaries or corporate affiliates, or (iii) against any party relating 22 | to the Software. Notwithstanding the foregoing, if Facebook or any of its 23 | subsidiaries or corporate affiliates files a lawsuit alleging patent 24 | infringement against you in the first instance, and you respond by filing a 25 | patent infringement counterclaim in that lawsuit against that party that is 26 | unrelated to the Software, the license granted hereunder will not terminate 27 | under section (i) of this paragraph due to such counterclaim. 28 | 29 | A "Necessary Claim" is a claim of a patent owned by Facebook that is 30 | necessarily infringed by the Software standing alone. 31 | 32 | A "Patent Assertion" is any lawsuit or other action alleging direct, indirect, 33 | or contributory infringement or inducement to infringe any patent, including a 34 | cross-claim or counterclaim. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # screenshot-tests-for-android 2 | 3 | screenshot-tests-for-android is a library that can generate fast 4 | deterministic screenshots while running instrumentation tests in 5 | android. 6 | 7 | We mimic Android's measure(), layout() and draw() to generate screenshots 8 | on the test thread. By not having to do the rendering on a separate 9 | thread we have control over animations and handler callbacks which 10 | makes the screenshots extremely deterministic and reliable for catching 11 | regressions in continuous integration. 12 | 13 | We also provide utilities for using screenshot tests during the development 14 | process. With these scripts you can iterate on a view or layout and quickly 15 | see how the view renders in real android code, without having to 16 | build the whole app. You can also render the view in multiple configurations 17 | at one go. 18 | 19 | ## Documentation 20 | 21 | Take a look at the documentation at http://facebook.github.io/screenshot-tests-for-android/#getting-started 22 | 23 | ## Requirements 24 | 25 | screenshot-tests-for-android is known to work with Mac OS X or Linux. 26 | 27 | The host tooling probably doesn't work on Windows, but can be made to 28 | work with a little effort. We'll happily accept pull requests! 29 | 30 | You need python-2.7 for the gradle plugin to work, and we also 31 | recommending installing the python-pillow library which is required 32 | for recording and verifying screenshots. 33 | 34 | ## Building screenshot-tests-for-android 35 | 36 | You don't have to build screenshot-tests-for-android from scratch if 37 | you don't plan to contribute. All artifacts are available from Maven 38 | Central. 39 | 40 | If you plan to contribute, this is the code is broken up into two 41 | parts: 42 | 43 | * The `core` library is packaged as part of your instrumentation tests 44 | and generates screenshots on the device. 45 | 46 | * The `plugin` library adds Gradle tasks to make it easier to work 47 | with screenshot tests. 48 | 49 | In addition you'll find python code inside `plugin/src/py`. This code 50 | is packaged into the gradle plugin. 51 | 52 | We have tests for the python code and the core library. Run these 53 | commands to run all the tests: 54 | 55 | ```bash 56 | $ gradle :plugin:pyTests 57 | $ gradle :core:connectedAndroidTest 58 | ``` 59 | 60 | Both need a running emulator. 61 | 62 | You can install all the artifacts to your local maven repository using 63 | 64 | ```bash 65 | $ gradle installAll 66 | ``` 67 | 68 | ## Running With a Remote Service 69 | 70 | For usage with a remote testing service (e.g. Google Cloud Test Lab) where ADB is not available directly the plugin supports a "disconnected" 71 | workflow. Collect all screenshots artifacts into a single directory and run the plugin in "local mode" using the pullScreenshotsFromDirectory task 72 | 73 | ### Example 74 | The location of the screenshot artifacts can be configured in the project's build.gradle: 75 | ```groovy 76 | screenshots { 77 | // Points to the directory containing all the files pulled from a device 78 | referenceDir = path/to/artifacts 79 | 80 | // Your app's application id 81 | targetPackage = "your.application.package" 82 | } 83 | ``` 84 | 85 | Then, screenshots may be verified by executing the following: 86 | ```bash 87 | $ gradle verifyMode pullScreenshotsFromDirectory 88 | ``` 89 | 90 | To record, simply change `verifyMode` to `recordMode` and the local screenshots will become the master copy 91 | 92 | ## Join the screenshot-tests-for-android community 93 | 94 | * Website: http://facebook.github.io/screenshot-tests-for-android 95 | * Discussion group: 96 | https://groups.google.com/forum/#!forum/screenshot-tests-for-android
97 | screenshot-tests-for-android@googlegroups.com 98 | 99 | See the CONTRIBUTING file for how to help out. 100 | 101 | ## Authors 102 | 103 | screenshot-tests-for-android has been written by Arnold Noronha (arnold@fb.com) 104 | You can reach him at @tdrhq on GitHub. 105 | 106 | ## License 107 | 108 | screenshot-tests-for-android is BSD-licensed. We also provide an 109 | additional patent grant. 110 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | } 5 | } 6 | 7 | allprojects { 8 | version = property('VERSION_NAME') 9 | } 10 | 11 | task uploadAllLocal(dependsOn: [ 12 | ":plugin:uploadArchives", 13 | ":core:uploadArchives"]) { 14 | } 15 | 16 | task installAll(dependsOn: [ 17 | ":core:install", 18 | ":plugin:install", 19 | ]) 20 | 21 | task uploadAllArchives(dependsOn: 22 | [":plugin:uploadArchives", 23 | ":core:uploadArchives"]) << { 24 | println("You need to close and release this from 'Staging Repositories' in http://oss.sonatype.org"); 25 | } 26 | 27 | 28 | // Run all the tests prior to release. Also run integration_test.sh 29 | task releaseTests(dependsOn: 30 | [":plugin:pyTests", 31 | ":plugin:test", 32 | ":core:connectedAndroidTest"]) 33 | -------------------------------------------------------------------------------- /core/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | } 5 | 6 | dependencies { 7 | classpath 'com.android.tools.build:gradle:1.3.1' 8 | 9 | // Can't use the default maven install for android libraries 10 | classpath 'com.github.dcendents:android-maven-gradle-plugin:1.3' 11 | } 12 | } 13 | 14 | apply plugin: 'com.android.library' 15 | apply plugin: 'com.github.dcendents.android-maven' 16 | 17 | group='com.facebook.testing.screenshot' 18 | 19 | repositories { 20 | mavenCentral() 21 | } 22 | 23 | dependencies { 24 | compile 'junit:junit:4.12' 25 | compile 'com.crittercism.dexmaker:dexmaker:1.4' 26 | compile 'com.crittercism.dexmaker:dexmaker-dx:1.4' 27 | 28 | androidTestCompile 'org.mockito:mockito-core:1.10.19' 29 | androidTestCompile 'com.crittercism.dexmaker:dexmaker-mockito:1.4' 30 | androidTestCompile 'com.android.support:support-v4:23.1.1' 31 | androidTestCompile 'com.android.support.test:rules:0.3' 32 | androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2' 33 | } 34 | 35 | android { 36 | compileSdkVersion 23 37 | buildToolsVersion "23.0.1" 38 | 39 | packagingOptions { 40 | exclude 'LICENSE.txt' 41 | } 42 | 43 | defaultConfig { 44 | minSdkVersion 9 45 | testInstrumentationRunner "com.facebook.testing.screenshot.CustomScreenshotTestRunner" 46 | } 47 | 48 | lintOptions { 49 | abortOnError false 50 | disable 'InvalidPackage' 51 | } 52 | 53 | sourceSets { 54 | } 55 | } 56 | 57 | uploadArchives { 58 | repositories { 59 | mavenInstaller { 60 | } 61 | } 62 | } 63 | 64 | apply from: rootProject.file("release.gradle") 65 | -------------------------------------------------------------------------------- /core/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=Screenshot tests core android library 2 | POM_ARTIFACT_ID=core 3 | POM_PACKAGING=aar 4 | -------------------------------------------------------------------------------- /core/src/androidTest/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 9 | 10 | 12 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /core/src/androidTest/java/com/facebook/testing/screenshot/CustomScreenshotTestRunner.java: -------------------------------------------------------------------------------- 1 | // Copyright 2004-present Facebook. All Rights Reserved. 2 | 3 | package com.facebook.testing.screenshot; 4 | 5 | import android.os.Bundle; 6 | import android.test.InstrumentationTestRunner; 7 | import android.support.test.runner.AndroidJUnitRunner; 8 | 9 | public class CustomScreenshotTestRunner extends AndroidJUnitRunner { 10 | @Override 11 | public void onCreate(Bundle args) { 12 | ScreenshotRunner.onCreate(this, args); 13 | super.onCreate(args); 14 | } 15 | 16 | @Override 17 | public void finish(int resultCode, Bundle results) { 18 | ScreenshotRunner.onDestroy(); 19 | super.finish(resultCode, results); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/androidTest/java/com/facebook/testing/screenshot/MyActivity.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.facebook.testing.screenshot; 11 | 12 | import android.app.Activity; 13 | 14 | /** 15 | * A dummy activity used in {@link BaseActivityInstrumentationTestCase2Test} 16 | */ 17 | public class MyActivity extends Activity { 18 | public boolean destroyed = false; 19 | 20 | @Override 21 | public void onDestroy() { 22 | super.onDestroy(); 23 | destroyed = true; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /core/src/androidTest/java/com/facebook/testing/screenshot/ScriptsFixtureTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.facebook.testing.screenshot; 11 | 12 | import android.test.InstrumentationTestCase; 13 | import android.view.View; 14 | import android.view.ViewGroup; 15 | import android.widget.FrameLayout; 16 | import android.widget.TextView; 17 | 18 | /** 19 | * This is not really a test, this test is just a "fixture" for all 20 | * the tests for the scripts related to running tests and getting 21 | * screenshots. 22 | */ 23 | public class ScriptsFixtureTest extends InstrumentationTestCase { 24 | private static final int HEIGHT = 100; 25 | private static final int WIDTH = 200; 26 | 27 | private TextView mTextView; 28 | 29 | @Override 30 | public void setUp() throws Exception { 31 | super.setUp(); 32 | mTextView = new TextView(getInstrumentation().getTargetContext()); 33 | mTextView.setText("foobar"); 34 | 35 | // Unfortunately TextView needs a LayoutParams for onDraw 36 | mTextView.setLayoutParams(new FrameLayout.LayoutParams( 37 | ViewGroup.LayoutParams.MATCH_PARENT, 38 | ViewGroup.LayoutParams.MATCH_PARENT)); 39 | 40 | measureAndLayout(); 41 | } 42 | 43 | @Override 44 | public void tearDown() throws Exception { 45 | super.tearDown(); 46 | } 47 | 48 | public void testGetTextViewScreenshot() throws Throwable { 49 | Screenshot.snap(mTextView).record(); 50 | } 51 | 52 | public void testSecondScreenshot() throws Throwable { 53 | mTextView.setText("foobar3"); 54 | measureAndLayout(); 55 | Screenshot.snap(mTextView).record(); 56 | } 57 | 58 | private void measureAndLayout() { 59 | try { 60 | runTestOnUiThread(new Runnable() { 61 | @Override 62 | public void run() { 63 | mTextView.measure( 64 | View.MeasureSpec.makeMeasureSpec(WIDTH, View.MeasureSpec.EXACTLY), 65 | View.MeasureSpec.makeMeasureSpec(HEIGHT, View.MeasureSpec.EXACTLY)); 66 | mTextView.layout(0, 0, mTextView.getMeasuredWidth(), mTextView.getMeasuredHeight()); 67 | } 68 | }); 69 | } catch (Throwable t) { 70 | throw new RuntimeException(t); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /core/src/androidTest/java/com/facebook/testing/screenshot/ViewHelpersTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.facebook.testing.screenshot; 11 | 12 | import android.test.InstrumentationTestCase; 13 | import android.widget.ArrayAdapter; 14 | import android.widget.ListView; 15 | import android.widget.TextView; 16 | 17 | import com.facebook.testing.screenshot.test.R; 18 | 19 | import static org.hamcrest.MatcherAssert.assertThat; 20 | import static org.hamcrest.Matchers.*; 21 | 22 | import static org.junit.Assert.*; 23 | 24 | /** 25 | * Tests {@link ViewHelpers} 26 | */ 27 | public class ViewHelpersTest extends InstrumentationTestCase { 28 | private TextView mTextView; 29 | 30 | @Override 31 | public void setUp() throws Exception { 32 | super.setUp(); 33 | mTextView = new TextView(getInstrumentation().getTargetContext()); 34 | mTextView.setText("foobar"); 35 | } 36 | 37 | public void testPreconditions() throws Throwable { 38 | assertEquals(0, mTextView.getMeasuredHeight()); 39 | } 40 | 41 | public void testMeasureWithoutHeight() throws Throwable { 42 | ViewHelpers.setupView(mTextView) 43 | .setExactWidthDp(100) 44 | .layout(); 45 | 46 | assertThat(mTextView.getMeasuredHeight(), greaterThan(0)); 47 | } 48 | 49 | public void testMeasureWithoutHeightPx() throws Throwable { 50 | ViewHelpers.setupView(mTextView) 51 | .setExactWidthPx(100) 52 | .layout(); 53 | 54 | assertThat(mTextView.getMeasuredHeight(), greaterThan(0)); 55 | } 56 | 57 | public void testMeasureForOnlyWidth() throws Throwable { 58 | ViewHelpers.setupView(mTextView) 59 | .setExactHeightPx(100) 60 | .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) 68 | .layout(); 69 | 70 | assertThat(mTextView.getMeasuredHeight(), greaterThan(0)); 71 | assertThat(mTextView.getMeasuredWidth(), greaterThan(0)); 72 | } 73 | 74 | public void testHeightAndWidthCorrectlyPropagated() throws Throwable { 75 | ViewHelpers.setupView(mTextView) 76 | .setExactHeightDp(100) 77 | .setExactWidthDp(1000) 78 | .layout(); 79 | 80 | assertThat(mTextView.getMeasuredWidth(), 81 | greaterThan(mTextView.getMeasuredHeight())); 82 | } 83 | 84 | public void testListViewHeight() throws Throwable { 85 | ListView view = new ListView(getInstrumentation().getTargetContext()); 86 | view.setDividerHeight(0); 87 | ArrayAdapter adapter = new ArrayAdapter( 88 | getInstrumentation().getTargetContext(), R.layout.testing_simple_textview); 89 | view.setAdapter(adapter); 90 | 91 | for (int i = 0; i < 20; i++) { 92 | adapter.add("foo"); 93 | } 94 | 95 | ViewHelpers.setupView(view) 96 | .guessListViewHeight() 97 | .setExactWidthDp(200) 98 | .layout(); 99 | 100 | assertThat(view.getMeasuredHeight(), 101 | greaterThan(10)); 102 | 103 | int oneHeight = view.getChildAt(0).getMeasuredHeight(); 104 | assertThat(view.getMeasuredHeight(), equalTo(oneHeight * 20)); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /core/src/androidTest/java/com/facebook/testing/screenshot/WindowAttachmentTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.facebook.testing.screenshot; 11 | 12 | import android.app.KeyguardManager; 13 | import android.content.Context; 14 | import android.support.test.InstrumentationRegistry; 15 | import android.support.test.espresso.Espresso; 16 | import android.support.test.runner.AndroidJUnit4; 17 | import android.test.ActivityInstrumentationTestCase2; 18 | import android.view.View; 19 | import android.widget.LinearLayout; 20 | 21 | import org.junit.After; 22 | import org.junit.Before; 23 | import org.junit.Test; 24 | import org.junit.runner.RunWith; 25 | import static android.support.test.espresso.action.ViewActions.*; 26 | import static android.support.test.espresso.matcher.ViewMatchers.*; 27 | import static org.junit.Assert.*; 28 | 29 | /** 30 | * Tests {@link WindowAttachment} 31 | */ 32 | @RunWith(AndroidJUnit4.class) 33 | public class WindowAttachmentTest extends ActivityInstrumentationTestCase2 { 34 | 35 | public WindowAttachmentTest() { 36 | super(MyActivity.class); 37 | } 38 | 39 | private Context mContext; 40 | private int mAttachedCalled = 0; 41 | private int mDetachedCalled = 0; 42 | private KeyguardManager.KeyguardLock mLock; 43 | 44 | @Before 45 | public void setUp() throws Exception { 46 | mContext = InstrumentationRegistry.getTargetContext(); 47 | injectInstrumentation(InstrumentationRegistry.getInstrumentation()); 48 | super.setUp(); 49 | KeyguardManager km = (KeyguardManager) 50 | getInstrumentation().getTargetContext().getSystemService(Context.KEYGUARD_SERVICE); 51 | mLock = km.newKeyguardLock("SelectAtTagActivityTest"); 52 | mLock.disableKeyguard(); 53 | } 54 | 55 | @After 56 | public void tearDown() throws Exception { 57 | mLock.reenableKeyguard(); 58 | super.tearDown(); 59 | } 60 | 61 | @Test 62 | public void testCalled() throws Throwable { 63 | MyView view = new MyView(mContext); 64 | WindowAttachment.Detacher detacher = WindowAttachment.dispatchAttach(view); 65 | assertEquals(1, mAttachedCalled); 66 | assertEquals(0, mDetachedCalled); 67 | 68 | detacher.detach(); 69 | assertEquals(1, mDetachedCalled); 70 | } 71 | 72 | @Test 73 | public void testCalledForViewGroup() throws Throwable { 74 | Parent view = new Parent(mContext); 75 | WindowAttachment.Detacher detacher = WindowAttachment.dispatchAttach(view); 76 | assertEquals(1, mAttachedCalled); 77 | assertEquals(0, mDetachedCalled); 78 | 79 | detacher.detach(); 80 | assertEquals(1, mDetachedCalled); 81 | } 82 | 83 | @Test 84 | public void testForNested() throws Throwable { 85 | Parent view = new Parent(mContext); 86 | MyView child = new MyView(mContext); 87 | view.addView(child); 88 | 89 | WindowAttachment.Detacher detacher = WindowAttachment.dispatchAttach(view); 90 | assertEquals(2, mAttachedCalled); 91 | assertEquals(0, mDetachedCalled); 92 | 93 | detacher.detach(); 94 | assertEquals(2, mDetachedCalled); 95 | } 96 | 97 | @Test 98 | public void testAReallyAttachedViewIsntAttacedAgain() throws Throwable { 99 | final View[] view = new View[1]; 100 | 101 | getActivity(); 102 | InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { 103 | @Override 104 | public void run() { 105 | view[0] = new MyView(getActivity()); 106 | getActivity().setContentView(view[0]); 107 | } 108 | }); 109 | 110 | InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 111 | 112 | // Call some espress method to make sure we're ready: 113 | Espresso.onView(withId(android.R.id.content)) 114 | .perform(click()); 115 | 116 | mAttachedCalled = 0; 117 | mDetachedCalled = 0; 118 | 119 | WindowAttachment.Detacher detacher = WindowAttachment.dispatchAttach(view[0]); 120 | detacher.detach(); 121 | 122 | assertEquals(0, mAttachedCalled); 123 | assertEquals(0, mDetachedCalled); 124 | } 125 | 126 | @Test 127 | public void testSetAttachInfo() throws Throwable { 128 | final MyView view = new MyView(mContext); 129 | InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { 130 | @Override 131 | public void run() { 132 | WindowAttachment.setAttachInfo(view); 133 | } 134 | }); 135 | 136 | assertNotNull(view.getWindowToken()); 137 | } 138 | 139 | 140 | public class MyView extends View { 141 | public MyView(Context context) { 142 | super(context); 143 | } 144 | 145 | @Override 146 | protected void onAttachedToWindow() { 147 | super.onAttachedToWindow(); 148 | mAttachedCalled++; 149 | } 150 | 151 | @Override 152 | protected void onDetachedFromWindow() { 153 | super.onDetachedFromWindow(); 154 | mDetachedCalled++; 155 | } 156 | } 157 | 158 | public class Parent extends LinearLayout { 159 | public Parent(Context context) { 160 | super(context); 161 | } 162 | 163 | @Override 164 | protected void onAttachedToWindow() { 165 | super.onAttachedToWindow(); 166 | mAttachedCalled++; 167 | } 168 | 169 | @Override 170 | protected void onDetachedFromWindow() { 171 | super.onDetachedFromWindow(); 172 | mDetachedCalled++; 173 | } 174 | } 175 | 176 | } 177 | -------------------------------------------------------------------------------- /core/src/androidTest/java/com/facebook/testing/screenshot/internal/HostFileSenderTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.facebook.testing.screenshot.internal; 11 | 12 | import java.io.File; 13 | import java.io.IOException; 14 | import java.io.PrintWriter; 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | import android.app.Instrumentation; 19 | import android.os.Bundle; 20 | import android.support.test.InstrumentationRegistry; 21 | import android.support.test.runner.AndroidJUnit4; 22 | 23 | import org.junit.Before; 24 | import org.junit.Rule; 25 | import org.junit.Test; 26 | import org.junit.rules.TemporaryFolder; 27 | import org.junit.runner.RunWith; 28 | import static org.hamcrest.Matchers.*; 29 | import static org.junit.Assert.*; 30 | 31 | /** 32 | * Tests {@link HostFileSender} 33 | */ 34 | @RunWith(AndroidJUnit4.class) 35 | public class HostFileSenderTest { 36 | Instrumentation mInstrumentation; 37 | HostFileSender mHostFileSender; 38 | final List mStatus = new ArrayList<>(); 39 | 40 | private boolean mDeleted = false; 41 | private boolean mFinished = false; 42 | 43 | @Rule 44 | public TemporaryFolder mFolder = new TemporaryFolder(); 45 | 46 | @Before 47 | public void before() throws Exception { 48 | Bundle arguments = new Bundle(); 49 | arguments.putString("HostFileSender_supported", "true"); 50 | arguments.putString("keep_files", "true"); 51 | 52 | mInstrumentation = new Instrumentation() { 53 | @Override 54 | public void sendStatus(int code, Bundle status) { 55 | mStatus.add(status); 56 | } 57 | }; 58 | mHostFileSender = new HostFileSender(mInstrumentation, arguments); 59 | } 60 | 61 | private File newFile(String filename) throws IOException { 62 | File file = mFolder.newFile(filename); 63 | PrintWriter printWriter = null; 64 | 65 | try { 66 | printWriter = new PrintWriter(file); 67 | printWriter.append("foobar"); 68 | } finally { 69 | if (printWriter != null) { 70 | printWriter.close(); 71 | } 72 | } 73 | 74 | return file; 75 | } 76 | 77 | @Test 78 | public void testCreation() throws Throwable { 79 | } 80 | 81 | @Test 82 | public void testSendFileSendsStatus() throws Throwable { 83 | File file = newFile("foo"); 84 | mHostFileSender.send(file); 85 | 86 | assertEquals(1, mStatus.size()); 87 | assertEquals(file.getAbsolutePath(), mStatus.get(0).getString("HostFileSender_filename")); 88 | } 89 | 90 | @Test 91 | public void testQueueSize() throws Throwable { 92 | assertEquals(0, mHostFileSender.getQueueSize()); 93 | mHostFileSender.send(newFile("foo")); 94 | assertEquals(1, mHostFileSender.getQueueSize()); 95 | } 96 | 97 | @Test 98 | public void testUpdateQueueCleansUpStuff() throws Throwable { 99 | File file = newFile("foo"); 100 | assertEquals(0, mHostFileSender.getQueueSize()); 101 | mHostFileSender.send(file); 102 | assertEquals(1, mHostFileSender.getQueueSize()); 103 | 104 | mHostFileSender.updateQueue(); 105 | assertEquals(1, mHostFileSender.getQueueSize()); 106 | 107 | file.delete(); 108 | mHostFileSender.updateQueue(); 109 | assertEquals(0, mHostFileSender.getQueueSize()); 110 | } 111 | 112 | @Test 113 | public void testWaitForQueue() throws Throwable { 114 | final File toDelete = newFile("foo"); 115 | mHostFileSender.send(toDelete); 116 | for (int i = 0; i < 4; i++) { 117 | mHostFileSender.send(newFile(String.valueOf(i))); 118 | } 119 | 120 | // The next one should block until we delete a file 121 | Thread thread = new Thread() { 122 | @Override 123 | public void run() { 124 | try { 125 | Thread.sleep(100); 126 | } catch (InterruptedException e) { 127 | throw new RuntimeException(e); 128 | } 129 | 130 | synchronized(HostFileSenderTest.this) { 131 | mDeleted = true; 132 | toDelete.delete(); 133 | } 134 | 135 | } 136 | }; 137 | thread.start(); 138 | mHostFileSender.send(newFile("6")); 139 | 140 | synchronized(this) { 141 | assertTrue(mDeleted); 142 | } 143 | thread.join(); 144 | } 145 | 146 | @Test 147 | public void testFlushWithoutDiscard() throws Throwable { 148 | final File one = newFile("one"); 149 | final File two = newFile("two"); 150 | mHostFileSender.send(one); 151 | mHostFileSender.send(two); 152 | 153 | Thread thread = new Thread() { 154 | @Override 155 | public void run() { 156 | try { 157 | Thread.sleep(100); 158 | } catch (InterruptedException e) { 159 | throw new RuntimeException(e); 160 | } 161 | 162 | assertFalse(mFinished); 163 | one.delete(); 164 | try { 165 | Thread.sleep(100); 166 | } catch (InterruptedException e) { 167 | throw new RuntimeException(e); 168 | } 169 | 170 | assertFalse(mFinished); 171 | two.delete(); 172 | } 173 | }; 174 | 175 | synchronized(this) { 176 | thread.start(); 177 | mHostFileSender.flush(); 178 | mFinished = true; 179 | } 180 | 181 | thread.join(); 182 | } 183 | 184 | @Test 185 | public void testDiscardMode() throws Throwable { 186 | Bundle args = new Bundle(); 187 | args.putString("HostFileSender_supported", "true"); 188 | mHostFileSender = new HostFileSender(mInstrumentation, args); 189 | 190 | File file = newFile("foo"); 191 | assertTrue(file.exists()); 192 | mHostFileSender.send(file); 193 | assertTrue(file.exists()); 194 | } 195 | 196 | @Test 197 | public void testExplicitDiscard() throws Throwable { 198 | Bundle args = new Bundle(); 199 | args.putString("HostFileSender_supported", "true"); 200 | args.putString("discard_screenshot_files", "true"); 201 | mHostFileSender = new HostFileSender(mInstrumentation, args); 202 | 203 | File file = newFile("foo"); 204 | assertTrue(file.exists()); 205 | mHostFileSender.send(file); 206 | assertFalse(file.exists()); 207 | } 208 | 209 | @Test 210 | public void testNoArgsDontDiscard() throws Throwable { 211 | Bundle args = new Bundle(); 212 | mHostFileSender = new HostFileSender(mInstrumentation, args); 213 | 214 | File file = newFile("foo"); 215 | assertTrue(file.exists()); 216 | mHostFileSender.send(file); 217 | assertTrue(file.exists()); 218 | } 219 | 220 | @Test 221 | public void testFixesExternalDirectory() throws Throwable { 222 | Bundle args = new Bundle(); 223 | String externalDirectory = System.getenv("EXTERNAL_STORAGE"); 224 | assertThat(externalDirectory, 225 | is(not(isEmptyOrNullString()))); 226 | 227 | ScreenshotDirectories sd = new ScreenshotDirectories(InstrumentationRegistry.getTargetContext()); 228 | File file = sd.get("default"); 229 | mHostFileSender.send(file); 230 | 231 | assertThat(mStatus.get(0).getString("HostFileSender_filename"), 232 | startsWith(externalDirectory)); 233 | 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /core/src/androidTest/java/com/facebook/testing/screenshot/internal/RecordBuilderImplTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.facebook.testing.screenshot.internal; 11 | 12 | import android.test.AndroidTestCase; 13 | import android.test.MoreAsserts; 14 | 15 | import static org.mockito.Mockito.*; 16 | 17 | /** 18 | * Tests {@link RecordBuilderImpl} 19 | */ 20 | public class RecordBuilderImplTest extends AndroidTestCase { 21 | private ScreenshotImpl mScreenshotImpl; 22 | 23 | @Override 24 | public void setUp() throws Exception { 25 | super.setUp(); 26 | mScreenshotImpl = mock(ScreenshotImpl.class); 27 | } 28 | 29 | public void testIncompleteTiles() throws Throwable { 30 | RecordBuilderImpl recordBuilder = new RecordBuilderImpl(mScreenshotImpl) 31 | .setTiling(new Tiling(3, 4)); 32 | 33 | try { 34 | recordBuilder.record(); 35 | fail("expected exception"); 36 | } catch (IllegalStateException e) { 37 | MoreAsserts.assertMatchesRegex(".*tiles.*", e.getMessage()); 38 | } 39 | } 40 | 41 | public void testCompleteTiles() throws Throwable { 42 | RecordBuilderImpl recordBuilder = new RecordBuilderImpl(mScreenshotImpl) 43 | .setTiling(new Tiling(3, 4)); 44 | 45 | for (int i = 0; i < 3; i++) { 46 | for (int j = 0; j < 4; j++) { 47 | recordBuilder.getTiling().setAt(i, j, "foobar"); 48 | } 49 | } 50 | 51 | recordBuilder.record(); 52 | } 53 | 54 | public void testWithErrorStillDoesntFail() throws Throwable { 55 | RecordBuilderImpl recordBuilder = new RecordBuilderImpl(mScreenshotImpl); 56 | 57 | recordBuilder.setError("foo"); 58 | recordBuilder.record(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /core/src/androidTest/java/com/facebook/testing/screenshot/internal/ScreenshotDirectoriesTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.facebook.testing.screenshot.internal; 11 | 12 | import java.io.File; 13 | 14 | import android.content.Context; 15 | import org.junit.Test; 16 | import org.junit.After; 17 | import android.support.test.InstrumentationRegistry; 18 | 19 | import static org.junit.Assert.*; 20 | 21 | public class ScreenshotDirectoriesTest { 22 | File mDir; 23 | 24 | @After 25 | public void teardown() throws Exception { 26 | if (mDir != null) { 27 | mDir.delete(); 28 | } 29 | } 30 | 31 | @Test 32 | public void testUsesSdcard() { 33 | Context context = InstrumentationRegistry.getTargetContext(); 34 | ScreenshotDirectories dirs = new ScreenshotDirectories(context); 35 | 36 | mDir = dirs.get("foobar"); 37 | assertTrue(mDir.exists()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /core/src/androidTest/java/com/facebook/testing/screenshot/internal/TestNameDetectorForJUnit4Test.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.facebook.testing.screenshot.internal; 11 | 12 | import android.support.test.runner.AndroidJUnit4; 13 | 14 | import org.junit.Test; 15 | import org.junit.runner.RunWith; 16 | import static org.junit.Assert.*; 17 | 18 | /** 19 | * Tests {@link TestNameDetector} (for JUnit4 style tests) 20 | */ 21 | public class TestNameDetectorForJUnit4Test { 22 | @Test 23 | public void testTestNameIsDetectedOnNonUiThread() throws Throwable { 24 | assertEquals("testTestNameIsDetectedOnNonUiThread", TestNameDetector.getTestName()); 25 | assertEquals( 26 | "com.facebook.testing.screenshot.internal.TestNameDetectorForJUnit4Test", 27 | TestNameDetector.getTestClass()); 28 | } 29 | 30 | @Test 31 | public void testDelegated() throws Throwable { 32 | delegate(true); 33 | delegatePrivate(); 34 | } 35 | 36 | public void delegate(boolean foobar) { 37 | assertEquals("testDelegated", TestNameDetector.getTestName()); 38 | } 39 | 40 | private void delegatePrivate() { 41 | assertEquals("testDelegated", TestNameDetector.getTestName()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /core/src/androidTest/java/com/facebook/testing/screenshot/internal/TestNameDetectorTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.facebook.testing.screenshot.internal; 11 | 12 | import android.test.InstrumentationTestCase; 13 | import android.test.UiThreadTest; 14 | 15 | /** 16 | * Tests {@link TestNameDetector} 17 | */ 18 | public class TestNameDetectorTest extends InstrumentationTestCase { 19 | @UiThreadTest 20 | public void testTestNameIsDetected() throws Throwable { 21 | assertEquals("testTestNameIsDetected", TestNameDetector.getTestName()); 22 | assertEquals( 23 | "com.facebook.testing.screenshot.internal.TestNameDetectorTest", 24 | TestNameDetector.getTestClass()); 25 | } 26 | 27 | public void testTestNameIsDetectedOnNonUiThread() throws Throwable { 28 | assertEquals("testTestNameIsDetectedOnNonUiThread", TestNameDetector.getTestName()); 29 | assertEquals( 30 | "com.facebook.testing.screenshot.internal.TestNameDetectorTest", 31 | TestNameDetector.getTestClass()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /core/src/androidTest/java/com/facebook/testing/screenshot/plugin/TextViewDumperTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.facebook.testing.screenshot.plugin; 11 | 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | 15 | import android.support.test.InstrumentationRegistry; 16 | import android.widget.TextView; 17 | 18 | import org.junit.Before; 19 | import org.junit.Test; 20 | import static org.junit.Assert.*; 21 | 22 | /** 23 | * Dumps useful details from a TextView 24 | */ 25 | public class TextViewDumperTest { 26 | TextViewDumper mTextViewDumper; 27 | Map mOutput = new HashMap(); 28 | TextView tv; 29 | 30 | @Before 31 | public void before() throws Throwable { 32 | mTextViewDumper = new TextViewDumper(); 33 | tv = new TextView(InstrumentationRegistry.getTargetContext()); 34 | } 35 | 36 | @Test 37 | public void testDumpsCorrectly() throws Throwable { 38 | tv.setText("foobar"); 39 | 40 | mTextViewDumper.dump(tv, mOutput); 41 | assertEquals("foobar", mOutput.get("text")); 42 | } 43 | 44 | @Test 45 | public void testNullDoesntKillUs() throws Throwable { 46 | tv.setText(null); 47 | mTextViewDumper.dump(tv, mOutput); 48 | assertEquals("", mOutput.get("text")); 49 | } 50 | 51 | @Test 52 | public void testABadTextViewDoesntKillUs() throws Throwable { 53 | // Android engineers like to break the world 54 | tv = new TextView(InstrumentationRegistry.getTargetContext()) { 55 | @Override 56 | public CharSequence getText() { 57 | throw new RuntimeException("Foobar"); 58 | } 59 | }; 60 | 61 | tv.setText("bleh"); 62 | mTextViewDumper.dump(tv, mOutput); 63 | 64 | assertEquals("unsupported", mOutput.get("text")); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/RecordBuilder.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.facebook.testing.screenshot; 11 | 12 | /** 13 | * Builds all the information related to a screenshot. 14 | */ 15 | public interface RecordBuilder { 16 | /** 17 | * Set a name (identifier) for the screenshot. If you skip the name 18 | * a name will be generated based on the Test class and Test method 19 | * name this is being run from. That means if you have multiple 20 | * screenshots in the same test, then you have to explicitly specify 21 | * names to disambiguate. 22 | */ 23 | public RecordBuilder setName(String name); 24 | 25 | /** 26 | * Set a long description of the what the screenshot is about. 27 | */ 28 | public RecordBuilder setDescription(String description); 29 | 30 | /** 31 | * Add extra metadata about this screenshots. 32 | * 33 | * There will be no semantic information associated with this 34 | * metadata, but we'll try to provide this as debugging information 35 | * whenever you're viewing screenshots. 36 | */ 37 | public RecordBuilder addExtra(String key, String value); 38 | 39 | /** 40 | * Groups similar or identical screenshots which makes it easier to 41 | * compare. 42 | */ 43 | public RecordBuilder setGroup(String groupName); 44 | 45 | /** 46 | * Finish the recording. 47 | */ 48 | public void record(); 49 | } 50 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/Screenshot.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.facebook.testing.screenshot; 11 | 12 | import android.app.Activity; 13 | import android.view.View; 14 | 15 | import com.facebook.testing.screenshot.internal.ScreenshotImpl; 16 | 17 | /** 18 | * A testing tool for taking a screenshot during an Activity 19 | * instrumentation test. This is really useful while manually 20 | * investigating how the rendering looks like after setting up some 21 | * complex set of conditions in the test. (Which might be hard to 22 | * manually recreate) 23 | * 24 | * Eventually we can use this to catch rendering changes, with very 25 | * little work added to the instrumentation test. 26 | */ 27 | public class Screenshot { 28 | /** 29 | * Take a snapshot of an already measured and layout-ed view. See 30 | * adb-logcat for how to pull the screenshot. 31 | * 32 | * This method is thread safe. 33 | */ 34 | public static RecordBuilder snap(View measuredView) { 35 | return ScreenshotImpl.getInstance().snap(measuredView); 36 | } 37 | 38 | /** 39 | * Take a snapshot of the activity and store it with the the 40 | * testName. See the adb-logcat for how to pull the screenshot. 41 | * 42 | * This method is thread safe. 43 | */ 44 | public static RecordBuilder snapActivity(Activity activity) { 45 | return ScreenshotImpl.getInstance().snapActivity(activity); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/ScreenshotRunner.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.facebook.testing.screenshot; 11 | 12 | import android.app.Instrumentation; 13 | import android.os.Bundle; 14 | 15 | import com.facebook.testing.screenshot.internal.Registry; 16 | import com.facebook.testing.screenshot.internal.ScreenshotImpl; 17 | 18 | /** 19 | * The ScreenshotRunner needs to be called from the top level 20 | * Instrumentation runner before and after all the tests run. 21 | * 22 | * You don't need to call this directly if you're using {@code 23 | * ScreenshotTestRunner} as your instrumentation. 24 | */ 25 | public abstract class ScreenshotRunner { 26 | /** 27 | * Call this exactly once in your process before any screenshots are 28 | * generated. 29 | * 30 | * Typically this will be in {@code InstrumentationTestRunner#onCreate()} 31 | */ 32 | public static void onCreate(Instrumentation instrumentation, Bundle arguments) { 33 | Registry registry = Registry.getRegistry(); 34 | registry.instrumentation = instrumentation; 35 | registry.arguments = arguments; 36 | } 37 | 38 | /** 39 | * Call this exactly once after all your tests have run. 40 | * 41 | * Typically this can be in {@code InstrumentationTestRunner#finish()} 42 | */ 43 | public static void onDestroy() { 44 | if (ScreenshotImpl.hasBeenCreated()) { 45 | ScreenshotImpl.getInstance().flush(); 46 | } 47 | 48 | Registry.clear(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/ScreenshotTestRunner.java: -------------------------------------------------------------------------------- 1 | // Copyright 2004-present Facebook. All Rights Reserved. 2 | 3 | package com.facebook.testing.screenshot; 4 | 5 | import android.os.Bundle; 6 | import android.test.InstrumentationTestRunner; 7 | 8 | public class ScreenshotTestRunner extends InstrumentationTestRunner { 9 | @Override 10 | public void onCreate(Bundle args) { 11 | ScreenshotRunner.onCreate(this, args); 12 | super.onCreate(args); 13 | } 14 | 15 | @Override 16 | public void finish(int resultCode, Bundle results) { 17 | ScreenshotRunner.onDestroy(); 18 | super.finish(resultCode, results); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/ViewHelpers.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.facebook.testing.screenshot; 11 | 12 | import android.content.res.Resources; 13 | import android.graphics.Bitmap; 14 | import android.graphics.Canvas; 15 | import android.util.TypedValue; 16 | import android.view.View; 17 | import android.view.View.MeasureSpec; 18 | import android.view.ViewGroup; 19 | import android.widget.ListView; 20 | 21 | import static android.view.View.MeasureSpec.makeMeasureSpec; 22 | 23 | /** 24 | * A collection of static utilities for measuring and pre-drawing a 25 | * view, usually a pre-requirement for taking a Screenshot. 26 | * 27 | * This will mostly be used something like this: 28 | * 29 | * 30 | * ViewHelpers.setupView(view) 31 | * .setExactHeightPx(1000) 32 | * .setExactWidthPx(100) 33 | * .layout(); 34 | * 35 | */ 36 | public class ViewHelpers { 37 | private static final int HEIGHT_LIMIT = 100000; 38 | 39 | private View mView; 40 | private int mWidthMeasureSpec; 41 | private int mHeightMeasureSpec; 42 | private boolean mGuessListViewHeight; 43 | 44 | private ViewHelpers(View view) { 45 | mView = view; 46 | 47 | mWidthMeasureSpec = makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 48 | mHeightMeasureSpec = makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 49 | } 50 | 51 | /** 52 | * Start setup for a view, see class documentation for details. 53 | */ 54 | public static ViewHelpers setupView(View view) { 55 | return new ViewHelpers(view); 56 | } 57 | 58 | /** 59 | * Measure and layout the view after all the configuration is done. 60 | * 61 | * @returns an AfterLayout object that can be used to perform common 62 | * operations after the layout is done such as dispatchPreDraw()l. 63 | */ 64 | public AfterLayout layout() { 65 | if (!mGuessListViewHeight) { 66 | layoutInternal(); 67 | } else { 68 | layoutWithHeightDetection(); 69 | } 70 | dispatchPreDraw(mView); 71 | return new AfterLayout(); 72 | } 73 | 74 | private void layoutInternal() { 75 | do { 76 | mView.measure( 77 | mWidthMeasureSpec, 78 | mHeightMeasureSpec); 79 | layoutView(); 80 | } while (mView.isLayoutRequested()); 81 | } 82 | 83 | private void layoutWithHeightDetection() { 84 | ListView view = (ListView) mView; 85 | mHeightMeasureSpec = makeMeasureSpec(HEIGHT_LIMIT, MeasureSpec.EXACTLY); 86 | layoutInternal(); 87 | 88 | if (view.getCount() != view.getChildCount()) { 89 | throw new IllegalStateException("the ListView is too big to be auto measured"); 90 | } 91 | 92 | int bottom = 0; 93 | 94 | if (view.getCount() > 0) { 95 | bottom = view.getChildAt(view.getCount() - 1).getBottom(); 96 | } 97 | 98 | if (bottom == 0) { 99 | bottom = 1; 100 | } 101 | 102 | mHeightMeasureSpec = makeMeasureSpec(bottom, MeasureSpec.EXACTLY); 103 | layoutInternal(); 104 | } 105 | 106 | /** 107 | * Configure the height in pixel 108 | */ 109 | public ViewHelpers setExactHeightPx(int px) { 110 | mHeightMeasureSpec = makeMeasureSpec(px, MeasureSpec.EXACTLY); 111 | validateHeight(); 112 | return this; 113 | } 114 | 115 | public ViewHelpers guessListViewHeight() { 116 | if (!(mView instanceof ListView)) { 117 | throw new IllegalArgumentException("guessListViewHeight needs to be used with a ListView"); 118 | } 119 | mGuessListViewHeight = true; 120 | validateHeight(); 121 | return this; 122 | } 123 | 124 | private void validateHeight() { 125 | if (mGuessListViewHeight && mHeightMeasureSpec != 0) { 126 | throw new IllegalStateException("Can't call both setExactHeight && guessListViewHeight"); 127 | } 128 | } 129 | 130 | /** 131 | * Configure the width in pixels 132 | */ 133 | public ViewHelpers setExactWidthPx(int px) { 134 | mWidthMeasureSpec = makeMeasureSpec(px, MeasureSpec.EXACTLY); 135 | return this; 136 | } 137 | 138 | /** 139 | * Configure the height in dip 140 | */ 141 | public ViewHelpers setExactWidthDp(int dp) { 142 | setExactWidthPx(dpToPx(dp)); 143 | return this; 144 | } 145 | 146 | /** 147 | * Configure the width in dip 148 | */ 149 | public ViewHelpers setExactHeightDp(int dp) { 150 | setExactHeightPx(dpToPx(dp)); 151 | return this; 152 | } 153 | 154 | /** 155 | * Some views (e.g. SimpleVariableTextLayoutView) in FB4A rely on 156 | * the predraw. Actually I don't know why, ideally it shouldn't. 157 | * 158 | * However if you find that text is not showing in your layout, try 159 | * dispatching the pre draw using this method. Note this method is 160 | * only supported for views that are not attached to a Window, and 161 | * the behavior is slightly different than views attached to a 162 | * window. (Views attached to a window have a single 163 | * ViewTreeObserver for all child views, whereas for unattached 164 | * views, each child has its own ViewTreeObserver.) 165 | */ 166 | private void dispatchPreDraw(View view) { 167 | while (view.getViewTreeObserver().dispatchOnPreDraw()) {} 168 | 169 | if (view instanceof ViewGroup) { 170 | ViewGroup vg = (ViewGroup) view; 171 | for (int i = 0 ; i < vg.getChildCount(); i++) { 172 | dispatchPreDraw(vg.getChildAt(i)); 173 | } 174 | } 175 | } 176 | 177 | public class AfterLayout { 178 | public Bitmap draw() { 179 | WindowAttachment.Detacher detacher = WindowAttachment.dispatchAttach(mView); 180 | try { 181 | Bitmap bmp = Bitmap.createBitmap( 182 | mView.getWidth(), mView.getHeight(), Bitmap.Config.ARGB_8888); 183 | Canvas canvas = new Canvas(bmp); 184 | mView.draw(canvas); 185 | return bmp; 186 | } finally { 187 | detacher.detach(); 188 | } 189 | } 190 | } 191 | 192 | private void layoutView() { 193 | mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight()); 194 | } 195 | 196 | private int dpToPx(int dp) { 197 | Resources resources = mView.getContext().getResources(); 198 | return (int) TypedValue.applyDimension( 199 | TypedValue.COMPLEX_UNIT_DIP, dp, resources.getDisplayMetrics()); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/WindowAttachment.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.facebook.testing.screenshot; 11 | 12 | import java.io.IOException; 13 | import java.lang.reflect.Constructor; 14 | import java.lang.reflect.Field; 15 | import java.lang.reflect.InvocationHandler; 16 | import java.lang.reflect.Method; 17 | import java.lang.reflect.Proxy; 18 | import java.util.WeakHashMap; 19 | 20 | import android.content.Context; 21 | import android.os.Binder; 22 | import android.os.Build; 23 | import android.os.Handler; 24 | import android.util.Log; 25 | import android.view.Display; 26 | import android.view.View; 27 | import android.view.ViewGroup; 28 | import android.view.WindowManager; 29 | 30 | import com.android.dx.stock.ProxyBuilder; 31 | 32 | public abstract class WindowAttachment { 33 | 34 | /** 35 | * Keep track of all the attached windows here so that we don't 36 | * double attach them. 37 | */ 38 | private static final WeakHashMap sAttachments = new WeakHashMap<>(); 39 | 40 | /** 41 | * Dispatch onAttachedToWindow to all the views in the view 42 | * hierarchy. 43 | * 44 | * Detach the view by calling {@code detach()} on the returned {@code Detacher}. 45 | * 46 | * Note that if the view is already attached (either via 47 | * WindowAttachment or to a real window), then both the attach and 48 | * the corresponding detach will be no-ops. 49 | * 50 | * Note that this is hacky, after these calls the views will still 51 | * say that isAttachedToWindow() is false and getWindowToken() == 52 | * null. 53 | */ 54 | public static Detacher dispatchAttach(View view) { 55 | if (view.getWindowToken() != null || sAttachments.containsKey(view)) { 56 | // Screnshot tests can often be run against a View that's 57 | // attached to a real activity, in which case we have nothing to 58 | // do 59 | Log.i("WindowAttachment", "Skipping window attach hack since it's really attached"); 60 | return new NoopDetacher(); 61 | } 62 | 63 | sAttachments.put(view, true); 64 | invoke(view, "onAttachedToWindow"); 65 | 66 | return new RealDetacher(view); 67 | } 68 | 69 | public interface Detacher { 70 | public void detach(); 71 | } 72 | 73 | private static class NoopDetacher implements Detacher { 74 | @Override 75 | public void detach() {} 76 | } 77 | 78 | private static class RealDetacher implements Detacher { 79 | private View mView; 80 | 81 | public RealDetacher(View view) { 82 | mView = view; 83 | } 84 | @Override 85 | public void detach() { 86 | dispatchDetach(mView); 87 | sAttachments.remove(mView); 88 | } 89 | } 90 | 91 | /** 92 | * Similar to dispatchAttach, except dispatchest the corresponding 93 | * detach. 94 | */ 95 | private static void dispatchDetach(View view) { 96 | invoke(view, "onDetachedFromWindow"); 97 | } 98 | 99 | private static void invoke(View view, String methodName) { 100 | invokeUnchecked(view, methodName); 101 | } 102 | 103 | private static void invokeUnchecked(View view, String methodName) { 104 | try { 105 | Method method = View.class.getDeclaredMethod(methodName); 106 | method.setAccessible(true); 107 | method.invoke(view); 108 | } catch (Exception e) { 109 | throw new RuntimeException(e); 110 | } 111 | 112 | if (view instanceof ViewGroup) { 113 | ViewGroup vg = (ViewGroup) view; 114 | for (int i = 0 ; i < vg.getChildCount(); i++) { 115 | invokeUnchecked(vg.getChildAt(i), methodName); 116 | } 117 | } 118 | } 119 | 120 | /** 121 | * Simulates the view as being attached. 122 | */ 123 | public static void setAttachInfo(View view) { 124 | try { 125 | Class cAttachInfo = Class.forName("android.view.View$AttachInfo"); 126 | Class cViewRootImpl = null; 127 | 128 | if (Build.VERSION.SDK_INT >= 11) { 129 | cViewRootImpl = Class.forName("android.view.ViewRootImpl"); 130 | } 131 | 132 | Class cIWindowSession = Class.forName("android.view.IWindowSession"); 133 | Class cIWindow = Class.forName("android.view.IWindow"); 134 | Class cCallbacks = Class.forName("android.view.View$AttachInfo$Callbacks"); 135 | 136 | Context context = view.getContext(); 137 | WindowManager wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); 138 | Display display = wm.getDefaultDisplay(); 139 | 140 | Object viewRootImpl = null; 141 | 142 | Object window = createIWindow(); 143 | 144 | Class[] params = null; 145 | Object[] values = null; 146 | 147 | if (Build.VERSION.SDK_INT >= 17) { 148 | viewRootImpl = cViewRootImpl.getConstructor(Context.class, Display.class) 149 | .newInstance(context, display); 150 | params = new Class[] { 151 | cIWindowSession, 152 | cIWindow, 153 | Display.class, 154 | cViewRootImpl, 155 | Handler.class, 156 | cCallbacks 157 | }; 158 | 159 | values = new Object[] { 160 | stub(cIWindowSession), 161 | window, 162 | display, 163 | viewRootImpl, 164 | new Handler(), 165 | stub(cCallbacks) 166 | }; 167 | } 168 | else if (Build.VERSION.SDK_INT >= 16) { 169 | viewRootImpl = cViewRootImpl.getConstructor(Context.class) 170 | .newInstance(context); 171 | params = new Class[] { 172 | cIWindowSession, 173 | cIWindow, 174 | cViewRootImpl, 175 | Handler.class, 176 | cCallbacks 177 | }; 178 | 179 | values = new Object[] { 180 | stub(cIWindowSession), 181 | window, 182 | viewRootImpl, 183 | new Handler(), 184 | stub(cCallbacks) 185 | }; 186 | } 187 | else if (Build.VERSION.SDK_INT <= 15) { 188 | params = new Class[] { 189 | cIWindowSession, 190 | cIWindow, 191 | Handler.class, 192 | cCallbacks 193 | }; 194 | 195 | values = new Object[] { 196 | stub(cIWindowSession), 197 | window, 198 | new Handler(), 199 | stub(cCallbacks) 200 | }; 201 | } 202 | 203 | Object attachInfo = invokeConstructor(cAttachInfo, params, values); 204 | 205 | setField(attachInfo, "mHasWindowFocus", true); 206 | setField(attachInfo, "mWindowVisibility", View.VISIBLE); 207 | setField(attachInfo, "mInTouchMode", false); 208 | if (Build.VERSION.SDK_INT >= 11) { 209 | setField(attachInfo, "mHardwareAccelerated", false); 210 | } 211 | 212 | Method dispatch = View.class 213 | .getDeclaredMethod("dispatchAttachedToWindow", cAttachInfo, int.class); 214 | dispatch.setAccessible(true); 215 | dispatch.invoke(view, attachInfo, 0); 216 | } catch (Exception e) { 217 | throw new RuntimeException(e); 218 | } 219 | } 220 | 221 | private static Object invokeConstructor( 222 | Class clazz, 223 | Class[] params, 224 | Object[] values) throws Exception { 225 | Constructor cons = clazz.getDeclaredConstructor(params); 226 | cons.setAccessible(true); 227 | return cons.newInstance(values); 228 | } 229 | 230 | private static Object createIWindow() throws Exception { 231 | Class cIWindow = Class.forName("android.view.IWindow"); 232 | 233 | // Since IWindow is an interface, I don't need dexmaker for this 234 | InvocationHandler handler = new InvocationHandler() { 235 | @Override 236 | public Object invoke(Object proxy, Method method, Object[] args) { 237 | if (method.getName().equals("asBinder")) { 238 | return new Binder(); 239 | } 240 | return null; 241 | } 242 | }; 243 | 244 | Object ret = Proxy.newProxyInstance( 245 | cIWindow.getClassLoader(), 246 | new Class[] { cIWindow }, 247 | handler); 248 | 249 | return ret; 250 | } 251 | 252 | private static Object stub(Class klass) { 253 | try { 254 | InvocationHandler handler = new InvocationHandler() { 255 | @Override 256 | public Object invoke(Object project, Method method, Object[] args) { 257 | return null; 258 | } 259 | }; 260 | 261 | if (klass.isInterface()) { 262 | return Proxy.newProxyInstance( 263 | klass.getClassLoader(), 264 | new Class[] { klass }, 265 | handler); 266 | } else { 267 | return ProxyBuilder.forClass(klass) 268 | .handler(handler) 269 | .build(); 270 | } 271 | } catch (IOException e) { 272 | throw new RuntimeException(e); 273 | } 274 | } 275 | 276 | private static void setField(Object o, String fieldName, Object value) throws Exception { 277 | Class clazz = o.getClass(); 278 | Field field = clazz.getDeclaredField(fieldName); 279 | field.setAccessible(true); 280 | field.set(o, value); 281 | } 282 | 283 | private WindowAttachment() { 284 | } 285 | 286 | } 287 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/internal/Album.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.facebook.testing.screenshot.internal; 11 | 12 | import java.io.File; 13 | import java.io.IOException; 14 | import java.io.OutputStream; 15 | 16 | import android.graphics.Bitmap; 17 | 18 | /** 19 | * Stores metadata about an album of screenshots during an 20 | * instrumentation test run. 21 | */ 22 | public interface Album { 23 | 24 | /** 25 | * Writes the bitmap corresponding to the screenshot with the name 26 | * {@code name} in the {@code (tilei, tilej)} position. 27 | */ 28 | public String writeBitmap(String name, int tilei, int tilej, Bitmap bitmap) throws IOException; 29 | 30 | /** 31 | * Call after all the screenshots are done. 32 | */ 33 | public void flush(); 34 | 35 | /** 36 | * Cleanup any disk state associated with this album. 37 | */ 38 | public void cleanup(); 39 | 40 | /** 41 | * Opens a stream to dump the view hierarchy into. This should be 42 | * called before addRecord() is called for the given name. 43 | * 44 | * It is the callers responsibility to call {@code close()} on the 45 | * returned stream. 46 | */ 47 | public OutputStream openViewHierarchyFile(String name) throws IOException; 48 | 49 | /** 50 | * This is called after every record is finally set up. 51 | */ 52 | public void addRecord(RecordBuilderImpl recordBuilder) throws IOException; 53 | } 54 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/internal/HostFileSender.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.facebook.testing.screenshot.internal; 11 | 12 | import java.io.File; 13 | import java.io.IOException; 14 | import java.util.ArrayList; 15 | import java.util.Iterator; 16 | import java.util.List; 17 | 18 | import android.app.Activity; 19 | import android.app.Instrumentation; 20 | import android.os.Bundle; 21 | 22 | /** 23 | * Abstraction for sending a file to the host system while the test is 24 | * running. 25 | * 26 | * When running screenshot tests, the space on the emulator disk can 27 | * fill up quickly, therefore this tool starts streaming the 28 | * screenshots while the test is running. However it is the 29 | * responsibility of the host script to parse the bundles sent over by 30 | * as instrumentation statuses, and retrieve the file specified in 31 | * "HostFileSender_filename", and then deleting the said file. If the 32 | * host script does not support this and does not pass 33 | * "HostFileSender_supported" argument, then the HostFileSender will 34 | * discard all files sent to it immediately. 35 | */ 36 | public class HostFileSender { 37 | final int QUEUE_SIZE = 5; 38 | private final List mQueue = new ArrayList<>(); 39 | 40 | private Instrumentation mInstrumentation; 41 | private Bundle mArguments; 42 | 43 | public HostFileSender(Instrumentation instrumentation, Bundle arguments) { 44 | mInstrumentation = instrumentation; 45 | mArguments = arguments; 46 | } 47 | 48 | /** 49 | * Sends the given file to the host system. 50 | * 51 | * Once passed in the file is "owned" by HostFileSender and should 52 | * not be modified beyond this point. 53 | */ 54 | public synchronized void send(File file) { 55 | if (!isHostFileSenderSupported()) { 56 | return; 57 | } 58 | 59 | if (isDiscardMode()) { 60 | file.delete(); 61 | return; 62 | } 63 | 64 | waitForQueue(); 65 | 66 | Bundle bundle = new Bundle(); 67 | bundle.putString("HostFileSender_filename", file.getAbsolutePath()); 68 | mInstrumentation.sendStatus(Activity.RESULT_OK, bundle); 69 | mQueue.add(file); 70 | } 71 | 72 | /** 73 | * Wait for all the files to be sent to host system. 74 | */ 75 | public void flush() { 76 | updateQueue(); 77 | while (getQueueSize() > 0) { 78 | updateQueue(); 79 | try { 80 | Thread.sleep(10); 81 | } catch (InterruptedException e) { 82 | throw new RuntimeException(e); 83 | } 84 | } 85 | } 86 | 87 | /* VisibleForTesting */ 88 | synchronized void updateQueue() { 89 | Iterator iterator = mQueue.iterator(); 90 | while (iterator.hasNext()) { 91 | File next = iterator.next(); 92 | if (!next.exists()) { 93 | iterator.remove(); 94 | } 95 | } 96 | } 97 | 98 | /* VisibleForTesting */ 99 | synchronized int getQueueSize() { 100 | return mQueue.size(); 101 | } 102 | 103 | synchronized private void waitForQueue() { 104 | updateQueue(); 105 | while (getQueueSize() >= QUEUE_SIZE) { 106 | try { 107 | Thread.sleep(20); 108 | } catch (InterruptedException e) { 109 | throw new RuntimeException(e); 110 | } 111 | 112 | updateQueue(); 113 | } 114 | } 115 | 116 | /** 117 | * Returns true if we should discard files immediately instead of 118 | * waiting for the host system to pull them. 119 | */ 120 | private boolean isDiscardMode() { 121 | return "true".equals(mArguments.getString("discard_screenshot_files")); 122 | } 123 | 124 | private boolean isHostFileSenderSupported() { 125 | return "true".equals(mArguments.getString("HostFileSender_supported")); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/internal/RecordBuilderImpl.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.facebook.testing.screenshot.internal; 11 | 12 | import java.io.File; 13 | import java.nio.charset.Charset; 14 | import java.nio.charset.CharsetEncoder; 15 | import java.util.HashMap; 16 | import java.util.Map; 17 | 18 | import android.graphics.Bitmap; 19 | import android.view.View; 20 | 21 | import com.facebook.testing.screenshot.RecordBuilder; 22 | 23 | /** 24 | * A builder for all the metadata associated with a screenshot. 25 | * 26 | * Use Screenshot#snap() or Screenshot#snapActivity() to get an 27 | * instance of this, and commit the record with #record(). 28 | */ 29 | public class RecordBuilderImpl implements RecordBuilder { 30 | private final ScreenshotImpl mScreenshotImpl; 31 | 32 | private String mDescription; 33 | private String mName; 34 | private String mTestClass; 35 | private String mTestName; 36 | private String mError; 37 | private String mGroup; 38 | 39 | private Tiling mTiling = new Tiling(1, 1); 40 | private View mView; 41 | private final Map mExtras = new HashMap(); 42 | 43 | /* package */ RecordBuilderImpl(ScreenshotImpl screenshotImpl) { 44 | mScreenshotImpl = screenshotImpl; 45 | } 46 | 47 | /** 48 | * @inherit 49 | */ 50 | @Override 51 | public RecordBuilderImpl setDescription(String description) { 52 | mDescription = description; 53 | return this; 54 | } 55 | 56 | public String getDescription() { 57 | return mDescription; 58 | } 59 | 60 | /** 61 | * @inherit 62 | */ 63 | @Override 64 | public RecordBuilderImpl setName(String name) { 65 | CharsetEncoder charsetEncoder = Charset.forName("latin-1").newEncoder(); 66 | 67 | if (!charsetEncoder.canEncode(name)) { 68 | throw new IllegalArgumentException( 69 | "Screenshot names must have only latin characters: " + name); 70 | } 71 | if (name.contains(File.separator)) { 72 | throw new IllegalArgumentException( 73 | "Screenshot names cannot contain '" + File.separator + "': " + name); 74 | } 75 | 76 | mName = name; 77 | return this; 78 | } 79 | 80 | public String getName() { 81 | if (mName == null) { 82 | return getTestClass() + "_" + getTestName(); 83 | } 84 | return mName; 85 | } 86 | 87 | /** 88 | * Set the name of the test from which this screenshot is 89 | * generated. This should be detected by default most of the time. 90 | */ 91 | public RecordBuilderImpl setTestName(String testName) { 92 | mTestName = testName; 93 | return this; 94 | } 95 | 96 | public String getTestName() { 97 | return mTestName; 98 | } 99 | 100 | /** 101 | * Set the class name of the TestCase from which this screenshot is 102 | * generated. This should be detected by default most of the time. 103 | */ 104 | public RecordBuilderImpl setTestClass(String testClass) { 105 | mTestClass = testClass; 106 | return this; 107 | } 108 | 109 | public String getTestClass() { 110 | return mTestClass; 111 | } 112 | 113 | /** 114 | * Stops the recording and returns the generated bitmap, possibly 115 | * compressed. 116 | * 117 | * You cannot call this after record(), nor can you call record() 118 | * after this call. 119 | */ 120 | public Bitmap getBitmap() { 121 | return mScreenshotImpl.getBitmap(this); 122 | } 123 | 124 | /** 125 | * Returns true if this record has been given an explicit name using 126 | * setName(). If false, getName() will still generate a name. 127 | */ 128 | public boolean hasExplicitName() { 129 | return mName != null; 130 | } 131 | 132 | /* package */ RecordBuilderImpl setError(String error) { 133 | mError = error; 134 | return this; 135 | } 136 | 137 | /** 138 | * Get's any error that was encountered while creating the 139 | * screenshot. 140 | */ 141 | public String getError() { 142 | return mError; 143 | } 144 | 145 | /** 146 | * @inherit 147 | */ 148 | @Override 149 | public void record() { 150 | mScreenshotImpl.record(this); 151 | checkState(); 152 | } 153 | 154 | @Override 155 | public RecordBuilderImpl setGroup(String groupName) { 156 | mGroup = groupName; 157 | return this; 158 | } 159 | 160 | /** 161 | * Sanity checks that the record is ready to be persisted 162 | */ 163 | /* package */ void checkState() { 164 | if (mError != null) { 165 | return; 166 | } 167 | for (int i = 0; i < mTiling.getWidth(); i++) { 168 | for (int j = 0; j < mTiling.getHeight(); j++) { 169 | if (mTiling.getAt(i, j) == null) { 170 | throw new IllegalStateException("expected all tiles to be filled"); 171 | } 172 | } 173 | } 174 | } 175 | 176 | /* package */ RecordBuilderImpl setView(View view) { 177 | mView = view; 178 | return this; 179 | } 180 | 181 | public View getView() { 182 | return mView; 183 | } 184 | 185 | /* package */ RecordBuilderImpl setTiling(Tiling tiling) { 186 | mTiling = tiling; 187 | return this; 188 | } 189 | 190 | public Tiling getTiling() { 191 | return mTiling; 192 | } 193 | 194 | @Override 195 | public RecordBuilderImpl addExtra(String key, String value) { 196 | mExtras.put(key, value); 197 | return this; 198 | } 199 | 200 | public Map getExtras() { 201 | return mExtras; 202 | } 203 | 204 | public String getGroup() { 205 | return mGroup; 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/internal/Registry.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.facebook.testing.screenshot.internal; 11 | 12 | import android.app.Instrumentation; 13 | import android.os.Bundle; 14 | 15 | /** 16 | * Stores some of the static state. We bundle this into a class for 17 | * easy cleanup. 18 | */ 19 | public class Registry { 20 | public Instrumentation instrumentation; 21 | public Bundle arguments; 22 | 23 | private static Registry sRegistry; 24 | public static Registry getRegistry() { 25 | if (sRegistry == null) { 26 | sRegistry = new Registry(); 27 | } 28 | 29 | return sRegistry; 30 | } 31 | 32 | public static void clear() { 33 | sRegistry = null; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/internal/ScreenshotDirectories.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.facebook.testing.screenshot.internal; 11 | 12 | import android.content.Context; 13 | import android.content.pm.PackageManager; 14 | import android.os.Build; 15 | import android.os.Environment; 16 | 17 | import java.io.File; 18 | 19 | /** 20 | * Provides a directory for an Album to store its screenshots in. 21 | */ 22 | class ScreenshotDirectories { 23 | private Context mContext; 24 | 25 | public ScreenshotDirectories(Context context) { 26 | mContext = context; 27 | } 28 | 29 | public File get(String type) { 30 | checkPermissions(); 31 | return getSdcardDir(type); 32 | } 33 | 34 | private void checkPermissions() { 35 | int res = mContext.checkCallingOrSelfPermission("android.permission.WRITE_EXTERNAL_STORAGE"); 36 | if (res != PackageManager.PERMISSION_GRANTED) { 37 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 38 | throw new RuntimeException("This does not currently work on API 23+, see " 39 | + "https://github.com/facebook/screenshot-tests-for-android/issues/16 for details."); 40 | } else { 41 | throw new RuntimeException("We need WRITE_EXTERNAL_STORAGE permission for screenshot tests"); 42 | } 43 | } 44 | } 45 | 46 | private File getSdcardDir(String type) { 47 | String externalStorage = System.getenv("EXTERNAL_STORAGE"); 48 | 49 | if (externalStorage == null) { 50 | throw new RuntimeException("No $EXTERNAL_STORAGE has been set on the device, please report this bug!"); 51 | } 52 | 53 | String parent = String.format( 54 | "%s/screenshots/%s/", 55 | externalStorage, 56 | mContext.getPackageName()); 57 | 58 | String child = String.format("%s/screenshots-%s", parent, type); 59 | 60 | new File(parent).mkdirs(); 61 | 62 | File dir = new File(child); 63 | dir.mkdir(); 64 | 65 | if (!dir.exists()) { 66 | throw new RuntimeException("Failed to create the directory for screenshots. Is your sdcard directory read-only?"); 67 | } 68 | 69 | setWorldWriteable(dir); 70 | return dir; 71 | } 72 | 73 | private File getDataDir(String type) { 74 | File dir = mContext.getDir("screenshots-" + type, Context.MODE_WORLD_READABLE); 75 | 76 | setWorldWriteable(dir); 77 | return dir; 78 | } 79 | 80 | private void setWorldWriteable(File dir) { 81 | // Context.MODE_WORLD_WRITEABLE has been deprecated, so let's 82 | // manually set this 83 | dir.setWritable(/* writeable = */ true, /* ownerOnly = */ false); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/internal/TestNameDetector.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.facebook.testing.screenshot.internal; 11 | 12 | import java.lang.reflect.Method; 13 | 14 | import android.util.Log; 15 | import junit.framework.TestCase; 16 | import org.junit.Test; 17 | import org.junit.runner.RunWith; 18 | 19 | /** 20 | * Detect the test name and class that is being run currently. 21 | */ 22 | public class TestNameDetector { 23 | private static final String UNKNOWN = "unknown"; 24 | 25 | /** 26 | * Get the current test class in a standard JUnit3 or JUnit4 test, 27 | * or "unknown" if we couldn't detect it. 28 | */ 29 | public static String getTestClass() { 30 | try { 31 | throw new RuntimeException(); 32 | } catch (RuntimeException e) { 33 | StackTraceElement[] stack = e.getStackTrace(); 34 | for (StackTraceElement elem : stack) { 35 | try { 36 | if (isTestElement(elem)) { 37 | return elem.getClassName(); 38 | } 39 | } catch (ClassNotFoundException c) { 40 | Log.e("ScreenshotImpl", "Class not found in stack", c); 41 | return UNKNOWN; 42 | } 43 | } 44 | } 45 | return "unknown"; 46 | } 47 | 48 | /** 49 | * Get the current test name in a standard JUnit3 or JUnit4 test, or 50 | * "unknown" if we couldn't detect it. 51 | */ 52 | public static String getTestName() { 53 | try { 54 | throw new RuntimeException(); 55 | } catch (RuntimeException e) { 56 | StackTraceElement[] stack = e.getStackTrace(); 57 | String testClass = getTestClass(); 58 | 59 | // Find the first call from this class: 60 | String finalName = UNKNOWN; 61 | for (StackTraceElement elem : stack) { 62 | if (testClass.equals(elem.getClassName())) { 63 | finalName = elem.getMethodName(); 64 | } 65 | } 66 | return finalName; 67 | } 68 | } 69 | 70 | private static boolean isTestCase(Class clazz) { 71 | if (clazz.equals(TestCase.class) || clazz.getAnnotation(RunWith.class) != null) { 72 | return true; 73 | } 74 | 75 | if (clazz.equals(Object.class)) { 76 | return false; 77 | } 78 | 79 | return isTestCase(clazz.getSuperclass()); 80 | } 81 | 82 | private static boolean isTestElement(StackTraceElement elem) throws ClassNotFoundException { 83 | try { 84 | Class clazz = Class.forName(elem.getClassName()); 85 | Method method = clazz.getMethod(elem.getMethodName()); 86 | return isTestCase(clazz) || method.getAnnotation(Test.class) != null; 87 | } catch (NoSuchMethodException e) { 88 | return false; 89 | } 90 | } 91 | 92 | private TestNameDetector() { 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/internal/Tiling.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.facebook.testing.screenshot.internal; 11 | 12 | /** 13 | * A 2D layout of image tiles. We represent images as strings which 14 | * can be looked up in an {@code AlbumImpl} 15 | */ 16 | public class Tiling { 17 | private int mWidth; 18 | private int mHeight; 19 | private String[][] mContents; 20 | 21 | public Tiling(int width, int height) { 22 | mWidth = width; 23 | mHeight = height; 24 | mContents = new String[width][height]; 25 | } 26 | 27 | public int getHeight() { 28 | return mHeight; 29 | } 30 | 31 | public int getWidth() { 32 | return mWidth; 33 | } 34 | 35 | public String getAt(int x, int y) { 36 | return mContents[x][y]; 37 | } 38 | 39 | public void setAt(int x, int y, String name) { 40 | mContents[x][y] = name; 41 | } 42 | 43 | /** 44 | * Convenience factory method for tests 45 | */ 46 | public static Tiling singleTile(String name) { 47 | Tiling ret = new Tiling(1, 1); 48 | ret.setAt(0, 0, name); 49 | return ret; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/internal/ViewHierarchy.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.facebook.testing.screenshot.internal; 11 | 12 | import javax.xml.parsers.DocumentBuilderFactory; 13 | import javax.xml.parsers.ParserConfigurationException; 14 | import javax.xml.transform.OutputKeys; 15 | import javax.xml.transform.Transformer; 16 | import javax.xml.transform.TransformerException; 17 | import javax.xml.transform.TransformerFactory; 18 | import javax.xml.transform.dom.DOMSource; 19 | import javax.xml.transform.stream.StreamResult; 20 | 21 | import java.io.IOException; 22 | import java.io.OutputStream; 23 | import java.util.HashMap; 24 | import java.util.Map; 25 | 26 | import android.graphics.Point; 27 | import android.graphics.Rect; 28 | import android.os.Build; 29 | import android.view.View; 30 | import android.view.ViewGroup; 31 | 32 | import com.facebook.testing.screenshot.plugin.PluginRegistry; 33 | import com.facebook.testing.screenshot.plugin.ViewDumpPlugin; 34 | 35 | import org.w3c.dom.Document; 36 | import org.w3c.dom.Element; 37 | 38 | /** 39 | * Dumps information about the view hierarchy. 40 | */ 41 | public class ViewHierarchy { 42 | /** 43 | * Creates an XML dump for the view into given OutputStream 44 | * 45 | * This is meant for debugging purposes only, and we don't 46 | * guarantee that it's format will remain the same. 47 | */ 48 | public void deflate(View view, OutputStream out) throws IOException { 49 | Document doc = deflateToDocument(view); 50 | try { 51 | DOMSource source = new DOMSource(doc); 52 | TransformerFactory transformerFactory = TransformerFactory.newInstance(); 53 | Transformer transformer = transformerFactory.newTransformer(); 54 | StreamResult result = new StreamResult(out); 55 | transformer.setOutputProperty(OutputKeys.INDENT, "yes"); 56 | transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); 57 | transformer.transform(source, result); 58 | } catch (TransformerException e) { 59 | throw new RuntimeException(e); 60 | } 61 | } 62 | 63 | private Document deflateToDocument(View view) { 64 | Document doc; 65 | 66 | try { 67 | doc = DocumentBuilderFactory.newInstance() 68 | .newDocumentBuilder() 69 | .newDocument(); 70 | } catch (ParserConfigurationException e) { 71 | throw new RuntimeException(e); 72 | } 73 | 74 | doc.appendChild(deflateRelative(view, new Point(-view.getLeft(), -view.getTop()), doc)); 75 | return doc; 76 | } 77 | 78 | private Element deflateRelative(View view, Point topLeft, Document doc) { 79 | Element el = doc.createElement("view"); 80 | 81 | addTextNode(el, "name", view.getClass().getName()); 82 | 83 | Rect rect = new Rect( 84 | topLeft.x + view.getLeft(), 85 | topLeft.y + view.getTop(), 86 | topLeft.x + view.getRight(), 87 | topLeft.y + view.getBottom()); 88 | 89 | addTextNode(el, "left", String.valueOf(rect.left)); 90 | addTextNode(el, "top", String.valueOf(rect.top)); 91 | addTextNode(el, "right", String.valueOf(rect.right)); 92 | addTextNode(el, "bottom", String.valueOf(rect.bottom)); 93 | if (Build.VERSION.SDK_INT >= 11) { 94 | addTextNode(el, "isLayoutRequested", String.valueOf(view.isLayoutRequested())); 95 | } 96 | addTextNode(el, "isDirty", String.valueOf(view.isDirty())); 97 | 98 | Map extraValues = new HashMap<>(); 99 | for (ViewDumpPlugin plugin : PluginRegistry.getPlugins()) { 100 | plugin.dump(view, extraValues); 101 | } 102 | 103 | for (Map.Entry extraValue : extraValues.entrySet()) { 104 | addExtraValue(el, extraValue.getKey(), extraValue.getValue()); 105 | } 106 | 107 | Element children = doc.createElement("children"); 108 | el.appendChild(children); 109 | 110 | if (view instanceof ViewGroup) { 111 | Point myTopLeft = new Point(rect.left, rect.top); 112 | ViewGroup vg = (ViewGroup) view; 113 | for (int i = 0; i < vg.getChildCount(); i++) { 114 | Element child = deflateRelative(vg.getChildAt(i), myTopLeft, doc); 115 | children.appendChild(child); 116 | } 117 | } 118 | return el; 119 | } 120 | 121 | private void addExtraValue(Element parent, String name, String value) { 122 | Element elem = parent.getOwnerDocument().createElement("extra-value"); 123 | elem.setAttribute("key", name); 124 | elem.setTextContent(value); 125 | parent.appendChild(elem); 126 | } 127 | 128 | private void addTextNode(Element parent, String name, String value) { 129 | Element elem = parent.getOwnerDocument().createElement(name); 130 | elem.setTextContent(value); 131 | parent.appendChild(elem); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/plugin/PluginRegistry.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | package com.facebook.testing.screenshot.plugin; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | /** 15 | * Maintains a global list of {@code ViewDumpPlugin}s 16 | */ 17 | public class PluginRegistry { 18 | private static ArrayList sPlugins = new ArrayList<>(); 19 | 20 | static { 21 | sPlugins.add(new TextViewDumper()); 22 | } 23 | 24 | /** 25 | * Adds a new plugin 26 | */ 27 | public static void addPlugin(ViewDumpPlugin viewDumpPlugin) { 28 | sPlugins.add(viewDumpPlugin); 29 | } 30 | 31 | /** 32 | * Removes a previously added plugin 33 | */ 34 | public static void removePlugin(ViewDumpPlugin viewDumpPlugin) { 35 | sPlugins.remove(viewDumpPlugin); 36 | } 37 | 38 | /** 39 | * Get the list of plugins, for internal use. 40 | */ 41 | public static List getPlugins() { 42 | return new ArrayList(sPlugins); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/plugin/TextViewDumper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.facebook.testing.screenshot.plugin; 11 | 12 | import java.util.Map; 13 | 14 | import android.view.View; 15 | import android.widget.TextView; 16 | 17 | /** 18 | * Dumps useful details from a TextView 19 | */ 20 | public class TextViewDumper implements ViewDumpPlugin { 21 | @Override 22 | public void dump(View view, Map output) { 23 | if (!(view instanceof TextView)) { 24 | return; 25 | } 26 | 27 | TextView tv = (TextView) view; 28 | 29 | CharSequence text; 30 | 31 | try { 32 | text = tv.getText(); 33 | } catch (RuntimeException e) { 34 | // Somebody has a custom TextView that misbehaves 35 | text = "unsupported"; 36 | } 37 | 38 | if (text == null) { 39 | text = "null"; 40 | } 41 | output.put("text", text.toString()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /core/src/main/java/com/facebook/testing/screenshot/plugin/ViewDumpPlugin.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.facebook.testing.screenshot.plugin; 11 | 12 | import java.util.Map; 13 | 14 | import android.view.View; 15 | 16 | /** 17 | * A plugin to get more metadata about a View. 18 | * 19 | * When screenshots are generated we use all registered plugins to 20 | * generate metadata for each of the views in the hierarchy. 21 | */ 22 | public interface ViewDumpPlugin { 23 | public void dump(View view, Map output); 24 | } 25 | -------------------------------------------------------------------------------- /examples/app-example-androidjunitrunner/.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 | -------------------------------------------------------------------------------- /examples/app-example-androidjunitrunner/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /examples/app-example-androidjunitrunner/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'com.facebook.testing.screenshot' 3 | 4 | project.screenshots.customTestRunner = true 5 | 6 | android { 7 | compileSdkVersion 25 8 | buildToolsVersion "25.0.0" 9 | defaultConfig { 10 | 11 | multiDexEnabled true 12 | applicationId "com.example.screenshots" 13 | minSdkVersion 15 14 | targetSdkVersion 25 15 | versionCode 1 16 | versionName "1.0" 17 | testInstrumentationRunner "com.example.screenshots.CustomTestRunner" 18 | } 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | 26 | } 27 | 28 | 29 | dependencies { 30 | compile fileTree(dir: 'libs', include: ['*.jar']) 31 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 32 | exclude group: 'com.android.support', module: 'support-annotations' 33 | }) 34 | compile 'com.android.support:appcompat-v7:25.1.0' 35 | compile 'com.android.support:design:25.1.0' 36 | compile 'com.android.support:multidex:1.0.1' 37 | testCompile 'junit:junit:4.12' 38 | } 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/app-example-androidjunitrunner/app/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runtastic/screenshot-tests-for-android/f174dfaa62af4535983d351db1d162441cb4a3b5/examples/app-example-androidjunitrunner/app/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /examples/app-example-androidjunitrunner/app/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Dec 28 10:00:20 PST 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip 7 | -------------------------------------------------------------------------------- /examples/app-example-androidjunitrunner/app/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /examples/app-example-androidjunitrunner/app/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /examples/app-example-androidjunitrunner/app/local.properties: -------------------------------------------------------------------------------- 1 | ## This file is automatically generated by Android Studio. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must *NOT* be checked into Version Control Systems, 5 | # as it contains information specific to your local configuration. 6 | # 7 | # Location of the SDK. This is only used by Gradle. 8 | # For customization when using a Version Control System, please read the 9 | # header note. 10 | #Fri Dec 30 12:33:21 NZDT 2016 11 | sdk.dir=/Users/nadera/Library/Android/sdk 12 | -------------------------------------------------------------------------------- /examples/app-example-androidjunitrunner/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/nadera/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /examples/app-example-androidjunitrunner/app/screenshots/com.example.screenshots.MainActivityTest_mainActivityTest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runtastic/screenshot-tests-for-android/f174dfaa62af4535983d351db1d162441cb4a3b5/examples/app-example-androidjunitrunner/app/screenshots/com.example.screenshots.MainActivityTest_mainActivityTest.png -------------------------------------------------------------------------------- /examples/app-example-androidjunitrunner/app/screenshots/com.example.screenshots.MainActivityTest_mainActivityTestSettingsOpen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runtastic/screenshot-tests-for-android/f174dfaa62af4535983d351db1d162441cb4a3b5/examples/app-example-androidjunitrunner/app/screenshots/com.example.screenshots.MainActivityTest_mainActivityTestSettingsOpen.png -------------------------------------------------------------------------------- /examples/app-example-androidjunitrunner/app/src/androidTest/java/com/example/screenshots/CustomTestRunner.java: -------------------------------------------------------------------------------- 1 | package com.example.screenshots; 2 | 3 | import android.os.Bundle; 4 | import android.support.multidex.MultiDex; 5 | import android.support.test.runner.AndroidJUnitRunner; 6 | 7 | import com.facebook.testing.screenshot.ScreenshotRunner; 8 | 9 | 10 | public class CustomTestRunner extends AndroidJUnitRunner { 11 | 12 | @Override 13 | public void onCreate(Bundle arguments) { 14 | MultiDex.install(getTargetContext()); 15 | ScreenshotRunner.onCreate(this, arguments); 16 | super.onCreate(arguments); 17 | } 18 | 19 | @Override 20 | public void finish(int resultCode, Bundle results) { 21 | ScreenshotRunner.onDestroy(); 22 | super.finish(resultCode, results); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/app-example-androidjunitrunner/app/src/androidTest/java/com/example/screenshots/MainActivityTest.java: -------------------------------------------------------------------------------- 1 | package com.example.screenshots; 2 | 3 | 4 | import android.support.test.espresso.ViewInteraction; 5 | import android.support.test.rule.ActivityTestRule; 6 | 7 | import com.facebook.testing.screenshot.Screenshot; 8 | 9 | import org.junit.Rule; 10 | import org.junit.Test; 11 | 12 | import static android.support.test.InstrumentationRegistry.getInstrumentation; 13 | import static android.support.test.espresso.Espresso.onView; 14 | import static android.support.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu; 15 | import static android.support.test.espresso.action.ViewActions.click; 16 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 17 | import static android.support.test.espresso.matcher.ViewMatchers.withId; 18 | import static org.hamcrest.Matchers.allOf; 19 | 20 | 21 | public class MainActivityTest { 22 | 23 | @Rule 24 | public ActivityTestRule mActivityTestRule = new ActivityTestRule<>(MainActivity.class); 25 | 26 | @Test 27 | public void mainActivityTest() { 28 | Screenshot.snapActivity(mActivityTestRule.getActivity()).record(); 29 | } 30 | 31 | @Test 32 | public void mainActivityTestSettingsOpen() { 33 | ViewInteraction floatingActionButton = onView( 34 | allOf(withId(R.id.fab), isDisplayed())); 35 | floatingActionButton.perform(click()); 36 | 37 | openActionBarOverflowOrOptionsMenu(getInstrumentation().getTargetContext()); 38 | Screenshot.snapActivity(mActivityTestRule.getActivity()).record(); 39 | 40 | } 41 | 42 | 43 | } 44 | -------------------------------------------------------------------------------- /examples/app-example-androidjunitrunner/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/app-example-androidjunitrunner/app/src/main/java/com/example/screenshots/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.screenshots; 2 | 3 | import android.os.Bundle; 4 | import android.support.design.widget.FloatingActionButton; 5 | import android.support.design.widget.Snackbar; 6 | import android.support.v7.app.AppCompatActivity; 7 | import android.support.v7.widget.Toolbar; 8 | import android.view.Menu; 9 | import android.view.MenuItem; 10 | import android.view.View; 11 | 12 | 13 | public class MainActivity extends AppCompatActivity { 14 | 15 | @Override 16 | protected void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | setContentView(R.layout.activity_main); 19 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 20 | setSupportActionBar(toolbar); 21 | 22 | FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); 23 | fab.setOnClickListener(new View.OnClickListener() { 24 | @Override 25 | public void onClick(View view) { 26 | Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG) 27 | .setAction("Action", null).show(); 28 | } 29 | }); 30 | } 31 | 32 | @Override 33 | public boolean onCreateOptionsMenu(Menu menu) { 34 | // Inflate the menu; this adds items to the action bar if it is present. 35 | getMenuInflater().inflate(R.menu.menu_main, menu); 36 | return true; 37 | } 38 | 39 | @Override 40 | public boolean onOptionsItemSelected(MenuItem item) { 41 | // Handle action bar item clicks here. The action bar will 42 | // automatically handle clicks on the Home/Up button, so long 43 | // as you specify a parent activity in AndroidManifest.xml. 44 | int id = item.getItemId(); 45 | 46 | //noinspection SimplifiableIfStatement 47 | if (id == R.id.action_settings) { 48 | return true; 49 | } 50 | 51 | return super.onOptionsItemSelected(item); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/app-example-androidjunitrunner/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 21 | 22 | 23 | 24 | 25 | 26 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /examples/app-example-androidjunitrunner/app/src/main/res/layout/content_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 20 | 21 | -------------------------------------------------------------------------------- /examples/app-example-androidjunitrunner/app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /examples/app-example-androidjunitrunner/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runtastic/screenshot-tests-for-android/f174dfaa62af4535983d351db1d162441cb4a3b5/examples/app-example-androidjunitrunner/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /examples/app-example-androidjunitrunner/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runtastic/screenshot-tests-for-android/f174dfaa62af4535983d351db1d162441cb4a3b5/examples/app-example-androidjunitrunner/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /examples/app-example-androidjunitrunner/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runtastic/screenshot-tests-for-android/f174dfaa62af4535983d351db1d162441cb4a3b5/examples/app-example-androidjunitrunner/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /examples/app-example-androidjunitrunner/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runtastic/screenshot-tests-for-android/f174dfaa62af4535983d351db1d162441cb4a3b5/examples/app-example-androidjunitrunner/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /examples/app-example-androidjunitrunner/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runtastic/screenshot-tests-for-android/f174dfaa62af4535983d351db1d162441cb4a3b5/examples/app-example-androidjunitrunner/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /examples/app-example-androidjunitrunner/app/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /examples/app-example-androidjunitrunner/app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /examples/app-example-androidjunitrunner/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /examples/app-example-androidjunitrunner/app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 16dp 6 | 7 | -------------------------------------------------------------------------------- /examples/app-example-androidjunitrunner/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | My Application 3 | Settings 4 | 5 | -------------------------------------------------------------------------------- /examples/app-example-androidjunitrunner/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 15 | 16 |