├── plugin
├── src
│ ├── py
│ │ ├── __init__.py
│ │ └── android_screenshot_tests
│ │ │ ├── fixtures
│ │ │ ├── sdcard
│ │ │ │ └── screenshots
│ │ │ │ │ └── com.foo
│ │ │ │ │ └── screenshots-default
│ │ │ │ │ ├── one_dump.xml
│ │ │ │ │ ├── com.foo.ScriptsFixtureTest_testSecondScreenshot.png
│ │ │ │ │ ├── com.foo.ScriptsFixtureTest_testGetTextViewScreenshot.png
│ │ │ │ │ ├── metadata_no_errors.xml
│ │ │ │ │ └── metadata.xml
│ │ │ ├── dummy.zip
│ │ │ └── AndroidManifest.xml
│ │ │ ├── example.apk
│ │ │ ├── background.png
│ │ │ ├── __init__.py
│ │ │ ├── default.css
│ │ │ ├── default.js
│ │ │ ├── metadata.py
│ │ │ ├── simple_puller.py
│ │ │ ├── common.py
│ │ │ ├── test_common.py
│ │ │ ├── aapt.py
│ │ │ ├── test_simple_puller.py
│ │ │ ├── test_metadata.py
│ │ │ ├── test_aapt.py
│ │ │ ├── recorder.py
│ │ │ ├── metadata_fixture.xml
│ │ │ └── test_recorder.py
│ ├── main
│ │ ├── resources
│ │ │ └── META-INF
│ │ │ │ └── gradle-plugins
│ │ │ │ └── com.facebook.testing.screenshot.properties
│ │ └── groovy
│ │ │ └── com
│ │ │ └── facebook
│ │ │ └── testing
│ │ │ └── screenshot
│ │ │ └── build
│ │ │ └── ScreenshotsPlugin.groovy
│ └── test
│ │ └── groovy
│ │ └── com
│ │ └── facebook
│ │ └── testing
│ │ └── screenshot
│ │ └── build
│ │ └── ScreenshotsPluginTest.groovy
├── gradle.properties
└── build.gradle
├── settings.gradle
├── examples
├── app-example-androidjunitrunner
│ ├── app
│ │ ├── .gitignore
│ │ ├── src
│ │ │ ├── main
│ │ │ │ ├── res
│ │ │ │ │ ├── values
│ │ │ │ │ │ ├── strings.xml
│ │ │ │ │ │ ├── colors.xml
│ │ │ │ │ │ ├── dimens.xml
│ │ │ │ │ │ └── styles.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
│ │ │ │ │ ├── menu
│ │ │ │ │ │ └── menu_main.xml
│ │ │ │ │ └── layout
│ │ │ │ │ │ ├── content_main.xml
│ │ │ │ │ │ └── activity_main.xml
│ │ │ │ ├── AndroidManifest.xml
│ │ │ │ └── java
│ │ │ │ │ └── com
│ │ │ │ │ └── example
│ │ │ │ │ └── screenshots
│ │ │ │ │ └── MainActivity.java
│ │ │ ├── test
│ │ │ │ └── java
│ │ │ │ │ └── com
│ │ │ │ │ └── example
│ │ │ │ │ └── screenshots
│ │ │ │ │ └── ExampleUnitTest.java
│ │ │ └── androidTest
│ │ │ │ └── java
│ │ │ │ └── com
│ │ │ │ └── example
│ │ │ │ └── screenshots
│ │ │ │ ├── CustomTestRunner.java
│ │ │ │ └── MainActivityTest.java
│ │ ├── gradle
│ │ │ └── wrapper
│ │ │ │ ├── gradle-wrapper.jar
│ │ │ │ └── gradle-wrapper.properties
│ │ ├── screenshots
│ │ │ ├── com.example.screenshots.MainActivityTest_mainActivityTest.png
│ │ │ └── com.example.screenshots.MainActivityTest_mainActivityTestSettingsOpen.png
│ │ ├── local.properties
│ │ ├── proguard-rules.pro
│ │ ├── build.gradle
│ │ ├── gradlew.bat
│ │ └── gradlew
│ ├── settings.gradle
│ ├── .gitignore
│ ├── gradle
│ │ └── wrapper
│ │ │ ├── gradle-wrapper.jar
│ │ │ └── gradle-wrapper.properties
│ ├── build.gradle
│ ├── gradle.properties
│ ├── gradlew.bat
│ └── gradlew
└── app-example
│ ├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
│ ├── screenshots
│ ├── com.facebook.testing.screenshot.SearchBarTest_testChinese.png
│ ├── com.facebook.testing.screenshot.SearchBarTest_testLongText.png
│ └── com.facebook.testing.screenshot.SearchBarTest_testRendering.png
│ ├── build.gradle
│ ├── src
│ ├── main
│ │ ├── AndroidManifest.xml
│ │ └── res
│ │ │ └── layout
│ │ │ └── search_bar.xml
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── facebook
│ │ └── testing
│ │ └── screenshot
│ │ └── SearchBarTest.java
│ ├── gradlew.bat
│ └── gradlew
├── gradle.properties
├── core
├── gradle.properties
├── src
│ ├── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── com
│ │ │ └── facebook
│ │ │ └── testing
│ │ │ └── screenshot
│ │ │ ├── ScreenshotTestRunner.java
│ │ │ ├── plugin
│ │ │ ├── ViewDumpPlugin.java
│ │ │ ├── TextViewDumper.java
│ │ │ └── PluginRegistry.java
│ │ │ ├── internal
│ │ │ ├── Registry.java
│ │ │ ├── Tiling.java
│ │ │ ├── Album.java
│ │ │ ├── ScreenshotDirectories.java
│ │ │ ├── TestNameDetector.java
│ │ │ ├── HostFileSender.java
│ │ │ ├── ViewHierarchy.java
│ │ │ └── RecordBuilderImpl.java
│ │ │ ├── RecordBuilder.java
│ │ │ ├── Screenshot.java
│ │ │ ├── ScreenshotRunner.java
│ │ │ ├── ViewHelpers.java
│ │ │ └── WindowAttachment.java
│ └── androidTest
│ │ ├── res
│ │ └── layout
│ │ │ ├── testing_simple_textview.xml
│ │ │ └── testing_for_view_hierarchy.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── facebook
│ │ └── testing
│ │ └── screenshot
│ │ ├── CustomScreenshotTestRunner.java
│ │ ├── MyActivity.java
│ │ ├── internal
│ │ ├── ScreenshotDirectoriesTest.java
│ │ ├── TestNameDetectorTest.java
│ │ ├── TestNameDetectorForJUnit4Test.java
│ │ ├── RecordBuilderImplTest.java
│ │ └── HostFileSenderTest.java
│ │ ├── plugin
│ │ └── TextViewDumperTest.java
│ │ ├── ScriptsFixtureTest.java
│ │ ├── ViewHelpersTest.java
│ │ └── WindowAttachmentTest.java
└── build.gradle
├── .gitignore
├── CHANGELOG
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── pull_screenshots
├── integration_test.sh
├── LICENSE-examples
├── LICENSE
├── PATENTS
├── CONTRIBUTING.md
├── gradlew.bat
├── README.md
├── release.gradle
└── gradlew
/plugin/src/py/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include 'core', 'plugin'
--------------------------------------------------------------------------------
/examples/app-example-androidjunitrunner/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/examples/app-example-androidjunitrunner/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | VERSION_NAME=0.4.2
2 | GROUP=com.facebook.testing.screenshot
3 |
--------------------------------------------------------------------------------
/core/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_NAME=Screenshot tests core android library
2 | POM_ARTIFACT_ID=core
3 | POM_PACKAGING=aar
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | */build/
3 | .gradle
4 | /repo/
5 | /build
6 | repo
7 | examples/**/build
8 | .idea
9 | *.iml
10 |
--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/fixtures/sdcard/screenshots/com.foo/screenshots-default/one_dump.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/CHANGELOG:
--------------------------------------------------------------------------------
1 |
2 | * 0.4.2
3 |
4 | - Support for Android gradle plugin 2.2.0
5 | - Make ViewHierarchy dump more useful information
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/runtastic/screenshot-tests-for-android/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/plugin/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_NAME=Gradle build plugin for Screenshot tests for android library
2 | POM_ARTIFACT_ID=plugin
3 | POM_PACKAGING=jar
4 |
--------------------------------------------------------------------------------
/pull_screenshots:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | export PYTHONPATH=`dirname $0`/src/py:$PYTHONPATH
4 | python -m android_screenshot_tests.pull_screenshots "$@"
5 |
--------------------------------------------------------------------------------
/plugin/src/main/resources/META-INF/gradle-plugins/com.facebook.testing.screenshot.properties:
--------------------------------------------------------------------------------
1 | implementation-class=com.facebook.testing.screenshot.build.ScreenshotsPlugin
--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/example.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/runtastic/screenshot-tests-for-android/HEAD/plugin/src/py/android_screenshot_tests/example.apk
--------------------------------------------------------------------------------
/examples/app-example/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/runtastic/screenshot-tests-for-android/HEAD/examples/app-example/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/runtastic/screenshot-tests-for-android/HEAD/plugin/src/py/android_screenshot_tests/background.png
--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/fixtures/dummy.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/runtastic/screenshot-tests-for-android/HEAD/plugin/src/py/android_screenshot_tests/fixtures/dummy.zip
--------------------------------------------------------------------------------
/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/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | My Application
3 | Settings
4 |
5 |
--------------------------------------------------------------------------------
/examples/app-example-androidjunitrunner/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/runtastic/screenshot-tests-for-android/HEAD/examples/app-example-androidjunitrunner/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/examples/app-example-androidjunitrunner/app/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/runtastic/screenshot-tests-for-android/HEAD/examples/app-example-androidjunitrunner/app/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/examples/app-example-androidjunitrunner/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/runtastic/screenshot-tests-for-android/HEAD/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/HEAD/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/HEAD/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/HEAD/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/HEAD/examples/app-example-androidjunitrunner/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/examples/app-example/screenshots/com.facebook.testing.screenshot.SearchBarTest_testChinese.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/runtastic/screenshot-tests-for-android/HEAD/examples/app-example/screenshots/com.facebook.testing.screenshot.SearchBarTest_testChinese.png
--------------------------------------------------------------------------------
/examples/app-example/screenshots/com.facebook.testing.screenshot.SearchBarTest_testLongText.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/runtastic/screenshot-tests-for-android/HEAD/examples/app-example/screenshots/com.facebook.testing.screenshot.SearchBarTest_testLongText.png
--------------------------------------------------------------------------------
/examples/app-example/screenshots/com.facebook.testing.screenshot.SearchBarTest_testRendering.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/runtastic/screenshot-tests-for-android/HEAD/examples/app-example/screenshots/com.facebook.testing.screenshot.SearchBarTest_testRendering.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Aug 25 16:18:11 EDT 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.6-bin.zip
7 |
--------------------------------------------------------------------------------
/core/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/examples/app-example-androidjunitrunner/app/screenshots/com.example.screenshots.MainActivityTest_mainActivityTest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/runtastic/screenshot-tests-for-android/HEAD/examples/app-example-androidjunitrunner/app/screenshots/com.example.screenshots.MainActivityTest_mainActivityTest.png
--------------------------------------------------------------------------------
/examples/app-example-androidjunitrunner/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/examples/app-example/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Oct 07 14:29:02 EDT 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-bin.zip
7 |
--------------------------------------------------------------------------------
/core/src/androidTest/res/layout/testing_simple_textview.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
--------------------------------------------------------------------------------
/examples/app-example-androidjunitrunner/app/screenshots/com.example.screenshots.MainActivityTest_mainActivityTestSettingsOpen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/runtastic/screenshot-tests-for-android/HEAD/examples/app-example-androidjunitrunner/app/screenshots/com.example.screenshots.MainActivityTest_mainActivityTestSettingsOpen.png
--------------------------------------------------------------------------------
/examples/app-example-androidjunitrunner/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/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/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 | 16dp
6 |
7 |
--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/fixtures/sdcard/screenshots/com.foo/screenshots-default/com.foo.ScriptsFixtureTest_testSecondScreenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/runtastic/screenshot-tests-for-android/HEAD/plugin/src/py/android_screenshot_tests/fixtures/sdcard/screenshots/com.foo/screenshots-default/com.foo.ScriptsFixtureTest_testSecondScreenshot.png
--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/fixtures/sdcard/screenshots/com.foo/screenshots-default/com.foo.ScriptsFixtureTest_testGetTextViewScreenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/runtastic/screenshot-tests-for-android/HEAD/plugin/src/py/android_screenshot_tests/fixtures/sdcard/screenshots/com.foo/screenshots-default/com.foo.ScriptsFixtureTest_testGetTextViewScreenshot.png
--------------------------------------------------------------------------------
/examples/app-example-androidjunitrunner/app/src/main/res/values-v21/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
--------------------------------------------------------------------------------
/integration_test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -x
4 | set -e
5 | set -o pipefail
6 |
7 | echo $ANDROID_SERIAL
8 |
9 | cleanup() {
10 | rm -rf ~/.m2 ~/.gradle/caches
11 | rm -rf */build/
12 | rm -rf examples/one/build/
13 | }
14 |
15 | cleanup
16 |
17 | ./gradlew :plugin:install
18 | ./gradlew :core:install
19 |
20 | cd examples/app-example
21 | ./gradlew connectedAndroidTest
22 | ./gradlew screenshotTests
23 |
24 | cleanup
25 |
--------------------------------------------------------------------------------
/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/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/__init__.py:
--------------------------------------------------------------------------------
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 | from __future__ import absolute_import
10 | from __future__ import division
11 | from __future__ import print_function
12 | from __future__ import unicode_literals
13 |
--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/fixtures/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/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/src/test/java/com/example/screenshots/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.example.nadera.myapplication;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() throws Exception {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/core/src/androidTest/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
8 |
9 |
10 |
12 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/default.css:
--------------------------------------------------------------------------------
1 | body {
2 | background: #D3D3D3;
3 | }
4 |
5 | img {
6 | display:block;
7 | }
8 |
9 | .img-wrapper {
10 | background-image: url("background.png");
11 | }
12 |
13 | div.screenshot {
14 | padding: 10px;
15 | }
16 |
17 | div.alternate {
18 | background: #E0E0E0;
19 | }
20 |
21 | div.screenshot_error {
22 | color: red;
23 | }
24 |
25 | table {
26 | border-collapse: collapse;
27 | }
28 |
29 | table, th, tr, td, img{
30 | padding: 0;
31 | margin: 0;
32 | border: 0;
33 | }
34 |
35 | table {
36 | border-spacing: 0;
37 | border-collapse: collapse;
38 | }
39 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/examples/app-example/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | repositories {
3 | mavenLocal()
4 | mavenCentral()
5 | jcenter()
6 | }
7 |
8 | dependencies {
9 | classpath 'com.android.tools.build:gradle:2.2.0'
10 | classpath 'com.facebook.testing.screenshot:plugin:0.4.2'
11 | }
12 | }
13 |
14 | apply plugin: 'com.android.application'
15 | apply plugin: 'com.facebook.testing.screenshot'
16 |
17 | repositories {
18 | mavenLocal()
19 | mavenCentral()
20 | }
21 |
22 | android {
23 | compileSdkVersion 22
24 | buildToolsVersion "23.0.1"
25 |
26 | sourceSets {
27 | }
28 | }
29 |
30 | dependencies {
31 | }
32 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/examples/app-example-androidjunitrunner/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | repositories {
5 | jcenter()
6 | }
7 | dependencies {
8 | classpath 'com.android.tools.build:gradle:2.2.3'
9 | classpath 'com.facebook.testing.screenshot:plugin:0.4.2'
10 | // NOTE: Do not place your application dependencies here; they belong
11 | // in the individual module build.gradle files
12 | }
13 | }
14 |
15 | allprojects {
16 | repositories {
17 | jcenter()
18 | }
19 | }
20 |
21 | task clean(type: Delete) {
22 | delete rootProject.buildDir
23 | }
24 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/examples/app-example/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
12 |
13 |
14 |
15 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/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/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx1536m
13 |
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 |
18 | org.gradle.parallel=true
--------------------------------------------------------------------------------
/examples/app-example-androidjunitrunner/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/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/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
12 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/examples/app-example/src/main/res/layout/search_bar.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
13 |
21 |
26 |
27 |
--------------------------------------------------------------------------------
/examples/app-example-androidjunitrunner/app/src/main/res/layout/content_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
20 |
21 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/fixtures/sdcard/screenshots/com.foo/screenshots-default/metadata_no_errors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | com.foo.ScriptsFixtureTest_testGetTextViewScreenshot
5 |
6 | com.facebook.testing.screenshot.ScriptsFixtureTest
7 | testGetTextViewScreenshot
8 | 1
9 | 1
10 | /sdcard/screenshots/com.foo/screenshots-default/com.foo.ScriptsFixtureTest_testGetTextViewScreenshot.png
11 |
12 |
13 |
14 | com.foo.ScriptsFixtureTest_testSecondScreenshot
15 |
16 | com.facebook.testing.screenshot.ScriptsFixtureTest
17 | testSecondScreenshot
18 | 1
19 | 1
20 | /sdcard/screenshots/com.foo/screenshots-default/com.foo.ScriptsFixtureTest_testSecondScreenshot.png
21 |
22 |
23 |
--------------------------------------------------------------------------------
/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/res/layout/testing_for_view_hierarchy.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
15 |
16 |
21 |
22 |
27 |
32 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/default.js:
--------------------------------------------------------------------------------
1 | $(function () {
2 | $(".view_dump").click(function () {
3 | var name = $(this).attr('data-name');
4 | $.ajax(
5 | {
6 | url: name + "_dump.xml",
7 | dataType: "text",
8 | success: function(result) {
9 | window.alert(result);
10 | },
11 | error: function(result) {
12 | window.alert("could not load the view hierarchy for " + name);
13 | }
14 | });
15 |
16 | });
17 |
18 | $(".extra").click(function () {
19 | var str = $(this).attr('data');
20 | $('').dialog({
21 | modal: true,
22 | title: "Extra Info",
23 | open: function () {
24 | $(this).html(str);
25 | },
26 | buttons: {
27 | Ok: function () {
28 | $(this).dialog("close");
29 | }
30 | },
31 | width:'1000px',
32 | position: {
33 | at: 'top',
34 | },
35 | });
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/metadata.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright (c) 2014-present, Facebook, Inc.
4 | # All rights reserved.
5 | #
6 | # This source code is licensed under the BSD-style license found in the
7 | # LICENSE file in the root directory of this source tree. An additional grant
8 | # of patent rights can be found in the PATENTS file in the same directory.
9 | #
10 |
11 | from __future__ import absolute_import
12 | from __future__ import division
13 | from __future__ import print_function
14 | from __future__ import unicode_literals
15 | import unittest
16 | import tempfile
17 | import shutil
18 | import os
19 | import xml.etree.ElementTree as ET
20 | import re
21 |
22 | # Given a metadata file locally, this transforms it (in-place), to
23 | # remove any screenshot elements that don't satisfy the given filter
24 | # criteria
25 | def filter_screenshots(metadata_file, name_regex=None):
26 | parsed = ET.parse(metadata_file)
27 | root = parsed.getroot()
28 | to_remove = []
29 | for s in root.iter('screenshot'):
30 | if name_regex and not (re.search(name_regex, s.find('name').text)):
31 | to_remove.append(s)
32 |
33 | for s in to_remove:
34 | root.remove(s)
35 |
36 | parsed.write(metadata_file)
37 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/fixtures/sdcard/screenshots/com.foo/screenshots-default/metadata.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | com.foo.ScriptsFixtureTest_testGetTextViewScreenshot
5 |
6 | com.facebook.testing.screenshot.ScriptsFixtureTest
7 | testGetTextViewScreenshot
8 | 1
9 | 1
10 | com.foo.ScriptsFixtureTest_testGetTextViewScreenshot.png
11 | one_dump.xml
12 |
13 |
14 |
15 | com.foo.ScriptsFixtureTest_testSecondScreenshot
16 |
17 | com.facebook.testing.screenshot.ScriptsFixtureTest
18 | testSecondScreenshot
19 | 1
20 | 1
21 | com.foo.ScriptsFixtureTest_testSecondScreenshot.png
22 |
23 |
24 |
25 |
26 | com.facebook.something.FooBar2.png
27 | unknown
28 | unknown
29 | Outofmem and such
30 |
31 |
32 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/simple_puller.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright (c) 2014-present, Facebook, Inc.
4 | # All rights reserved.
5 | #
6 | # This source code is licensed under the BSD-style license found in the
7 | # LICENSE file in the root directory of this source tree. An additional grant
8 | # of patent rights can be found in the PATENTS file in the same directory.
9 | #
10 |
11 | from __future__ import absolute_import
12 | from __future__ import division
13 | from __future__ import print_function
14 | from __future__ import unicode_literals
15 |
16 | import subprocess
17 | from . import common
18 | from .common import get_adb
19 |
20 | class SimplePuller:
21 | """Pulls a given file from the device"""
22 |
23 | def __init__(self, adb_args=[]):
24 | self._adb_args = list(adb_args)
25 |
26 | def remote_file_exists(self, src):
27 | output = common.check_output(
28 | [get_adb()] + self._adb_args + ["shell",
29 | "test -e %s && echo EXISTS" % src])
30 | return "EXISTS" in output
31 |
32 | def pull(self, src, dest):
33 | subprocess.check_call(
34 | [get_adb()] + self._adb_args + ["pull", src, dest],
35 | stderr=subprocess.STDOUT)
36 |
37 | def get_external_data_dir(self):
38 | output = common.check_output(
39 | [get_adb()] + self._adb_args + ["shell", "echo", "$EXTERNAL_STORAGE"])
40 | return output.strip().split()[-1]
41 |
--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/common.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright (c) 2014-present, Facebook, Inc.
4 | # All rights reserved.
5 | #
6 | # This source code is licensed under the BSD-style license found in the
7 | # LICENSE file in the root directory of this source tree. An additional grant
8 | # of patent rights can be found in the PATENTS file in the same directory.
9 | #
10 |
11 | import os
12 | import sys
13 | import subprocess
14 |
15 | def get_image_file_name(name, x, y):
16 | image_file = name
17 | if x != 0 or y != 0:
18 | image_file += "_%d_%d" % (x, y)
19 |
20 | image_file += ".png"
21 | return image_file
22 |
23 | def get_android_sdk():
24 | android_sdk = os.environ.get('ANDROID_SDK') or os.environ.get('ANDROID_HOME')
25 |
26 | if not android_sdk:
27 | raise RuntimeError("ANDROID_SDK or ANDROID_HOME needs to be set")
28 |
29 | return os.path.expanduser(android_sdk)
30 |
31 | def get_adb():
32 | return os.path.join(get_android_sdk(), "platform-tools", "adb")
33 |
34 | # a version of subprocess.check_output that returns a utf-8 string
35 | def check_output(args, **kwargs):
36 | return subprocess.check_output(args, **kwargs).decode('utf-8')
37 |
38 | # a compat version for py3, since assertRegexpMatches is deprecated
39 | def assertRegex(testcase, regex, string):
40 | if sys.version_info >= (3,):
41 | testcase.assertRegex(regex, string)
42 | else:
43 | testcase.assertRegexpMatches(regex, string)
44 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/test_common.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright (c) 2014-present, Facebook, Inc.
4 | # All rights reserved.
5 | #
6 | # This source code is licensed under the BSD-style license found in the
7 | # LICENSE file in the root directory of this source tree. An additional grant
8 | # of patent rights can be found in the PATENTS file in the same directory.
9 | #
10 |
11 | import unittest
12 | import os
13 | from . import common
14 | import subprocess
15 | import sys
16 |
17 | class TestCommon(unittest.TestCase):
18 | def setUp(self):
19 | self.android_sdk = common.get_android_sdk()
20 | self._environ = dict(os.environ)
21 | os.environ.pop('ANDROID_SDK', None)
22 | os.environ.pop('ANDROID_HOME', None)
23 |
24 | def tearDown(self):
25 | os.environ.clear()
26 | os.environ.update(self._environ)
27 |
28 | def test_get_android_sdk_happy_path(self):
29 | os.environ['ANDROID_SDK'] = '/tmp/foo'
30 | self.assertEqual("/tmp/foo", common.get_android_sdk())
31 |
32 | def test_tilde_is_expanded(self):
33 | if sys.version_info >= (3,):
34 | return
35 |
36 | os.environ['ANDROID_SDK'] = '~/foobar'
37 |
38 | home = os.environ['HOME']
39 |
40 | self.assertEqual(os.path.join(home, 'foobar'), common.get_android_sdk())
41 |
42 | def test_get_adb_can_run_in_subprocess(self):
43 | os.environ['ANDROID_SDK'] = self.android_sdk
44 | subprocess.check_call([common.get_adb(), "devices"])
45 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/plugin/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'groovy'
2 | jar {
3 | manifest {
4 | attributes "Implementation-Version": project.version.toString()
5 | }
6 | }
7 |
8 | apply plugin: 'maven'
9 |
10 | repositories {
11 | mavenCentral()
12 | }
13 |
14 | dependencies {
15 | compile gradleApi()
16 | compile group: 'org.eclipse.jetty', name: 'jetty-server', version: '9.3.10.v20160621'
17 |
18 | testCompile group: 'org.mockito', name: 'mockito-all', version: '1.9.5'
19 | testCompile 'com.android.tools.build:gradle:1.3.1'
20 | testCompile 'org.hamcrest:hamcrest-all:1.3'
21 | }
22 |
23 | repositories {
24 | mavenCentral()
25 | }
26 |
27 | group='com.facebook.testing.screenshot'
28 |
29 | uploadArchives {
30 | repositories {
31 | mavenInstaller {
32 | }
33 | }
34 | }
35 |
36 | compileJava {
37 | sourceSets {
38 | main {
39 | resources.srcDirs 'src/py'
40 | resources {
41 | exclude '**/*.pyc'
42 | exclude '**/test_*.py'
43 | exclude '**/fixtures/**'
44 | }
45 | }
46 | }
47 | }
48 |
49 | task pyTests(type: Exec) {
50 | workingDir file('./src/py')
51 | commandLine 'python', '-m', 'unittest', 'discover'
52 | }
53 |
54 | task py3Tests(type: Exec) {
55 | workingDir file('./src/py')
56 | commandLine 'python3', '-m', 'unittest', 'discover'
57 | }
58 |
59 | task sampleServer() << {
60 | file = new File("plugin/src/py/android_screenshot_tests/fixtures/sdcard/screenshots/com.foo/screenshots-default/metadata.xml")
61 | }
62 |
63 | apply from: rootProject.file("release.gradle")
64 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/aapt.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 | from __future__ import division
3 | from __future__ import print_function
4 | from __future__ import unicode_literals
5 |
6 | import os
7 | import subprocess
8 | import tempfile
9 | from os.path import exists, join
10 |
11 | from . import common
12 |
13 | def _check_output(args, **kwargs):
14 | with tempfile.TemporaryFile() as f:
15 | kwargs['stderr'] = f
16 | return common.check_output(args, **kwargs)
17 |
18 | def parse_package_line(line):
19 | """The line looks like this:
20 | package: name='com.facebook.testing.tests' versionCode='1' versionName=''"""
21 |
22 | for word in line.split():
23 | if word.startswith("name='"):
24 | return word[len("name='"):-1]
25 |
26 | def get_aapt_bin():
27 | """Find the binary for aapt from $ANDROID_SDK"""
28 | android_sdk = common.get_android_sdk()
29 |
30 | build_tools = os.path.join(android_sdk, 'build-tools')
31 |
32 | versions = os.listdir(build_tools)
33 | versions = sorted(versions, key=lambda x: "0000000" + x if x.startswith("android") else x, reverse=True)
34 |
35 | for v in versions:
36 | aapt = join(build_tools, v, "aapt")
37 | if exists(aapt) or exists(aapt + ".exe"):
38 | return aapt
39 |
40 | raise RuntimeError("Could not find build-tools in " + android_sdk)
41 |
42 | def get_package(apk):
43 | output = _check_output([get_aapt_bin(), 'dump', 'badging', apk], stderr=os.devnull)
44 | for line in output.split('\n'):
45 | if line.startswith('package:'):
46 | return parse_package_line(line)
47 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/examples/app-example/src/androidTest/java/com/facebook/testing/screenshot/SearchBarTest.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2014-present, Facebook, Inc.
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the license found in the
6 | * LICENSE-examples file in the root directory of this source tree.
7 | */
8 |
9 | package com.facebook.testing.screenshot;
10 |
11 | import android.test.InstrumentationTestCase;
12 | import android.view.LayoutInflater;
13 | import android.view.View;
14 | import android.widget.TextView;
15 |
16 | import com.facebook.testing.screenshot.examples.R;
17 |
18 | public class SearchBarTest extends InstrumentationTestCase {
19 | public void testRendering() throws Throwable {
20 | LayoutInflater inflater = LayoutInflater.from(getInstrumentation().getTargetContext());
21 | View view = inflater.inflate(R.layout.search_bar, null, false);
22 |
23 | ViewHelpers.setupView(view)
24 | .setExactWidthDp(300)
25 | .layout();
26 |
27 | Screenshot.snap(view)
28 | .record();
29 | }
30 |
31 | public void testLongText() throws Throwable {
32 | LayoutInflater inflater = LayoutInflater.from(getInstrumentation().getTargetContext());
33 | View view = inflater.inflate(R.layout.search_bar, null, false);
34 |
35 | TextView tv = (TextView) view.findViewById(R.id.search_box);
36 |
37 | tv.setText("This is a really long text and should overflow");
38 | ViewHelpers.setupView(view)
39 | .setExactWidthDp(300)
40 | .layout();
41 |
42 | Screenshot.snap(view)
43 | .record();
44 | }
45 |
46 | public void testChinese() throws Throwable {
47 | LayoutInflater inflater = LayoutInflater.from(getInstrumentation().getTargetContext());
48 | View view = inflater.inflate(R.layout.search_bar, null, false);
49 |
50 | TextView tv = (TextView) view.findViewById(R.id.search_box);
51 | TextView btn = (TextView) view.findViewById(R.id.button);
52 |
53 | tv.setHint("搜索世界");
54 | btn.setText("搜");
55 |
56 | ViewHelpers.setupView(view)
57 | .setExactWidthDp(300)
58 | .layout();
59 |
60 | Screenshot.snap(view)
61 | .record();
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/test_simple_puller.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright (c) 2014-present, Facebook, Inc.
4 | # All rights reserved.
5 | #
6 | # This source code is licensed under the BSD-style license found in the
7 | # LICENSE file in the root directory of this source tree. An additional grant
8 | # of patent rights can be found in the PATENTS file in the same directory.
9 | #
10 |
11 | from __future__ import absolute_import
12 | from __future__ import division
13 | from __future__ import print_function
14 | from __future__ import unicode_literals
15 |
16 | import unittest
17 | from .simple_puller import SimplePuller
18 | import subprocess
19 | import tempfile
20 | from .common import get_adb
21 | import shutil
22 | import os
23 | from . import common
24 |
25 | class TestSimplePuller(unittest.TestCase):
26 | def setUp(self):
27 | self.puller = SimplePuller()
28 | self.serial = common.check_output(
29 | [get_adb(), "get-serialno"]).strip()
30 |
31 | subprocess.check_call([
32 | get_adb(), "shell",
33 | "echo foobar > /sdcard/blah"])
34 | self.tmpdir = tempfile.mkdtemp()
35 |
36 | def tearDown(self):
37 | shutil.rmtree(self.tmpdir)
38 | subprocess.check_call([
39 | get_adb(), "shell", "rm", "-f", "/sdcard/blah"])
40 |
41 | def test_pull_integration(self):
42 | file = os.path.join(self.tmpdir, "foo")
43 | self.puller.pull("/sdcard/blah", file)
44 |
45 | with open(file, "rt") as f2:
46 | self.assertEqual("foobar\n", f2.read())
47 |
48 | def test_file_exists(self):
49 | self.assertTrue(self.puller.remote_file_exists("/sdcard/blah"))
50 | self.assertFalse(self.puller.remote_file_exists("/sdcard/sdfdsfdf"))
51 |
52 | def test_pull_with_filter(self):
53 | self.puller = SimplePuller(["-s", self.serial])
54 | self.test_pull_integration()
55 |
56 | def test_get_external_data_dir(self):
57 | accepted_dirs = [
58 | '/sdcard',
59 | '/storage/sdcard',
60 | '/storage/emulated/legacy',
61 | ]
62 | self.assertIn(self.puller.get_external_data_dir(), accepted_dirs)
63 |
64 | if __name__ == '__main__':
65 | unittest.main()
66 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/test_metadata.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright (c) 2014-present, Facebook, Inc.
4 | # All rights reserved.
5 | #
6 | # This source code is licensed under the BSD-style license found in the
7 | # LICENSE file in the root directory of this source tree. An additional grant
8 | # of patent rights can be found in the PATENTS file in the same directory.
9 | #
10 |
11 | from __future__ import absolute_import
12 | from __future__ import division
13 | from __future__ import print_function
14 | from __future__ import unicode_literals
15 | import unittest
16 | import tempfile
17 | import shutil
18 | import os
19 | import xml.etree.ElementTree as ET
20 | from . import metadata
21 |
22 | # Tests for the metadata package
23 | class TestMetadata(unittest.TestCase):
24 | def setUp(self):
25 | fd, self.tmp_metadata = tempfile.mkstemp(prefix="TempMetadataXml")
26 | os.close(fd)
27 | os.unlink(self.tmp_metadata)
28 |
29 | self.fixture_metadata = os.path.join(os.path.dirname(__file__), 'metadata_fixture.xml')
30 | shutil.copyfile(self.fixture_metadata, self.tmp_metadata)
31 |
32 | def tearDown(self):
33 | if os.path.exists(self.tmp_metadata):
34 | os.unlink(self.tmp_metadata)
35 |
36 | def test_nothing_removed_for_empty_filter(self):
37 | metadata.filter_screenshots(self.tmp_metadata)
38 |
39 | self.assertEqual(
40 | self.get_num_screenshots_in(self.fixture_metadata),
41 | self.get_num_screenshots_in(self.tmp_metadata))
42 |
43 | def test_exactly_one_result(self):
44 | metadata.filter_screenshots(self.tmp_metadata,
45 | name_regex="testAddPlaceIsShowing")
46 |
47 | self.assertEqual(1, self.get_num_screenshots_in(self.tmp_metadata))
48 |
49 | def test_regex(self):
50 | metadata.filter_screenshots(self.tmp_metadata,
51 | name_regex=".*testAddPlaceIsShowing.*")
52 |
53 | self.assertEqual(1, self.get_num_screenshots_in(self.tmp_metadata))
54 |
55 | def test_regex(self):
56 | metadata.filter_screenshots(self.tmp_metadata,
57 | name_regex=".*CheckinTitleBar.*")
58 |
59 | self.assertEqual(7, self.get_num_screenshots_in(self.tmp_metadata))
60 |
61 | def get_num_screenshots_in(self, metadata_file):
62 | """Gets the number of screenshots in the given metadata file"""
63 | return len(list(ET.parse(metadata_file).getroot().iter('screenshot')))
64 |
65 | if __name__ == '__main__':
66 | unittest.main()
67 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/test_aapt.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 | from __future__ import division
3 | from __future__ import print_function
4 | from __future__ import unicode_literals
5 |
6 | import unittest
7 | from . import aapt
8 | import os
9 | import tempfile
10 | import shutil
11 | from os.path import join, dirname
12 | from .common import assertRegex
13 |
14 | CURDIR = dirname(__file__)
15 |
16 | class TestAapt(unittest.TestCase):
17 | def setUp(self):
18 | os.oldenviron = dict(os.environ)
19 | self.android_sdk = tempfile.mkdtemp()
20 | os.mkdir(join(self.android_sdk, "build-tools"))
21 | os.environ['ANDROID_SDK'] = os.environ.get('ANDROID_SDK') or os.environ.get('ANDROID_HOME')
22 | os.environ.pop('ANDROID_HOME', None)
23 |
24 | def tearDown(self):
25 | os.environ.clear()
26 | os.environ.update(os.oldenviron)
27 | shutil.rmtree(self.android_sdk)
28 |
29 | def _use_mock(self):
30 | os.environ['ANDROID_SDK'] = self.android_sdk
31 |
32 | def _add_aapt(self, version):
33 | f = join(self.android_sdk, "build-tools", version)
34 | os.mkdir(f)
35 | open(join(f, "aapt"), "w").close()
36 |
37 | def test_finds_aapt_happy_path(self):
38 | self.assertTrue(aapt.get_aapt_bin().endswith("aapt"))
39 |
40 | def test_finds_an_aapt_happy_path(self):
41 | self._use_mock()
42 | self._add_aapt("21.0")
43 | self.assertEqual(join(self.android_sdk, "build-tools", "21.0", "aapt"), aapt.get_aapt_bin())
44 |
45 | def test_finds_the_aapt_with_highest_version(self):
46 | self._use_mock()
47 | self._add_aapt("21.0")
48 | self._add_aapt("22.0")
49 | self.assertEqual(join(self.android_sdk, "build-tools", "22.0", "aapt"), aapt.get_aapt_bin())
50 |
51 | def test_does_not_use_old_android_versions(self):
52 | self._use_mock()
53 | self._add_aapt("21.0")
54 | self._add_aapt("22.0")
55 | self._add_aapt("android-4.1")
56 | self.assertEqual(join(self.android_sdk, "build-tools", "22.0", "aapt"), aapt.get_aapt_bin())
57 |
58 | def test_no_android_sdk(self):
59 | os.environ.pop('ANDROID_SDK')
60 |
61 | try:
62 | aapt.get_aapt_bin()
63 | self.fail("expected exception")
64 | except RuntimeError as e:
65 | assertRegex(self, e.args[0], ".*ANDROID_SDK.*")
66 |
67 | def test_no_build_tools(self):
68 | self._use_mock()
69 |
70 | try:
71 | aapt.get_aapt_bin()
72 | self.fail("expected exception")
73 | except RuntimeError as e:
74 | assertRegex(self, e.args[0], ".*Could not find build-tools.*")
75 |
76 | def test_get_package_name(self):
77 | self.assertEqual('com.facebook.testing.screenshot.examples',
78 | aapt.get_package(join(CURDIR, "example.apk")))
79 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/recorder.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright (c) 2014-present, Facebook, Inc.
4 | # All rights reserved.
5 | #
6 | # This source code is licensed under the BSD-style license found in the
7 | # LICENSE file in the root directory of this source tree. An additional grant
8 | # of patent rights can be found in the PATENTS file in the same directory.
9 | #
10 |
11 | import xml.etree.ElementTree as ET
12 | import os
13 | import sys
14 |
15 | from os.path import join
16 | from PIL import Image, ImageChops
17 |
18 | from . import common
19 | import shutil
20 | import tempfile
21 |
22 | class VerifyError(Exception):
23 | pass
24 |
25 | class Recorder:
26 | def __init__(self, input, output):
27 | self._input = input
28 | self._output = output
29 | self._realoutput = output
30 |
31 | def _get_image_size(self, file_name):
32 | with Image.open(file_name) as im:
33 | return im.size
34 |
35 | def _copy(self, name, w, h):
36 | tilewidth, tileheight = self._get_image_size(
37 | join(self._input,
38 | common.get_image_file_name(name, 0, 0)))
39 |
40 | canvaswidth = 0
41 |
42 | for i in range(w):
43 | input_file = common.get_image_file_name(name, i, 0)
44 | canvaswidth += self._get_image_size(join(self._input, input_file))[0]
45 |
46 |
47 | canvasheight = 0
48 |
49 | for j in range(h):
50 | input_file = common.get_image_file_name(name, 0, j)
51 | canvasheight += self._get_image_size(join(self._input, input_file))[1]
52 |
53 | im = Image.new("RGBA", (canvaswidth, canvasheight))
54 |
55 | for i in range(w):
56 | for j in range(h):
57 | input_file = common.get_image_file_name(name, i, j)
58 | with Image.open(join(self._input, input_file)) as input_image:
59 | im.paste(input_image, (i * tilewidth, j * tileheight))
60 | input_image.close()
61 |
62 | im.save(join(self._output, name + ".png"))
63 | im.close()
64 |
65 | def _get_metadata_root(self):
66 | return ET.parse(join(self._input, "metadata.xml")).getroot()
67 |
68 | def _record(self):
69 | root = self._get_metadata_root()
70 | for screenshot in root.iter("screenshot"):
71 | self._copy(screenshot.find('name').text,
72 | int(screenshot.find('tile_width').text),
73 | int(screenshot.find('tile_height').text))
74 |
75 | def _clean(self):
76 | if os.path.exists(self._output):
77 | shutil.rmtree(self._output)
78 | os.mkdir(self._output)
79 |
80 | def _is_image_same(self, file1, file2):
81 | with Image.open(file1) as im1, Image.open(file2) as im2:
82 | diff_image = ImageChops.difference(im1, im2)
83 | try:
84 | return diff_image.getbbox() is None
85 | finally:
86 | diff_image.close()
87 |
88 | def record(self):
89 | self._clean()
90 | self._record()
91 |
92 | def verify(self):
93 | self._output = tempfile.mkdtemp()
94 | self._record()
95 |
96 | root = self._get_metadata_root()
97 | for screenshot in root.iter("screenshot"):
98 | name = screenshot.find('name').text + ".png"
99 | actual = join(self._output, name)
100 | expected = join(self._realoutput, name)
101 | if not self._is_image_same(expected,
102 | actual):
103 | raise VerifyError("Image %s is not same as %s" % (actual, expected))
104 |
105 | shutil.rmtree(self._output)
106 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/metadata_fixture.xml:
--------------------------------------------------------------------------------
1 | com.facebook.places.checkin.CheckinTitleBarTest_testEditBoxIsCenteredcom.facebook.places.checkin.CheckinTitleBarTesttestEditBoxIsCentered/data/data/com.facebook.places.tests/app_screenshots-default/com.facebook.places.checkin.CheckinTitleBarTest_testEditBoxIsCentered.pngcom.facebook.places.checkin.CheckinTitleBarTest_testEditBoxWithBlueBarcom.facebook.places.checkin.CheckinTitleBarTesttestEditBoxWithBlueBar/data/data/com.facebook.places.tests/app_screenshots-default/com.facebook.places.checkin.CheckinTitleBarTest_testEditBoxWithBlueBar.pngcom.facebook.places.checkin.CheckinTitleBarTest_testEditBoxWithSkipcom.facebook.places.checkin.CheckinTitleBarTesttestEditBoxWithSkip/data/data/com.facebook.places.tests/app_screenshots-default/com.facebook.places.checkin.CheckinTitleBarTest_testEditBoxWithSkip.pngcom.facebook.places.checkin.CheckinTitleBarTest_testTitleBarInBlueModecom.facebook.places.checkin.CheckinTitleBarTesttestTitleBarInBlueMode/data/data/com.facebook.places.tests/app_screenshots-default/com.facebook.places.checkin.CheckinTitleBarTest_testTitleBarInBlueMode.pngcom.facebook.places.checkin.CheckinTitleBarTest_testTitleBarInBlueModeWithSkipcom.facebook.places.checkin.CheckinTitleBarTesttestTitleBarInBlueModeWithSkip/data/data/com.facebook.places.tests/app_screenshots-default/com.facebook.places.checkin.CheckinTitleBarTest_testTitleBarInBlueModeWithSkip.pngcom.facebook.places.checkin.CheckinTitleBarTest_testTitleBarIsCenteredcom.facebook.places.checkin.CheckinTitleBarTesttestTitleBarIsCentered/data/data/com.facebook.places.tests/app_screenshots-default/com.facebook.places.checkin.CheckinTitleBarTest_testTitleBarIsCentered.pngcom.facebook.places.checkin.CheckinTitleBarTest_testTitleBarWithSkipcom.facebook.places.checkin.CheckinTitleBarTesttestTitleBarWithSkip/data/data/com.facebook.places.tests/app_screenshots-default/com.facebook.places.checkin.CheckinTitleBarTest_testTitleBarWithSkip.pngcom.facebook.places.checkin.activity.SelectAtTagActivityTest_testAddPlaceIsShowingcom.facebook.places.checkin.activity.SelectAtTagActivityTesttestAddPlaceIsShowing/data/data/com.facebook.places.tests/app_screenshots-default/com.facebook.places.checkin.activity.SelectAtTagActivityTest_testAddPlaceIsShowing.pngcom.facebook.places.checkin.activity.SelectAtTagActivityTest_testSearchEditIsShowingInInlineSearchBarModecom.facebook.places.checkin.activity.SelectAtTagActivityTesttestSearchEditIsShowingInInlineSearchBarMode/data/data/com.facebook.places.tests/app_screenshots-default/com.facebook.places.checkin.activity.SelectAtTagActivityTest_testSearchEditIsShowingInInlineSearchBarMode.pngcom.facebook.places.checkin.activity.SelectAtTagActivityTest_testSearchEditIsShowingInTitleBarModecom.facebook.places.checkin.activity.SelectAtTagActivityTesttestSearchEditIsShowingInTitleBarMode/data/data/com.facebook.places.tests/app_screenshots-default/com.facebook.places.checkin.activity.SelectAtTagActivityTest_testSearchEditIsShowingInTitleBarMode.pngcom.facebook.places.checkin.activity.SelectAtTagActivityTest_testSimpleCreationcom.facebook.places.checkin.activity.SelectAtTagActivityTesttestSimpleCreation/data/data/com.facebook.places.tests/app_screenshots-default/com.facebook.places.checkin.activity.SelectAtTagActivityTest_testSimpleCreation.png
2 |
--------------------------------------------------------------------------------
/plugin/src/test/groovy/com/facebook/testing/screenshot/build/ScreenshotsPluginTest.groovy:
--------------------------------------------------------------------------------
1 | package com.facebook.testing.screenshot.build;
2 |
3 | import org.junit.*
4 | import static org.junit.Assert.*
5 | import org.gradle.api.*
6 | import org.gradle.testfixtures.*
7 | import static org.hamcrest.Matchers.*
8 |
9 | class ScreenshotsPluginForTest extends ScreenshotsPlugin {
10 | static public runtimeDepAdded = false
11 |
12 | @Override
13 | void addRuntimeDep(Project project) {
14 | runtimeDepAdded = true
15 | }
16 | }
17 |
18 | class ScreenshotsPluginTest {
19 | def project;
20 |
21 | @Before
22 | public void setup() {
23 | project = ProjectBuilder.builder().build()
24 | }
25 |
26 | @After
27 | public void tearDown() {
28 | ScreenshotsPluginForTest.runtimeDepAdded = false
29 | }
30 |
31 | def setupProject() {
32 | // make an android manifest
33 | def mainDir = project.projectDir.toString() + "/src/main/"
34 | new File(mainDir).mkdirs()
35 |
36 | println("making directories" + mainDir.toString())
37 |
38 | def manifest = new File(mainDir + "/AndroidManifest.xml")
39 |
40 | manifest.withWriter('utf-8') { writer ->
41 | writer.writeLine('''
42 |
45 |
46 |
47 |
48 |
50 |
51 | ''')
52 | }
53 |
54 | project.repositories {
55 | mavenCentral()
56 | }
57 |
58 |
59 | project.android {
60 | compileSdkVersion 22
61 | buildToolsVersion "23.0.1"
62 | }
63 | }
64 |
65 | @Test
66 | public void testHasTestDep() {
67 | project.getPluginManager().apply 'com.android.library'
68 | project.getPluginManager().apply ScreenshotsPluginForTest
69 | setupProject()
70 |
71 | assertTrue(ScreenshotsPluginForTest.runtimeDepAdded)
72 | project.evaluate()
73 | }
74 |
75 | public void hasRuntimeDep(Project project) {
76 | def depSet = project.getConfigurations().getByName('androidTestCompile').getAllDependencies()
77 |
78 | def found = false
79 | for (dep in depSet) {
80 | if (dep.name == "core" && dep.group == 'com.facebook.testing.screenshot') {
81 | found = true;
82 | }
83 | }
84 |
85 | assertTrue(found)
86 | }
87 |
88 | @Test
89 | public void testApplicationHappyPath() {
90 | project.getPluginManager().apply 'com.android.application'
91 | project.getPluginManager().apply ScreenshotsPluginForTest
92 | setupProject()
93 |
94 | project.evaluate()
95 | }
96 |
97 | @Test
98 | public void testUsesTestApk() {
99 | def plugin = new ScreenshotsPlugin()
100 | project.getPluginManager().apply 'com.android.application'
101 | project.getPluginManager().apply ScreenshotsPluginForTest
102 | setupProject()
103 | project.evaluate()
104 |
105 | assert plugin.getTestApkOutput(project).contains("androidTest")
106 | }
107 |
108 | @Test
109 | public void testCanSetApkTarget() {
110 | def plugin = new ScreenshotsPlugin()
111 | project.getPluginManager().apply 'com.android.application'
112 | project.getPluginManager().apply ScreenshotsPluginForTest
113 | setupProject()
114 | project.screenshots.testApkTarget = "packageReleaseAndroidTest"
115 |
116 | project.evaluate()
117 | def deps = project.tasks.getByPath("pullScreenshots").getDependsOn()
118 |
119 | assert deps.contains("packageReleaseAndroidTest")
120 | }
121 |
122 | @Test
123 | public void testAddRuntimeDep() {
124 | project.getPluginManager().apply 'com.android.application'
125 |
126 | def plugin = new ScreenshotsPlugin()
127 | plugin.addRuntimeDep(project)
128 |
129 | hasRuntimeDep(project);
130 | }
131 |
132 | @Test
133 | public void testUsingAdbConfigurationThrowsError() {
134 | project.getPluginManager().apply 'com.android.application'
135 | project.getPluginManager().apply ScreenshotsPluginForTest
136 |
137 | try {
138 | project.screenshots.adb = "foobar"
139 | fail("Expected exception")
140 | } catch (IllegalArgumentException cause) {
141 | assertThat(cause.getMessage(), containsString("deprecated"));
142 | }
143 | }
144 |
145 | @Test
146 | public void addsLocalScreenshotsTask() {
147 | project.getPluginManager().apply 'com.android.application'
148 | project.getPluginManager().apply ScreenshotsPluginForTest
149 | setupProject()
150 |
151 | assertTrue(project.tasks.pullScreenshotsFromDirectory instanceof Task)
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/plugin/src/main/groovy/com/facebook/testing/screenshot/build/ScreenshotsPlugin.groovy:
--------------------------------------------------------------------------------
1 | package com.facebook.testing.screenshot.build
2 |
3 | import org.gradle.api.*
4 |
5 | class ScreenshotsPluginExtension {
6 | def testApkTarget = "packageDebugAndroidTest"
7 | def connectedAndroidTestTarget = "connectedAndroidTest"
8 | def customTestRunner = false
9 | def recordDir = "screenshots"
10 | def addCompileDeps = true
11 |
12 | // Only used for the pullScreenshotsFromDirectory task
13 | def referenceDir = ""
14 | def targetPackage = ""
15 |
16 | // Deprecated. We automatically detect adb now. Using this will
17 | // throw an error.
18 | @Deprecated
19 | public void setAdb(String path) {
20 | throw new IllegalArgumentException("Use of 'adb' is deprecated, we automatically detect it now")
21 | }
22 | }
23 |
24 | class ScreenshotsPlugin implements Plugin {
25 | void apply(Project project) {
26 | project.extensions.create("screenshots", ScreenshotsPluginExtension)
27 |
28 | def recordMode = false
29 | def verifyMode = false
30 |
31 | def codeSource = ScreenshotsPlugin.class.getProtectionDomain().getCodeSource();
32 | def jarFile = new File(codeSource.getLocation().toURI().getPath());
33 |
34 | // We'll figure out the adb in afterEvaluate
35 | def adb = null
36 |
37 | if (project.screenshots.addCompileDeps) {
38 | addRuntimeDep(project)
39 | }
40 |
41 | project.task('pullScreenshots') << {
42 | project.exec {
43 | def output = getTestApkOutput(project)
44 |
45 | executable = 'python'
46 | environment('PYTHONPATH', jarFile)
47 |
48 | args = ['-m', 'android_screenshot_tests.pull_screenshots', "--apk", output.toString()]
49 |
50 | if (recordMode) {
51 | args += ["--record", project.screenshots.recordDir]
52 | } else if (verifyMode) {
53 | args += ["--verify", project.screenshots.recordDir]
54 | }
55 | }
56 | }
57 |
58 | project.task('pullScreenshotsFromDirectory') << {
59 | project.exec {
60 |
61 | executable = 'python'
62 | environment('PYTHONPATH', jarFile)
63 |
64 | def referenceDir = project.screenshots.referenceDir
65 | def targetPackage = project.screenshots.targetPackage
66 |
67 | if (!referenceDir || !targetPackage) {
68 | printPullFromDirectoryUsage(getLogger(), referenceDir, targetPackage)
69 | return;
70 | }
71 |
72 | logger.quiet(" >>> Using (${referenceDir}) for screenshot verification")
73 |
74 | args = ['-m', 'android_screenshot_tests.pull_screenshots', targetPackage]
75 | args += ["--no-pull"]
76 | args += ["--temp-dir", referenceDir]
77 |
78 | if (recordMode) {
79 | args += ["--record", project.screenshots.recordDir]
80 | } else {
81 | args += ["--verify", project.screenshots.recordDir]
82 | }
83 | }
84 | }
85 |
86 | project.task("clearScreenshots") << {
87 | project.exec {
88 | executable = adb
89 | args = ["shell", "rm", "-rf", "\$EXTERNAL_STORAGE/screenshots"]
90 | ignoreExitValue = true
91 | }
92 | }
93 |
94 | project.afterEvaluate {
95 | adb = project.android.getAdbExe().toString()
96 | project.task("screenshotTests")
97 | project.screenshotTests.dependsOn project.clearScreenshots
98 | project.screenshotTests.dependsOn project.screenshots.connectedAndroidTestTarget
99 | project.screenshotTests.dependsOn project.pullScreenshots
100 |
101 | project.pullScreenshots.dependsOn project.screenshots.testApkTarget
102 | }
103 |
104 | if (!project.screenshots.customTestRunner) {
105 | project.android.defaultConfig {
106 | testInstrumentationRunner = 'com.facebook.testing.screenshot.ScreenshotTestRunner'
107 | }
108 | }
109 |
110 | project.task("recordMode") << {
111 | recordMode = true
112 | }
113 |
114 | project.task("verifyMode") << {
115 | verifyMode = true
116 | }
117 | }
118 |
119 | String getTestApkOutput(Project project) {
120 |
121 | return project.tasks.getByPath(project.screenshots.testApkTarget).getOutputs().getFiles().filter {
122 | it.getAbsolutePath().endsWith ".apk"
123 | }.getSingleFile().getAbsolutePath()
124 | }
125 |
126 | void printPullFromDirectoryUsage(def logger, def referenceDir, def targetPackage) {
127 | logger.error(" >>> You must specify referenceDir=[$referenceDir] and targetPackage=[$targetPackage]")
128 | logger.error("""
129 | EXAMPLE screenshot config
130 |
131 | screenshots {
132 | // This parameter points to the directory containing all the files pulled from a device
133 | referenceDir = path/to/artifacts
134 |
135 | // Your app's application id
136 | targetPackage = "your.application.package"
137 | }
138 | """)
139 | }
140 |
141 | void addRuntimeDep(Project project) {
142 | def implementationVersion = getClass().getPackage().getImplementationVersion()
143 |
144 | if (!implementationVersion) {
145 | println("WARNING: you shouldn't see this in normal operation, file a bug report if this is not a framework test")
146 | implementationVersion = '0.4.2'
147 | }
148 |
149 | project.dependencies.androidTestCompile('com.facebook.testing.screenshot:core:' + implementationVersion)
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/release.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'maven'
2 | apply plugin: 'signing'
3 |
4 | def isReleaseBuild() {
5 | return VERSION_NAME.contains("SNAPSHOT") == false
6 | }
7 |
8 | def getRepositoryUrl() {
9 | return hasProperty('repositoryUrl') ? property('repositoryUrl') : "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
10 | }
11 |
12 | def getRepositoryUsername() {
13 | return hasProperty('repositoryUsername') ? property('repositoryUsername') : ""
14 | }
15 |
16 | def getRepositoryPassword() {
17 | return hasProperty('repositoryPassword') ? property('repositoryPassword') : ""
18 | }
19 |
20 | def configureScreenshotTestsForAndroidPom(def pom) {
21 | pom.whenConfigured {
22 | applyOptionalDeps it, getOptionalDeps()
23 | }
24 | pom.project {
25 | name POM_NAME
26 | artifactId POM_ARTIFACT_ID
27 | packaging POM_PACKAGING
28 | description 'Screenshot Tests for Android'
29 | url 'https://github.com/facebook/screenshot-tests-for-android'
30 |
31 | scm {
32 | url 'https://github.com/facebook/screenshot-tests-for-android.git'
33 | connection 'scm:git:https://github.com/facebook/screenshot-tests-for-android.git'
34 | developerConnection 'scm:git:git@github.com:facebook/screenshot-tests-for-android.git'
35 | }
36 |
37 | licenses {
38 | license {
39 | name 'BSD License'
40 | url 'https://github.com/facebook/screenshot-tests-for-android/blob/master/LICENSE'
41 | distribution 'repo'
42 | }
43 | }
44 |
45 | developers {
46 | developer {
47 | id 'facebook'
48 | name 'Facebook'
49 | }
50 | }
51 | }
52 | }
53 |
54 | // Hack to modify the resulting pom's dependencies to use
55 | // true where appropriate.
56 | def applyOptionalDeps(def pom, def optionalDeps) {
57 | pom.dependencies.each { dep ->
58 | def artifactLabel = dep.groupId + ':' + dep.artifactId
59 | if (optionalDeps.contains(artifactLabel)) {
60 | dep.optional = true
61 | }
62 | }
63 | }
64 |
65 | def getOptionalDeps() {
66 | if (hasProperty('POM_OPTIONAL_DEPS')) {
67 | return property('POM_OPTIONAL_DEPS').split(',') as Set
68 | } else {
69 | return []
70 | }
71 | }
72 |
73 | if (property('POM_PACKAGING') == 'aar') {
74 | // We must be doing an android build I guess
75 |
76 | afterEvaluate { project ->
77 |
78 | task androidJavadoc(type: Javadoc) {
79 | source = android.sourceSets.main.java.srcDirs
80 | classpath += files(android.bootClasspath)
81 |
82 | project.android.libraryVariants.all { variant ->
83 | if (variant.name == "release") {
84 | classpath += files(variant.javaCompile.classpath.files)
85 | }
86 | }
87 |
88 | if (JavaVersion.current().isJava8Compatible()) {
89 | println("this is java8")
90 | options.addStringOption('Xdoclint:none', '-quiet')
91 | }
92 | }
93 |
94 | task androidJavadocJar(type: Jar, dependsOn: androidJavadoc) {
95 | classifier = 'javadoc'
96 | from androidJavadoc.destinationDir
97 | }
98 |
99 | task androidSourcesJar(type: Jar) {
100 | classifier = 'sources'
101 | from android.sourceSets.main.java.srcDirs
102 | }
103 |
104 | android.libraryVariants.all { variant ->
105 | def name = variant.name.capitalize()
106 | task "jar${name}"(type: Jar, dependsOn: variant.javaCompile) {
107 | from variant.javaCompile.destinationDir
108 | }
109 | }
110 | artifacts {
111 | archives androidSourcesJar
112 | archives androidJavadocJar
113 | archives jarRelease
114 | }
115 |
116 | }
117 | } else {
118 | task sourcesJar(type: Jar, dependsOn: classes) {
119 | classifier = 'sources'
120 | from sourceSets.main.allSource
121 | }
122 |
123 | task javadocJar(type: Jar, dependsOn: javadoc) {
124 | classifier = 'javadoc'
125 | from javadoc.destinationDir
126 | }
127 |
128 | artifacts {
129 | archives sourcesJar
130 | archives javadocJar
131 | }
132 | }
133 |
134 | afterEvaluate { project ->
135 | version = VERSION_NAME
136 | group = GROUP
137 |
138 | signing {
139 | required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") }
140 | sign configurations.archives
141 | }
142 |
143 | uploadArchives {
144 | configuration = configurations.archives
145 | repositories.mavenDeployer {
146 | beforeDeployment {
147 | MavenDeployment deployment -> signing.signPom(deployment)
148 | }
149 |
150 | repository(url: getRepositoryUrl()) {
151 | authentication(
152 | userName: getRepositoryUsername(),
153 | password: getRepositoryPassword())
154 | }
155 |
156 | // repository(url: "file://localhost/")
157 |
158 | configureScreenshotTestsForAndroidPom pom
159 | }
160 | }
161 |
162 | task installArchives(type: Upload) {
163 | configuration = configurations.archives
164 | repositories {
165 | mavenDeployer {
166 | repository url: "file://${System.properties['user.home']}/.m2/repository"
167 | configureScreenshotTestsForAndroidPom pom
168 | }
169 | }
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/examples/app-example-androidjunitrunner/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:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------
/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 | # For Cygwin, ensure paths are in UNIX format before anything is touched.
46 | if $cygwin ; then
47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
48 | fi
49 |
50 | # Attempt to set APP_HOME
51 | # Resolve links: $0 may be a link
52 | PRG="$0"
53 | # Need this for relative symlinks.
54 | while [ -h "$PRG" ] ; do
55 | ls=`ls -ld "$PRG"`
56 | link=`expr "$ls" : '.*-> \(.*\)$'`
57 | if expr "$link" : '/.*' > /dev/null; then
58 | PRG="$link"
59 | else
60 | PRG=`dirname "$PRG"`"/$link"
61 | fi
62 | done
63 | SAVED="`pwd`"
64 | cd "`dirname \"$PRG\"`/" >&-
65 | APP_HOME="`pwd -P`"
66 | cd "$SAVED" >&-
67 |
68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69 |
70 | # Determine the Java command to use to start the JVM.
71 | if [ -n "$JAVA_HOME" ] ; then
72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
73 | # IBM's JDK on AIX uses strange locations for the executables
74 | JAVACMD="$JAVA_HOME/jre/sh/java"
75 | else
76 | JAVACMD="$JAVA_HOME/bin/java"
77 | fi
78 | if [ ! -x "$JAVACMD" ] ; then
79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
80 |
81 | Please set the JAVA_HOME variable in your environment to match the
82 | location of your Java installation."
83 | fi
84 | else
85 | JAVACMD="java"
86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
87 |
88 | Please set the JAVA_HOME variable in your environment to match the
89 | location of your Java installation."
90 | fi
91 |
92 | # Increase the maximum file descriptors if we can.
93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
94 | MAX_FD_LIMIT=`ulimit -H -n`
95 | if [ $? -eq 0 ] ; then
96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
97 | MAX_FD="$MAX_FD_LIMIT"
98 | fi
99 | ulimit -n $MAX_FD
100 | if [ $? -ne 0 ] ; then
101 | warn "Could not set maximum file descriptor limit: $MAX_FD"
102 | fi
103 | else
104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
105 | fi
106 | fi
107 |
108 | # For Darwin, add options to specify how the application appears in the dock
109 | if $darwin; then
110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
111 | fi
112 |
113 | # For Cygwin, switch paths to Windows format before running java
114 | if $cygwin ; then
115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/examples/app-example/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 | # For Cygwin, ensure paths are in UNIX format before anything is touched.
46 | if $cygwin ; then
47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
48 | fi
49 |
50 | # Attempt to set APP_HOME
51 | # Resolve links: $0 may be a link
52 | PRG="$0"
53 | # Need this for relative symlinks.
54 | while [ -h "$PRG" ] ; do
55 | ls=`ls -ld "$PRG"`
56 | link=`expr "$ls" : '.*-> \(.*\)$'`
57 | if expr "$link" : '/.*' > /dev/null; then
58 | PRG="$link"
59 | else
60 | PRG=`dirname "$PRG"`"/$link"
61 | fi
62 | done
63 | SAVED="`pwd`"
64 | cd "`dirname \"$PRG\"`/" >&-
65 | APP_HOME="`pwd -P`"
66 | cd "$SAVED" >&-
67 |
68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69 |
70 | # Determine the Java command to use to start the JVM.
71 | if [ -n "$JAVA_HOME" ] ; then
72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
73 | # IBM's JDK on AIX uses strange locations for the executables
74 | JAVACMD="$JAVA_HOME/jre/sh/java"
75 | else
76 | JAVACMD="$JAVA_HOME/bin/java"
77 | fi
78 | if [ ! -x "$JAVACMD" ] ; then
79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
80 |
81 | Please set the JAVA_HOME variable in your environment to match the
82 | location of your Java installation."
83 | fi
84 | else
85 | JAVACMD="java"
86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
87 |
88 | Please set the JAVA_HOME variable in your environment to match the
89 | location of your Java installation."
90 | fi
91 |
92 | # Increase the maximum file descriptors if we can.
93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
94 | MAX_FD_LIMIT=`ulimit -H -n`
95 | if [ $? -eq 0 ] ; then
96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
97 | MAX_FD="$MAX_FD_LIMIT"
98 | fi
99 | ulimit -n $MAX_FD
100 | if [ $? -ne 0 ] ; then
101 | warn "Could not set maximum file descriptor limit: $MAX_FD"
102 | fi
103 | else
104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
105 | fi
106 | fi
107 |
108 | # For Darwin, add options to specify how the application appears in the dock
109 | if $darwin; then
110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
111 | fi
112 |
113 | # For Cygwin, switch paths to Windows format before running java
114 | if $cygwin ; then
115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/plugin/src/py/android_screenshot_tests/test_recorder.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright (c) 2014-present, Facebook, Inc.
4 | # All rights reserved.
5 | #
6 | # This source code is licensed under the BSD-style license found in the
7 | # LICENSE file in the root directory of this source tree. An additional grant
8 | # of patent rights can be found in the PATENTS file in the same directory.
9 | #
10 |
11 | import tempfile
12 | import unittest
13 | import shutil
14 | import os
15 | from os.path import join, exists
16 | from .recorder import Recorder, VerifyError
17 |
18 | from PIL import Image
19 |
20 | class TestRecorder(unittest.TestCase):
21 | def setUp(self):
22 | self.outputdir = tempfile.mkdtemp()
23 | self.inputdir = tempfile.mkdtemp()
24 | self.tmpimages = []
25 | self.recorder = Recorder(self.inputdir, self.outputdir)
26 |
27 | def create_temp_image(self, name, dimens, color):
28 | im = Image.new("RGBA", dimens, color)
29 | filename = os.path.join(self.inputdir, name)
30 | im.save(filename, "PNG")
31 | im.close()
32 | return filename
33 |
34 | def make_metadata(self, str):
35 | with open(os.path.join(self.inputdir, "metadata.xml"), "w") as f:
36 | f.write(str)
37 |
38 | def tearDown(self):
39 | for f in self.tmpimages:
40 | f.close()
41 |
42 | shutil.rmtree(self.outputdir)
43 | shutil.rmtree(self.inputdir)
44 |
45 | def test_create_temp_image(self):
46 | im = self.create_temp_image("foobar", (100, 10), "blue")
47 | self.assertTrue(os.path.exists(im))
48 |
49 | def test_recorder_creates_dir(self):
50 | shutil.rmtree(self.outputdir)
51 | self.make_metadata("""""")
52 | self.recorder.record()
53 |
54 | self.assertTrue(os.path.exists(self.outputdir))
55 |
56 | def test_single_input(self):
57 | self.create_temp_image("foobar.png", (10, 10), "blue")
58 | self.make_metadata("""
59 |
60 | foobar
61 | 1
62 | 1
63 |
64 | """)
65 |
66 | self.recorder.record()
67 | self.assertTrue(exists(join(self.outputdir, "foobar.png")))
68 |
69 | def test_two_files(self):
70 | self.create_temp_image("foo.png", (10, 10), "blue")
71 | self.create_temp_image("bar.png", (10, 10), "red")
72 | self.make_metadata("""
73 |
74 | foo
75 | 1
76 | 1
77 |
78 |
79 | bar
80 | 1
81 | 1
82 |
83 | """)
84 |
85 | self.recorder.record()
86 | self.assertTrue(exists(join(self.outputdir, "foo.png")))
87 | self.assertTrue(exists(join(self.outputdir, "bar.png")))
88 |
89 | def test_one_col_tiles(self):
90 | self.create_temp_image("foobar.png", (10, 10), "blue")
91 | self.create_temp_image("foobar_0_1.png", (10, 10), "red")
92 |
93 | self.make_metadata("""
94 |
95 | foobar
96 | 1
97 | 2
98 |
99 | """)
100 |
101 | self.recorder.record()
102 |
103 | with Image.open(join(self.outputdir, "foobar.png")) as im:
104 | (w, h) = im.size
105 |
106 | self.assertEqual(10, w)
107 | self.assertEqual(20, h)
108 |
109 | self.assertEqual((0, 0, 255, 255), im.getpixel((1, 1)))
110 | self.assertEqual((255, 0, 0, 255), im.getpixel((1, 11)))
111 |
112 | def test_one_row_tiles(self):
113 | self.create_temp_image("foobar.png", (10, 10), "blue")
114 | self.create_temp_image("foobar_1_0.png", (10, 10), "red")
115 |
116 | self.make_metadata("""
117 |
118 | foobar
119 | 2
120 | 1
121 |
122 | """)
123 |
124 | self.recorder.record()
125 |
126 | with Image.open(join(self.outputdir, "foobar.png")) as im:
127 | (w, h) = im.size
128 | self.assertEqual(20, w)
129 | self.assertEqual(10, h)
130 |
131 | self.assertEqual((0, 0, 255, 255), im.getpixel((1, 1)))
132 | self.assertEqual((255, 0, 0, 255), im.getpixel((11, 1)))
133 |
134 | def test_fractional_tiles(self):
135 | self.create_temp_image("foobar.png", (10, 10), "blue")
136 | self.create_temp_image("foobar_1_0.png", (9, 10), "red")
137 | self.create_temp_image("foobar_0_1.png", (10, 8), "red")
138 | self.create_temp_image("foobar_1_1.png", (9, 8), "blue")
139 |
140 | self.make_metadata("""
141 |
142 | foobar
143 | 2
144 | 2
145 |
146 | """)
147 |
148 | self.recorder.record()
149 |
150 | with Image.open(join(self.outputdir, "foobar.png")) as im:
151 | (w, h) = im.size
152 | self.assertEqual(19, w)
153 | self.assertEqual(18, h)
154 |
155 | self.assertEqual((0, 0, 255, 255), im.getpixel((1, 1)))
156 | self.assertEqual((255, 0, 0, 255), im.getpixel((11, 1)))
157 |
158 | self.assertEqual((0, 0, 255, 255), im.getpixel((11, 11)))
159 | self.assertEqual((255, 0, 0, 255), im.getpixel((1, 11)))
160 |
161 | def test_verify_success(self):
162 | self.create_temp_image("foobar.png", (10, 10), "blue")
163 | self.make_metadata("""
164 |
165 | foobar
166 | 1
167 | 1
168 |
169 | """)
170 |
171 | self.recorder.record()
172 | self.recorder.verify()
173 |
174 | def test_verify_failure(self):
175 | self.create_temp_image("foobar.png", (10, 10), "blue")
176 | self.make_metadata("""
177 |
178 | foobar
179 | 1
180 | 1
181 |
182 | """)
183 |
184 | self.recorder.record()
185 | os.unlink(join(self.inputdir, "foobar.png"))
186 | self.create_temp_image("foobar.png", (10, 10), "red")
187 |
188 | try:
189 | self.recorder.verify()
190 | self.fail("expected exception")
191 | except VerifyError:
192 | pass # expected
193 |
194 | if __name__ == '__main__':
195 | unittest.main()
196 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------