├── settings.gradle
├── core
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ └── com
│ │ │ └── novoda
│ │ │ └── espresso
│ │ │ ├── Condition.java
│ │ │ ├── ViewCreator.java
│ │ │ ├── FontScale.java
│ │ │ ├── InflateFromXmlViewCreator.java
│ │ │ ├── FontScaleSetting.java
│ │ │ ├── FontScaleRules.java
│ │ │ ├── FontScaleTestRule.java
│ │ │ ├── AccessibilityRules.java
│ │ │ ├── AccessibilityServiceTestRule.java
│ │ │ ├── ViewTestRule.java
│ │ │ └── AccessibilityViewMatchers.java
│ │ └── res
│ │ └── values
│ │ └── ids.xml
└── build.gradle
├── demo
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── README.md
│ │ │ ├── drawable-hdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── drawable-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── movie_arrival.jpg
│ │ │ │ ├── movie_irongiant.jpg
│ │ │ │ ├── movie_whiplash.jpg
│ │ │ │ ├── movie_beetlejuice.jpg
│ │ │ │ ├── movie_interstellar.jpg
│ │ │ │ ├── movie_takeshelter.jpg
│ │ │ │ ├── movie_fantasticmrfox.jpg
│ │ │ │ ├── movie_royaltenenbaums.jpg
│ │ │ │ ├── movie_milliondollarbaby.jpg
│ │ │ │ └── movie_planestrainsautomobiles.jpg
│ │ │ ├── drawable-xhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── drawable-xxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── values
│ │ │ │ ├── styles.xml
│ │ │ │ ├── ids.xml
│ │ │ │ └── strings.xml
│ │ │ ├── layout
│ │ │ │ ├── activity_rateable_movies.xml
│ │ │ │ ├── merge_movie_item.xml
│ │ │ │ └── item_view_rateable_movie.xml
│ │ │ └── drawable
│ │ │ │ ├── ic_favorite_24dp.xml
│ │ │ │ └── ic_favorite_border_24dp.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── novoda
│ │ │ │ └── movies
│ │ │ │ ├── DemoMainActivity.java
│ │ │ │ ├── Movie.java
│ │ │ │ ├── rateable
│ │ │ │ ├── Movie.java
│ │ │ │ ├── RateableMoviesAdapter.java
│ │ │ │ ├── RateableMovieViewModel.java
│ │ │ │ ├── RateableMovieViewModelBuilder.java
│ │ │ │ ├── RateableMoviesActivity.java
│ │ │ │ ├── FakeMovieRepository.java
│ │ │ │ └── RateableMovieViewHolder.java
│ │ │ │ └── MovieItemView.java
│ │ └── AndroidManifest.xml
│ ├── debug
│ │ └── res
│ │ │ └── layout
│ │ │ └── test_movie_item_view.xml
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── novoda
│ │ └── movies
│ │ ├── rateable
│ │ ├── RateableMovieViewModelFixtures.java
│ │ ├── TouchMode_RateableMovieViewHolderTest.java
│ │ ├── TalkBack_RateableMovieViewHolderTest.java
│ │ └── SwitchAccess_RateableMovieViewHolderTest.java
│ │ ├── DemoMainActivityTest.java
│ │ ├── ProgrammaticallyCreatedViewTest.java
│ │ ├── MovieItemViewTest.java
│ │ └── MovieItemViewTalkBackTest.java
└── build.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── extras
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── novoda
│ │ │ │ └── espresso
│ │ │ │ ├── EmptyActivity.java
│ │ │ │ ├── AccessibilityServiceTogglingActivity.java
│ │ │ │ └── AccessibilityServiceToggler.java
│ │ └── AndroidManifest.xml
│ └── test
│ │ └── java
│ │ └── com
│ │ └── novoda
│ │ └── espresso
│ │ └── AccessibilityServiceTogglerTest.java
└── build.gradle
├── lint-config.xml
├── .gitignore
├── gradle.properties
├── dependencies.gradle
├── gradlew.bat
├── README.md
├── gradlew
└── LICENSE
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':core'
2 | include ':extras'
3 | include ':demo'
4 |
--------------------------------------------------------------------------------
/core/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/demo/src/main/res/README.md:
--------------------------------------------------------------------------------
1 | Movie posters taken from [The Movie DB](https://www.themoviedb.org/)
2 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/novoda/espresso-support/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/core/src/main/java/com/novoda/espresso/Condition.java:
--------------------------------------------------------------------------------
1 | package com.novoda.espresso;
2 |
3 | interface Condition {
4 |
5 | boolean holds();
6 | }
7 |
--------------------------------------------------------------------------------
/demo/src/main/res/drawable-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/novoda/espresso-support/HEAD/demo/src/main/res/drawable-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/drawable-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/novoda/espresso-support/HEAD/demo/src/main/res/drawable-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/drawable-mdpi/movie_arrival.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/novoda/espresso-support/HEAD/demo/src/main/res/drawable-mdpi/movie_arrival.jpg
--------------------------------------------------------------------------------
/demo/src/main/res/drawable-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/novoda/espresso-support/HEAD/demo/src/main/res/drawable-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/drawable-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/novoda/espresso-support/HEAD/demo/src/main/res/drawable-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/demo/src/main/res/drawable-mdpi/movie_irongiant.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/novoda/espresso-support/HEAD/demo/src/main/res/drawable-mdpi/movie_irongiant.jpg
--------------------------------------------------------------------------------
/demo/src/main/res/drawable-mdpi/movie_whiplash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/novoda/espresso-support/HEAD/demo/src/main/res/drawable-mdpi/movie_whiplash.jpg
--------------------------------------------------------------------------------
/demo/src/main/res/drawable-mdpi/movie_beetlejuice.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/novoda/espresso-support/HEAD/demo/src/main/res/drawable-mdpi/movie_beetlejuice.jpg
--------------------------------------------------------------------------------
/demo/src/main/res/drawable-mdpi/movie_interstellar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/novoda/espresso-support/HEAD/demo/src/main/res/drawable-mdpi/movie_interstellar.jpg
--------------------------------------------------------------------------------
/demo/src/main/res/drawable-mdpi/movie_takeshelter.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/novoda/espresso-support/HEAD/demo/src/main/res/drawable-mdpi/movie_takeshelter.jpg
--------------------------------------------------------------------------------
/demo/src/main/res/drawable-mdpi/movie_fantasticmrfox.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/novoda/espresso-support/HEAD/demo/src/main/res/drawable-mdpi/movie_fantasticmrfox.jpg
--------------------------------------------------------------------------------
/demo/src/main/res/drawable-mdpi/movie_royaltenenbaums.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/novoda/espresso-support/HEAD/demo/src/main/res/drawable-mdpi/movie_royaltenenbaums.jpg
--------------------------------------------------------------------------------
/demo/src/main/res/drawable-mdpi/movie_milliondollarbaby.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/novoda/espresso-support/HEAD/demo/src/main/res/drawable-mdpi/movie_milliondollarbaby.jpg
--------------------------------------------------------------------------------
/core/src/main/res/values/ids.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/demo/src/main/java/com/novoda/movies/DemoMainActivity.java:
--------------------------------------------------------------------------------
1 | package com.novoda.movies;
2 |
3 | import android.app.Activity;
4 |
5 | public class DemoMainActivity extends Activity {
6 | }
7 |
--------------------------------------------------------------------------------
/demo/src/main/res/drawable-mdpi/movie_planestrainsautomobiles.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/novoda/espresso-support/HEAD/demo/src/main/res/drawable-mdpi/movie_planestrainsautomobiles.jpg
--------------------------------------------------------------------------------
/extras/src/main/java/com/novoda/espresso/EmptyActivity.java:
--------------------------------------------------------------------------------
1 | package com.novoda.espresso;
2 |
3 | import android.support.v7.app.AppCompatActivity;
4 |
5 | public class EmptyActivity extends AppCompatActivity {
6 | }
7 |
--------------------------------------------------------------------------------
/lint-config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/demo/src/debug/res/layout/test_movie_item_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Oct 31 21:43:49 GMT 2017
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-4.1-all.zip
7 |
--------------------------------------------------------------------------------
/demo/src/main/res/layout/activity_rateable_movies.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea/
2 | /.idea/workspace.xml
3 | /.idea/libraries
4 | *.iml
5 | .DS_Store
6 |
7 | # Built application files
8 | *.apk
9 | *.ap_
10 |
11 | # Files for the Dalvik VM
12 | *.dex
13 |
14 | # Java class files
15 | *.class
16 |
17 | # Generated files
18 | bin/
19 | gen/
20 |
21 | # Gradle files
22 | .gradle/
23 | build/
24 |
25 | # Local configuration file (sdk path, etc)
26 | local.properties
27 |
28 | # Proguard folder generated by Eclipse
29 | proguard/
30 |
31 | # Log Files
32 | *.log
33 |
34 |
--------------------------------------------------------------------------------
/demo/src/main/res/drawable/ic_favorite_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/core/src/main/java/com/novoda/espresso/ViewCreator.java:
--------------------------------------------------------------------------------
1 | package com.novoda.espresso;
2 |
3 | import android.content.Context;
4 | import android.view.View;
5 | import android.view.ViewGroup;
6 |
7 | public interface ViewCreator {
8 |
9 | /**
10 | * Create the view, inflating or programmatically. Do not attach the created view to the parentView.
11 | *
12 | * @param context Activity Context
13 | * @param parentView Activity root content View
14 | */
15 | T createView(Context context, ViewGroup parentView);
16 | }
17 |
--------------------------------------------------------------------------------
/demo/src/androidTest/java/com/novoda/movies/rateable/RateableMovieViewModelFixtures.java:
--------------------------------------------------------------------------------
1 | package com.novoda.movies.rateable;
2 |
3 | import com.novoda.movies.R;
4 |
5 | class RateableMovieViewModelFixtures {
6 |
7 | static RateableMovieViewModelBuilder viewModel(RateableMovieViewModel.UserActions userActions) {
8 | return viewModel().actions(userActions);
9 | }
10 |
11 | static RateableMovieViewModelBuilder viewModel() {
12 | return new RateableMovieViewModelBuilder("Arrival".hashCode(), "Arrival", R.drawable.movie_arrival)
13 | .rating(5)
14 | .liked(true);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/demo/src/main/java/com/novoda/movies/Movie.java:
--------------------------------------------------------------------------------
1 | package com.novoda.movies;
2 |
3 | public class Movie {
4 |
5 | public final String name;
6 |
7 | public Movie(String name) {
8 | this.name = name;
9 | }
10 |
11 | @Override
12 | public boolean equals(Object o) {
13 | if (this == o) {
14 | return true;
15 | }
16 | if (o == null || getClass() != o.getClass()) {
17 | return false;
18 | }
19 |
20 | Movie movie = (Movie) o;
21 |
22 | return name.equals(movie.name);
23 |
24 | }
25 |
26 | @Override
27 | public int hashCode() {
28 | return name.hashCode();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/demo/src/main/java/com/novoda/movies/rateable/Movie.java:
--------------------------------------------------------------------------------
1 | package com.novoda.movies.rateable;
2 |
3 | import android.support.annotation.DrawableRes;
4 |
5 | class Movie {
6 |
7 | final long id;
8 | final String title;
9 | final float rating;
10 | final boolean liked;
11 | @DrawableRes
12 | final int poster;
13 |
14 | Movie(long id, String title, float rating, boolean liked, int poster) {
15 | this.id = id;
16 | this.title = title;
17 | this.rating = rating;
18 | this.liked = liked;
19 | this.poster = poster;
20 | }
21 |
22 | Movie(Movie movie) {
23 | this(movie.id, movie.title, movie.rating, movie.liked, movie.poster);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/demo/src/main/res/drawable/ic_favorite_border_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/core/src/main/java/com/novoda/espresso/FontScale.java:
--------------------------------------------------------------------------------
1 | package com.novoda.espresso;
2 |
3 | enum FontScale {
4 |
5 | SMALL(String.valueOf(0.85f)),
6 | NORMAL(String.valueOf(1f)),
7 | LARGE(String.valueOf(1.15f)),
8 | HUGE(String.valueOf(1.3f));
9 |
10 | private final String value;
11 |
12 | FontScale(String value) {
13 | this.value = value;
14 | }
15 |
16 | static FontScale from(float scale) {
17 | for (FontScale fontScale : values()) {
18 | if (fontScale.value().equals(String.valueOf(scale))) {
19 | return fontScale;
20 | }
21 | }
22 | throw new IllegalArgumentException("Unknown scale: " + scale);
23 | }
24 |
25 | String value() {
26 | return value;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/demo/src/androidTest/java/com/novoda/movies/DemoMainActivityTest.java:
--------------------------------------------------------------------------------
1 | package com.novoda.movies;
2 |
3 | import android.support.test.filters.LargeTest;
4 | import android.support.test.rule.ActivityTestRule;
5 | import android.support.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Rule;
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 |
11 | @RunWith(AndroidJUnit4.class)
12 | @LargeTest
13 | public class DemoMainActivityTest {
14 |
15 | @Rule
16 | public ActivityTestRule activityRule = new ActivityTestRule<>(DemoMainActivity.class);
17 |
18 | @Test
19 | public void testActivityNotNull() {
20 | if (activityRule.getActivity() == null) {
21 | throw new AssertionError("activity null");
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/core/src/main/java/com/novoda/espresso/InflateFromXmlViewCreator.java:
--------------------------------------------------------------------------------
1 | package com.novoda.espresso;
2 |
3 | import android.content.Context;
4 | import android.support.annotation.LayoutRes;
5 | import android.view.LayoutInflater;
6 | import android.view.View;
7 | import android.view.ViewGroup;
8 |
9 | class InflateFromXmlViewCreator implements ViewCreator {
10 |
11 | @LayoutRes
12 | private final int id;
13 |
14 | InflateFromXmlViewCreator(@LayoutRes int id) {
15 | this.id = id;
16 | }
17 |
18 | @SuppressWarnings("unchecked") // unchecked cast of View from XML
19 | @Override
20 | public T createView(Context context, ViewGroup parentView) {
21 | LayoutInflater layoutInflater = LayoutInflater.from(context);
22 | return (T) layoutInflater.inflate(id, parentView, false);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/core/src/main/java/com/novoda/espresso/FontScaleSetting.java:
--------------------------------------------------------------------------------
1 | package com.novoda.espresso;
2 |
3 | import android.annotation.TargetApi;
4 | import android.app.UiAutomation;
5 | import android.content.res.Resources;
6 | import android.os.Build;
7 |
8 | class FontScaleSetting {
9 |
10 | private final UiAutomation uiAutomation;
11 | private final Resources resources;
12 |
13 | FontScaleSetting(UiAutomation uiAutomation, Resources resources) {
14 | this.uiAutomation = uiAutomation;
15 | this.resources = resources;
16 | }
17 |
18 | FontScale get() {
19 | return FontScale.from(resources.getConfiguration().fontScale);
20 | }
21 |
22 | @TargetApi(Build.VERSION_CODES.LOLLIPOP)
23 | void set(FontScale scale) {
24 | uiAutomation.executeShellCommand("settings put system font_scale " + scale.value());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/demo/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/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 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
19 |
--------------------------------------------------------------------------------
/extras/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/core/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | repositories {
3 | jcenter()
4 | }
5 | dependencies {
6 | classpath libraries.build.bintrayRelease
7 | }
8 | }
9 |
10 | apply plugin: 'com.android.library'
11 | apply plugin: 'bintray-release'
12 |
13 | android {
14 | compileSdkVersion versions.androidSdk.compile
15 | buildToolsVersion versions.androidSdk.buildTools
16 |
17 | defaultConfig {
18 | minSdkVersion versions.androidSdk.min
19 | }
20 |
21 | lintOptions {
22 | lintConfig file("../lint-config.xml")
23 | }
24 | }
25 |
26 | dependencies {
27 | provided project(':extras')
28 | compile libraries.androidTestRunner
29 | compile libraries.androidEspressoCore
30 | compile libraries.supportAnnotations
31 | }
32 |
33 | publish {
34 | userOrg = artifactsPublish.userOrg
35 | groupId = artifactsPublish.groupId
36 | version = artifactsPublish.version
37 | website = artifactsPublish.website
38 | artifactId = 'espresso-support'
39 | description = 'Support classes and functions for Espresso testing'
40 | }
41 |
--------------------------------------------------------------------------------
/extras/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | repositories {
3 | jcenter()
4 | }
5 | dependencies {
6 | classpath libraries.build.bintrayRelease
7 | }
8 | }
9 |
10 | apply plugin: 'com.android.library'
11 | apply plugin: 'bintray-release'
12 |
13 | android {
14 | compileSdkVersion versions.androidSdk.compile
15 | buildToolsVersion versions.androidSdk.buildTools
16 |
17 | defaultConfig {
18 | minSdkVersion versions.androidSdk.min
19 | }
20 |
21 | lintOptions {
22 | lintConfig file("../lint-config.xml")
23 | }
24 | }
25 |
26 | dependencies {
27 | compile libraries.supportAnnotations
28 | compile libraries.accessibilitools
29 | testImplementation libraries.mockitoCore
30 | testImplementation libraries.jUnit
31 | }
32 |
33 | publish {
34 | userOrg = artifactsPublish.userOrg
35 | groupId = artifactsPublish.groupId
36 | version = artifactsPublish.version
37 | website = artifactsPublish.website
38 | artifactId = 'espresso-support-extras'
39 | description = 'Additional app resources used by espresso-support'
40 | }
41 |
--------------------------------------------------------------------------------
/demo/src/main/res/values/ids.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/demo/src/main/java/com/novoda/movies/rateable/RateableMoviesAdapter.java:
--------------------------------------------------------------------------------
1 | package com.novoda.movies.rateable;
2 |
3 | import android.support.v7.widget.RecyclerView;
4 | import android.view.ViewGroup;
5 |
6 | import java.util.List;
7 |
8 | class RateableMoviesAdapter extends RecyclerView.Adapter {
9 |
10 | private List viewModels;
11 |
12 | RateableMoviesAdapter() {
13 | super.setHasStableIds(true);
14 | }
15 |
16 | void update(List viewModels) {
17 | this.viewModels = viewModels;
18 | notifyDataSetChanged();
19 | }
20 |
21 | @Override
22 | public RateableMovieViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
23 | return RateableMovieViewHolder.inflate(parent);
24 | }
25 |
26 | @Override
27 | public void onBindViewHolder(RateableMovieViewHolder holder, int position) {
28 | holder.bind(viewModels.get(position));
29 | }
30 |
31 | @Override
32 | public int getItemCount() {
33 | return viewModels == null ? 0 : viewModels.size();
34 | }
35 |
36 | @Override
37 | public long getItemId(int position) {
38 | return viewModels.get(position).id;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/demo/src/main/java/com/novoda/movies/rateable/RateableMovieViewModel.java:
--------------------------------------------------------------------------------
1 | package com.novoda.movies.rateable;
2 |
3 | import android.support.annotation.DrawableRes;
4 |
5 | class RateableMovieViewModel {
6 |
7 | final long id;
8 | final String title;
9 | final float rating;
10 | final boolean liked;
11 | @DrawableRes
12 | final int poster;
13 | final UserActions actions;
14 |
15 | RateableMovieViewModel(long id, String title, float rating, boolean liked, int poster, UserActions actions) {
16 | this.id = id;
17 | this.title = title;
18 | this.rating = rating;
19 | this.liked = liked;
20 | this.poster = poster;
21 | this.actions = actions;
22 | }
23 |
24 | interface UserActions {
25 |
26 | UserActions NO_OP = new UserActions() {
27 | @Override
28 | public void onSelectMovie() {
29 | }
30 |
31 | @Override
32 | public void onToggleLike() {
33 | }
34 |
35 | @Override
36 | public void onRate(float rating) {
37 | }
38 | };
39 |
40 | void onSelectMovie();
41 |
42 | void onToggleLike();
43 |
44 | void onRate(float rating);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/core/src/main/java/com/novoda/espresso/FontScaleRules.java:
--------------------------------------------------------------------------------
1 | package com.novoda.espresso;
2 |
3 | import android.annotation.TargetApi;
4 | import android.app.Instrumentation;
5 | import android.os.Build;
6 | import android.support.test.InstrumentationRegistry;
7 |
8 | import org.junit.rules.TestRule;
9 |
10 | @TargetApi(Build.VERSION_CODES.LOLLIPOP)
11 | public class FontScaleRules {
12 |
13 | public static TestRule smallFontScaleTestRule() {
14 | return new FontScaleTestRule(createFontScaleSetting(), FontScale.SMALL);
15 | }
16 |
17 | public static TestRule normalFontScaleTestRule() {
18 | return new FontScaleTestRule(createFontScaleSetting(), FontScale.NORMAL);
19 | }
20 |
21 | public static TestRule largeFontScaleTestRule() {
22 | return new FontScaleTestRule(createFontScaleSetting(), FontScale.LARGE);
23 | }
24 |
25 | public static TestRule hugeFontScaleTestRule() {
26 | return new FontScaleTestRule(createFontScaleSetting(), FontScale.HUGE);
27 | }
28 |
29 | private static FontScaleSetting createFontScaleSetting() {
30 | Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
31 | return new FontScaleSetting(instrumentation.getUiAutomation(), instrumentation.getTargetContext().getResources());
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/dependencies.gradle:
--------------------------------------------------------------------------------
1 | ext {
2 | versions = [
3 | androidSdk : [
4 | compile : 27,
5 | min : 16,
6 | target : 27,
7 | buildTools: '27.0.3'
8 | ],
9 | mockito : '2.6.3',
10 | androidSupport: '27.0.2'
11 | ]
12 |
13 | libraries = [
14 | build : [
15 | androidGradle : 'com.android.tools.build:gradle:3.0.1',
16 | bintrayRelease: 'com.novoda:bintray-release:0.7.0'
17 | ],
18 | accessibilitools : 'com.novoda:accessibilitools:1.5.0',
19 | androidTestRunner : 'com.android.support.test:runner:1.0.1',
20 | androidEspressoCore : 'com.android.support.test.espresso:espresso-core:3.0.1',
21 | jUnit : 'junit:junit:4.12',
22 | supportAnnotations : "com.android.support:support-annotations:${versions.androidSupport}",
23 | supportAppCompatV7 : "com.android.support:appcompat-v7:${versions.androidSupport}",
24 | supportConstraintLayout: 'com.android.support.constraint:constraint-layout:1.1.0-beta4',
25 | supportRecyclerView : "com.android.support:recyclerview-v7:${versions.androidSupport}",
26 | mockitoCore : "org.mockito:mockito-core:${versions.mockito}",
27 | mockitoAndroid : "org.mockito:mockito-android:${versions.mockito}"
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/demo/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Movies
4 | Open details
5 | Play
6 | Favorite
7 |
8 | see options
9 | Open details
10 | Undo like movie
11 | Like movie
12 | Rate movie
13 | Rate movie
14 | 0.5 stars
15 | 1 star
16 | 1.5 stars
17 | 2 stars
18 | 2.5 stars
19 | 3 stars
20 | 3.5 stars
21 | 4 stars
22 | 4.5 stars
23 | 5 stars
24 |
25 |
26 |
--------------------------------------------------------------------------------
/demo/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion versions.androidSdk.compile
5 | buildToolsVersion versions.androidSdk.buildTools
6 |
7 | defaultConfig {
8 | applicationId "com.novoda.movies"
9 | minSdkVersion versions.androidSdk.min
10 | targetSdkVersion versions.androidSdk.target
11 | versionCode 1
12 | versionName "1.0"
13 |
14 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
15 | }
16 |
17 | buildTypes {
18 | release {
19 | minifyEnabled true
20 | proguardFiles getDefaultProguardFile('proguard-android.txt')
21 | }
22 | }
23 |
24 | compileOptions {
25 | sourceCompatibility JavaVersion.VERSION_1_8
26 | targetCompatibility JavaVersion.VERSION_1_8
27 | }
28 | }
29 |
30 | dependencies {
31 | compile libraries.accessibilitools
32 | compile libraries.supportAppCompatV7
33 | compile libraries.supportConstraintLayout
34 | compile libraries.supportRecyclerView
35 |
36 | debugCompile project(':extras')
37 | androidTestCompile(project(':core')) {
38 | exclude group: 'com.android.support', module: 'support-annotations'
39 | }
40 |
41 | androidTestCompile libraries.mockitoCore
42 | androidTestCompile libraries.mockitoAndroid
43 | androidTestCompile(libraries.androidTestRunner) {
44 | exclude group: 'com.android.support', module: 'support-annotations'
45 | }
46 | androidTestCompile(libraries.androidEspressoCore) {
47 | exclude group: 'com.android.support', module: 'support-annotations'
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/demo/src/main/java/com/novoda/movies/rateable/RateableMovieViewModelBuilder.java:
--------------------------------------------------------------------------------
1 | package com.novoda.movies.rateable;
2 |
3 | import android.support.annotation.DrawableRes;
4 |
5 | class RateableMovieViewModelBuilder {
6 |
7 | private long id;
8 | private String title;
9 | private float rating;
10 | private boolean liked;
11 | private int poster;
12 | private RateableMovieViewModel.UserActions actions;
13 |
14 | RateableMovieViewModelBuilder(long id, String title, @DrawableRes int poster) {
15 | this.id = id;
16 | this.title = title;
17 | this.poster = poster;
18 | this.liked = false;
19 | this.rating = 0;
20 | this.actions = RateableMovieViewModel.UserActions.NO_OP;
21 | }
22 |
23 | RateableMovieViewModelBuilder id(long id) {
24 | this.id = id;
25 | return this;
26 | }
27 |
28 | RateableMovieViewModelBuilder title(String title) {
29 | this.title = title;
30 | return this;
31 | }
32 |
33 | RateableMovieViewModelBuilder rating(float rating) {
34 | this.rating = rating;
35 | return this;
36 | }
37 |
38 | RateableMovieViewModelBuilder liked(boolean liked) {
39 | this.liked = liked;
40 | return this;
41 | }
42 |
43 | RateableMovieViewModelBuilder poster(@DrawableRes int poster) {
44 | this.poster = poster;
45 | return this;
46 | }
47 |
48 | RateableMovieViewModelBuilder actions(RateableMovieViewModel.UserActions actions) {
49 | this.actions = actions;
50 | return this;
51 | }
52 |
53 | RateableMovieViewModel build() {
54 | return new RateableMovieViewModel(id, title, rating, liked, poster, actions);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/extras/src/main/java/com/novoda/espresso/AccessibilityServiceTogglingActivity.java:
--------------------------------------------------------------------------------
1 | package com.novoda.espresso;
2 |
3 | import android.app.Activity;
4 | import android.content.Intent;
5 | import android.os.Bundle;
6 |
7 | import com.novoda.accessibility.Service;
8 |
9 | public class AccessibilityServiceTogglingActivity extends Activity {
10 |
11 | public static final String ACTION_DISABLE_ALL_SERVICES = "com.novoda.espresso.DISABLE_ALL_SERVICES";
12 | public static final String ACTION_SET_SERVICE = "com.novoda.espresso.SET_SERVICE";
13 | public static final String ENABLED = "enabled";
14 | public static final String DISABLED = "disabled";
15 |
16 | private AccessibilityServiceToggler serviceToggler;
17 |
18 | @Override
19 | protected void onCreate(Bundle savedInstanceState) {
20 | super.onCreate(savedInstanceState);
21 | serviceToggler = AccessibilityServiceToggler.create(getContentResolver());
22 |
23 | Intent intent = getIntent();
24 | String action = intent.getAction();
25 |
26 | if (ACTION_SET_SERVICE.equalsIgnoreCase(action)) {
27 | performAction(intent);
28 | } else if (ACTION_DISABLE_ALL_SERVICES.equalsIgnoreCase(action)) {
29 | serviceToggler.disableAll();
30 | }
31 |
32 | finish();
33 | }
34 |
35 | private void performAction(Intent intent) {
36 | for (Service service : Service.values()) {
37 | String value = intent.getStringExtra(service.name());
38 | if (ENABLED.equalsIgnoreCase(value)) {
39 | serviceToggler.enable(service);
40 | } else if (DISABLED.equalsIgnoreCase(value)) {
41 | serviceToggler.disable(service);
42 | }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/demo/src/androidTest/java/com/novoda/movies/ProgrammaticallyCreatedViewTest.java:
--------------------------------------------------------------------------------
1 | package com.novoda.movies;
2 |
3 | import android.content.Context;
4 | import android.support.test.filters.LargeTest;
5 | import android.support.test.runner.AndroidJUnit4;
6 | import android.view.ViewGroup;
7 | import android.widget.TextView;
8 |
9 | import com.novoda.espresso.ViewCreator;
10 | import com.novoda.espresso.ViewTestRule;
11 |
12 | import org.junit.Rule;
13 | import org.junit.Test;
14 | import org.junit.runner.RunWith;
15 |
16 | import static android.support.test.espresso.Espresso.onView;
17 | import static android.support.test.espresso.assertion.ViewAssertions.matches;
18 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
19 | import static android.support.test.espresso.matcher.ViewMatchers.withText;
20 | import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
21 |
22 | @RunWith(AndroidJUnit4.class)
23 | @LargeTest
24 | public class ProgrammaticallyCreatedViewTest {
25 |
26 | private static final String TEXT = "Hello world!";
27 |
28 | @Rule
29 | public ViewTestRule viewTestRule = new ViewTestRule<>(new TextViewCreator());
30 |
31 | @Test
32 | public void createdViewIsDisplayed() {
33 | onView(withText(TEXT)).check(matches(isDisplayed()));
34 | }
35 |
36 | private static class TextViewCreator implements ViewCreator {
37 |
38 | @Override
39 | public TextView createView(Context context, ViewGroup parentView) {
40 | TextView textView = new TextView(context);
41 | ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
42 | textView.setLayoutParams(layoutParams);
43 | textView.setText(TEXT);
44 | return textView;
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/demo/src/main/res/layout/merge_movie_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
17 |
18 |
27 |
28 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/demo/src/main/res/layout/item_view_rateable_movie.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
15 |
16 |
26 |
27 |
37 |
38 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/core/src/main/java/com/novoda/espresso/FontScaleTestRule.java:
--------------------------------------------------------------------------------
1 | package com.novoda.espresso;
2 |
3 | import android.os.SystemClock;
4 |
5 | import org.junit.rules.TestRule;
6 | import org.junit.runner.Description;
7 | import org.junit.runners.model.Statement;
8 |
9 | class FontScaleTestRule implements TestRule {
10 |
11 | private final FontScaleSetting fontScaleSetting;
12 | private final FontScale fontScale;
13 |
14 | FontScaleTestRule(FontScaleSetting fontScaleSetting, FontScale fontScale) {
15 | this.fontScaleSetting = fontScaleSetting;
16 | this.fontScale = fontScale;
17 | }
18 |
19 | @Override
20 | public Statement apply(Statement base, Description description) {
21 | return new FontScaleStatement(base, fontScaleSetting, fontScale);
22 | }
23 |
24 | private static class FontScaleStatement extends Statement {
25 |
26 | private static final int SLEEP_TO_WAIT_FOR_SETTING_MILLIS = 100;
27 | private static final int MAX_RETRIES_TO_WAIT_FOR_SETTING = 15;
28 |
29 | private final Statement baseStatement;
30 | private final FontScaleSetting scaleSetting;
31 | private final FontScale scale;
32 |
33 | FontScaleStatement(Statement baseStatement, FontScaleSetting scaleSetting, FontScale scale) {
34 | this.baseStatement = baseStatement;
35 | this.scaleSetting = scaleSetting;
36 | this.scale = scale;
37 | }
38 |
39 | @Override
40 | public void evaluate() throws Throwable {
41 | FontScale initialScale = scaleSetting.get();
42 | scaleSetting.set(scale);
43 | sleepUntil(scaleMatches(scale));
44 |
45 | baseStatement.evaluate();
46 |
47 | scaleSetting.set(initialScale);
48 | sleepUntil(scaleMatches(initialScale));
49 | }
50 |
51 | private Condition scaleMatches(final FontScale scale) {
52 | return new Condition() {
53 | @Override
54 | public boolean holds() {
55 | return scaleSetting.get() == scale;
56 | }
57 | };
58 | }
59 |
60 | private void sleepUntil(Condition condition) {
61 | int retries = 0;
62 | while (!condition.holds()) {
63 | SystemClock.sleep(SLEEP_TO_WAIT_FOR_SETTING_MILLIS);
64 | if (retries == MAX_RETRIES_TO_WAIT_FOR_SETTING) {
65 | throw timeoutError();
66 | }
67 | retries++;
68 | }
69 | }
70 |
71 | private AssertionError timeoutError() {
72 | return new AssertionError("Spent too long waiting trying to set scale.");
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/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 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
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 Windows 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 |
--------------------------------------------------------------------------------
/demo/src/main/java/com/novoda/movies/rateable/RateableMoviesActivity.java:
--------------------------------------------------------------------------------
1 | package com.novoda.movies.rateable;
2 |
3 | import android.os.Bundle;
4 | import android.support.annotation.Nullable;
5 | import android.support.v7.app.AppCompatActivity;
6 | import android.support.v7.widget.LinearLayoutManager;
7 | import android.support.v7.widget.RecyclerView;
8 | import android.widget.Toast;
9 |
10 | import com.novoda.movies.R;
11 |
12 | import java.util.ArrayList;
13 | import java.util.List;
14 |
15 | public class RateableMoviesActivity extends AppCompatActivity {
16 |
17 | private final FakeMovieRepository fakeMovieRepository = new FakeMovieRepository();
18 | private final RateableMoviesAdapter rateableMoviesAdapter = new RateableMoviesAdapter();
19 |
20 | @Override
21 | protected void onCreate(@Nullable Bundle savedInstanceState) {
22 | super.onCreate(savedInstanceState);
23 | setContentView(R.layout.activity_rateable_movies);
24 |
25 | RecyclerView recyclerView = findViewById(R.id.rateable_movies_recycler_view);
26 | recyclerView.setLayoutManager(new LinearLayoutManager(this));
27 | recyclerView.setAdapter(rateableMoviesAdapter);
28 |
29 | fakeMovieRepository.observeChanges(observer);
30 | }
31 |
32 | private final FakeMovieRepository.Observer observer = new FakeMovieRepository.Observer() {
33 |
34 | @Override
35 | public void onUpdated(List movies) {
36 | List viewModels = new ArrayList<>(movies.size());
37 | for (Movie movie : movies) {
38 | viewModels.add(convertToViewModel(movie));
39 | }
40 | rateableMoviesAdapter.update(viewModels);
41 | }
42 |
43 | private RateableMovieViewModel convertToViewModel(Movie movie) {
44 | return new RateableMovieViewModelBuilder(movie.id, movie.title, movie.poster)
45 | .rating(movie.rating)
46 | .liked(movie.liked)
47 | .actions(new RateableMovieViewModel.UserActions() {
48 | @Override
49 | public void onSelectMovie() {
50 | Toast.makeText(RateableMoviesActivity.this, "onSelect: " + movie.title, Toast.LENGTH_SHORT).show();
51 | }
52 |
53 | @Override
54 | public void onToggleLike() {
55 | fakeMovieRepository.onToggleLike(movie.id);
56 | }
57 |
58 | @Override
59 | public void onRate(float rating) {
60 | fakeMovieRepository.onRate(movie.id, rating);
61 | }
62 | }).build();
63 | }
64 | };
65 | }
66 |
--------------------------------------------------------------------------------
/core/src/main/java/com/novoda/espresso/AccessibilityRules.java:
--------------------------------------------------------------------------------
1 | package com.novoda.espresso;
2 |
3 | import android.accessibilityservice.AccessibilityServiceInfo;
4 | import android.content.Context;
5 | import android.content.Intent;
6 | import android.support.test.InstrumentationRegistry;
7 | import android.view.accessibility.AccessibilityManager;
8 |
9 | import com.novoda.accessibility.Service;
10 |
11 | import org.junit.rules.TestRule;
12 |
13 | import java.util.List;
14 |
15 | public class AccessibilityRules {
16 |
17 | public static TestRule createTalkBackTestRule() {
18 | return new AccessibilityServiceTestRule(createHelper(Service.TALKBACK));
19 | }
20 |
21 | public static TestRule createSwitchAccessTestRule() {
22 | return new AccessibilityServiceTestRule(createHelper(Service.SWITCH_ACCESS));
23 | }
24 |
25 | public static TestRule createSelectToSpeakTestRule() {
26 | return new AccessibilityServiceTestRule(createHelper(Service.SELECT_TO_SPEAK));
27 | }
28 |
29 | private static AccessibilityServiceTestRule.Helper createHelper(final Service service) {
30 | final Context context = InstrumentationRegistry.getTargetContext();
31 | final AccessibilityManager a11yManager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
32 |
33 | return new AccessibilityServiceTestRule.Helper() {
34 | @Override
35 | public String serviceName() {
36 | return service.name();
37 | }
38 |
39 | @Override
40 | public void requestEnableService() {
41 | requestState(AccessibilityServiceTogglingActivity.ENABLED);
42 | }
43 |
44 | @Override
45 | public void requestDisableService() {
46 | requestState(AccessibilityServiceTogglingActivity.DISABLED);
47 | }
48 |
49 | private void requestState(String value) {
50 | Intent intent = new Intent(AccessibilityServiceTogglingActivity.ACTION_SET_SERVICE);
51 | intent.putExtra(service.name(), value);
52 | context.startActivity(intent);
53 | }
54 |
55 | @Override
56 | public boolean reportsEnabled() {
57 | List enabledAccessibilityServiceList = a11yManager.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK);
58 | for (AccessibilityServiceInfo accessibilityServiceInfo : enabledAccessibilityServiceList) {
59 | if (accessibilityServiceInfo.getId().equals(service.flattenedComponentName())) {
60 | return true;
61 | }
62 | }
63 | return false;
64 | }
65 | };
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/core/src/main/java/com/novoda/espresso/AccessibilityServiceTestRule.java:
--------------------------------------------------------------------------------
1 | package com.novoda.espresso;
2 |
3 | import android.os.SystemClock;
4 |
5 | import org.junit.rules.TestRule;
6 | import org.junit.runner.Description;
7 | import org.junit.runners.model.Statement;
8 |
9 | class AccessibilityServiceTestRule implements TestRule {
10 |
11 | private final Helper helper;
12 |
13 | AccessibilityServiceTestRule(Helper helper) {
14 | this.helper = helper;
15 | }
16 |
17 | @Override
18 | public Statement apply(Statement base, Description description) {
19 | return new AccessibilityServiceStatement(base, helper);
20 | }
21 |
22 | private static class AccessibilityServiceStatement extends Statement {
23 |
24 | private static final int SLEEP_TO_WAIT_FOR_TOGGLE_MILLIS = 100;
25 | private static final int MAX_RETRIES_TO_WAIT_FOR_TOGGLE = 15;
26 |
27 | private final Statement baseStatement;
28 | private final Helper helper;
29 |
30 | AccessibilityServiceStatement(Statement baseStatement, Helper helper) {
31 | this.baseStatement = baseStatement;
32 | this.helper = helper;
33 | }
34 |
35 | @Override
36 | public void evaluate() throws Throwable {
37 | helper.requestEnableService();
38 | sleepUntil(serviceIsEnabled());
39 |
40 | baseStatement.evaluate();
41 |
42 | helper.requestDisableService();
43 | sleepUntil(serviceIsDisabled());
44 | }
45 |
46 | private void sleepUntil(Condition condition) {
47 | int retries = 0;
48 | while (!condition.holds()) {
49 | SystemClock.sleep(SLEEP_TO_WAIT_FOR_TOGGLE_MILLIS);
50 | if (retries == MAX_RETRIES_TO_WAIT_FOR_TOGGLE) {
51 | throw serviceToggleTimeOutError();
52 | }
53 | retries++;
54 | }
55 | }
56 |
57 | private Condition serviceIsEnabled() {
58 | return new Condition() {
59 | @Override
60 | public boolean holds() {
61 | return helper.reportsEnabled();
62 | }
63 | };
64 | }
65 |
66 | private Condition serviceIsDisabled() {
67 | return new Condition() {
68 | @Override
69 | public boolean holds() {
70 | return !helper.reportsEnabled();
71 | }
72 | };
73 | }
74 |
75 | private AssertionError serviceToggleTimeOutError() {
76 | return new AssertionError("Spent too long waiting for " + helper.serviceName() + " to toggle.");
77 | }
78 | }
79 |
80 | interface Helper {
81 |
82 | String serviceName();
83 |
84 | void requestEnableService();
85 |
86 | void requestDisableService();
87 |
88 | boolean reportsEnabled();
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/extras/src/main/java/com/novoda/espresso/AccessibilityServiceToggler.java:
--------------------------------------------------------------------------------
1 | package com.novoda.espresso;
2 |
3 | import android.content.ContentResolver;
4 | import android.provider.Settings;
5 |
6 | import com.novoda.accessibility.Service;
7 |
8 | class AccessibilityServiceToggler {
9 |
10 | private static final String EMPTY_STRING = "";
11 | private static final String SERVICES_SEPARATOR = ":";
12 |
13 | private final SecureSettings secureSettings;
14 |
15 | static AccessibilityServiceToggler create(ContentResolver contentResolver) {
16 | return new AccessibilityServiceToggler(new SecureSettings(contentResolver));
17 | }
18 |
19 | AccessibilityServiceToggler(SecureSettings secureSettings) {
20 | this.secureSettings = secureSettings;
21 | }
22 |
23 | void enable(Service service) {
24 | String enabledServices = secureSettings.enabledAccessibilityServices();
25 | if (enabledServices.contains(service.flattenedComponentName())) {
26 | return;
27 | }
28 | secureSettings.enabledAccessibilityServices(enabledServices + SERVICES_SEPARATOR + service.flattenedComponentName());
29 | }
30 |
31 | void disable(Service service) {
32 | String enabledServices = secureSettings.enabledAccessibilityServices();
33 | if (!enabledServices.contains(service.flattenedComponentName())) {
34 | return;
35 | }
36 |
37 | String remainingServices = enabledServices
38 | .replace(SERVICES_SEPARATOR + service.flattenedComponentName(), EMPTY_STRING)
39 | .replace(service.flattenedComponentName() + SERVICES_SEPARATOR, EMPTY_STRING)
40 | .replace(service.flattenedComponentName(), EMPTY_STRING);
41 | secureSettings.enabledAccessibilityServices(remainingServices);
42 | }
43 |
44 | void disableAll() {
45 | secureSettings.enabledAccessibilityServices(EMPTY_STRING);
46 | }
47 |
48 | static class SecureSettings {
49 |
50 | private static final String VALUE_DISABLED = "0";
51 | private static final String VALUE_ENABLED = "1";
52 |
53 | private final ContentResolver contentResolver;
54 |
55 | SecureSettings(ContentResolver contentResolver) {
56 | this.contentResolver = contentResolver;
57 | }
58 |
59 | String enabledAccessibilityServices() {
60 | return Settings.Secure.getString(contentResolver, Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
61 | }
62 |
63 | void enabledAccessibilityServices(String services) {
64 | Settings.Secure.putString(contentResolver, Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, services);
65 | accessibilityEnabled(services.isEmpty() ? VALUE_DISABLED : VALUE_ENABLED);
66 | }
67 |
68 | private void accessibilityEnabled(String enabled) {
69 | Settings.Secure.putString(contentResolver, Settings.Secure.ACCESSIBILITY_ENABLED, enabled);
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/demo/src/main/java/com/novoda/movies/rateable/FakeMovieRepository.java:
--------------------------------------------------------------------------------
1 | package com.novoda.movies.rateable;
2 |
3 | import android.support.annotation.DrawableRes;
4 |
5 | import com.novoda.movies.R;
6 |
7 | import java.util.ArrayList;
8 | import java.util.Arrays;
9 | import java.util.List;
10 |
11 | class FakeMovieRepository {
12 |
13 | private final List movies = initialFetchFromSomewhere();
14 |
15 | private Observer observer;
16 |
17 | void observeChanges(Observer observer) {
18 | this.observer = observer;
19 | observer.onUpdated(movies);
20 | }
21 |
22 | void onRate(long id, float newRating) {
23 | List updated = new ArrayList<>();
24 | for (Movie movie : movies) {
25 | if (id == movie.id) {
26 | updated.add(new Movie(movie.id, movie.title, newRating, movie.liked, movie.poster));
27 | } else {
28 | updated.add(new Movie(movie));
29 | }
30 | }
31 | updateWith(updated);
32 | }
33 |
34 | void onToggleLike(long id) {
35 | List updated = new ArrayList<>();
36 | for (Movie movie : movies) {
37 | if (id == movie.id) {
38 | updated.add(new Movie(movie.id, movie.title, movie.rating, !movie.liked, movie.poster));
39 | } else {
40 | updated.add(new Movie(movie));
41 | }
42 | }
43 | updateWith(updated);
44 | }
45 |
46 | private void updateWith(List updated) {
47 | movies.clear();
48 | movies.addAll(updated);
49 | observer.onUpdated(updated);
50 | }
51 |
52 | private static List initialFetchFromSomewhere() {
53 | return new ArrayList<>(Arrays.asList(
54 | createMovie("Arrival", 4.5f, true, R.drawable.movie_arrival),
55 | createMovie("Interstellar", 4.5f, true, R.drawable.movie_interstellar),
56 | createMovie("The Royal Tenenbaums", 5, true, R.drawable.movie_royaltenenbaums),
57 | createMovie("Whiplash", 4.5f, true, R.drawable.movie_whiplash),
58 | createMovie("Beetlejuice", 5, false, R.drawable.movie_beetlejuice),
59 | createMovie("Iron Giant", 4.5f, true, R.drawable.movie_irongiant),
60 | createMovie("Million Dollar Baby", 4.5f, true, R.drawable.movie_milliondollarbaby),
61 | createMovie("Take Shelter", 4.5f, true, R.drawable.movie_takeshelter),
62 | createMovie("Planes, Trains and Automobiles", 4, true, R.drawable.movie_planestrainsautomobiles),
63 | createMovie("Fantastic Mr Fox", 4, false, R.drawable.movie_fantasticmrfox)
64 | ));
65 | }
66 |
67 | private static Movie createMovie(String title, float rating, boolean liked, @DrawableRes int poster) {
68 | return new Movie(title.hashCode(), title, rating, liked, poster);
69 | }
70 |
71 | interface Observer {
72 |
73 | void onUpdated(List viewModels);
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/core/src/main/java/com/novoda/espresso/ViewTestRule.java:
--------------------------------------------------------------------------------
1 | package com.novoda.espresso;
2 |
3 | import android.app.Activity;
4 | import android.app.Instrumentation;
5 | import android.support.annotation.LayoutRes;
6 | import android.support.test.InstrumentationRegistry;
7 | import android.support.test.rule.ActivityTestRule;
8 | import android.view.View;
9 | import android.view.ViewGroup;
10 |
11 | import com.novoda.espresso.core.R;
12 |
13 | import org.hamcrest.Description;
14 | import org.hamcrest.Matcher;
15 | import org.hamcrest.TypeSafeMatcher;
16 |
17 | public class ViewTestRule extends ActivityTestRule {
18 |
19 | private final Instrumentation instrumentation;
20 | private final ViewCreator viewCreator;
21 |
22 | private T view;
23 |
24 | public ViewTestRule(@LayoutRes int layoutId) {
25 | this(new InflateFromXmlViewCreator(layoutId));
26 | }
27 |
28 | public ViewTestRule(ViewCreator viewCreator) {
29 | this(InstrumentationRegistry.getInstrumentation(), viewCreator);
30 | }
31 |
32 | protected ViewTestRule(Instrumentation instrumentation, ViewCreator viewCreator) {
33 | super(EmptyActivity.class);
34 | this.instrumentation = instrumentation;
35 | this.viewCreator = viewCreator;
36 | }
37 |
38 | @Override
39 | protected void afterActivityLaunched() {
40 | super.afterActivityLaunched();
41 | final EmptyActivity activity = getActivity();
42 | createView(activity);
43 | runOnMainSynchronously(new Runner() {
44 | @Override
45 | public void run(T view) {
46 | activity.setContentView(view, view.getLayoutParams());
47 | }
48 | });
49 | }
50 |
51 | private void createView(final Activity activity) {
52 | instrumentation.runOnMainSync(new Runnable() {
53 | @Override
54 | public void run() {
55 | view = viewCreator.createView(activity, (ViewGroup) activity.findViewById(android.R.id.content));
56 | view.setTag(R.id.espresso_support__view_test_rule, true);
57 | }
58 | });
59 | }
60 |
61 | public void runOnMainSynchronously(final Runner runner) {
62 | instrumentation.runOnMainSync(new Runnable() {
63 | @Override
64 | public void run() {
65 | runner.run(view);
66 | }
67 | });
68 | }
69 |
70 | public T getView() {
71 | return view;
72 | }
73 |
74 | public interface Runner {
75 |
76 | void run(T view);
77 | }
78 |
79 | public static Matcher underTest() {
80 | return new TypeSafeMatcher() {
81 | @Override
82 | protected boolean matchesSafely(View item) {
83 | Object tag = item.getTag(R.id.espresso_support__view_test_rule);
84 | return tag != null && ((Boolean) tag);
85 | }
86 |
87 | @Override
88 | public void describeTo(Description description) {
89 | description.appendText("is View under test, managed by this " + ViewTestRule.class.getSimpleName());
90 | }
91 | };
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/demo/src/androidTest/java/com/novoda/movies/MovieItemViewTest.java:
--------------------------------------------------------------------------------
1 | package com.novoda.movies;
2 |
3 | import android.support.test.filters.LargeTest;
4 | import android.support.test.runner.AndroidJUnit4;
5 |
6 | import com.novoda.espresso.ViewTestRule;
7 |
8 | import org.junit.After;
9 | import org.junit.Before;
10 | import org.junit.Rule;
11 | import org.junit.Test;
12 | import org.junit.runner.RunWith;
13 | import org.mockito.Mock;
14 | import org.mockito.junit.MockitoJUnit;
15 | import org.mockito.junit.MockitoRule;
16 |
17 | import static android.support.test.espresso.Espresso.onView;
18 | import static android.support.test.espresso.action.ViewActions.click;
19 | import static android.support.test.espresso.matcher.ViewMatchers.withClassName;
20 | import static android.support.test.espresso.matcher.ViewMatchers.withId;
21 | import static org.hamcrest.CoreMatchers.is;
22 | import static org.mockito.Matchers.eq;
23 | import static org.mockito.Mockito.never;
24 | import static org.mockito.Mockito.verify;
25 |
26 | @RunWith(AndroidJUnit4.class)
27 | @LargeTest
28 | public class MovieItemViewTest {
29 |
30 | private static final Movie EDWARD_SCISSORHANDS = new Movie("Edward Scissorhands");
31 | private static final Movie NOT_EDWARD_SCISSORHANDS = new Movie("NOT Edward Scissorhands");
32 |
33 | @Rule
34 | public MockitoRule mockitoRule = MockitoJUnit.rule();
35 |
36 | @Rule
37 | public ViewTestRule viewTestRule = new ViewTestRule<>(R.layout.test_movie_item_view);
38 |
39 | @Mock
40 | MovieItemView.Listener movieItemListener;
41 |
42 | @Before
43 | public void setUp() {
44 | MovieItemView view = viewTestRule.getView();
45 |
46 | view.attachListener(movieItemListener);
47 |
48 | givenMovieItemViewIsBoundTo(EDWARD_SCISSORHANDS);
49 | }
50 |
51 | @After
52 | public void tearDown() {
53 | viewTestRule.getView().detachListeners();
54 | }
55 |
56 | @Test
57 | public void clickMovieItemView() {
58 | onView(withClassName(is(MovieItemView.class.getName()))).perform(click());
59 |
60 | verify(movieItemListener).onClick(eq(EDWARD_SCISSORHANDS));
61 | }
62 |
63 | @Test
64 | public void clickMovieFavoriteView() {
65 | onView(withId(R.id.movie_item_button_favorite)).perform(click());
66 |
67 | verify(movieItemListener).onClickFavorite(eq(EDWARD_SCISSORHANDS));
68 | }
69 |
70 | @Test
71 | public void clickMoviePlayView() {
72 | onView(withId(R.id.movie_item_button_play)).perform(click());
73 |
74 | verify(movieItemListener).onClickPlay(eq(EDWARD_SCISSORHANDS));
75 | }
76 |
77 | @Test
78 | public void rebindWithDifferentMovie_noClickMoviePlayView() {
79 | givenMovieItemViewIsBoundTo(NOT_EDWARD_SCISSORHANDS);
80 |
81 | onView(withId(R.id.movie_item_button_play)).perform(click());
82 |
83 | verify(movieItemListener, never()).onClickPlay(eq(EDWARD_SCISSORHANDS));
84 | }
85 |
86 | private void givenMovieItemViewIsBoundTo(final Movie movie) {
87 | viewTestRule.runOnMainSynchronously(new ViewTestRule.Runner() {
88 | @Override
89 | public void run(MovieItemView view) {
90 | view.bind(movie);
91 | }
92 | });
93 | }
94 |
95 | }
96 |
--------------------------------------------------------------------------------
/demo/src/androidTest/java/com/novoda/movies/MovieItemViewTalkBackTest.java:
--------------------------------------------------------------------------------
1 | package com.novoda.movies;
2 |
3 | import android.support.test.filters.LargeTest;
4 | import android.support.test.runner.AndroidJUnit4;
5 |
6 | import com.novoda.espresso.AccessibilityRules;
7 | import com.novoda.espresso.ViewTestRule;
8 |
9 | import org.junit.After;
10 | import org.junit.Before;
11 | import org.junit.Rule;
12 | import org.junit.Test;
13 | import org.junit.rules.RuleChain;
14 | import org.junit.runner.RunWith;
15 | import org.mockito.Mock;
16 | import org.mockito.junit.MockitoJUnit;
17 | import org.mockito.junit.MockitoRule;
18 |
19 | import static android.support.test.espresso.Espresso.onView;
20 | import static android.support.test.espresso.action.ViewActions.click;
21 | import static android.support.test.espresso.assertion.ViewAssertions.matches;
22 | import static android.support.test.espresso.matcher.ViewMatchers.*;
23 | import static com.novoda.espresso.AccessibilityViewMatchers.withUsageHintOnClick;
24 | import static com.novoda.espresso.AccessibilityViewMatchers.withUsageHintOnLongClick;
25 | import static org.hamcrest.CoreMatchers.is;
26 |
27 | @RunWith(AndroidJUnit4.class)
28 | @LargeTest
29 | public class MovieItemViewTalkBackTest {
30 |
31 | private static final Movie EDWARD_SCISSORHANDS = new Movie("Edward Scissorhands");
32 |
33 | @Rule
34 | public MockitoRule mockitoRule = MockitoJUnit.rule();
35 |
36 | private ViewTestRule viewTestRule = new ViewTestRule<>(R.layout.test_movie_item_view);
37 |
38 | @Rule
39 | public RuleChain chain = RuleChain.outerRule(AccessibilityRules.createTalkBackTestRule()).around(viewTestRule);
40 |
41 | @Mock
42 | MovieItemView.Listener movieItemListener;
43 |
44 | @Before
45 | public void setUp() {
46 | MovieItemView view = viewTestRule.getView();
47 |
48 | view.attachListener(movieItemListener);
49 |
50 | givenMovieItemViewIsBoundTo(EDWARD_SCISSORHANDS);
51 | }
52 |
53 | @After
54 | public void tearDown() {
55 | viewTestRule.getView().detachListeners();
56 | }
57 |
58 | @Test
59 | public void clickMovieItemView() {
60 | onView(withClassName(is(MovieItemView.class.getName()))).perform(click());
61 |
62 | checkMenuDisplayed();
63 | }
64 |
65 | @Test
66 | public void movieItemViewHasCustomUsageHintOnClick() {
67 | onView(withClassName(is(MovieItemView.class.getName())))
68 | .check(matches(withUsageHintOnClick("see actions")));
69 | }
70 |
71 | @Test
72 | public void movieItemViewHasCustomUsageHintOnLongClick() {
73 | onView(withClassName(is(MovieItemView.class.getName())))
74 | .check(matches(withUsageHintOnLongClick("open details")));
75 | }
76 |
77 | private void givenMovieItemViewIsBoundTo(final Movie movie) {
78 | viewTestRule.runOnMainSynchronously(new ViewTestRule.Runner() {
79 | @Override
80 | public void run(MovieItemView view) {
81 | view.bind(movie);
82 | }
83 | });
84 | }
85 |
86 | private void checkMenuDisplayed() {
87 | checkViewsWithTextDisplayed(
88 | R.string.action_click_movie,
89 | R.string.action_click_play_movie,
90 | R.string.action_click_favorite_movie
91 | );
92 | }
93 |
94 | private void checkViewsWithTextDisplayed(int... ids) {
95 | for (int id : ids) {
96 | onView(withText(id)).check(matches(isDisplayed()));
97 | }
98 | }
99 |
100 | }
101 |
--------------------------------------------------------------------------------
/extras/src/test/java/com/novoda/espresso/AccessibilityServiceTogglerTest.java:
--------------------------------------------------------------------------------
1 | package com.novoda.espresso;
2 |
3 |
4 | import com.novoda.accessibility.Service;
5 |
6 | import org.junit.Test;
7 | import org.junit.experimental.runners.Enclosed;
8 | import org.junit.runner.RunWith;
9 | import org.junit.runners.Parameterized;
10 |
11 | import static org.mockito.ArgumentMatchers.anyString;
12 | import static org.mockito.ArgumentMatchers.endsWith;
13 | import static org.mockito.BDDMockito.given;
14 | import static org.mockito.Mockito.mock;
15 | import static org.mockito.Mockito.never;
16 | import static org.mockito.Mockito.verify;
17 |
18 | @RunWith(Enclosed.class)
19 | public class AccessibilityServiceTogglerTest {
20 |
21 | private static abstract class Base {
22 |
23 | final AccessibilityServiceToggler.SecureSettings secureSettings = mock(AccessibilityServiceToggler.SecureSettings.class);
24 | final AccessibilityServiceToggler serviceToggler = new AccessibilityServiceToggler(secureSettings);
25 | }
26 |
27 | public static class NoParameters extends Base {
28 |
29 | @Test
30 | public void disableAll() {
31 | serviceToggler.disableAll();
32 |
33 | verify(secureSettings).enabledAccessibilityServices("");
34 | }
35 | }
36 |
37 | @RunWith(Parameterized.class)
38 | public static class ForEachService extends Base {
39 |
40 | @Parameterized.Parameters
41 | public static Service[] data() {
42 | return Service.values();
43 | }
44 |
45 | private final Service service;
46 |
47 | public ForEachService(Service service) {
48 | this.service = service;
49 | }
50 |
51 | @Test
52 | public void whenCallingEnable_thenAppendsServiceToList() {
53 | given(secureSettings.enabledAccessibilityServices()).willReturn("foo");
54 |
55 | serviceToggler.enable(service);
56 |
57 | verify(secureSettings).enabledAccessibilityServices(endsWith(":" + service.flattenedComponentName()));
58 | }
59 |
60 | @Test
61 | public void givenAlreadyEnabled_whenCallingEnable_thenDoesNotModifyState() {
62 | given(secureSettings.enabledAccessibilityServices()).willReturn(service.flattenedComponentName());
63 |
64 | serviceToggler.enable(service);
65 |
66 | verify(secureSettings, never()).enabledAccessibilityServices(anyString());
67 | }
68 |
69 | @Test
70 | public void givenServiceEnabledLastAmongOthers_whenCallingDisable_thenRemovesServiceFromList() {
71 | given(secureSettings.enabledAccessibilityServices()).willReturn("foo:" + service.flattenedComponentName());
72 |
73 | serviceToggler.disable(service);
74 |
75 | verify(secureSettings).enabledAccessibilityServices("foo");
76 | }
77 |
78 | @Test
79 | public void givenServiceEnabledFirstAmongOthers_whenCallingDisable_thenRemovesServiceFromList() {
80 | given(secureSettings.enabledAccessibilityServices()).willReturn(service.flattenedComponentName() + ":foo");
81 |
82 | serviceToggler.disable(service);
83 |
84 | verify(secureSettings).enabledAccessibilityServices("foo");
85 | }
86 |
87 | @Test
88 | public void givenServiceEnabledAmongOthers_whenCallingDisable_thenRemovesServiceFromList() {
89 | given(secureSettings.enabledAccessibilityServices()).willReturn("foo:" + service.flattenedComponentName() + ":bar");
90 |
91 | serviceToggler.disable(service);
92 |
93 | verify(secureSettings).enabledAccessibilityServices("foo:bar");
94 | }
95 |
96 | @Test
97 | public void givenServiceEnabledIsOnlyOneEnabled_whenCallingDisable_thenSetsEmptyList() {
98 | given(secureSettings.enabledAccessibilityServices()).willReturn(service.flattenedComponentName());
99 |
100 | serviceToggler.disable(service);
101 |
102 | verify(secureSettings).enabledAccessibilityServices("");
103 | }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/demo/src/androidTest/java/com/novoda/movies/rateable/TouchMode_RateableMovieViewHolderTest.java:
--------------------------------------------------------------------------------
1 | package com.novoda.movies.rateable;
2 |
3 | import android.support.test.espresso.UiController;
4 | import android.support.test.espresso.ViewAction;
5 | import android.support.test.filters.LargeTest;
6 | import android.support.test.runner.AndroidJUnit4;
7 | import android.view.View;
8 | import android.widget.RatingBar;
9 |
10 | import com.novoda.espresso.ViewTestRule;
11 | import com.novoda.movies.R;
12 |
13 | import org.hamcrest.Matcher;
14 | import org.junit.Before;
15 | import org.junit.Rule;
16 | import org.junit.Test;
17 | import org.junit.runner.RunWith;
18 |
19 | import static android.support.test.espresso.Espresso.onView;
20 | import static android.support.test.espresso.action.ViewActions.click;
21 | import static android.support.test.espresso.assertion.ViewAssertions.matches;
22 | import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
23 | import static android.support.test.espresso.matcher.ViewMatchers.withId;
24 | import static android.support.test.espresso.matcher.ViewMatchers.withText;
25 | import static com.novoda.espresso.ViewTestRule.underTest;
26 | import static com.novoda.movies.rateable.RateableMovieViewModelFixtures.viewModel;
27 | import static org.mockito.Mockito.mock;
28 | import static org.mockito.Mockito.verify;
29 |
30 | @RunWith(AndroidJUnit4.class)
31 | @LargeTest
32 | public class TouchMode_RateableMovieViewHolderTest {
33 |
34 | @Rule
35 | public ViewTestRule viewTestRule = new ViewTestRule<>(R.layout.item_view_rateable_movie);
36 |
37 | private RateableMovieViewHolder rateableMovieViewHolder;
38 | private RateableMovieViewModel.UserActions userActions = mock(RateableMovieViewModel.UserActions.class);
39 |
40 | @Before
41 | public void setUp() {
42 | rateableMovieViewHolder = new RateableMovieViewHolder(viewTestRule.getView());
43 | }
44 |
45 | @Test
46 | public void bindsTitle() {
47 | RateableMovieViewModel viewModel = viewModel().title("Arrival").rating(5).liked(true).build();
48 |
49 | viewTestRule.runOnMainSynchronously(view -> rateableMovieViewHolder.bind(viewModel));
50 |
51 | onView(withId(R.id.item_rateable_text_title)).check(matches(withText("Arrival")));
52 | }
53 |
54 | @Test
55 | public void bindsOnSelectAction() {
56 | RateableMovieViewModel viewModel = viewModel(userActions).build();
57 | viewTestRule.runOnMainSynchronously(view -> rateableMovieViewHolder.bind(viewModel));
58 |
59 | onView(underTest()).perform(click());
60 |
61 | verify(userActions).onSelectMovie();
62 | }
63 |
64 | @Test
65 | public void bindsOnToggleLikeAction() {
66 | RateableMovieViewModel viewModel = viewModel(userActions).build();
67 | viewTestRule.runOnMainSynchronously(view -> rateableMovieViewHolder.bind(viewModel));
68 |
69 | onView(withId(R.id.item_rateable_image_like)).perform(click());
70 |
71 | verify(userActions).onToggleLike();
72 | }
73 |
74 | @Test
75 | public void bindsOnRateAction() {
76 | RateableMovieViewModel viewModel = viewModel(userActions).build();
77 | viewTestRule.runOnMainSynchronously(view -> rateableMovieViewHolder.bind(viewModel));
78 |
79 | onView(withId(R.id.item_rateable_rating)).perform(setRating(4.5f));
80 |
81 | verify(userActions).onRate(4.5f);
82 | }
83 |
84 | private static ViewAction setRating(float rating) {
85 | if (rating % 0.5 != 0) {
86 | throw new IllegalArgumentException("Rating must be multiple of 0.5f");
87 | }
88 |
89 | return new ViewAction() {
90 | @Override
91 | public Matcher getConstraints() {
92 | return isAssignableFrom(RatingBar.class);
93 | }
94 |
95 | @Override
96 | public String getDescription() {
97 | return "Set rating on RatingBar in 0.5f increments";
98 | }
99 |
100 | @Override
101 | public void perform(UiController uiController, View view) {
102 | ((RatingBar) view).setRating(rating);
103 | }
104 | };
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🛑 THIS REPOSITORY IS OFFICIALLY NO LONGER UNDER MAINTENANCE since 10/02/2022 🛑
2 |
3 | # espresso-support [](https://bintray.com/novoda/maven/espresso-support/_latestVersion) [](LICENSE)
4 |
5 | This library includes components which facilitate [testing Views in isolation](https://www.novoda.com/blog/testing-views-in-isolation-with-espresso/) and [running Espresso tests with Google TalkBack enabled](https://www.novoda.com/blog/testing-talkback-in-isolation-with-espresso/).
6 |
7 | - it allows you to inflate layouts/instantiate Views in their own Activity
8 | - adds automatic toggling of TalkBack before/after each test to assert custom behavior
9 |
10 | ## Usage
11 |
12 | The library is split into two artifacts, available on JCenter.
13 |
14 | - `core` includes most of this library's classes and functions. It should be included as an `androidTestCompile` dependency, since you'll only use it for instrumentation tests.
15 | - `extras` include two Activity components that are used by `core`. It's not enough to use `androidTestCompile` for this because that will include it as part of the test APK that _instruments_ your app; they must be part of the app _under test_. Use `debugCompile` or `Compile` to include this dependency.
16 |
17 | ```
18 | androidTestCompile 'com.novoda:espresso-support:'
19 | debugCompile 'com.novoda:espresso-support-extras:'
20 | ```
21 |
22 | See `demo/build.gradle` for examples.
23 |
24 | ## Testing views in isolation
25 |
26 | Use the `ViewTestRule`, passing in a layout resource. It'll inflate the resource into the `ViewActivity` with `MATCH_PARENT` for both dimensions. You can use `rule.getView()` to obtain an instance of the View and it'll be typed to the class you specified.
27 |
28 | ```java
29 | @Rule
30 | public ViewTestRule rule = new ViewTestRule<>(R.layout.test_movie_item_view);
31 | ```
32 |
33 | You can write BDD style tests here, highlighting the expected behavior for your custom views, using a mixture of Espresso ViewActions and Mockito verifies:
34 |
35 | ```java
36 | @Test
37 | public void givenViewIsUpdatedWithDifferentMovie_whenClicking_thenListenerDoesNotGetFiredForOriginalMovie() {
38 | givenMovieItemViewIsBoundTo(EDWARD_SCISSORHANDS);
39 | givenMovieItemViewIsBoundTo(NOT_EDWARD_SCISSORHANDS);
40 |
41 | onView(withId(R.id.movie_item_button_play)).perform(click());
42 |
43 | verify(movieItemListener, never()).onClickPlay(eq(EDWARD_SCISSORHANDS));
44 | }
45 |
46 | private void givenMovieItemViewIsBoundTo(final Movie movie) {
47 | viewTestRule.runOnUiThread(new ViewTestRule.Runner() {
48 | @Override
49 | public void run(MovieItemView view) {
50 | view.bind(movie);
51 | }
52 | });
53 | }
54 | ```
55 |
56 | ## Testing behavior with TalkBack enabled
57 |
58 | Often, our apps will behave differently when TalkBack is enabled to offer a more streamlined experience for users of screen readers.
59 |
60 | Use either `TalkBackTestRule` wrapped around a `ViewTestRule`/`ActivityTestRule` to start/stop TalkBack before and after each test.
61 |
62 | ```java
63 | private ViewTestRule viewTestRule = new ViewTestRule<>(R.layout.test_movie_item_view);
64 |
65 | @Rule
66 | public RuleChain chain = RuleChain.outerRule(new TalkBackTestRule()).around(viewTestRule);
67 | ```
68 |
69 | TalkBack will be enabled before each test is run and disabled after each test finishes.
70 |
71 | :warning: Toggling TalkBack state requires the `WRITE_SECURE_SETTINGS` permission being set for the app under test.
72 |
73 | ## Demo
74 |
75 | You can run the demo tests with the following command:
76 |
77 | ```bash
78 | ./gradlew demo:installDebug;\
79 | adb shell pm grant com.novoda.movies android.permission.WRITE_SECURE_SETTINGS;\
80 | adb shell am start -a com.novoda.espresso.DISABLE_ALL_SERVICES;\
81 | ./gradlew demo:cAT;
82 | ```
83 |
84 | 1. First the app is installed
85 | 2. The `WRITE_SECURE_SETTINGS` permission is set for the app (`com.novoda.movies` - replace this with your app's package name)
86 | 3. Disable all accessibility services initially
87 | 4. Run all the connected Android tests
88 |
89 | ## Links
90 |
91 | Here are a list of useful links:
92 |
93 | * We always welcome people to contribute new features or bug fixes, [here is how](https://github.com/novoda/novoda/blob/master/CONTRIBUTING.md)
94 | * If you have a problem check the [Issues Page](https://github.com/novoda/espresso-support/issues) first to see if we are working on it
95 |
96 |
97 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn ( ) {
37 | echo "$*"
38 | }
39 |
40 | die ( ) {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
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 |
--------------------------------------------------------------------------------
/demo/src/main/java/com/novoda/movies/MovieItemView.java:
--------------------------------------------------------------------------------
1 | package com.novoda.movies;
2 |
3 | import android.content.Context;
4 | import android.support.v4.view.ViewCompat;
5 | import android.support.v7.app.AlertDialog;
6 | import android.util.AttributeSet;
7 | import android.view.View;
8 | import android.widget.RelativeLayout;
9 | import android.widget.TextView;
10 |
11 | import com.novoda.accessibility.AccessibilityServices;
12 | import com.novoda.accessibility.Action;
13 | import com.novoda.accessibility.Actions;
14 | import com.novoda.accessibility.ActionsAccessibilityDelegate;
15 | import com.novoda.accessibility.ActionsAlertDialogCreator;
16 |
17 | import java.util.Arrays;
18 | import java.util.Locale;
19 |
20 | public class MovieItemView extends RelativeLayout {
21 |
22 | private final AccessibilityServices a11yServices;
23 | private final ActionsAlertDialogCreator dialogCreator;
24 |
25 | private View playButtonView;
26 |
27 | private View favoriteButtonView;
28 |
29 | private TextView nameTextView;
30 |
31 | private Listener listener;
32 |
33 | public MovieItemView(Context context, AttributeSet attrs) {
34 | super(context, attrs);
35 | a11yServices = AccessibilityServices.newInstance(context);
36 | dialogCreator = new ActionsAlertDialogCreator(context);
37 | }
38 |
39 | @Override
40 | protected void onFinishInflate() {
41 | super.onFinishInflate();
42 | View.inflate(getContext(), R.layout.merge_movie_item, this);
43 | playButtonView = findViewById(R.id.movie_item_button_play);
44 | favoriteButtonView = findViewById(R.id.movie_item_button_favorite);
45 | nameTextView = ((TextView) findViewById(R.id.movie_item_text_name));
46 | }
47 |
48 | public void attachListener(Listener listener) {
49 | this.listener = listener;
50 | }
51 |
52 | public void detachListeners() {
53 | this.listener = null;
54 | }
55 |
56 | public void bind(Movie movie) {
57 | final Action actionClick = createActionClick(movie);
58 | final Action actionClickPlay = createActionClickPlay(movie);
59 | final Action actionClickFavorite = createActionClickFavorite(movie);
60 |
61 | nameTextView.setText(movie.name);
62 | setContentDescription(movie.name);
63 |
64 | if (a11yServices.isSpokenFeedbackEnabled()) {
65 | playButtonView.setOnClickListener(null);
66 | playButtonView.setClickable(false);
67 | } else {
68 | playButtonView.setOnClickListener(new OnClickListener() {
69 | @Override
70 | public void onClick(View v) {
71 | actionClickPlay.run();
72 | }
73 | });
74 | }
75 |
76 | if (a11yServices.isSpokenFeedbackEnabled()) {
77 | favoriteButtonView.setOnClickListener(null);
78 | favoriteButtonView.setClickable(false);
79 | } else {
80 | favoriteButtonView.setOnClickListener(new OnClickListener() {
81 | @Override
82 | public void onClick(View v) {
83 | actionClickFavorite.run();
84 | }
85 | });
86 | }
87 |
88 | final Actions allActions = collateActions(actionClick, actionClickPlay, actionClickFavorite);
89 | ActionsAccessibilityDelegate accessibilityDelegate = new ActionsAccessibilityDelegate(getResources(), allActions);
90 | accessibilityDelegate.setClickLabel("see actions");
91 | accessibilityDelegate.setLongClickLabel(getResources().getString(actionClick.getLabel()).toLowerCase(Locale.UK));
92 | ViewCompat.setAccessibilityDelegate(this, accessibilityDelegate);
93 |
94 | if (a11yServices.isSpokenFeedbackEnabled()) {
95 | setOnLongClickListener(new OnLongClickListener() {
96 | @Override
97 | public boolean onLongClick(View v) {
98 | actionClick.run();
99 | return true;
100 | }
101 | });
102 | } else {
103 | setOnClickListener(null);
104 | setLongClickable(false);
105 | }
106 |
107 | setOnClickListener(new OnClickListener() {
108 | @Override
109 | public void onClick(View v) {
110 | if (a11yServices.isSpokenFeedbackEnabled()) {
111 | AlertDialog alertDialog = dialogCreator.create(allActions);
112 | alertDialog.show();
113 | } else {
114 | actionClick.run();
115 | }
116 | }
117 | });
118 | }
119 |
120 | private Actions collateActions(Action... action) {
121 | return new Actions(Arrays.asList(action));
122 | }
123 |
124 | private Action createActionClickFavorite(final Movie movie) {
125 | return new Action(R.id.action_click_favorite_movie, R.string.action_click_favorite_movie, new Runnable() {
126 | @Override
127 | public void run() {
128 | listener.onClickFavorite(movie);
129 | }
130 | });
131 | }
132 |
133 | private Action createActionClickPlay(final Movie movie) {
134 | return new Action(R.id.action_click_play_movie, R.string.action_click_play_movie, new Runnable() {
135 | @Override
136 | public void run() {
137 | listener.onClickPlay(movie);
138 | }
139 | });
140 | }
141 |
142 | private Action createActionClick(final Movie movie) {
143 | return new Action(R.id.action_click_movie, R.string.action_click_movie, new Runnable() {
144 | @Override
145 | public void run() {
146 | listener.onClick(movie);
147 | }
148 | });
149 | }
150 |
151 | public interface Listener {
152 |
153 | void onClick(Movie movie);
154 |
155 | void onClickPlay(Movie movie);
156 |
157 | void onClickFavorite(Movie movie);
158 |
159 | }
160 |
161 | }
162 |
--------------------------------------------------------------------------------
/demo/src/androidTest/java/com/novoda/movies/rateable/TalkBack_RateableMovieViewHolderTest.java:
--------------------------------------------------------------------------------
1 | package com.novoda.movies.rateable;
2 |
3 | import android.support.test.filters.LargeTest;
4 | import android.support.test.runner.AndroidJUnit4;
5 | import android.view.View;
6 |
7 | import com.novoda.espresso.AccessibilityRules;
8 | import com.novoda.espresso.ViewTestRule;
9 | import com.novoda.movies.R;
10 |
11 | import org.junit.Before;
12 | import org.junit.Rule;
13 | import org.junit.Test;
14 | import org.junit.rules.RuleChain;
15 | import org.junit.runner.RunWith;
16 |
17 | import static android.support.test.espresso.Espresso.onView;
18 | import static android.support.test.espresso.action.ViewActions.click;
19 | import static android.support.test.espresso.assertion.ViewAssertions.matches;
20 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
21 | import static android.support.test.espresso.matcher.ViewMatchers.withId;
22 | import static android.support.test.espresso.matcher.ViewMatchers.withText;
23 | import static com.novoda.espresso.ViewTestRule.underTest;
24 | import static com.novoda.movies.rateable.RateableMovieViewModelFixtures.viewModel;
25 | import static org.mockito.Mockito.mock;
26 | import static org.mockito.Mockito.verify;
27 |
28 | @RunWith(AndroidJUnit4.class)
29 | @LargeTest
30 | public class TalkBack_RateableMovieViewHolderTest {
31 |
32 | private ViewTestRule viewTestRule = new ViewTestRule<>(R.layout.item_view_rateable_movie);
33 |
34 | @Rule
35 | public RuleChain ruleChain = RuleChain.outerRule(AccessibilityRules.createTalkBackTestRule()).around(viewTestRule);
36 |
37 | private RateableMovieViewHolder rateableMovieViewHolder;
38 | private RateableMovieViewModel.UserActions userActions = mock(RateableMovieViewModel.UserActions.class);
39 |
40 | @Before
41 | public void setUp() {
42 | rateableMovieViewHolder = new RateableMovieViewHolder(viewTestRule.getView());
43 | }
44 |
45 | @Test
46 | public void bindsTitle() {
47 | RateableMovieViewModel viewModel = viewModel().title("Arrival").rating(5).liked(true).build();
48 |
49 | viewTestRule.runOnMainSynchronously(view -> rateableMovieViewHolder.bind(viewModel));
50 |
51 | onView(withId(R.id.item_rateable_text_title)).check(matches(withText("Arrival")));
52 | }
53 |
54 | @Test
55 | public void clickingOnItemViewOpensMenu() {
56 | RateableMovieViewModel viewModel = viewModel().liked(false).build();
57 | viewTestRule.runOnMainSynchronously(view -> rateableMovieViewHolder.bind(viewModel));
58 |
59 | onView(underTest()).perform(click());
60 |
61 | checkSeeMoreOptionsMenuDisplayed();
62 | }
63 |
64 | @Test
65 | public void clickingOnItemViewOpensMenuWithUndoLike() {
66 | RateableMovieViewModel viewModel = viewModel().liked(true).build();
67 | viewTestRule.runOnMainSynchronously(view -> rateableMovieViewHolder.bind(viewModel));
68 |
69 | onView(underTest()).perform(click());
70 |
71 | checkSeeMoreOptionsMenuWithUndoLikeDisplayed();
72 | }
73 |
74 | @Test
75 | public void clickingOnLikeOpensMenu() {
76 | RateableMovieViewModel viewModel = viewModel().liked(false).build();
77 | viewTestRule.runOnMainSynchronously(view -> rateableMovieViewHolder.bind(viewModel));
78 |
79 | onView(withId(R.id.item_rateable_image_like)).perform(click());
80 |
81 | checkSeeMoreOptionsMenuDisplayed();
82 | }
83 |
84 | @Test
85 | public void clickingOnRateOpensMenu() {
86 | RateableMovieViewModel viewModel = viewModel().liked(false).build();
87 | viewTestRule.runOnMainSynchronously(view -> rateableMovieViewHolder.bind(viewModel));
88 |
89 | onView(withId(R.id.item_rateable_rating)).perform(click());
90 |
91 | checkSeeMoreOptionsMenuDisplayed();
92 | }
93 |
94 | @Test
95 | public void clickingOnRateActionOpensRateMenu() {
96 | RateableMovieViewModel viewModel = viewModel().build();
97 | viewTestRule.runOnMainSynchronously(view -> rateableMovieViewHolder.bind(viewModel));
98 | onView(underTest()).perform(click());
99 |
100 | onView(withText(R.string.action_rateable_movie_click_rate)).perform(click());
101 |
102 | checkRateMenuDisplayed();
103 | }
104 |
105 | @Test
106 | public void bindsRateAction() {
107 | RateableMovieViewModel viewModel = viewModel(userActions).build();
108 | viewTestRule.runOnMainSynchronously(view -> rateableMovieViewHolder.bind(viewModel));
109 | onView(underTest()).perform(click());
110 | onView(withText(R.string.action_rateable_movie_click_rate)).perform(click());
111 |
112 | onView(withText(R.string.action_rateable_movie_rate_one_half_star)).perform(click());
113 |
114 | verify(userActions).onRate(1.5f);
115 | }
116 |
117 | private void checkRateMenuDisplayed() {
118 | checkViewsWithTextDisplayed(
119 | R.string.action_rateable_movie_rate_half_star,
120 | R.string.action_rateable_movie_rate_one_star,
121 | R.string.action_rateable_movie_rate_one_half_star,
122 | R.string.action_rateable_movie_rate_two_star,
123 | R.string.action_rateable_movie_rate_two_half_star,
124 | R.string.action_rateable_movie_rate_three_star,
125 | R.string.action_rateable_movie_rate_three_half_star,
126 | R.string.action_rateable_movie_rate_four_star,
127 | R.string.action_rateable_movie_rate_four_half_star,
128 | R.string.action_rateable_movie_rate_five_star
129 | );
130 | }
131 |
132 | private void checkSeeMoreOptionsMenuDisplayed() {
133 | checkViewsWithTextDisplayed(
134 | R.string.action_rateable_movie_click_select,
135 | R.string.action_rateable_movie_click_like,
136 | R.string.action_rateable_movie_click_rate
137 | );
138 | }
139 |
140 | private void checkSeeMoreOptionsMenuWithUndoLikeDisplayed() {
141 | checkViewsWithTextDisplayed(
142 | R.string.action_rateable_movie_click_select,
143 | R.string.action_rateable_movie_click_remove_like,
144 | R.string.action_rateable_movie_click_rate
145 | );
146 | }
147 |
148 | private void checkViewsWithTextDisplayed(int... ids) {
149 | for (int id : ids) {
150 | onView(withText(id)).check(matches(isDisplayed()));
151 | }
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/demo/src/androidTest/java/com/novoda/movies/rateable/SwitchAccess_RateableMovieViewHolderTest.java:
--------------------------------------------------------------------------------
1 | package com.novoda.movies.rateable;
2 |
3 | import android.support.test.filters.LargeTest;
4 | import android.support.test.runner.AndroidJUnit4;
5 | import android.view.View;
6 |
7 | import com.novoda.espresso.AccessibilityRules;
8 | import com.novoda.espresso.ViewTestRule;
9 | import com.novoda.movies.R;
10 |
11 | import org.junit.Before;
12 | import org.junit.Rule;
13 | import org.junit.Test;
14 | import org.junit.rules.RuleChain;
15 | import org.junit.runner.RunWith;
16 |
17 | import static android.support.test.espresso.Espresso.onView;
18 | import static android.support.test.espresso.action.ViewActions.click;
19 | import static android.support.test.espresso.assertion.ViewAssertions.matches;
20 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
21 | import static android.support.test.espresso.matcher.ViewMatchers.withId;
22 | import static android.support.test.espresso.matcher.ViewMatchers.withText;
23 | import static com.novoda.espresso.ViewTestRule.underTest;
24 | import static com.novoda.movies.rateable.RateableMovieViewModelFixtures.viewModel;
25 | import static org.mockito.Mockito.mock;
26 | import static org.mockito.Mockito.verify;
27 |
28 | @RunWith(AndroidJUnit4.class)
29 | @LargeTest
30 | public class SwitchAccess_RateableMovieViewHolderTest {
31 |
32 | private ViewTestRule viewTestRule = new ViewTestRule<>(R.layout.item_view_rateable_movie);
33 |
34 | @Rule
35 | public RuleChain ruleChain = RuleChain.outerRule(AccessibilityRules.createSwitchAccessTestRule()).around(viewTestRule);
36 |
37 | private RateableMovieViewHolder rateableMovieViewHolder;
38 | private RateableMovieViewModel.UserActions userActions = mock(RateableMovieViewModel.UserActions.class);
39 |
40 | @Before
41 | public void setUp() {
42 | rateableMovieViewHolder = new RateableMovieViewHolder(viewTestRule.getView());
43 | }
44 |
45 | @Test
46 | public void bindsTitle() {
47 | RateableMovieViewModel viewModel = viewModel().title("Arrival").rating(5).liked(true).build();
48 |
49 | viewTestRule.runOnMainSynchronously(view -> rateableMovieViewHolder.bind(viewModel));
50 |
51 | onView(withId(R.id.item_rateable_text_title)).check(matches(withText("Arrival")));
52 | }
53 |
54 | @Test
55 | public void clickingOnItemViewOpensMenu() {
56 | RateableMovieViewModel viewModel = viewModel().liked(false).build();
57 | viewTestRule.runOnMainSynchronously(view -> rateableMovieViewHolder.bind(viewModel));
58 |
59 | onView(underTest()).perform(click());
60 |
61 | checkSeeMoreOptionsMenuDisplayed();
62 | }
63 |
64 | @Test
65 | public void clickingOnItemViewOpensMenuWithUndoLike() {
66 | RateableMovieViewModel viewModel = viewModel().liked(true).build();
67 | viewTestRule.runOnMainSynchronously(view -> rateableMovieViewHolder.bind(viewModel));
68 |
69 | onView(underTest()).perform(click());
70 |
71 | checkSeeMoreOptionsMenuWithUndoLikeDisplayed();
72 | }
73 |
74 | @Test
75 | public void clickingOnLikeOpensMenu() {
76 | RateableMovieViewModel viewModel = viewModel().liked(false).build();
77 | viewTestRule.runOnMainSynchronously(view -> rateableMovieViewHolder.bind(viewModel));
78 |
79 | onView(withId(R.id.item_rateable_image_like)).perform(click());
80 |
81 | checkSeeMoreOptionsMenuDisplayed();
82 | }
83 |
84 | @Test
85 | public void clickingOnRateOpensMenu() {
86 | RateableMovieViewModel viewModel = viewModel().liked(false).build();
87 | viewTestRule.runOnMainSynchronously(view -> rateableMovieViewHolder.bind(viewModel));
88 |
89 | onView(withId(R.id.item_rateable_rating)).perform(click());
90 |
91 | checkSeeMoreOptionsMenuDisplayed();
92 | }
93 |
94 | @Test
95 | public void clickingOnRateActionOpensRateMenu() {
96 | RateableMovieViewModel viewModel = viewModel().build();
97 | viewTestRule.runOnMainSynchronously(view -> rateableMovieViewHolder.bind(viewModel));
98 | onView(underTest()).perform(click());
99 |
100 | onView(withText(R.string.action_rateable_movie_click_rate)).perform(click());
101 |
102 | checkRateMenuDisplayed();
103 | }
104 |
105 | @Test
106 | public void bindsRateAction() {
107 | RateableMovieViewModel viewModel = viewModel(userActions).build();
108 | viewTestRule.runOnMainSynchronously(view -> rateableMovieViewHolder.bind(viewModel));
109 | onView(underTest()).perform(click());
110 | onView(withText(R.string.action_rateable_movie_click_rate)).perform(click());
111 |
112 | onView(withText(R.string.action_rateable_movie_rate_one_half_star)).perform(click());
113 |
114 | verify(userActions).onRate(1.5f);
115 | }
116 |
117 | private void checkRateMenuDisplayed() {
118 | checkViewsWithTextDisplayed(
119 | R.string.action_rateable_movie_rate_half_star,
120 | R.string.action_rateable_movie_rate_one_star,
121 | R.string.action_rateable_movie_rate_one_half_star,
122 | R.string.action_rateable_movie_rate_two_star,
123 | R.string.action_rateable_movie_rate_two_half_star,
124 | R.string.action_rateable_movie_rate_three_star,
125 | R.string.action_rateable_movie_rate_three_half_star,
126 | R.string.action_rateable_movie_rate_four_star,
127 | R.string.action_rateable_movie_rate_four_half_star,
128 | R.string.action_rateable_movie_rate_five_star
129 | );
130 | }
131 |
132 | private void checkSeeMoreOptionsMenuDisplayed() {
133 | checkViewsWithTextDisplayed(
134 | R.string.action_rateable_movie_click_select,
135 | R.string.action_rateable_movie_click_like,
136 | R.string.action_rateable_movie_click_rate
137 | );
138 | }
139 |
140 | private void checkSeeMoreOptionsMenuWithUndoLikeDisplayed() {
141 | checkViewsWithTextDisplayed(
142 | R.string.action_rateable_movie_click_select,
143 | R.string.action_rateable_movie_click_remove_like,
144 | R.string.action_rateable_movie_click_rate
145 | );
146 | }
147 |
148 | private void checkViewsWithTextDisplayed(int... ids) {
149 | for (int id : ids) {
150 | onView(withText(id)).check(matches(isDisplayed()));
151 | }
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/core/src/main/java/com/novoda/espresso/AccessibilityViewMatchers.java:
--------------------------------------------------------------------------------
1 | package com.novoda.espresso;
2 |
3 | import android.os.Build;
4 | import android.support.annotation.Nullable;
5 | import android.support.annotation.RequiresApi;
6 | import android.support.annotation.StringRes;
7 | import android.view.View;
8 | import android.view.accessibility.AccessibilityNodeInfo;
9 |
10 | import org.hamcrest.Description;
11 | import org.hamcrest.Matcher;
12 | import org.hamcrest.TypeSafeMatcher;
13 |
14 | import static org.hamcrest.Matchers.is;
15 |
16 | public class AccessibilityViewMatchers {
17 |
18 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
19 | public static Matcher super View> withUsageHintOnClick(@StringRes final int resourceId) {
20 | return new TypeSafeMatcher() {
21 |
22 | private String resourceName;
23 | private String expectedText;
24 |
25 | @Override
26 | protected boolean matchesSafely(View view) {
27 | if (!view.isClickable()) {
28 | return false;
29 | }
30 |
31 | expectedText = view.getResources().getString(resourceId);
32 | resourceName = view.getResources().getResourceEntryName(resourceId);
33 |
34 | if (expectedText == null) {
35 | return false;
36 | }
37 |
38 | AccessibilityNodeInfo.AccessibilityAction clickAction = findAction(view, AccessibilityNodeInfo.ACTION_CLICK);
39 | return expectedText.equals(clickAction.getLabel());
40 | }
41 |
42 | @Override
43 | public void describeTo(Description description) {
44 | description.appendText("is clickable and has custom usage hint for ACTION_CLICK from resource id: ").appendValue(resourceId);
45 | appendResourceNameAndExpectedTextToDescription(description, resourceName, expectedText);
46 | }
47 | };
48 | }
49 |
50 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
51 | public static Matcher super View> withUsageHintOnClick(CharSequence text) {
52 | return withUsageHintOnClick(is(text));
53 | }
54 |
55 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
56 | public static Matcher super View> withUsageHintOnClick(final Matcher extends CharSequence> charSequenceMatcher) {
57 | return new TypeSafeMatcher() {
58 | @Override
59 | protected boolean matchesSafely(View view) {
60 | if (!view.isClickable()) {
61 | return false;
62 | }
63 | AccessibilityNodeInfo.AccessibilityAction clickAction = findAction(view, AccessibilityNodeInfo.ACTION_CLICK);
64 | return charSequenceMatcher.matches(clickAction.getLabel());
65 | }
66 |
67 | @Override
68 | public void describeTo(Description description) {
69 | description.appendText("is clickable and has custom usage hint for ACTION_CLICK: ");
70 | charSequenceMatcher.describeTo(description);
71 | }
72 | };
73 | }
74 |
75 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
76 | public static Matcher super View> withUsageHintOnLongClick(@StringRes final int resourceId) {
77 | return new TypeSafeMatcher() {
78 |
79 | private String resourceName;
80 | private String expectedText;
81 |
82 | @Override
83 | protected boolean matchesSafely(View view) {
84 | if (!view.isLongClickable()) {
85 | return false;
86 | }
87 |
88 | expectedText = view.getResources().getString(resourceId);
89 | resourceName = view.getResources().getResourceEntryName(resourceId);
90 |
91 | if (expectedText == null) {
92 | return false;
93 | }
94 |
95 | AccessibilityNodeInfo.AccessibilityAction clickAction = findAction(view, AccessibilityNodeInfo.ACTION_LONG_CLICK);
96 | return expectedText.equals(clickAction.getLabel());
97 | }
98 |
99 | @Override
100 | public void describeTo(Description description) {
101 | description.appendText("is long clickable and has custom usage hint for ACTION_LONG_CLICK from resource id: ").appendValue(resourceId);
102 | appendResourceNameAndExpectedTextToDescription(description, resourceName, expectedText);
103 | }
104 | };
105 | }
106 |
107 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
108 | public static Matcher super View> withUsageHintOnLongClick(CharSequence text) {
109 | return withUsageHintOnLongClick(is(text));
110 | }
111 |
112 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
113 | public static Matcher super View> withUsageHintOnLongClick(final Matcher extends CharSequence> charSequenceMatcher) {
114 | return new TypeSafeMatcher() {
115 | @Override
116 | protected boolean matchesSafely(View view) {
117 | if (!view.isLongClickable()) {
118 | return false;
119 | }
120 | AccessibilityNodeInfo.AccessibilityAction clickAction = findAction(view, AccessibilityNodeInfo.ACTION_LONG_CLICK);
121 | return charSequenceMatcher.matches(clickAction.getLabel());
122 | }
123 |
124 | @Override
125 | public void describeTo(Description description) {
126 | description.appendText("is long clickable and has custom usage hint for ACTION_LONG_CLICK: ");
127 | charSequenceMatcher.describeTo(description);
128 | }
129 | };
130 | }
131 |
132 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
133 | public static AccessibilityNodeInfo.AccessibilityAction findAction(View view, int actionId) {
134 | AccessibilityNodeInfo accessibilityNodeInfo = view.createAccessibilityNodeInfo();
135 | for (AccessibilityNodeInfo.AccessibilityAction accessibilityAction : accessibilityNodeInfo.getActionList()) {
136 | if (actionId == accessibilityAction.getId()) {
137 | return accessibilityAction;
138 | }
139 | }
140 | throw new AccessibilityActionNotFoundException(actionId);
141 | }
142 |
143 | public static void appendResourceNameAndExpectedTextToDescription(Description description, @Nullable String resourceName, @Nullable String expectedText) {
144 | if (resourceName != null) {
145 | description.appendText("[").appendText(resourceName).appendText("]");
146 | }
147 | if (expectedText != null) {
148 | description.appendText(" value: ").appendText(expectedText);
149 | }
150 | }
151 |
152 | public static class AccessibilityActionNotFoundException extends RuntimeException {
153 |
154 | public AccessibilityActionNotFoundException(int actionId) {
155 | super("Could not find AccessibilityAction with id: " + actionId);
156 | }
157 |
158 | }
159 |
160 | }
161 |
--------------------------------------------------------------------------------
/demo/src/main/java/com/novoda/movies/rateable/RateableMovieViewHolder.java:
--------------------------------------------------------------------------------
1 | package com.novoda.movies.rateable;
2 |
3 | import android.support.v4.view.ViewCompat;
4 | import android.support.v7.widget.RecyclerView;
5 | import android.view.LayoutInflater;
6 | import android.view.View;
7 | import android.view.ViewGroup;
8 | import android.widget.ImageView;
9 | import android.widget.RatingBar;
10 | import android.widget.TextView;
11 |
12 | import com.novoda.accessibility.AccessibilityServices;
13 | import com.novoda.accessibility.Action;
14 | import com.novoda.accessibility.Actions;
15 | import com.novoda.accessibility.ActionsAccessibilityDelegate;
16 | import com.novoda.accessibility.ActionsAlertDialogCreator;
17 | import com.novoda.movies.R;
18 |
19 | import java.util.Arrays;
20 |
21 | class RateableMovieViewHolder extends RecyclerView.ViewHolder {
22 |
23 | static RateableMovieViewHolder inflate(ViewGroup parent) {
24 | LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
25 | View view = layoutInflater.inflate(R.layout.item_view_rateable_movie, parent, false);
26 | return new RateableMovieViewHolder(view);
27 | }
28 |
29 | private final ImageView posterImageView;
30 | private final TextView titleTextView;
31 | private final ImageView likeImageView;
32 | private final RatingBar ratingBar;
33 | private final AccessibilityServices a11yServices;
34 |
35 | RateableMovieViewHolder(View itemView) {
36 | super(itemView);
37 | posterImageView = itemView.findViewById(R.id.item_rateable_image_poster);
38 | titleTextView = itemView.findViewById(R.id.item_rateable_text_title);
39 | likeImageView = itemView.findViewById(R.id.item_rateable_image_like);
40 | ratingBar = itemView.findViewById(R.id.item_rateable_rating);
41 | a11yServices = AccessibilityServices.newInstance(itemView.getContext());
42 | }
43 |
44 | void bind(RateableMovieViewModel viewModel) {
45 | posterImageView.setImageResource(viewModel.poster);
46 | titleTextView.setText(viewModel.title);
47 | likeImageView.setImageResource(viewModel.liked ? R.drawable.ic_favorite_24dp : R.drawable.ic_favorite_border_24dp);
48 | ratingBar.setOnRatingBarChangeListener(null);
49 | ratingBar.setRating(viewModel.rating);
50 | Actions actions = collateActionsFor(viewModel);
51 | ActionsAccessibilityDelegate a11yDelegate = new ActionsAccessibilityDelegate(itemView.getResources(), actions);
52 | ViewCompat.setAccessibilityDelegate(itemView, a11yDelegate);
53 | itemView.setContentDescription(viewModel.title + ", rating " + viewModel.rating + ", liked: " + viewModel.liked);
54 |
55 | if (a11yServices.isSpokenFeedbackEnabled() || a11yServices.isSwitchAccessEnabled()) {
56 | bindForIndirectAccess(actions);
57 | a11yDelegate.setClickLabel(R.string.action_rateable_movie_usage_hint_click);
58 | } else {
59 | bindForTouchAccess(viewModel);
60 | }
61 | }
62 |
63 | private Actions collateActionsFor(RateableMovieViewModel viewModel) {
64 | return new Actions(
65 | Arrays.asList(
66 | selectActionFor(viewModel),
67 | toggleLikeActionFor(viewModel),
68 | rateActionFor(viewModel)
69 | )
70 | );
71 | }
72 |
73 | private void bindForIndirectAccess(Actions actions) {
74 | itemView.setOnClickListener(v -> new ActionsAlertDialogCreator(itemView.getContext()).create(actions).show());
75 | ratingBar.setOnRatingBarChangeListener(null);
76 | ratingBar.setIsIndicator(true);
77 | }
78 |
79 | private Action selectActionFor(RateableMovieViewModel viewModel) {
80 | return new Action(R.id.action_rateable_movie_click_select, R.string.action_rateable_movie_click_select, () -> viewModel.actions.onSelectMovie());
81 | }
82 |
83 | private Action toggleLikeActionFor(RateableMovieViewModel viewModel) {
84 | Runnable onToggleLikeRunnable = () -> viewModel.actions.onToggleLike();
85 | if (viewModel.liked) {
86 | return new Action(R.id.action_rateable_movie_click_toggle_like, R.string.action_rateable_movie_click_remove_like, onToggleLikeRunnable);
87 | } else {
88 | return new Action(R.id.action_rateable_movie_click_toggle_like, R.string.action_rateable_movie_click_like, onToggleLikeRunnable);
89 | }
90 | }
91 |
92 | private Action rateActionFor(RateableMovieViewModel viewModel) {
93 | return new Action(R.id.action_rateable_movie_click_rate, R.string.action_rateable_movie_click_rate, () -> showRatingDialog(viewModel));
94 | }
95 |
96 | private void showRatingDialog(RateableMovieViewModel viewModel) {
97 | ActionsAlertDialogCreator actionsAlertDialogCreator = new ActionsAlertDialogCreator(itemView.getContext(), R.string.action_rateable_movie_dialog_title_rate_movie);
98 | actionsAlertDialogCreator.create(new Actions(Arrays.asList(
99 | new Action(R.id.action_rateable_movie_rate_half_star, R.string.action_rateable_movie_rate_half_star, () -> viewModel.actions.onRate(0.5f)),
100 | new Action(R.id.action_rateable_movie_rate_one_star, R.string.action_rateable_movie_rate_one_star, () -> viewModel.actions.onRate((float) 1)),
101 | new Action(R.id.action_rateable_movie_rate_one_half_star, R.string.action_rateable_movie_rate_one_half_star, () -> viewModel.actions.onRate(1.5f)),
102 | new Action(R.id.action_rateable_movie_rate_two_star, R.string.action_rateable_movie_rate_two_star, () -> viewModel.actions.onRate((float) 2)),
103 | new Action(R.id.action_rateable_movie_rate_two_half_star, R.string.action_rateable_movie_rate_two_half_star, () -> viewModel.actions.onRate(2.5f)),
104 | new Action(R.id.action_rateable_movie_rate_three_star, R.string.action_rateable_movie_rate_three_star, () -> viewModel.actions.onRate((float) 3)),
105 | new Action(R.id.action_rateable_movie_rate_three_half_star, R.string.action_rateable_movie_rate_three_half_star, () -> viewModel.actions.onRate(3.5f)),
106 | new Action(R.id.action_rateable_movie_rate_four_star, R.string.action_rateable_movie_rate_four_star, () -> viewModel.actions.onRate((float) 4)),
107 | new Action(R.id.action_rateable_movie_rate_four_half_star, R.string.action_rateable_movie_rate_four_half_star, () -> viewModel.actions.onRate(4.5f)),
108 | new Action(R.id.action_rateable_movie_rate_five_star, R.string.action_rateable_movie_rate_five_star, () -> viewModel.actions.onRate((float) 5))
109 | ))).show();
110 | }
111 |
112 | private void bindForTouchAccess(RateableMovieViewModel viewModel) {
113 | itemView.setOnClickListener(v -> viewModel.actions.onSelectMovie());
114 | likeImageView.setOnClickListener(v -> viewModel.actions.onToggleLike());
115 | ratingBar.setOnRatingBarChangeListener((ratingBar, rating, fromUser) -> {
116 | viewModel.actions.onRate(rating);
117 | });
118 | ratingBar.setIsIndicator(false);
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------