diff options
author | Yuki Hamada <hummer@google.com> | 2018-11-29 14:33:36 -0800 |
---|---|---|
committer | Yuki Hamada <hummer@google.com> | 2018-11-29 16:26:42 -0800 |
commit | f2e510857c0c47c710e8c98aa7aa680a82b8e62e (patch) | |
tree | c74b4b99e867ca4227a140a145319d0a3d9aa9a5 /fragment | |
parent | aaca8e4b69cc34a5ab94962001ec6997052e8899 (diff) | |
download | support-f2e510857c0c47c710e8c98aa7aa680a82b8e62e.tar.gz |
Add support for theme in FragmentScenario
Change-Id: I88ad233269b5a93e0c4ae81a265020eb74c3b70b
Test: ./gradlew fragment-testing:connectedCheck
fixes: 119054431
Diffstat (limited to 'fragment')
8 files changed, 224 insertions, 28 deletions
diff --git a/fragment/testing/api/1.1.0-alpha03.txt b/fragment/testing/api/1.1.0-alpha03.txt index 2ad98867f97..8c83aa33862 100644 --- a/fragment/testing/api/1.1.0-alpha03.txt +++ b/fragment/testing/api/1.1.0-alpha03.txt @@ -5,9 +5,11 @@ package androidx.fragment.app.testing { method public static <F extends androidx.fragment.app.Fragment> androidx.fragment.app.testing.FragmentScenario<F> launch(Class<F>); method public static <F extends androidx.fragment.app.Fragment> androidx.fragment.app.testing.FragmentScenario<F> launch(Class<F>, android.os.Bundle?); method public static <F extends androidx.fragment.app.Fragment> androidx.fragment.app.testing.FragmentScenario<F> launch(Class<F>, android.os.Bundle?, androidx.fragment.app.FragmentFactory?); + method public static <F extends androidx.fragment.app.Fragment> androidx.fragment.app.testing.FragmentScenario<F> launch(Class<F>, android.os.Bundle?, int, androidx.fragment.app.FragmentFactory?); method public static <F extends androidx.fragment.app.Fragment> androidx.fragment.app.testing.FragmentScenario<F> launchInContainer(Class<F>); method public static <F extends androidx.fragment.app.Fragment> androidx.fragment.app.testing.FragmentScenario<F> launchInContainer(Class<F>, android.os.Bundle?); method public static <F extends androidx.fragment.app.Fragment> androidx.fragment.app.testing.FragmentScenario<F> launchInContainer(Class<F>, android.os.Bundle?, androidx.fragment.app.FragmentFactory?); + method public static <F extends androidx.fragment.app.Fragment> androidx.fragment.app.testing.FragmentScenario<F> launchInContainer(Class<F>, android.os.Bundle?, int, androidx.fragment.app.FragmentFactory?); method public androidx.fragment.app.testing.FragmentScenario<F> moveToState(androidx.lifecycle.Lifecycle.State); method public androidx.fragment.app.testing.FragmentScenario<F> onFragment(androidx.fragment.app.testing.FragmentScenario.FragmentAction<F>); method public androidx.fragment.app.testing.FragmentScenario<F> recreate(); diff --git a/fragment/testing/api/current.txt b/fragment/testing/api/current.txt index 2ad98867f97..8c83aa33862 100644 --- a/fragment/testing/api/current.txt +++ b/fragment/testing/api/current.txt @@ -5,9 +5,11 @@ package androidx.fragment.app.testing { method public static <F extends androidx.fragment.app.Fragment> androidx.fragment.app.testing.FragmentScenario<F> launch(Class<F>); method public static <F extends androidx.fragment.app.Fragment> androidx.fragment.app.testing.FragmentScenario<F> launch(Class<F>, android.os.Bundle?); method public static <F extends androidx.fragment.app.Fragment> androidx.fragment.app.testing.FragmentScenario<F> launch(Class<F>, android.os.Bundle?, androidx.fragment.app.FragmentFactory?); + method public static <F extends androidx.fragment.app.Fragment> androidx.fragment.app.testing.FragmentScenario<F> launch(Class<F>, android.os.Bundle?, int, androidx.fragment.app.FragmentFactory?); method public static <F extends androidx.fragment.app.Fragment> androidx.fragment.app.testing.FragmentScenario<F> launchInContainer(Class<F>); method public static <F extends androidx.fragment.app.Fragment> androidx.fragment.app.testing.FragmentScenario<F> launchInContainer(Class<F>, android.os.Bundle?); method public static <F extends androidx.fragment.app.Fragment> androidx.fragment.app.testing.FragmentScenario<F> launchInContainer(Class<F>, android.os.Bundle?, androidx.fragment.app.FragmentFactory?); + method public static <F extends androidx.fragment.app.Fragment> androidx.fragment.app.testing.FragmentScenario<F> launchInContainer(Class<F>, android.os.Bundle?, int, androidx.fragment.app.FragmentFactory?); method public androidx.fragment.app.testing.FragmentScenario<F> moveToState(androidx.lifecycle.Lifecycle.State); method public androidx.fragment.app.testing.FragmentScenario<F> onFragment(androidx.fragment.app.testing.FragmentScenario.FragmentAction<F>); method public androidx.fragment.app.testing.FragmentScenario<F> recreate(); diff --git a/fragment/testing/src/androidTest/java/androidx/fragment/app/testing/FragmentScenarioTest.kt b/fragment/testing/src/androidTest/java/androidx/fragment/app/testing/FragmentScenarioTest.kt index b79067662ba..0413ee2783f 100644 --- a/fragment/testing/src/androidTest/java/androidx/fragment/app/testing/FragmentScenarioTest.kt +++ b/fragment/testing/src/androidTest/java/androidx/fragment/app/testing/FragmentScenarioTest.kt @@ -18,6 +18,7 @@ package androidx.fragment.app.testing import android.os.Bundle import androidx.fragment.app.FragmentFactory +import androidx.fragment.testing.test.R.style.ThemedFragmentTheme import androidx.lifecycle.Lifecycle.State import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -69,6 +70,17 @@ class FragmentScenarioTest { } @Test + fun launchThemedFragment() { + with(launchFragment<ThemedFragment>(themeResId = ThemedFragmentTheme)) { + onFragment { fragment -> + assertThat(fragment.state).isEqualTo(State.RESUMED) + assertThat(fragment.isViewAttachedToWindow).isFalse() + assertThat(fragment.hasThemedFragmentTheme()).isTrue() + } + } + } + + @Test fun launchFragmentInContainer() { with(launchFragmentInContainer<StateRecordingFragment>()) { onFragment { fragment -> @@ -92,10 +104,21 @@ class FragmentScenarioTest { } @Test + fun launchThemedFragmentInContainer() { + with(launchFragmentInContainer<ThemedFragment>(themeResId = ThemedFragmentTheme)) { + onFragment { fragment -> + assertThat(fragment.state).isEqualTo(State.RESUMED) + assertThat(fragment.isViewAttachedToWindow).isTrue() + assertThat(fragment.hasThemedFragmentTheme()).isTrue() + } + } + } + + @Test fun launchFragmentWithFragmentFactory() { with( - launchFragment<NoDefaultConstructorFragment>(/*fragmentArgs=*/null, - NoDefaultConstructorFragmentFactory("my constructor arg") + launchFragment<NoDefaultConstructorFragment>( + factory = NoDefaultConstructorFragmentFactory("my constructor arg") ) ) { onFragment { fragment -> @@ -109,8 +132,8 @@ class FragmentScenarioTest { @Test fun launchInContainerFragmentWithFragmentFactory() { with( - launchFragmentInContainer<NoDefaultConstructorFragment>(/*fragmentArgs=*/null, - NoDefaultConstructorFragmentFactory("my constructor arg") + launchFragmentInContainer<NoDefaultConstructorFragment>( + factory = NoDefaultConstructorFragmentFactory("my constructor arg") ) ) { onFragment { fragment -> @@ -124,7 +147,7 @@ class FragmentScenarioTest { @Test fun launchWithCrossInlineFactoryFunction() { var numberOfInstantiations = 0 - with(launchFragment(/*fragmentArgs=*/null) { + with(launchFragment { numberOfInstantiations++ NoDefaultConstructorFragment("my constructor arg") }) { @@ -140,7 +163,7 @@ class FragmentScenarioTest { @Test fun launchInContainerWithCrossInlineFactoryFunction() { var numberOfInstantiations = 0 - with(launchFragmentInContainer(/*fragmentArgs=*/null) { + with(launchFragmentInContainer { numberOfInstantiations++ NoDefaultConstructorFragment("my constructor arg") }) { @@ -328,7 +351,7 @@ class FragmentScenarioTest { @Test fun recreateFragmentWithFragmentFactory() { var numberOfInstantiations = 0 - with(launchFragment(/*fragmentArgs=*/null) { + with(launchFragment { numberOfInstantiations++ NoDefaultConstructorFragment("my constructor arg") }) { @@ -347,4 +370,23 @@ class FragmentScenarioTest { } } } + + @Test + fun recreateThemedFragment() { + with(launchFragmentInContainer<ThemedFragment>(themeResId = ThemedFragmentTheme)) { + onFragment { fragment -> + assertThat(fragment.state).isEqualTo(State.RESUMED) + assertThat(fragment.isViewAttachedToWindow).isTrue() + assertThat(fragment.hasThemedFragmentTheme()).isTrue() + assertThat(fragment.numberOfRecreations).isEqualTo(0) + } + recreate() + onFragment { fragment -> + assertThat(fragment.state).isEqualTo(State.RESUMED) + assertThat(fragment.isViewAttachedToWindow).isTrue() + assertThat(fragment.hasThemedFragmentTheme()).isTrue() + assertThat(fragment.numberOfRecreations).isEqualTo(1) + } + } + } } diff --git a/fragment/testing/src/androidTest/java/androidx/fragment/app/testing/ThemedFragment.kt b/fragment/testing/src/androidTest/java/androidx/fragment/app/testing/ThemedFragment.kt new file mode 100644 index 00000000000..40dda590928 --- /dev/null +++ b/fragment/testing/src/androidTest/java/androidx/fragment/app/testing/ThemedFragment.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.fragment.app.testing + +import androidx.fragment.testing.test.R + +/** + * A minimum example fragment which requires a specific theme. + * For example, if your fragment uses material components views, those views require + * Theme.MaterialComponents. + */ +class ThemedFragment : StateRecordingFragment() { + fun hasThemedFragmentTheme(): Boolean { + val attrs = + context!!.theme.obtainStyledAttributes(intArrayOf(R.attr.exampleRequiredAttribute)) + try { + return attrs.hasValue(R.styleable.ThemedFragmentTheme_exampleRequiredAttribute) + } finally { + attrs.recycle() + } + } +}
\ No newline at end of file diff --git a/fragment/testing/src/androidTest/res/values/attrs.xml b/fragment/testing/src/androidTest/res/values/attrs.xml new file mode 100644 index 00000000000..cb748bfb00e --- /dev/null +++ b/fragment/testing/src/androidTest/res/values/attrs.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2018 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<resources xmlns:tools="http://schemas.android.com/tools"> + <declare-styleable name="ThemedFragmentTheme"> + <attr name="exampleRequiredAttribute" format="boolean"/> + </declare-styleable> +</resources>
\ No newline at end of file diff --git a/fragment/testing/src/androidTest/res/values/styles.xml b/fragment/testing/src/androidTest/res/values/styles.xml new file mode 100644 index 00000000000..abb88feb380 --- /dev/null +++ b/fragment/testing/src/androidTest/res/values/styles.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2018 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<resources xmlns:tools="http://schemas.android.com/tools"> + <style name="ThemedFragmentTheme" parent="android:Theme"> + <item name="exampleRequiredAttribute">true</item> + </style> +</resources>
\ No newline at end of file diff --git a/fragment/testing/src/main/java/androidx/fragment/app/testing/FragmentScenario.java b/fragment/testing/src/main/java/androidx/fragment/app/testing/FragmentScenario.java index 0eccbbf6af4..b146da77568 100644 --- a/fragment/testing/src/main/java/androidx/fragment/app/testing/FragmentScenario.java +++ b/fragment/testing/src/main/java/androidx/fragment/app/testing/FragmentScenario.java @@ -19,7 +19,10 @@ package androidx.fragment.app.testing; import static androidx.annotation.RestrictTo.Scope.LIBRARY; import static androidx.core.util.Preconditions.checkNotNull; import static androidx.core.util.Preconditions.checkState; +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; +import android.content.ComponentName; +import android.content.Intent; import android.os.Bundle; import androidx.annotation.NonNull; @@ -29,6 +32,7 @@ import androidx.core.util.Preconditions; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentFactory; +import androidx.fragment.testing.R; import androidx.lifecycle.Lifecycle.State; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; @@ -63,8 +67,17 @@ public final class FragmentScenario<F extends Fragment> { */ @RestrictTo(LIBRARY) public static class EmptyFragmentActivity extends FragmentActivity { + + @NonNull + public static final String THEME_EXTRAS_BUNDLE_KEY = + "androidx.fragment.app.testing.FragmentScenario.EmptyFragmentActivity" + + ".THEME_EXTRAS_BUNDLE_KEY"; + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { + setTheme(getIntent().getIntExtra(THEME_EXTRAS_BUNDLE_KEY, + R.style.FragmentScenarioEmptyFragmentActivityTheme)); + // Checks if we have a custom FragmentFactory and set it. ViewModelProvider viewModelProvider = new ViewModelProvider( this, ViewModelProvider.AndroidViewModelFactory.getInstance(getApplication())); @@ -158,7 +171,28 @@ public final class FragmentScenario<F extends Fragment> { public static <F extends Fragment> FragmentScenario<F> launch( @NonNull Class<F> fragmentClass, @Nullable Bundle fragmentArgs, @Nullable FragmentFactory factory) { - return internalLaunch(fragmentClass, fragmentArgs, factory, /*containerViewId=*/ 0); + return launch(fragmentClass, fragmentArgs, + R.style.FragmentScenarioEmptyFragmentActivityTheme, factory); + } + + /** + * Launches a Fragment with given arguments hosted by an empty {@link FragmentActivity} themed + * by {@code themeResId}, using the given {@link FragmentFactory} and waits for it to reach the + * resumed state. + * <p> + * This method cannot be called from the main thread. + * + * @param fragmentClass a fragment class to instantiate + * @param fragmentArgs a bundle to passed into fragment + * @param themeResId a style resource id to be set to the host activity's theme + * @param factory a fragment factory to use or null to use default factory + */ + @NonNull + public static <F extends Fragment> FragmentScenario<F> launch( + @NonNull Class<F> fragmentClass, @Nullable Bundle fragmentArgs, + int themeResId, @Nullable FragmentFactory factory) { + return internalLaunch(fragmentClass, fragmentArgs, themeResId, factory, + /*containerViewId=*/ 0); } /** @@ -177,8 +211,8 @@ public final class FragmentScenario<F extends Fragment> { /** * Launches a Fragment in the Activity's root view container {@code android.R.id.content}, with - * given arguments hosted by an empty {@link FragmentActivity} using the given - * {@link FragmentFactory} and waits for it to reach the resumed state. + * given arguments hosted by an empty {@link FragmentActivity} and waits for it to reach the + * resumed state. * <p> * This method cannot be called from the main thread. * @@ -193,8 +227,8 @@ public final class FragmentScenario<F extends Fragment> { /** * Launches a Fragment in the Activity's root view container {@code android.R.id.content}, with - * given arguments hosted by an empty {@link FragmentActivity} and waits for it to reach the - * resumed state. + * given arguments hosted by an empty {@link FragmentActivity} using the given + * {@link FragmentFactory} and waits for it to reach the resumed state. * <p> * This method cannot be called from the main thread. * @@ -206,17 +240,45 @@ public final class FragmentScenario<F extends Fragment> { public static <F extends Fragment> FragmentScenario<F> launchInContainer( @NonNull Class<F> fragmentClass, @Nullable Bundle fragmentArgs, @Nullable FragmentFactory factory) { + return launchInContainer( + fragmentClass, fragmentArgs, R.style.FragmentScenarioEmptyFragmentActivityTheme, + factory); + } + + /** + * Launches a Fragment in the Activity's root view container {@code android.R.id.content}, with + * given arguments hosted by an empty {@link FragmentActivity} themed by {@code themeResId}, + * using the given {@link FragmentFactory} and waits for it to reach the resumed state. + * <p> + * This method cannot be called from the main thread. + * + * @param fragmentClass a fragment class to instantiate + * @param fragmentArgs a bundle to passed into fragment + * @param themeResId a style resource id to be set to the host activity's theme + * @param factory a fragment factory to use or null to use default factory + */ + @NonNull + public static <F extends Fragment> FragmentScenario<F> launchInContainer( + @NonNull Class<F> fragmentClass, @Nullable Bundle fragmentArgs, + int themeResId, @Nullable FragmentFactory factory) { return internalLaunch( - fragmentClass, fragmentArgs, factory, /*containerViewId=*/ android.R.id.content); + fragmentClass, fragmentArgs, themeResId, factory, + /*containerViewId=*/ android.R.id.content); } @NonNull private static <F extends Fragment> FragmentScenario<F> internalLaunch( @NonNull final Class<F> fragmentClass, final @Nullable Bundle fragmentArgs, - @Nullable final FragmentFactory factory, final int containerViewId) { + final int themeResId, @Nullable final FragmentFactory factory, + final int containerViewId) { + Intent startActivityIntent = + Intent.makeMainActivity( + new ComponentName(getApplicationContext(), + EmptyFragmentActivity.class)) + .putExtra(EmptyFragmentActivity.THEME_EXTRAS_BUNDLE_KEY, themeResId); FragmentScenario<F> scenario = new FragmentScenario<>( fragmentClass, factory, - ActivityScenario.launch(EmptyFragmentActivity.class)); + ActivityScenario.<EmptyFragmentActivity>launch(startActivityIntent)); scenario.mActivityScenario.onActivity( new ActivityScenario.ActivityAction<EmptyFragmentActivity>() { @Override diff --git a/fragment/testing/src/main/java/androidx/fragment/app/testing/FragmentScenario.kt b/fragment/testing/src/main/java/androidx/fragment/app/testing/FragmentScenario.kt index 92463869d7b..48da729f6fd 100644 --- a/fragment/testing/src/main/java/androidx/fragment/app/testing/FragmentScenario.kt +++ b/fragment/testing/src/main/java/androidx/fragment/app/testing/FragmentScenario.kt @@ -20,6 +20,7 @@ import android.os.Bundle import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentFactory +import androidx.fragment.testing.R /** * Launches a Fragment with given arguments hosted by an empty [FragmentActivity] using @@ -28,12 +29,14 @@ import androidx.fragment.app.FragmentFactory * This method cannot be called from the main thread. * * @param fragmentArgs a bundle to passed into fragment + * @param themeResId a style resource id to be set to the host activity's theme * @param factory a fragment factory to use or null to use default factory */ inline fun <reified F : Fragment> launchFragment( fragmentArgs: Bundle? = null, + themeResId: Int = R.style.FragmentScenarioEmptyFragmentActivityTheme, factory: FragmentFactory? = null -) = FragmentScenario.launch(F::class.java, fragmentArgs, factory) +) = FragmentScenario.launch(F::class.java, fragmentArgs, themeResId, factory) /** * Launches a Fragment with given arguments hosted by an empty [FragmentActivity] using @@ -42,12 +45,14 @@ inline fun <reified F : Fragment> launchFragment( * This method cannot be called from the main thread. * * @param fragmentArgs a bundle to passed into fragment + * @param themeResId a style resource id to be set to the host activity's theme * @param instantiate method which will be used to instantiate the Fragment. */ inline fun <reified F : Fragment> launchFragment( fragmentArgs: Bundle? = null, + themeResId: Int = R.style.FragmentScenarioEmptyFragmentActivityTheme, crossinline instantiate: (args: Bundle?) -> F -) = FragmentScenario.launch(F::class.java, fragmentArgs, object : FragmentFactory() { +) = FragmentScenario.launch(F::class.java, fragmentArgs, themeResId, object : FragmentFactory() { override fun instantiate( classLoader: ClassLoader, className: String, @@ -66,12 +71,14 @@ inline fun <reified F : Fragment> launchFragment( * This method cannot be called from the main thread. * * @param fragmentArgs a bundle to passed into fragment + * @param themeResId a style resource id to be set to the host activity's theme * @param factory a fragment factory to use or null to use default factory */ inline fun <reified F : Fragment> launchFragmentInContainer( fragmentArgs: Bundle? = null, + themeResId: Int = R.style.FragmentScenarioEmptyFragmentActivityTheme, factory: FragmentFactory? = null -) = FragmentScenario.launchInContainer(F::class.java, fragmentArgs, factory) +) = FragmentScenario.launchInContainer(F::class.java, fragmentArgs, themeResId, factory) /** * Launches a Fragment in the Activity's root view container `android.R.id.content`, with @@ -82,20 +89,23 @@ inline fun <reified F : Fragment> launchFragmentInContainer( * This method cannot be called from the main thread. * * @param fragmentArgs a bundle to passed into fragment + * @param themeResId a style resource id to be set to the host activity's theme * @param instantiate method which will be used to instantiate the Fragment. This is a * simplification of the [FragmentFactory] interface for cases where only a single class * needs a custom constructor called. */ inline fun <reified F : Fragment> launchFragmentInContainer( fragmentArgs: Bundle? = null, + themeResId: Int = R.style.FragmentScenarioEmptyFragmentActivityTheme, crossinline instantiate: (args: Bundle?) -> F -) = FragmentScenario.launchInContainer(F::class.java, fragmentArgs, object : FragmentFactory() { - override fun instantiate( - classLoader: ClassLoader, - className: String, - args: Bundle? - ) = when (className) { - F::class.java.name -> instantiate(args) - else -> super.instantiate(classLoader, className, args) - } -}) +) = FragmentScenario.launchInContainer(F::class.java, fragmentArgs, themeResId, + object : FragmentFactory() { + override fun instantiate( + classLoader: ClassLoader, + className: String, + args: Bundle? + ) = when (className) { + F::class.java.name -> instantiate(args) + else -> super.instantiate(classLoader, className, args) + } + }) |