kopia lustrzana https://github.com/ryukoposting/Signal-Android
Add espresso test for usernames.
rodzic
4882a4d11c
commit
a340ebf74a
|
@ -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
|
||||
|
|
|
@ -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<UsernameEditFragment> {
|
||||
val fragmentArgs = UsernameEditFragmentArgs.Builder().setIsInRegistration(isInRegistration).build().toBundle()
|
||||
return launchFragmentInContainer(
|
||||
fragmentArgs = fragmentArgs,
|
||||
themeResId = R.style.Signal_DayNight_NoActionBar
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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<Result<ReserveUsernameResponse, UsernameSetResult>> callback) {
|
||||
executor.execute(() -> callback.onComplete(reserveUsernameInternal(nickname)));
|
||||
@NonNull Single<Result<ReserveUsernameResponse, UsernameSetResult>> reserveUsername(@NonNull String nickname) {
|
||||
return Single.fromCallable(() -> reserveUsernameInternal(nickname)).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
void confirmUsername(@NonNull ReserveUsernameResponse reserveUsernameResponse, @NonNull Callback<UsernameSetResult> callback) {
|
||||
executor.execute(() -> callback.onComplete(confirmUsernameInternal(reserveUsernameResponse)));
|
||||
@NonNull Single<UsernameSetResult> confirmUsername(@NonNull ReserveUsernameResponse reserveUsernameResponse) {
|
||||
return Single.fromCallable(() -> confirmUsernameInternal(reserveUsernameResponse)).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
void deleteUsername(@NonNull Callback<UsernameDeleteResult> callback) {
|
||||
executor.execute(() -> callback.onComplete(deleteUsernameInternal()));
|
||||
@NonNull Single<UsernameDeleteResult> deleteUsername() {
|
||||
return Single.fromCallable(this::deleteUsernameInternal).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
* <p>
|
||||
* A note on naming conventions:
|
||||
*
|
||||
* <p>
|
||||
* Usernames are made up of two discrete components, a nickname and a discriminator. They are formatted thusly:
|
||||
*
|
||||
* <p>
|
||||
* [nickname]#[discriminator]
|
||||
*
|
||||
* <p>
|
||||
* 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<Event> events;
|
||||
private final PublishSubject<Event> events;
|
||||
private final UsernameEditRepository repo;
|
||||
private final RxStore<State> uiState;
|
||||
private final PublishProcessor<String> 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().<UsernameState>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().<UsernameState>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<State> getUiState() {
|
||||
return uiState.getStateFlowable().observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
@NonNull LiveData<Event> getEvents() {
|
||||
return events;
|
||||
@NonNull Observable<Event> 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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -229,6 +229,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="44a9e30abf56af1025c52a0af506fee9c4131aa55efda52f9fd9451211c5e8cb" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.core" name="core" version="1.1.0">
|
||||
<artifact name="core-1.1.0.aar">
|
||||
<sha256 value="76c7cfbe596fe3c09a6983bf1c89e889299c08ac9a3b52ce5182a088d056647e" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.core" name="core" version="1.2.0">
|
||||
<artifact name="core-1.2.0.aar">
|
||||
<sha256 value="524b8b88ceb6a74a7e44e6b567a135660f211799904cb218bfee5be1166820b2" origin="Generated by Gradle"/>
|
||||
|
@ -374,6 +379,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="f7b07632a0e21994a66a6dfb291a4f5ae55c2699827ff45df071168560509c9b" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.fragment" name="fragment" version="1.3.2">
|
||||
<artifact name="fragment-1.3.2.aar">
|
||||
<sha256 value="299879e2ce39d35214391db4ef2d7257a801f9b7fbe76488cc64897d16a98b25" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="fragment-1.3.2.module">
|
||||
<sha256 value="769e3922eb5500d2e10786f99abd3cf75a24d754f1dfcdad47d8f297b973bcc1" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.fragment" name="fragment" version="1.3.5">
|
||||
<artifact name="fragment-1.3.5.aar">
|
||||
<sha256 value="a2826109ce34d6bc3792c7791e911e526f3d9c5f8b41f7ff1650638897a5d445" origin="Generated by Gradle"/>
|
||||
|
@ -382,6 +395,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="cfeb7db9039743b44fc585590bf8af571bd04309d3d1aa86251cf6e07779ea97" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.fragment" name="fragment-ktx" version="1.3.2">
|
||||
<artifact name="fragment-ktx-1.3.2.aar">
|
||||
<sha256 value="29af1e9ee0e93b5fc638600c230705584aecc49205c363f0923ba1e5be675533" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="fragment-ktx-1.3.2.module">
|
||||
<sha256 value="b3955b619e8a16c38af39c19126867c72d1954db05551709e58c082b946078c4" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.fragment" name="fragment-ktx" version="1.3.5">
|
||||
<artifact name="fragment-ktx-1.3.5.aar">
|
||||
<sha256 value="549965cc33b69270b7b3ba5d9fcb2cd746ae9ceca17c5e12219888ea281edc0f" origin="Generated by Gradle"/>
|
||||
|
@ -390,6 +411,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="8198cba5ec555ae47332297219f984e8e686504e03a0c627a26892687dc2fb8a" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.fragment" name="fragment-testing" version="1.3.2">
|
||||
<artifact name="fragment-testing-1.3.2.aar">
|
||||
<sha256 value="c9e4b3bfd105fecf3aff8a9105fdccf2e86ff6d5e53dc342a3aa65c087c1591e" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="fragment-testing-1.3.2.module">
|
||||
<sha256 value="75714382c8f9e292b01762852fd0672d33973254c2bd2b9918b50f861792b171" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.gridlayout" name="gridlayout" version="1.0.0">
|
||||
<artifact name="gridlayout-1.0.0.aar">
|
||||
<sha256 value="a7e5dc6f39dbc3dc6ac6d57b02a9c6fd792e80f0e45ddb3bb08e8f03d23c8755" origin="Generated by Gradle"/>
|
||||
|
@ -804,6 +833,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="9761b3a809c9b093fd06a3c4bbc645756dec0e95b5c9da419bc9f2a3f3026e8d" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.test" name="core" version="1.3.0">
|
||||
<artifact name="core-1.3.0.aar">
|
||||
<sha256 value="86549cae8c5b848f817e2c716e174c7dab61caf0b4df9848680eeb753089a337" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.test" name="core" version="1.4.0">
|
||||
<artifact name="core-1.4.0.aar">
|
||||
<sha256 value="671284e62e393f16ceae1a99a3a9a07bf1aacda29f8fe7b6b884355ef34c09cf" origin="Generated by Gradle"/>
|
||||
|
@ -814,6 +848,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="e4f9ca2b8f700cc278d878ed3925730e1ad5d60135bbfd05ab6708a528ebfa58" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.test" name="monitor" version="1.3.0">
|
||||
<artifact name="monitor-1.3.0.aar">
|
||||
<sha256 value="f73a31306a783e63150c60c49e140dc38da39a1b7947690f4b73387b5ebad77e" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.test" name="monitor" version="1.4.0">
|
||||
<artifact name="monitor-1.4.0.aar">
|
||||
<sha256 value="46a912a1e175f27a97521af3f50e5af87c22c49275dd2c57c043740012806325" origin="Generated by Gradle"/>
|
||||
|
@ -3138,6 +3177,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="5ace22b102a96425e4ac44e0558b927f3857b56a33cbc289cf1b70aee645e6a7" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jetbrains.kotlin" name="kotlin-stdlib" version="1.4.20">
|
||||
<artifact name="kotlin-stdlib-1.4.20.jar">
|
||||
<sha256 value="b8ab1da5cdc89cb084d41e1f28f20a42bd431538642a5741c52bbfae3fa3e656" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jetbrains.kotlin" name="kotlin-stdlib" version="1.4.21">
|
||||
<artifact name="kotlin-stdlib-1.4.21.jar">
|
||||
<sha256 value="f78c5d8c09db985912ab83a1de3c3b53ddf208d7b151f06a72358ea3e137d01b" origin="Generated by Gradle"/>
|
||||
|
@ -3173,6 +3217,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="4681f2d436a68c7523595d84ed5758e1382f9da0f67c91e6a848690d711274fe" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jetbrains.kotlin" name="kotlin-stdlib-common" version="1.4.20">
|
||||
<artifact name="kotlin-stdlib-common-1.4.20.jar">
|
||||
<sha256 value="a7112c9b3cefee418286c9c9372f7af992bd1e6e030691d52f60cb36dbec8320" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jetbrains.kotlin" name="kotlin-stdlib-common" version="1.4.21">
|
||||
<artifact name="kotlin-stdlib-common-1.4.21.jar">
|
||||
<sha256 value="812cf197d9c4c67e1f47f95e2d72a9b600f0d1124560617bfe9850773eccbcff" origin="Generated by Gradle"/>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue