From a340ebf74ace6ef2fe177a38a04f7ed4497240d3 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Tue, 13 Sep 2022 10:15:07 -0300 Subject: [PATCH] Add espresso test for usernames. --- app/build.gradle | 4 + .../manage/UsernameEditFragmentTest.kt | 135 +++++++++++++++ .../securesms/testing/RxTestSchedulerRule.kt | 36 ++++ .../profiles/manage/UsernameEditFragment.java | 5 +- .../manage/UsernameEditRepository.java | 17 +- .../manage/UsernameEditViewModel.java | 163 +++++++++--------- .../views/CircularProgressMaterialButton.kt | 6 + dependencies.gradle | 1 + gradle/verification-metadata.xml | 49 ++++++ .../push/ReserveUsernameResponse.java | 8 + 10 files changed, 333 insertions(+), 91 deletions(-) create mode 100644 app/src/androidTest/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditFragmentTest.kt create mode 100644 app/src/androidTest/java/org/thoughtcrime/securesms/testing/RxTestSchedulerRule.kt diff --git a/app/build.gradle b/app/build.gradle index 44fcebb48..138628e72 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -561,6 +561,10 @@ dependencies { androidTestImplementation testLibs.mockito.kotlin androidTestImplementation testLibs.square.okhttp.mockserver + instrumentationImplementation (testLibs.androidx.fragment.testing) { + exclude group: 'androidx.test', module: 'core' + } + testImplementation testLibs.espresso.core implementation libs.kotlin.stdlib.jdk8 diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditFragmentTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditFragmentTest.kt new file mode 100644 index 000000000..be7c6deb5 --- /dev/null +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditFragmentTest.kt @@ -0,0 +1,135 @@ +package org.thoughtcrime.securesms.profiles.manage + +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.testing.FragmentScenario +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.lifecycle.Lifecycle +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.closeSoftKeyboard +import androidx.test.espresso.action.ViewActions.typeText +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isEnabled +import androidx.test.espresso.matcher.ViewMatchers.isNotEnabled +import androidx.test.espresso.matcher.ViewMatchers.withContentDescription +import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.reactivex.rxjava3.schedulers.TestScheduler +import okhttp3.mockwebserver.MockResponse +import org.junit.After +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider +import org.thoughtcrime.securesms.testing.Put +import org.thoughtcrime.securesms.testing.RxTestSchedulerRule +import org.thoughtcrime.securesms.testing.SignalActivityRule +import org.thoughtcrime.securesms.testing.assertIsNotNull +import org.thoughtcrime.securesms.testing.assertIsNull +import org.thoughtcrime.securesms.testing.success +import org.whispersystems.signalservice.internal.push.ReserveUsernameResponse +import java.util.concurrent.TimeUnit + +@RunWith(AndroidJUnit4::class) +class UsernameEditFragmentTest { + + @get:Rule + val harness = SignalActivityRule(othersCount = 10) + + private val ioScheduler = TestScheduler() + private val computationScheduler = TestScheduler() + + @get:Rule + val testSchedulerRule = RxTestSchedulerRule( + ioTestScheduler = ioScheduler, + computationTestScheduler = computationScheduler + ) + + @After + fun tearDown() { + InstrumentationApplicationDependencyProvider.clearHandlers() + } + + @Test + fun testUsernameCreationInRegistration() { + val scenario = createScenario(true) + + scenario.moveToState(Lifecycle.State.RESUMED) + + onView(withId(R.id.toolbar)).check { view, noViewFoundException -> + noViewFoundException.assertIsNull() + val toolbar = view as Toolbar + + toolbar.navigationIcon.assertIsNull() + } + + onView(withText(R.string.UsernameEditFragment__add_a_username)).check(matches(isDisplayed())) + onView(withContentDescription(R.string.load_more_header__loading)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))) + } + + @Test + fun testUsernameCreationOutsideOfRegistration() { + val scenario = createScenario() + + scenario.moveToState(Lifecycle.State.RESUMED) + + onView(withId(R.id.toolbar)).check { view, noViewFoundException -> + noViewFoundException.assertIsNull() + val toolbar = view as Toolbar + + toolbar.navigationIcon.assertIsNotNull() + } + + onView(withText(R.string.UsernameEditFragment_username)).check(matches(isDisplayed())) + onView(withContentDescription(R.string.load_more_header__loading)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))) + } + + @Test + fun testNicknameUpdateHappyPath() { + val nickname = "Spiderman" + val discriminator = "4578" + + InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers( + Put("/v1/accounts/username/reserved") { + MockResponse().success(ReserveUsernameResponse("$nickname#$discriminator", "reservationToken")) + }, + Put("/v1/accounts/username/confirm") { + MockResponse().success() + } + ) + + val scenario = createScenario(isInRegistration = true) + scenario.moveToState(Lifecycle.State.RESUMED) + + onView(withId(R.id.username_text)).perform(typeText(nickname)) + + computationScheduler.advanceTimeBy(501, TimeUnit.MILLISECONDS) + computationScheduler.triggerActions() + + onView(withContentDescription(R.string.load_more_header__loading)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) + + ioScheduler.triggerActions() + computationScheduler.triggerActions() + + onView(withId(R.id.username_text)).perform(closeSoftKeyboard()) + onView(withId(R.id.username_done_button)).check(matches(isDisplayed())) + onView(withId(R.id.username_done_button)).check(matches(isEnabled())) + onView(withId(R.id.username_done_button)).perform(click()) + + computationScheduler.triggerActions() + onView(withId(R.id.username_done_button)).check(matches(isNotEnabled())) + } + + private fun createScenario(isInRegistration: Boolean = false): FragmentScenario { + val fragmentArgs = UsernameEditFragmentArgs.Builder().setIsInRegistration(isInRegistration).build().toBundle() + return launchFragmentInContainer( + fragmentArgs = fragmentArgs, + themeResId = R.style.Signal_DayNight_NoActionBar + ) + } +} diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/RxTestSchedulerRule.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/RxTestSchedulerRule.kt new file mode 100644 index 000000000..26036889a --- /dev/null +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/RxTestSchedulerRule.kt @@ -0,0 +1,36 @@ +package org.thoughtcrime.securesms.testing + +import io.reactivex.rxjava3.plugins.RxJavaPlugins +import io.reactivex.rxjava3.schedulers.TestScheduler +import org.junit.rules.ExternalResource + +/** + * JUnit Rule which initialises Rx thread schedulers. If a specific + * scheduler is not specified, it defaults to the `defaultTestScheduler` + */ +class RxTestSchedulerRule( + val defaultTestScheduler: TestScheduler = TestScheduler(), + val ioTestScheduler: TestScheduler = defaultTestScheduler, + val computationTestScheduler: TestScheduler = defaultTestScheduler, + val singleTestScheduler: TestScheduler = defaultTestScheduler, + val newThreadTestScheduler: TestScheduler = defaultTestScheduler, +) : ExternalResource() { + + override fun before() { + RxJavaPlugins.setInitIoSchedulerHandler { ioTestScheduler } + RxJavaPlugins.setIoSchedulerHandler { ioTestScheduler } + + RxJavaPlugins.setInitComputationSchedulerHandler { computationTestScheduler } + RxJavaPlugins.setComputationSchedulerHandler { computationTestScheduler } + + RxJavaPlugins.setInitSingleSchedulerHandler { singleTestScheduler } + RxJavaPlugins.setSingleSchedulerHandler { singleTestScheduler } + + RxJavaPlugins.setInitNewThreadSchedulerHandler { newThreadTestScheduler } + RxJavaPlugins.setNewThreadSchedulerHandler { newThreadTestScheduler } + } + + override fun after() { + RxJavaPlugins.reset() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditFragment.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditFragment.java index 8d2ff8219..43f7fdf94 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditFragment.java @@ -35,7 +35,6 @@ import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher; import org.thoughtcrime.securesms.databinding.UsernameEditFragmentBinding; import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.util.AccessibilityUtil; import org.thoughtcrime.securesms.util.FragmentResultContract; import org.thoughtcrime.securesms.util.LifecycleDisposable; import org.thoughtcrime.securesms.util.UsernameUtil; @@ -91,7 +90,7 @@ public class UsernameEditFragment extends LoggingFragment { viewModel = new ViewModelProvider(this, new UsernameEditViewModel.Factory(args.getIsInRegistration())).get(UsernameEditViewModel.class); lifecycleDisposable.add(viewModel.getUiState().subscribe(this::onUiStateChanged)); - viewModel.getEvents().observe(getViewLifecycleOwner(), this::onEvent); + lifecycleDisposable.add(viewModel.getEvents().subscribe(this::onEvent)); binding.usernameSubmitButton.setOnClickListener(v -> viewModel.onUsernameSubmitted()); binding.usernameDeleteButton.setOnClickListener(v -> viewModel.onUsernameDeleted()); @@ -142,6 +141,8 @@ public class UsernameEditFragment extends LoggingFragment { suffixProgress = new ImageView(requireContext()); suffixProgress.setImageDrawable(getInProgressDrawable()); + suffixProgress.setContentDescription(getString(R.string.load_more_header__loading)); + suffixProgress.setVisibility(View.GONE); suffixParent.addView(suffixProgress, 0, layoutParams); suffixTextView.setOnClickListener(this::onLearnMore); diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditRepository.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditRepository.java index f2a71590c..4bee12c4f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditRepository.java @@ -18,28 +18,29 @@ import org.whispersystems.signalservice.internal.push.ReserveUsernameResponse; import java.io.IOException; import java.util.concurrent.Executor; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.schedulers.Schedulers; + class UsernameEditRepository { private static final String TAG = Log.tag(UsernameEditRepository.class); private final SignalServiceAccountManager accountManager; - private final Executor executor; UsernameEditRepository() { this.accountManager = ApplicationDependencies.getSignalServiceAccountManager(); - this.executor = SignalExecutors.UNBOUNDED; } - void reserveUsername(@NonNull String nickname, @NonNull Callback> callback) { - executor.execute(() -> callback.onComplete(reserveUsernameInternal(nickname))); + @NonNull Single> reserveUsername(@NonNull String nickname) { + return Single.fromCallable(() -> reserveUsernameInternal(nickname)).subscribeOn(Schedulers.io()); } - void confirmUsername(@NonNull ReserveUsernameResponse reserveUsernameResponse, @NonNull Callback callback) { - executor.execute(() -> callback.onComplete(confirmUsernameInternal(reserveUsernameResponse))); + @NonNull Single confirmUsername(@NonNull ReserveUsernameResponse reserveUsernameResponse) { + return Single.fromCallable(() -> confirmUsernameInternal(reserveUsernameResponse)).subscribeOn(Schedulers.io()); } - void deleteUsername(@NonNull Callback callback) { - executor.execute(() -> callback.onComplete(deleteUsernameInternal())); + @NonNull Single deleteUsername() { + return Single.fromCallable(this::deleteUsernameInternal).subscribeOn(Schedulers.io()); } @WorkerThread diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditViewModel.java index 231c54590..12a4adb51 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditViewModel.java @@ -3,14 +3,11 @@ package org.thoughtcrime.securesms.profiles.manage; import android.text.TextUtils; import androidx.annotation.NonNull; -import androidx.lifecycle.LiveData; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; -import org.signal.core.util.ThreadUtil; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.util.SingleLiveEvent; import org.thoughtcrime.securesms.util.UsernameUtil; import org.thoughtcrime.securesms.util.UsernameUtil.InvalidReason; import org.thoughtcrime.securesms.util.rx.RxStore; @@ -21,27 +18,29 @@ import java.util.concurrent.TimeUnit; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.processors.PublishProcessor; import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.PublishSubject; /** * Manages the state around username updates. - * + *

* A note on naming conventions: - * + *

* Usernames are made up of two discrete components, a nickname and a discriminator. They are formatted thusly: - * + *

* [nickname]#[discriminator] - * + *

* The nickname is user-controlled, whereas the discriminator is controlled by the server. */ class UsernameEditViewModel extends ViewModel { private static final long NICKNAME_PUBLISHER_DEBOUNCE_TIMEOUT_MILLIS = 500; - private final SingleLiveEvent events; + private final PublishSubject events; private final UsernameEditRepository repo; private final RxStore uiState; private final PublishProcessor nicknamePublisher; @@ -50,8 +49,9 @@ class UsernameEditViewModel extends ViewModel { private UsernameEditViewModel(boolean isInRegistration) { this.repo = new UsernameEditRepository(); - this.uiState = new RxStore<>(new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, Recipient.self().getUsername().map(UsernameState.Set::new).orElse(UsernameState.NoUsername.INSTANCE)), Schedulers.computation()); - this.events = new SingleLiveEvent<>(); + this.uiState = new RxStore<>(new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, Recipient.self().getUsername().map(UsernameState.Set::new) + .orElse(UsernameState.NoUsername.INSTANCE)), Schedulers.computation()); + this.events = PublishSubject.create(); this.nicknamePublisher = PublishProcessor.create(); this.disposables = new CompositeDisposable(); this.isInRegistration = isInRegistration; @@ -84,7 +84,7 @@ class UsernameEditViewModel extends ViewModel { void onUsernameSkipped() { SignalStore.uiHints().markHasSetOrSkippedUsernameCreation(); - events.setValue(Event.SKIPPED); + events.onNext(Event.SKIPPED); } void onUsernameSubmitted() { @@ -109,66 +109,67 @@ class UsernameEditViewModel extends ViewModel { uiState.update(state -> new State(ButtonState.SUBMIT_LOADING, UsernameStatus.NONE, state.usernameState)); - repo.confirmUsername(((UsernameState.Reserved) usernameState).getReserveUsernameResponse(), (result) -> { - ThreadUtil.runOnMain(() -> { - String nickname = usernameState.getNickname(); + Disposable confirmUsernameDisposable = repo.confirmUsername(((UsernameState.Reserved) usernameState).getReserveUsernameResponse()) + .subscribe(result -> { + String nickname = usernameState.getNickname(); - switch (result) { - case SUCCESS: - SignalStore.uiHints().markHasSetOrSkippedUsernameCreation(); - uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, state.usernameState)); - events.postValue(Event.SUBMIT_SUCCESS); - break; - case USERNAME_INVALID: - uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.INVALID_GENERIC, state.usernameState)); - events.postValue(Event.SUBMIT_FAIL_INVALID); + switch (result) { + case SUCCESS: + SignalStore.uiHints().markHasSetOrSkippedUsernameCreation(); + uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, state.usernameState)); + events.onNext(Event.SUBMIT_SUCCESS); + break; + case USERNAME_INVALID: + uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.INVALID_GENERIC, state.usernameState)); + events.onNext(Event.SUBMIT_FAIL_INVALID); - if (nickname != null) { - onNicknameUpdated(nickname); - } - break; - case USERNAME_UNAVAILABLE: - uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.TAKEN, state.usernameState)); - events.postValue(Event.SUBMIT_FAIL_TAKEN); + if (nickname != null) { + onNicknameUpdated(nickname); + } + break; + case USERNAME_UNAVAILABLE: + uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.TAKEN, state.usernameState)); + events.onNext(Event.SUBMIT_FAIL_TAKEN); - if (nickname != null) { - onNicknameUpdated(nickname); - } - break; - case NETWORK_ERROR: - uiState.update(state -> new State(ButtonState.SUBMIT, UsernameStatus.NONE, state.usernameState)); - events.postValue(Event.NETWORK_FAILURE); - break; - } - }); - }); + if (nickname != null) { + onNicknameUpdated(nickname); + } + break; + case NETWORK_ERROR: + uiState.update(state -> new State(ButtonState.SUBMIT, UsernameStatus.NONE, state.usernameState)); + events.onNext(Event.NETWORK_FAILURE); + break; + } + }); + + disposables.add(confirmUsernameDisposable); } void onUsernameDeleted() { uiState.update(state -> new State(ButtonState.DELETE_LOADING, UsernameStatus.NONE, state.usernameState)); - repo.deleteUsername((result) -> { - ThreadUtil.runOnMain(() -> { - switch (result) { - case SUCCESS: - uiState.update(state -> new State(ButtonState.DELETE_DISABLED, UsernameStatus.NONE, state.usernameState)); - events.postValue(Event.DELETE_SUCCESS); - break; - case NETWORK_ERROR: - uiState.update(state -> new State(ButtonState.DELETE, UsernameStatus.NONE, state.usernameState)); - events.postValue(Event.NETWORK_FAILURE); - break; - } - }); + Disposable deletionDisposable = repo.deleteUsername().subscribe(result -> { + switch (result) { + case SUCCESS: + uiState.update(state -> new State(ButtonState.DELETE_DISABLED, UsernameStatus.NONE, state.usernameState)); + events.onNext(Event.DELETE_SUCCESS); + break; + case NETWORK_ERROR: + uiState.update(state -> new State(ButtonState.DELETE, UsernameStatus.NONE, state.usernameState)); + events.onNext(Event.NETWORK_FAILURE); + break; + } }); + + disposables.add(deletionDisposable); } @NonNull Flowable getUiState() { return uiState.getStateFlowable().observeOn(AndroidSchedulers.mainThread()); } - @NonNull LiveData getEvents() { - return events; + @NonNull Observable getEvents() { + return events.observeOn(AndroidSchedulers.mainThread()); } private void onNicknameChanged(@NonNull String nickname) { @@ -177,33 +178,33 @@ class UsernameEditViewModel extends ViewModel { } uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, UsernameState.Loading.INSTANCE)); - repo.reserveUsername(nickname, result -> { - ThreadUtil.runOnMain(() -> { - result.either( - reserveUsernameJsonResponse -> { - uiState.update(state -> new State(ButtonState.SUBMIT, UsernameStatus.NONE, new UsernameState.Reserved(reserveUsernameJsonResponse))); - return null; - }, - failure -> { - switch (failure) { - case SUCCESS: - throw new AssertionError(); - case USERNAME_INVALID: - uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.INVALID_GENERIC, UsernameState.NoUsername.INSTANCE)); - break; - case USERNAME_UNAVAILABLE: - uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.TAKEN, UsernameState.NoUsername.INSTANCE)); - break; - case NETWORK_ERROR: - uiState.update(state -> new State(ButtonState.SUBMIT, UsernameStatus.NONE, UsernameState.NoUsername.INSTANCE)); - events.postValue(Event.NETWORK_FAILURE); - break; - } + Disposable reserveDisposable = repo.reserveUsername(nickname).subscribe(result -> { + result.either( + reserveUsernameJsonResponse -> { + uiState.update(state -> new State(ButtonState.SUBMIT, UsernameStatus.NONE, new UsernameState.Reserved(reserveUsernameJsonResponse))); + return null; + }, + failure -> { + switch (failure) { + case SUCCESS: + throw new AssertionError(); + case USERNAME_INVALID: + uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.INVALID_GENERIC, UsernameState.NoUsername.INSTANCE)); + break; + case USERNAME_UNAVAILABLE: + uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.TAKEN, UsernameState.NoUsername.INSTANCE)); + break; + case NETWORK_ERROR: + uiState.update(state -> new State(ButtonState.SUBMIT, UsernameStatus.NONE, UsernameState.NoUsername.INSTANCE)); + events.onNext(Event.NETWORK_FAILURE); + break; + } - return null; - }); - }); + return null; + }); }); + + disposables.add(reserveDisposable); } private static UsernameStatus mapUsernameError(@NonNull InvalidReason invalidReason) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/views/CircularProgressMaterialButton.kt b/app/src/main/java/org/thoughtcrime/securesms/util/views/CircularProgressMaterialButton.kt index e1dd35067..6b047fe3b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/views/CircularProgressMaterialButton.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/views/CircularProgressMaterialButton.kt @@ -9,6 +9,7 @@ import android.util.AttributeSet import android.view.ViewAnimationUtils import android.widget.FrameLayout import androidx.annotation.StringRes +import androidx.annotation.VisibleForTesting import androidx.core.animation.doOnEnd import androidx.core.content.withStyledAttributes import com.google.android.material.button.MaterialButton @@ -88,6 +89,11 @@ class CircularProgressMaterialButton @JvmOverloads constructor( materialButton.setOnClickListener(onClickListener) } + @VisibleForTesting + fun getRequestedState(): State { + return requestedState + } + fun setSpinning() { transformTo(State.PROGRESS, true) } diff --git a/dependencies.gradle b/dependencies.gradle index af86e134d..5af22b3b4 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -132,6 +132,7 @@ dependencyResolutionManagement { alias('androidx-test-core-ktx').to('androidx.test', 'core-ktx').versionRef('androidx-test') alias('androidx-test-ext-junit').to('androidx.test.ext:junit:1.1.1') alias('androidx-test-ext-junit-ktx').to('androidx.test.ext:junit-ktx:1.1.1') + alias('androidx-fragment-testing').to('androidx.fragment:fragment-testing:1.3.2') alias('espresso-core').to('androidx.test.espresso:espresso-core:3.4.0') alias('mockito-core').to('org.mockito:mockito-inline:4.6.1') alias('mockito-kotlin').to('org.mockito.kotlin:mockito-kotlin:4.0.0') diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index d169fdf6d..d788aa3ed 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -229,6 +229,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -374,6 +379,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -382,6 +395,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -390,6 +411,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -804,6 +833,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -814,6 +848,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -3138,6 +3177,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -3173,6 +3217,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/ReserveUsernameResponse.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/ReserveUsernameResponse.java index 24f4d326a..d3d20b12a 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/ReserveUsernameResponse.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/ReserveUsernameResponse.java @@ -11,6 +11,14 @@ public class ReserveUsernameResponse { ReserveUsernameResponse() {} + /** + * Visible for testing. + */ + public ReserveUsernameResponse(String username, String reservationToken) { + this.username = username; + this.reservationToken = reservationToken; + } + public String getUsername() { return username; }