Commit 178a7ae0 authored by Vaclav Brozek's avatar Vaclav Brozek Committed by Commit Bot

[Android settings] Progress bar stays up for >=1s

During exporting passwords, a progressbar is shown. The UI
requirements (go/lcfve) are: once shown, the progress bar needs to
stay up for at least one second to avoid flickering. This CL
implements the artificial delay.

Bug: 817370
Change-Id: I60729b86b1a684d6031dda2863842b0e056bce7a
Reviewed-on: https://chromium-review.googlesource.com/951586
Commit-Queue: Vaclav Brozek <vabr@chromium.org>
Reviewed-by: default avatarBernhard Bauer <bauerb@chromium.org>
Cr-Commit-Position: refs/heads/master@{#543117}
parent adbeb213
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.preferences.password;
/**
* This is an interface for delaying running of callbacks.
*/
public interface CallbackDelayer {
/**
* Run a callback after a delay specific to a particular implementation. The callback is always
* run asynchronously.
* @param callback The callback to be run.
*/
void delay(Runnable callback);
}
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.preferences.password;
import android.app.DialogFragment;
import android.app.FragmentManager;
import android.support.annotation.Nullable;
import org.chromium.base.ThreadUtils;
/**
* This class manages a {@link DialogFragment}.
* In particular, it ensures that the dialog stays visible for a minimum time period, so that
* earlier calls to hide it are delayed appropriately. It also allows to override the delaying for
* testing purposes.
*/
public final class DialogManager {
/**
* Contains the reference to a {@link android.app.DialogFragment} between the call to {@link
* show} and dismissing the dialog.
*/
@Nullable
private DialogFragment mDialogFragment;
/**
* The least amout of time for which {@link mDialogFragment} should stay visible to avoid
* flickering. It was chosen so that it is enough to read the approx. 3 words on it, but not too
* long (to avoid the user waiting while Chrome is already idle).
*/
private static final long MINIMUM_LIFE_SPAN_MILLIS = 1000L;
/** This is used to post the unblocking signal for hiding the dialog fragment. */
private CallbackDelayer mDelayer = new TimedCallbackDelayer(MINIMUM_LIFE_SPAN_MILLIS);
/** Allows to fake the timed delayer. */
public void replaceCallbackDelayerForTesting(CallbackDelayer newDelayer) {
mDelayer = newDelayer;
}
/**
* Used to gate hiding of a dialog on two actions: one automatic delayed signal and one manual
* call to {@link hide}. This is not null between the calls to {@link show} and {@link hide}.
*/
@Nullable
private SingleThreadBarrierClosure mBarrierClosure;
/** Callback to run after the dialog was hidden. Can be null if no hiding was requested.*/
@Nullable
private Runnable mCallback;
/**
* Shows the dialog.
* @param dialog to be shown.
* @param fragmentManager needed to call {@link android.app.DialogFragment#show}
*/
public void show(DialogFragment dialog, FragmentManager fragmentManager) {
assert mDialogFragment == null;
mDialogFragment = dialog;
mDialogFragment.show(fragmentManager, null);
// Initiate the barrier closure, expecting 2 runs: one automatic but delayed, and one
// explicit, to hide the dialog.
// TODO(crbug.com/821377) -- remove clang-format pragmas
// clang-format off
mBarrierClosure = new SingleThreadBarrierClosure(2, this::hideImmediately);
// clang-format on
// This is the automatic but delayed signal.
mDelayer.delay(mBarrierClosure);
}
/**
* Hides the dialog as soon as possible, but not sooner than {@link MINIMUM_LIFE_SPAN_MILLIS}
* milliseconds after it was shown. Attempts to hide the dialog when none is shown are
* gracefully ignored but the callback is called in any case.
* @param callback is asynchronously called as soon as the dialog is no longer visible.
*/
public void hide(Runnable callback) {
mCallback = callback;
// The barrier closure is null if the dialog was not shown. In that case don't wait before
// confirming the hidden state.
if (mBarrierClosure == null) {
hideImmediately();
} else {
mBarrierClosure.run();
}
}
/**
* Synchronously hides the dialog without any delay. Attempts to hide the dialog when
* none is shown are gracefully ignored but |mCallback| is called in any case if present.
*/
private void hideImmediately() {
if (mDialogFragment != null) mDialogFragment.dismiss();
// Post the callback to ensure that it is always run asynchronously, even if hide() took a
// shortcut for a missing shown().
if (mCallback != null) ThreadUtils.postOnUiThread(mCallback);
reset();
}
/** Resets the dialog reference and metadata related to it.*/
private void reset() {
mDialogFragment = null;
mCallback = null;
mBarrierClosure = null;
}
}
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.preferences.password;
import java.util.ArrayList;
import java.util.List;
/**
* An implementation of {@link CallbackDelayer} for tests. It runs callbacks after a manual signal.
*/
public final class ManualCallbackDelayer implements CallbackDelayer {
/** The callbacks to be run within {@link runCallbacksSynchronously}.*/
private List<Runnable> mCallbacks = new ArrayList<>();
@Override
public void delay(Runnable callback) {
mCallbacks.add(callback);
}
/** Run the callback previously passed into {@link delay}.*/
public void runCallbacksSynchronously() {
for (Runnable callback : mCallbacks) callback.run();
mCallbacks.clear();
}
}
......@@ -189,11 +189,9 @@ public class SavePasswordsPreferences
private boolean mSearchRecorded;
private Menu mMenuForTesting;
// Contains the reference to the progress-bar dialog after the user confirms the password
// export and before the serialized passwords arrive, so that the dialog can be dismissed on the
// passwords' arrival. It is null during all other times.
@Nullable
private ProgressBarDialogFragment mProgressBarDialogFragment;
// Takes care of displaying and hiding the progress bar for exporting, while avoiding
// flickering.
private DialogManager mProgressBarManager = new DialogManager();
// If an error dialog should be shown, this contains the arguments for it, such as the error
// message. If no error dialog should be shown, this is null.
......@@ -206,6 +204,10 @@ public class SavePasswordsPreferences
// times.
private ExportWarningDialogFragment mExportWarningDialogFragment;
public DialogManager getDialogManagerForTesting() {
return mProgressBarManager;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
......@@ -486,9 +488,8 @@ public class SavePasswordsPreferences
if (mExportFileUri == null) {
// The serialization has not finished. Until this finishes, a progress bar is
// displayed with an option to cancel the export.
assert mProgressBarDialogFragment == null;
mProgressBarDialogFragment = new ProgressBarDialogFragment();
mProgressBarDialogFragment.setCancelProgressHandler(
ProgressBarDialogFragment progressBarDialogFragment = new ProgressBarDialogFragment();
progressBarDialogFragment.setCancelProgressHandler(
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
......@@ -500,12 +501,15 @@ public class SavePasswordsPreferences
}
}
});
mProgressBarDialogFragment.show(getFragmentManager(), null);
mProgressBarManager.show(progressBarDialogFragment, getFragmentManager());
} else {
// Note: if the serialization is quicker than the user interacting with the
// confirmation dialog, then there is no progress bar shown.
if (mProgressBarDialogFragment != null) mProgressBarDialogFragment.dismiss();
sendExportIntent();
// confirmation dialog, then there is no progress bar shown, in which case hide() is
// just calling the callback synchronously.
// TODO(crbug.com/821377) -- remove clang-format pragmas
// clang-format off
mProgressBarManager.hide(this::sendExportIntent);
// clang-format on
}
}
......@@ -520,8 +524,15 @@ public class SavePasswordsPreferences
public void showExportErrorAndAbort(int descriptionId, @Nullable String detailedDescription,
int positiveButtonLabelId, @HistogramExportResult int histogramExportResult) {
assert mErrorDialogParams == null;
if (mProgressBarDialogFragment != null) mProgressBarDialogFragment.dismiss();
mProgressBarManager.hide(() -> {
showExportErrorAndAbortImmediately(descriptionId, detailedDescription,
positiveButtonLabelId, histogramExportResult);
});
}
public void showExportErrorAndAbortImmediately(int descriptionId,
@Nullable String detailedDescription, int positiveButtonLabelId,
@HistogramExportResult int histogramExportResult) {
RecordHistogram.recordEnumeratedHistogram("PasswordManager.ExportPasswordsToCSVResult",
histogramExportResult, EXPORT_RESULT_COUNT);
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.preferences.password;
/**
* A Runnable which postpones running a given callback until it is itself run for a pre-defined
* number of times. It is inspired by the native //base/barrier_closure.*. Unlike the native code,
* SingleThreadBarrierClosure is only meant to be used on a single thread and is not thread-safe.
*/
public final class SingleThreadBarrierClosure implements Runnable {
/** Counts the remaining number of runs. */
private int mRemainingRuns;
/** The callback to be run when {@link #mRemainingRuns} reaches 0.*/
private final Runnable mCallback;
/**
* Construct a {@link Runnable} such that the first {@code runsExpected-1} calls to {@link #run}
* are a no-op and the last one runs {@code callback}.
* @param runsExpected The number of total {@link #run} calls expected.
* @param callback The callback to be run once called enough times.
*/
public SingleThreadBarrierClosure(int runsExpected, Runnable callback) {
assert runsExpected > 0;
assert callback != null;
mRemainingRuns = runsExpected;
mCallback = callback;
}
@Override
public void run() {
if (mRemainingRuns == 0) return;
--mRemainingRuns;
if (mRemainingRuns == 0) mCallback.run();
}
}
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.preferences.password;
import android.os.Handler;
/**
* An implementation of {@link CallbackDelayer} which runs callbacks after a fixed time delay.
*/
public final class TimedCallbackDelayer implements CallbackDelayer {
/** The {@link Handler} used to delay the callbacks. */
private final Handler mHandler = new Handler();
/** How long to delay callbacks, in milliseconds. */
private final long mDelayMillis;
/**
* Constructs a delayer which posts callbacks with a fixed time delay.
* @param delayMillis The common delay of the callbacks, in milliseconds.
*/
public TimedCallbackDelayer(long delayMillis) {
assert delayMillis >= 0;
mDelayMillis = delayMillis;
}
@Override
public void delay(Runnable callback) {
mHandler.postDelayed(callback, mDelayMillis);
}
}
......@@ -993,8 +993,11 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/preferences/languages/LanguageListBaseAdapter.java",
"java/src/org/chromium/chrome/browser/preferences/languages/LanguageListPreference.java",
"java/src/org/chromium/chrome/browser/preferences/password/ByteArrayIntCallback.java",
"java/src/org/chromium/chrome/browser/preferences/password/CallbackDelayer.java",
"java/src/org/chromium/chrome/browser/preferences/password/DialogManager.java",
"java/src/org/chromium/chrome/browser/preferences/password/ExportErrorDialogFragment.java",
"java/src/org/chromium/chrome/browser/preferences/password/ExportWarningDialogFragment.java",
"java/src/org/chromium/chrome/browser/preferences/password/ManualCallbackDelayer.java",
"java/src/org/chromium/chrome/browser/preferences/password/PasswordEntryEditor.java",
"java/src/org/chromium/chrome/browser/preferences/password/PasswordManagerHandler.java",
"java/src/org/chromium/chrome/browser/preferences/password/PasswordManagerHandlerProvider.java",
......@@ -1004,6 +1007,8 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/preferences/password/ReauthenticationManager.java",
"java/src/org/chromium/chrome/browser/preferences/password/SavedPasswordEntry.java",
"java/src/org/chromium/chrome/browser/preferences/password/SavePasswordsPreferences.java",
"java/src/org/chromium/chrome/browser/preferences/password/SingleThreadBarrierClosure.java",
"java/src/org/chromium/chrome/browser/preferences/password/TimedCallbackDelayer.java",
"java/src/org/chromium/chrome/browser/preferences/privacy/BandwidthType.java",
"java/src/org/chromium/chrome/browser/preferences/privacy/BrowsingDataBridge.java",
"java/src/org/chromium/chrome/browser/preferences/privacy/BrowsingDataCounterBridge.java",
......@@ -1993,9 +1998,13 @@ chrome_junit_test_java_sources = [
"junit/src/org/chromium/chrome/browser/physicalweb/PwsResultTest.java",
"junit/src/org/chromium/chrome/browser/physicalweb/UrlInfoTest.java",
"junit/src/org/chromium/chrome/browser/preferences/privacy/PrivacyPreferencesManagerTest.java",
"junit/src/org/chromium/chrome/browser/preferences/password/DialogManagerTest.java",
"junit/src/org/chromium/chrome/browser/preferences/password/EnsureAsyncPostingRule.java",
"junit/src/org/chromium/chrome/browser/preferences/password/ExportWarningDialogFragmentTest.java",
"junit/src/org/chromium/chrome/browser/preferences/password/PasswordReauthenticationFragmentTest.java",
"junit/src/org/chromium/chrome/browser/preferences/password/ReauthenticationManagerTest.java",
"junit/src/org/chromium/chrome/browser/preferences/password/SingleThreadBarrierClosureTest.java",
"junit/src/org/chromium/chrome/browser/preferences/password/TimedCallbackDelayerTest.java",
"junit/src/org/chromium/chrome/browser/signin/SigninPromoUtilTest.java",
"junit/src/org/chromium/chrome/browser/snackbar/SnackbarCollectionUnitTest.java",
"junit/src/org/chromium/chrome/browser/suggestions/ImageFetcherTest.java",
......
......@@ -201,6 +201,12 @@ public class SavePasswordsPreferencesTest {
// to instantiate it.
FakePasswordManagerHandler mHandler;
/**
* Delayer controling hiding the progress bar during exporting passwords. This replaces a time
* delay used in production.
*/
private final ManualCallbackDelayer mManualDelayer = new ManualCallbackDelayer();
/**
* Helper to set up a fake source of displayed passwords.
* @param entry An entry to be added to saved passwords. Can be null.
......@@ -264,6 +270,9 @@ public class SavePasswordsPreferencesTest {
/**
* Taps the menu item to trigger exporting and ensures that reauthentication passes.
* It also disables the timer in {@link DialogManager} which is used to allow hiding the
* progress bar after an initial period. Hiding can be later allowed manually in tests with
* {@link #allowProgressBarToBeHidden}, to avoid time-dependent flakiness.
*/
private void reauthenticateAndRequestExport(Preferences preferences) {
openActionBarOverflowOrOptionsMenu(
......@@ -281,10 +290,15 @@ public class SavePasswordsPreferencesTest {
ReauthenticationManager.recordLastReauth(
System.currentTimeMillis(), ReauthenticationManager.REAUTH_SCOPE_BULK);
// Now call onResume to nudge Chrome into continuing the export flow.
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
// Disable the timer for progress bar.
SavePasswordsPreferences fragment =
(SavePasswordsPreferences) preferences.getFragmentForTest();
fragment.getDialogManagerForTesting().replaceCallbackDelayerForTesting(
mManualDelayer);
// Now call onResume to nudge Chrome into continuing the export flow.
preferences.getFragmentForTest().onResume();
}
});
......@@ -335,6 +349,13 @@ public class SavePasswordsPreferencesTest {
});
}
/**
* Sends the signal to {@link DialogManager} that the minimal time for showing the progress
* bar has passed. This results in the progress bar getting hidden as soon as requested.
*/
private void allowProgressBarToBeHidden(Preferences preferences) {
ThreadUtils.runOnUiThreadBlocking(mManualDelayer::runCallbacksSynchronously);
}
/**
* Call after activity.finish() to wait for the wrap up to complete. If it was already completed
* or could be finished within |timeout_ms|, stop waiting anyways.
......@@ -1062,6 +1083,64 @@ public class SavePasswordsPreferencesTest {
checkExportMenuItemState(MENU_ITEM_STATE_ENABLED);
}
/**
* Check that a progressbar is displayed for a minimal time duration to avoid flickering.
*/
@Test
@SmallTest
@Feature({"Preferences"})
@EnableFeatures("PasswordExport")
public void testExportProgressMinimalTime() throws Exception {
setPasswordSource(new SavedPasswordEntry("https://example.com", "test user", "password"));
ReauthenticationManager.setApiOverride(ReauthenticationManager.OVERRIDE_STATE_AVAILABLE);
ReauthenticationManager.setScreenLockSetUpOverride(
ReauthenticationManager.OVERRIDE_STATE_AVAILABLE);
final Preferences preferences =
PreferencesTest.startPreferences(InstrumentationRegistry.getInstrumentation(),
SavePasswordsPreferences.class.getName());
Intents.init();
// This also disables the timer for keeping the progress bar up. The test can thus emulate
// that timer going off by calling {@link allowProgressBarToBeHidden}.
reauthenticateAndRequestExport(preferences);
// Before triggering the sharing intent chooser, stub it out to avoid leaving system UI open
// after the test is finished.
intending(hasAction(equalTo(Intent.ACTION_CHOOSER)))
.respondWith(new Instrumentation.ActivityResult(Activity.RESULT_OK, null));
// Confirm the export warning to fire the sharing intent.
Espresso.onView(withText(R.string.save_password_preferences_export_action_title))
.perform(click());
// Before simulating the serialized passwords being received, check that the progress bar is
// shown.
Espresso.onView(withText(R.string.settings_passwords_preparing_export))
.check(matches(isDisplayed()));
// Now pretend that passwords have been serialized.
mHandler.getExportCallback().onResult(new byte[] {5, 6, 7}, 12);
// Check that the progress bar is still shown, though, because the timer has not gone off
// yet.
Espresso.onView(withText(R.string.settings_passwords_preparing_export))
.check(matches(isDisplayed()));
// Now mark the timer as gone off and check that the progress bar is hidden.
allowProgressBarToBeHidden(preferences);
Espresso.onView(withText(R.string.settings_passwords_preparing_export))
.check(doesNotExist());
intended(allOf(hasAction(equalTo(Intent.ACTION_CHOOSER)),
hasExtras(hasEntry(equalTo(Intent.EXTRA_INTENT),
allOf(hasAction(equalTo(Intent.ACTION_SEND)), hasType("text/csv"))))));
Intents.release();
}
/**
* Check that a progressbar is displayed when the user confirms the export and the serialized
* passwords are not ready yet.
......@@ -1100,6 +1179,7 @@ public class SavePasswordsPreferencesTest {
.check(matches(isDisplayed()));
// Now pretend that passwords have been serialized.
allowProgressBarToBeHidden(preferences);
mHandler.getExportCallback().onResult(new byte[] {5, 6, 7}, 12);
// After simulating the serialized passwords being received, check that the progress bar is
......@@ -1138,6 +1218,10 @@ public class SavePasswordsPreferencesTest {
Espresso.onView(withText(R.string.save_password_preferences_export_action_title))
.perform(click());
// Simulate the minimal time for showing the progress bar to have passed, to ensure that it
// is kept live because of the pending serialization.
allowProgressBarToBeHidden(preferences);
// Check that the progress bar is shown.
Espresso.onView(withText(R.string.settings_passwords_preparing_export))
.check(matches(isDisplayed()));
......@@ -1182,6 +1266,7 @@ public class SavePasswordsPreferencesTest {
// Show an arbitrary error. This should replace the progress bar if that has been shown in
// the meantime.
allowProgressBarToBeHidden(preferences);
requestShowingExportError(
preferences, R.string.save_password_preferences_export_learn_google_drive);
......@@ -1224,6 +1309,7 @@ public class SavePasswordsPreferencesTest {
// Show an arbitrary error but ensure that the positive button label is the one for "try
// again".
allowProgressBarToBeHidden(preferences);
requestShowingExportError(preferences, R.string.try_again);
// Hit the positive button to try again.
......@@ -1261,6 +1347,7 @@ public class SavePasswordsPreferencesTest {
// Show an arbitrary error but ensure that the positive button label is the one for the
// Google Drive help site.
allowProgressBarToBeHidden(preferences);
requestShowingExportError(
preferences, R.string.save_password_preferences_export_learn_google_drive);
......@@ -1312,6 +1399,7 @@ public class SavePasswordsPreferencesTest {
.perform(click());
// Check that now the error is displayed, instead of the progress bar.
allowProgressBarToBeHidden(preferences);
Espresso.onView(withText(R.string.settings_passwords_preparing_export))
.check(doesNotExist());
Espresso.onView(withText(R.string.save_password_preferences_export_no_app))
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.preferences.password;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.app.DialogFragment;
import android.app.FragmentManager;
import android.support.annotation.IntDef;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.Robolectric;
import org.robolectric.annotation.Config;
import org.chromium.base.test.BaseRobolectricTestRunner;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Tests for the {@link DialogManager} class.
*/
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class DialogManagerTest {
@Rule
public MockitoRule mMockitoRule = MockitoJUnit.rule();
/**
* Fake of {@link DialogFragment} allowing to detect calls to {@link DialogFragment#show} and
* {@link DialogFragment#dismiss}.
*/
private static class FakeDialogFragment extends DialogFragment {
@Retention(RetentionPolicy.SOURCE)
@IntDef({NEW, SHOWING, DISMISSED})
public @interface DialogState {}
/** The dialog has not been shown yet. */
public static final int NEW = 0;
/** The dialog is showing. */
public static final int SHOWING = 0;
/** The dialog has been dismissed. */
public static final int DISMISSED = 0;
@DialogState
private int mState = NEW;
@DialogState
public int getState() {
return mState;
}
@Override
public void show(FragmentManager manager, String tag) {
assert mState == NEW;
mState = SHOWING;
}
@Override
public void dismiss() {
assert mState == SHOWING;
mState = DISMISSED;
}
}
/** Used to detect showing and hiding without actually triggering any UI. */
private final FakeDialogFragment mDialogFragment = new FakeDialogFragment();
/** The object under test. */
private final DialogManager mDialogManager = new DialogManager();
/**
* Delayer to replace the timed one in the tested class. This gives exact control over hiding
* delays.
*/
private final ManualCallbackDelayer mManualDelayer = new ManualCallbackDelayer();
@Before
public void setUp() {
mDialogManager.replaceCallbackDelayerForTesting(mManualDelayer);
}
/**
* Check that a dialog is hidden eventually but not before a prescribed delay.
*/
@Test
public void testHiddenEventually() {
mDialogManager.show(mDialogFragment, null);
Runnable callback = mock(Runnable.class);
mDialogManager.hide(callback);
Robolectric.getForegroundThreadScheduler().advanceToLastPostedRunnable();
verify(callback, never()).run();
mManualDelayer.runCallbacksSynchronously();
verify(callback, times(1)).run();
assertEquals(FakeDialogFragment.DISMISSED, mDialogFragment.getState());
}
/**
* Check that the callback is called even if the dialog has not been shown at all.
*/
@Test
public void testCallbackCalled() {
Runnable callback = mock(Runnable.class);
mDialogManager.hide(callback);
Robolectric.getForegroundThreadScheduler().advanceToLastPostedRunnable();
verify(callback, times(1)).run();
assertEquals(FakeDialogFragment.NEW, mDialogFragment.getState());
}
}
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.preferences.password;
import org.junit.rules.ExternalResource;
import org.robolectric.Robolectric;
import org.robolectric.util.Scheduler;
/**
* A Rule for ensuring that posted tasks are run asynchronously (and on demand). To do so, the rule
* pauses the Roboelectric scheduler before running the test and restores its state afterwards.
*/
public class EnsureAsyncPostingRule extends ExternalResource {
/** Remembers the paused state of the scheduler so that it can be restored after the test. */
private boolean mWasSchedulerPaused;
/** A reference to the scheduler which needs to be paused.*/
private Scheduler mScheduler;
@Override
protected void before() {
mScheduler = Robolectric.getForegroundThreadScheduler();
mWasSchedulerPaused = mScheduler.isPaused();
// Pause the scheduler, otherwise tasks which should run asynchronously will run
// synchronously and confuse the tests.
mScheduler.pause();
};
@Override
protected void after() {
if (!mWasSchedulerPaused) mScheduler.unPause();
};
}
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.preferences.password;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.annotation.Config;
import org.chromium.base.test.BaseRobolectricTestRunner;
/**
* Tests for the {@link SingleThreadBarrierClosure} class.
*/
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class SingleThreadBarrierClosureTest {
@Rule
public final MockitoRule mMockitoRule = MockitoJUnit.rule();
@Rule
public final EnsureAsyncPostingRule mPostingRule = new EnsureAsyncPostingRule();
/**
* Check that the callback is posted after as many signals as specified.
*/
@Test
public void testCallbackPosted() {
// Arbitrary counts of signals to try.
final int[] signalCounts = {1, 2, 7};
for (int signalCount : signalCounts) {
Runnable callback = mock(Runnable.class);
SingleThreadBarrierClosure barrierClosure =
new SingleThreadBarrierClosure(signalCount, callback);
for (int i = 0; i < signalCount - 1; ++i) barrierClosure.run();
// No callback yet, too few run() invocations.
verify(callback, never()).run();
barrierClosure.run();
verify(callback, times(1)).run();
barrierClosure.run();
// Ensure that further run() calls are quietly ignored: the callback was not run a
// second time (the recorded call count is still at 1).
verify(callback, times(1)).run();
}
}
}
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.preferences.password;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.Robolectric;
import org.robolectric.annotation.Config;
import org.chromium.base.test.BaseRobolectricTestRunner;
/**
* Tests for the {@link TimedCallbackDelayer} class.
*/
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class TimedCallbackDelayerTest {
@Rule
public final MockitoRule mMockitoRule = MockitoJUnit.rule();
@Rule
public final EnsureAsyncPostingRule mPostingRule = new EnsureAsyncPostingRule();
/**
* Check that the callback is eventually called.
*/
@Test
public void testCallbackCalled() {
// Arbitrary time delays in milliseconds.
final long[] delays = {0, 2, 5432};
for (long delay : delays) {
Runnable callback = mock(Runnable.class);
TimedCallbackDelayer delayer = new TimedCallbackDelayer(delay);
delayer.delay(callback);
verify(callback, never()).run();
Robolectric.getForegroundThreadScheduler().advanceToLastPostedRunnable();
verify(callback, times(1)).run();
}
}
/**
* Check that the callback is not called synchronously, even if the time delay is 0.
*/
@Test
public void testCallbackAsync() {
Runnable callback = mock(Runnable.class);
TimedCallbackDelayer delayer = new TimedCallbackDelayer(0);
delayer.delay(callback);
verify(callback, never()).run();
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment