diff --git a/src/main/java/org/medicmobile/webapp/mobile/OpenSettingsDialogFragment.java b/src/main/java/org/medicmobile/webapp/mobile/OpenSettingsDialogFragment.java index bf9b4ec7..d92d03f1 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/OpenSettingsDialogFragment.java +++ b/src/main/java/org/medicmobile/webapp/mobile/OpenSettingsDialogFragment.java @@ -5,6 +5,8 @@ import android.app.Fragment; import android.content.Intent; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.view.MotionEvent; import android.view.View; import android.view.View.OnTouchListener; @@ -19,6 +21,8 @@ public class OpenSettingsDialogFragment extends Fragment { private long lastTimeTap = 0; private GestureHandler swipeGesture; private static final int TIME_BETWEEN_TAPS = 500; + private View mainView; + boolean isViewSetup = false; private final OnTouchListener onTouchListener = new OnTouchListener() { @SuppressLint("ClickableViewAccessibility") @@ -33,8 +37,44 @@ public boolean onTouch(View view, MotionEvent event) { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); + setRetainInstance(true); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + setupViewWithRetry(); + } + + private boolean shouldSetupView() { + return getActivity() != null && !isViewSetup; + } + + private boolean setupView() { View view = getActivity().findViewById(R.id.wbvMain); - view.setOnTouchListener(onTouchListener); + if (view != null) { + mainView = view; + mainView.setOnTouchListener(onTouchListener); + isViewSetup = true; + return true; + } + + return false; + } + + void setupViewWithRetry() { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + if (!shouldSetupView() || !setupView()) { + retrySetup(); + } + } + + private void retrySetup() { + new Handler(Looper.getMainLooper()).postDelayed(this, 100); + } + }); } private void countTaps(MotionEvent event) { diff --git a/src/test/java/org/medicmobile/webapp/mobile/OpenSettingsDialogFragmentTest.java b/src/test/java/org/medicmobile/webapp/mobile/OpenSettingsDialogFragmentTest.java index 62976d73..05dbce9a 100644 --- a/src/test/java/org/medicmobile/webapp/mobile/OpenSettingsDialogFragmentTest.java +++ b/src/test/java/org/medicmobile/webapp/mobile/OpenSettingsDialogFragmentTest.java @@ -1,232 +1,308 @@ package org.medicmobile.webapp.mobile; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.CALLS_REAL_METHODS; import static org.mockito.Mockito.RETURNS_SMART_NULLS; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.Mockito.withSettings; +import static org.robolectric.Shadows.shadowOf; import android.app.Activity; import android.content.Intent; +import android.os.Looper; import android.view.MotionEvent; import android.view.View; import android.view.View.OnTouchListener; import org.junit.Before; import org.junit.Test; +import org.junit.experimental.runners.Enclosed; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.MockSettings; import org.mockito.MockedStatic; import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.LooperMode; +import org.robolectric.shadows.ShadowLooper; import java.time.Clock; import java.time.Instant; import java.time.ZoneOffset; import java.util.stream.IntStream; -@RunWith(RobolectricTestRunner.class) +@RunWith(Enclosed.class) +@SuppressWarnings("PMD.TestClassWithoutTestCases") public class OpenSettingsDialogFragmentTest { + @RunWith(RobolectricTestRunner.class) + public static class BehaviorTest { + private OpenSettingsDialogFragment openSettingsDialogFragment; + private Activity activity; + private ArgumentCaptor argsOnTouch; + private ArgumentCaptor argsStartActivity; + private View view; + + @Before + public void setup() { + activity = mock(Activity.class, RETURNS_SMART_NULLS); + doNothing().when(activity).finish(); + + view = mock(View.class); + argsOnTouch = ArgumentCaptor.forClass(OnTouchListener.class); + doNothing().when(view).setOnTouchListener(argsOnTouch.capture()); + + MockSettings fragmentSettings = withSettings() + .useConstructor() + .defaultAnswer(CALLS_REAL_METHODS); + + openSettingsDialogFragment = mock(OpenSettingsDialogFragment.class, fragmentSettings); + when(openSettingsDialogFragment.getActivity()).thenReturn(activity); + when(openSettingsDialogFragment.getActivity().findViewById(R.id.wbvMain)).thenReturn(view); + argsStartActivity = ArgumentCaptor.forClass(Intent.class); + doNothing().when(openSettingsDialogFragment).startActivity(argsStartActivity.capture()); + + openSettingsDialogFragment.onCreate(null); + openSettingsDialogFragment.onActivityCreated(null); + shadowOf(Looper.getMainLooper()).idle(); + } - private OpenSettingsDialogFragment openSettingsDialogFragment; - private Activity activity; - private ArgumentCaptor argsOnTouch; - private ArgumentCaptor argsStartActivity; + private void tap(OnTouchListener onTouchListener, MotionEvent eventTap, int times) { + IntStream + .range(0, times) + .forEach(i -> onTouchListener.onTouch(null, eventTap)); + } - @Before - public void setup() { - activity = mock(Activity.class, RETURNS_SMART_NULLS); - doNothing().when(activity).finish(); + private void positionPointers(OnTouchListener onTouchListener, MotionEvent eventSwipe, float pointer1, float pointer2) { + when(eventSwipe.getX(0)).thenReturn(pointer1); + when(eventSwipe.getX(1)).thenReturn(pointer2); + onTouchListener.onTouch(null, eventSwipe); + } - View view = mock(View.class); - argsOnTouch = ArgumentCaptor.forClass(OnTouchListener.class); - doNothing().when(view).setOnTouchListener(argsOnTouch.capture()); + @Test + public void onTouch_withRightGestures_opensSettingsDialog() { + //> GIVEN + MotionEvent eventTap = mock(MotionEvent.class); + when(eventTap.getPointerCount()).thenReturn(1); + when(eventTap.getActionMasked()).thenReturn(MotionEvent.ACTION_DOWN); - MockSettings fragmentSettings = withSettings() - .useConstructor() - .defaultAnswer(CALLS_REAL_METHODS); + MotionEvent eventSwipe = mock(MotionEvent.class); + when(eventSwipe.getPointerCount()).thenReturn(2); - openSettingsDialogFragment = mock(OpenSettingsDialogFragment.class, fragmentSettings); - when(openSettingsDialogFragment.getActivity()).thenReturn(activity); - when(openSettingsDialogFragment.getActivity().findViewById(R.id.wbvMain)).thenReturn(view); - argsStartActivity = ArgumentCaptor.forClass(Intent.class); - doNothing().when(openSettingsDialogFragment).startActivity(argsStartActivity.capture()); + //> WHEN + OnTouchListener onTouchListener = argsOnTouch.getValue(); + tap(onTouchListener, eventTap, 6); - openSettingsDialogFragment.onCreate(null); - } + when(eventSwipe.getActionMasked()).thenReturn(MotionEvent.ACTION_POINTER_DOWN); + positionPointers(onTouchListener, eventSwipe, (float) 261.81, (float) 264.99); - private void tap(OnTouchListener onTouchListener, MotionEvent eventTap, int times) { - IntStream - .range(0, times) - .forEach(i -> onTouchListener.onTouch(null, eventTap)); - } + when(eventSwipe.getActionMasked()).thenReturn(MotionEvent.ACTION_MOVE); + positionPointers(onTouchListener, eventSwipe, (float) 800.90, (float) 850.13); - private void positionPointers(OnTouchListener onTouchListener, MotionEvent eventSwipe, float pointer1, float pointer2) { - when(eventSwipe.getX(0)).thenReturn(pointer1); - when(eventSwipe.getX(1)).thenReturn(pointer2); - onTouchListener.onTouch(null, eventSwipe); - } + //> THEN + Intent intent = argsStartActivity.getValue(); + assertEquals(SettingsDialogActivity.class.getName(), intent.getComponent().getClassName()); + verify(activity).finish(); + } - @Test - public void onTouch_withRightGestures_opensSettingsDialog() { - //> GIVEN - MotionEvent eventTap = mock(MotionEvent.class); - when(eventTap.getPointerCount()).thenReturn(1); - when(eventTap.getActionMasked()).thenReturn(MotionEvent.ACTION_DOWN); + @Test + public void onTouch_withNoSwipe_doesNotOpenSettingsDialog() { + //> GIVEN + MotionEvent eventTap = mock(MotionEvent.class); + when(eventTap.getPointerCount()).thenReturn(1); + when(eventTap.getActionMasked()).thenReturn(MotionEvent.ACTION_DOWN); - MotionEvent eventSwipe = mock(MotionEvent.class); - when(eventSwipe.getPointerCount()).thenReturn(2); + MotionEvent eventSwipe = mock(MotionEvent.class); + when(eventSwipe.getPointerCount()).thenReturn(2); - //> WHEN - OnTouchListener onTouchListener = argsOnTouch.getValue(); - tap(onTouchListener, eventTap, 6); + //> WHEN + OnTouchListener onTouchListener = argsOnTouch.getValue(); + tap(onTouchListener, eventTap, 6); - when(eventSwipe.getActionMasked()).thenReturn(MotionEvent.ACTION_POINTER_DOWN); - positionPointers(onTouchListener, eventSwipe, (float) 261.81, (float) 264.99); + when(eventSwipe.getActionMasked()).thenReturn(MotionEvent.ACTION_POINTER_DOWN); + positionPointers(onTouchListener, eventSwipe, (float) 261.81, (float) 264.99); - when(eventSwipe.getActionMasked()).thenReturn(MotionEvent.ACTION_MOVE); - positionPointers(onTouchListener, eventSwipe, (float) 800.90, (float) 850.13); + //> THEN + verify(openSettingsDialogFragment, never()).startActivity(any()); + verify(activity, never()).finish(); + } - //> THEN - Intent intent = argsStartActivity.getValue(); - assertEquals(SettingsDialogActivity.class.getName(), intent.getComponent().getClassName()); - verify(activity).finish(); - } + @Test + public void onTouch_with1FingerSwipe_doesNotOpenSettingsDialog() { + //> GIVEN + MotionEvent eventTap = mock(MotionEvent.class); + when(eventTap.getPointerCount()).thenReturn(1); + when(eventTap.getActionMasked()).thenReturn(MotionEvent.ACTION_DOWN); - @Test - public void onTouch_withNoSwipe_doesNotOpenSettingsDialog() { - //> GIVEN - MotionEvent eventTap = mock(MotionEvent.class); - when(eventTap.getPointerCount()).thenReturn(1); - when(eventTap.getActionMasked()).thenReturn(MotionEvent.ACTION_DOWN); + MotionEvent eventSwipe = mock(MotionEvent.class); + when(eventSwipe.getPointerCount()).thenReturn(1); - MotionEvent eventSwipe = mock(MotionEvent.class); - when(eventSwipe.getPointerCount()).thenReturn(2); + //> WHEN + OnTouchListener onTouchListener = argsOnTouch.getValue(); + tap(onTouchListener, eventTap, 6); - //> WHEN - OnTouchListener onTouchListener = argsOnTouch.getValue(); - tap(onTouchListener, eventTap, 6); + when(eventSwipe.getActionMasked()).thenReturn(MotionEvent.ACTION_POINTER_DOWN); + positionPointers(onTouchListener, eventSwipe, (float) 261.81, (float) 264.99); - when(eventSwipe.getActionMasked()).thenReturn(MotionEvent.ACTION_POINTER_DOWN); - positionPointers(onTouchListener, eventSwipe, (float) 261.81, (float) 264.99); + when(eventSwipe.getActionMasked()).thenReturn(MotionEvent.ACTION_MOVE); + positionPointers(onTouchListener, eventSwipe, (float) 800.90, (float) 850.13); - //> THEN - verify(openSettingsDialogFragment, never()).startActivity(any()); - verify(activity, never()).finish(); - } + //> THEN + verify(openSettingsDialogFragment, never()).startActivity(any()); + verify(activity, never()).finish(); + } - @Test - public void onTouch_with1FingerSwipe_doesNotOpenSettingsDialog() { - //> GIVEN - MotionEvent eventTap = mock(MotionEvent.class); - when(eventTap.getPointerCount()).thenReturn(1); - when(eventTap.getActionMasked()).thenReturn(MotionEvent.ACTION_DOWN); + @Test + public void onTouch_withNoEnoughTaps_doesNotOpenSettingsDialog() { + //> GIVEN + MotionEvent eventTap = mock(MotionEvent.class); + when(eventTap.getPointerCount()).thenReturn(1); + when(eventTap.getActionMasked()).thenReturn(MotionEvent.ACTION_DOWN); - MotionEvent eventSwipe = mock(MotionEvent.class); - when(eventSwipe.getPointerCount()).thenReturn(1); + MotionEvent eventSwipe = mock(MotionEvent.class); + when(eventSwipe.getPointerCount()).thenReturn(2); - //> WHEN - OnTouchListener onTouchListener = argsOnTouch.getValue(); - tap(onTouchListener, eventTap, 6); + //> WHEN + OnTouchListener onTouchListener = argsOnTouch.getValue(); + tap(onTouchListener, eventTap, 4); - when(eventSwipe.getActionMasked()).thenReturn(MotionEvent.ACTION_POINTER_DOWN); - positionPointers(onTouchListener, eventSwipe, (float) 261.81, (float) 264.99); + when(eventSwipe.getActionMasked()).thenReturn(MotionEvent.ACTION_POINTER_DOWN); + positionPointers(onTouchListener, eventSwipe, (float) 261.81, (float) 264.99); - when(eventSwipe.getActionMasked()).thenReturn(MotionEvent.ACTION_MOVE); - positionPointers(onTouchListener, eventSwipe, (float) 800.90, (float) 850.13); + when(eventSwipe.getActionMasked()).thenReturn(MotionEvent.ACTION_MOVE); + positionPointers(onTouchListener, eventSwipe, (float) 800.90, (float) 850.13); - //> THEN - verify(openSettingsDialogFragment, never()).startActivity(any()); - verify(activity, never()).finish(); - } + //> THEN + verify(openSettingsDialogFragment, never()).startActivity(any()); + verify(activity, never()).finish(); + } - @Test - public void onTouch_withNoEnoughTaps_doesNotOpenSettingsDialog() { - //> GIVEN - MotionEvent eventTap = mock(MotionEvent.class); - when(eventTap.getPointerCount()).thenReturn(1); - when(eventTap.getActionMasked()).thenReturn(MotionEvent.ACTION_DOWN); + @Test + public void onTouch_with2FingerTaps_doesNotOpenSettingsDialog() { + //> GIVEN + MotionEvent eventTap = mock(MotionEvent.class); + when(eventTap.getPointerCount()).thenReturn(2); + when(eventTap.getActionMasked()).thenReturn(MotionEvent.ACTION_DOWN); - MotionEvent eventSwipe = mock(MotionEvent.class); - when(eventSwipe.getPointerCount()).thenReturn(2); + MotionEvent eventSwipe = mock(MotionEvent.class); + when(eventSwipe.getPointerCount()).thenReturn(2); - //> WHEN - OnTouchListener onTouchListener = argsOnTouch.getValue(); - tap(onTouchListener, eventTap, 4); + //> WHEN + OnTouchListener onTouchListener = argsOnTouch.getValue(); + tap(onTouchListener, eventTap, 6); - when(eventSwipe.getActionMasked()).thenReturn(MotionEvent.ACTION_POINTER_DOWN); - positionPointers(onTouchListener, eventSwipe, (float) 261.81, (float) 264.99); + when(eventSwipe.getActionMasked()).thenReturn(MotionEvent.ACTION_POINTER_DOWN); + positionPointers(onTouchListener, eventSwipe, (float) 261.81, (float) 264.99); - when(eventSwipe.getActionMasked()).thenReturn(MotionEvent.ACTION_MOVE); - positionPointers(onTouchListener, eventSwipe, (float) 800.90, (float) 850.13); + when(eventSwipe.getActionMasked()).thenReturn(MotionEvent.ACTION_MOVE); + positionPointers(onTouchListener, eventSwipe, (float) 800.90, (float) 850.13); - //> THEN - verify(openSettingsDialogFragment, never()).startActivity(any()); - verify(activity, never()).finish(); - } + //> THEN + verify(openSettingsDialogFragment, never()).startActivity(any()); + verify(activity, never()).finish(); + } - @Test - public void onTouch_with2FingerTaps_doesNotOpenSettingsDialog() { - //> GIVEN - MotionEvent eventTap = mock(MotionEvent.class); - when(eventTap.getPointerCount()).thenReturn(2); - when(eventTap.getActionMasked()).thenReturn(MotionEvent.ACTION_DOWN); + @Test + public void onTouch_withTapTimeout_doesNotOpenSettingsDialog() { + Clock startTime = Clock.fixed(Instant.ofEpochMilli(1000), ZoneOffset.UTC); + Clock otherTapsTime = Clock.fixed(Instant.ofEpochMilli(1501), ZoneOffset.UTC); - MotionEvent eventSwipe = mock(MotionEvent.class); - when(eventSwipe.getPointerCount()).thenReturn(2); + try (MockedStatic mockClock = mockStatic(Clock.class)) { + //> GIVEN + MotionEvent eventTap = mock(MotionEvent.class); + when(eventTap.getPointerCount()).thenReturn(1); + when(eventTap.getActionMasked()).thenReturn(MotionEvent.ACTION_DOWN); - //> WHEN - OnTouchListener onTouchListener = argsOnTouch.getValue(); - tap(onTouchListener, eventTap, 6); + MotionEvent eventSwipe = mock(MotionEvent.class); + when(eventSwipe.getPointerCount()).thenReturn(2); - when(eventSwipe.getActionMasked()).thenReturn(MotionEvent.ACTION_POINTER_DOWN); - positionPointers(onTouchListener, eventSwipe, (float) 261.81, (float) 264.99); + mockClock.when(Clock::systemUTC).thenReturn(startTime); - when(eventSwipe.getActionMasked()).thenReturn(MotionEvent.ACTION_MOVE); - positionPointers(onTouchListener, eventSwipe, (float) 800.90, (float) 850.13); + //> WHEN + OnTouchListener onTouchListener = argsOnTouch.getValue(); + tap(onTouchListener, eventTap, 2); + mockClock.when(Clock::systemUTC).thenReturn(otherTapsTime); + tap(onTouchListener, eventTap, 4); - //> THEN - verify(openSettingsDialogFragment, never()).startActivity(any()); - verify(activity, never()).finish(); + when(eventSwipe.getActionMasked()).thenReturn(MotionEvent.ACTION_POINTER_DOWN); + positionPointers(onTouchListener, eventSwipe, (float) 261.81, (float) 264.99); + + when(eventSwipe.getActionMasked()).thenReturn(MotionEvent.ACTION_MOVE); + positionPointers(onTouchListener, eventSwipe, (float) 800.90, (float) 850.13); + + //> THEN + verify(openSettingsDialogFragment, never()).startActivity(any()); + verify(activity, never()).finish(); + } + } } - @Test - public void onTouch_withTapTimeout_doesNotOpenSettingsDialog() { - Clock startTime = Clock.fixed(Instant.ofEpochMilli(1000), ZoneOffset.UTC); - Clock otherTapsTime = Clock.fixed(Instant.ofEpochMilli(1501), ZoneOffset.UTC); + @RunWith(RobolectricTestRunner.class) + @LooperMode(LooperMode.Mode.PAUSED) + public static class ViewSetupTest { + private OpenSettingsDialogFragment fragment; + private Activity mockActivity; + private View mockView; + + @Before + public void setUp() { + fragment = spy(new OpenSettingsDialogFragment()); + mockActivity = mock(Activity.class); + mockView = mock(View.class); + doReturn(mockActivity).when(fragment).getActivity(); + } - try (MockedStatic mockClock = mockStatic(Clock.class)) { - //> GIVEN - MotionEvent eventTap = mock(MotionEvent.class); - when(eventTap.getPointerCount()).thenReturn(1); - when(eventTap.getActionMasked()).thenReturn(MotionEvent.ACTION_DOWN); + @Test + public void SetupViewWithRetry_Success() { + when(mockActivity.findViewById(R.id.wbvMain)).thenReturn(mockView); - MotionEvent eventSwipe = mock(MotionEvent.class); - when(eventSwipe.getPointerCount()).thenReturn(2); + fragment.setupViewWithRetry(); + ShadowLooper.runMainLooperOneTask(); - mockClock.when(Clock::systemUTC).thenReturn(startTime); + verify(mockView).setOnTouchListener(any()); + assertTrue(fragment.isViewSetup); + } - //> WHEN - OnTouchListener onTouchListener = argsOnTouch.getValue(); - tap(onTouchListener, eventTap, 2); - mockClock.when(Clock::systemUTC).thenReturn(otherTapsTime); - tap(onTouchListener, eventTap, 4); + @Test + public void testSetupViewWithRetry_NullView_Retries() { + when(mockActivity.findViewById(R.id.wbvMain)).thenReturn(null).thenReturn(mockView); - when(eventSwipe.getActionMasked()).thenReturn(MotionEvent.ACTION_POINTER_DOWN); - positionPointers(onTouchListener, eventSwipe, (float) 261.81, (float) 264.99); + fragment.setupViewWithRetry(); + ShadowLooper.runMainLooperOneTask(); + ShadowLooper.runMainLooperOneTask(); - when(eventSwipe.getActionMasked()).thenReturn(MotionEvent.ACTION_MOVE); - positionPointers(onTouchListener, eventSwipe, (float) 800.90, (float) 850.13); + verify(mockView).setOnTouchListener(any()); + assertTrue(fragment.isViewSetup); + } - //> THEN - verify(openSettingsDialogFragment, never()).startActivity(any()); - verify(activity, never()).finish(); + @Test + public void testSetupViewWithRetry_NullActivity_DoesNotSetup() { + doReturn(null).when(fragment).getActivity(); + + fragment.setupViewWithRetry(); + ShadowLooper.runMainLooperOneTask(); + + verify(mockActivity, never()).findViewById(anyInt()); + assertFalse(fragment.isViewSetup); + } + + @Test + public void testSetupViewWithRetry_AlreadySetup_DoesNotSetup() { + fragment.isViewSetup = true; + + fragment.setupViewWithRetry(); + ShadowLooper.runMainLooperOneTask(); + + verify(mockActivity, never()).findViewById(anyInt()); } } }