package com.bumptech.glide.request.target; import static android.view.ViewGroup.LayoutParams; import static android.view.ViewTreeObserver.OnPreDrawListener; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.inOrder; 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.graphics.drawable.Drawable; import android.os.Build; import android.view.View; import android.view.ViewTreeObserver; import com.bumptech.glide.request.Request; import com.bumptech.glide.request.transition.Transition; import com.bumptech.glide.tests.Util; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.shadow.api.Shadow; import org.robolectric.shadows.ShadowView; @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE, sdk = 19, shadows = { ViewTargetTest.SizedShadowView.class, ViewTargetTest.PreDrawShadowViewTreeObserver.class }) public class ViewTargetTest { private View view; private ViewTarget<View, Object> target; private SizedShadowView shadowView; private PreDrawShadowViewTreeObserver shadowObserver; @Mock private SizeReadyCallback cb; @Mock private Request request; private int sdkVersion; @Before public void setUp() { sdkVersion = Build.VERSION.SDK_INT; MockitoAnnotations.initMocks(this); view = new View(RuntimeEnvironment.application); target = new TestViewTarget(view); shadowView = Shadow.extract(view); shadowObserver = Shadow.extract(view.getViewTreeObserver()); } @After public void tearDown() { Util.setSdkVersionInt(sdkVersion); } @Test public void testReturnsWrappedView() { assertEquals(view, target.getView()); } @Test public void testReturnsNullFromGetRequestIfNoRequestSet() { assertNull(target.getRequest()); } @Test(expected = IllegalArgumentException.class) public void testThrowsIfViewTagIsNotRequestObject() { view.setTag(new Object()); target.getRequest(); } @Test public void testCanSetAndRetrieveRequest() { target.setRequest(request); assertEquals(request, target.getRequest()); } @Test public void testRetrievesRequestFromPreviousTargetForView() { target.setRequest(request); ViewTarget<View, Object> second = new TestViewTarget(view); assertEquals(request, second.getRequest()); } @Test public void testSizeCallbackIsCalledSynchronouslyIfViewSizeSet() { int dimens = 333; shadowView .setWidth(dimens) .setHeight(dimens) .setIsLaidOut(true); target.getSize(cb); verify(cb).onSizeReady(eq(dimens), eq(dimens)); } @Test public void testSizeCallbackIsCalledSynchronouslyIfLayoutParamsConcreteSizeSet() { int dimens = 444; LayoutParams layoutParams = new LayoutParams(dimens, dimens); view.setLayoutParams(layoutParams); shadowView.setIsLaidOut(true); target.getSize(cb); verify(cb).onSizeReady(eq(dimens), eq(dimens)); } @Test public void getSize_withBothWrapContent_returnsSizeOriginal() { LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); view.setLayoutParams(layoutParams); shadowView.setIsLaidOut(true); target.getSize(cb); verify(cb).onSizeReady(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL); } @Test public void getSize_withWrapContentWidthAndValidHeight_usesSizeOriginalWidthValidHeight() { int height = 100; LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, height); view.setLayoutParams(params); shadowView.setIsLaidOut(true); target.getSize(cb); verify(cb).onSizeReady(Target.SIZE_ORIGINAL, height); } @Test public void getSize_withWrapContentHeightAndValidWidth_returnsWidthAndSizeOriginalHeight() { int width = 100; LayoutParams params = new LayoutParams(width, LayoutParams.WRAP_CONTENT); view.setLayoutParams(params); shadowView.setIsLaidOut(true); target.getSize(cb); verify(cb).onSizeReady(width, Target.SIZE_ORIGINAL); } @Test public void getSize_withWrapContentWidthAndMatchParentHeight_usesSizeOriginalWidthAndHeight() { LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); view.setLayoutParams(params); target.getSize(cb); verify(cb, never()).onSizeReady(anyInt(), anyInt()); int height = 32; shadowView .setHeight(height) .setIsLaidOut(true); shadowObserver.fireOnPreDrawListeners(); verify(cb).onSizeReady(Target.SIZE_ORIGINAL, height); } @Test public void getSize_withMatchParentWidthAndWrapContentHeight_usesWidthAndSizeOriginalHeight() { LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); view.setLayoutParams(params); target.getSize(cb); verify(cb, never()).onSizeReady(anyInt(), anyInt()); int width = 32; shadowView .setWidth(width) .setIsLaidOut(true); shadowObserver.fireOnPreDrawListeners(); verify(cb).onSizeReady(width, Target.SIZE_ORIGINAL); } @Test public void testMatchParentWidthAndHeight() { LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); view.setLayoutParams(params); target.getSize(cb); verify(cb, never()).onSizeReady(anyInt(), anyInt()); int width = 32; int height = 45; shadowView .setWidth(width) .setHeight(height) .setIsLaidOut(true); shadowObserver.fireOnPreDrawListeners(); verify(cb).onSizeReady(eq(width), eq(height)); } @Test public void testSizeCallbackIsCalledPreDrawIfNoDimensAndNoLayoutParams() { target.getSize(cb); int width = 12; int height = 32; shadowView .setWidth(width) .setHeight(height) .setIsLaidOut(true); shadowObserver.fireOnPreDrawListeners(); verify(cb).onSizeReady(eq(width), eq(height)); } @Test public void testSizeCallbacksAreCalledInOrderPreDraw() { SizeReadyCallback[] cbs = new SizeReadyCallback[25]; for (int i = 0; i < cbs.length; i++) { cbs[i] = mock(SizeReadyCallback.class); target.getSize(cbs[i]); } int width = 100, height = 111; shadowView .setWidth(width) .setHeight(height) .setIsLaidOut(true); shadowObserver.fireOnPreDrawListeners(); InOrder order = inOrder((Object[]) cbs); for (SizeReadyCallback cb : cbs) { order.verify(cb).onSizeReady(eq(width), eq(height)); } } @Test public void testDoesNotNotifyCallbackTwiceIfAddedTwice() { target.getSize(cb); target.getSize(cb); view.setLayoutParams(new LayoutParams(100, 100)); shadowView.setIsLaidOut(true); shadowObserver.fireOnPreDrawListeners(); verify(cb, times(1)).onSizeReady(anyInt(), anyInt()); } @Test public void testDoesNotAddMultipleListenersIfMultipleCallbacksAreAdded() { SizeReadyCallback cb1 = mock(SizeReadyCallback.class); SizeReadyCallback cb2 = mock(SizeReadyCallback.class); target.getSize(cb1); target.getSize(cb2); assertThat(shadowObserver.getPreDrawListeners()).hasSize(1); } @Test public void testDoesAddSecondListenerIfFirstListenerIsRemovedBeforeSecondRequest() { SizeReadyCallback cb1 = mock(SizeReadyCallback.class); target.getSize(cb1); view.setLayoutParams(new LayoutParams(100, 100)); shadowView.setIsLaidOut(true); shadowObserver.fireOnPreDrawListeners(); assertThat(shadowObserver.getPreDrawListeners()).hasSize(0); SizeReadyCallback cb2 = mock(SizeReadyCallback.class); view.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); target.getSize(cb2); view.setLayoutParams(new LayoutParams(100, 100)); shadowObserver.fireOnPreDrawListeners(); verify(cb2).onSizeReady(anyInt(), anyInt()); } @Test public void testSizeCallbackIsNotCalledPreDrawIfNoDimensSetOnPreDraw() { target.getSize(cb); shadowObserver.fireOnPreDrawListeners(); verify(cb, never()).onSizeReady(anyInt(), anyInt()); assertThat(shadowObserver.getPreDrawListeners()).hasSize(1); } @Test public void testSizeCallbackIsCalledPreDrawIfNoDimensAndNoLayoutParamsButLayoutParamsSetLater() { target.getSize(cb); int width = 689; int height = 354; LayoutParams layoutParams = new LayoutParams(width, height); view.setLayoutParams(layoutParams); shadowView.setIsLaidOut(true); shadowObserver.fireOnPreDrawListeners(); verify(cb).onSizeReady(eq(width), eq(height)); } @Test public void testCallbackIsNotCalledTwiceIfPreDrawFiresTwice() { target.getSize(cb); LayoutParams layoutParams = new LayoutParams(1234, 4123); view.setLayoutParams(layoutParams); shadowView.setIsLaidOut(true); shadowObserver.fireOnPreDrawListeners(); shadowObserver.fireOnPreDrawListeners(); verify(cb, times(1)).onSizeReady(anyInt(), anyInt()); } @Test public void testCallbacksFromMultipleRequestsAreNotifiedOnPreDraw() { SizeReadyCallback firstCb = mock(SizeReadyCallback.class); SizeReadyCallback secondCb = mock(SizeReadyCallback.class); target.getSize(firstCb); target.getSize(secondCb); int width = 68; int height = 875; LayoutParams layoutParams = new LayoutParams(width, height); view.setLayoutParams(layoutParams); shadowView.setIsLaidOut(true); shadowObserver.fireOnPreDrawListeners(); shadowObserver.fireOnPreDrawListeners(); verify(firstCb, times(1)).onSizeReady(eq(width), eq(height)); verify(secondCb, times(1)).onSizeReady(eq(width), eq(height)); } @Test public void testDoesNotThrowOnPreDrawIfViewTreeObserverIsDead() { target.getSize(cb); int width = 1; int height = 2; LayoutParams layoutParams = new LayoutParams(width, height); view.setLayoutParams(layoutParams); shadowView.setIsLaidOut(true); shadowObserver.setIsAlive(false); shadowObserver.fireOnPreDrawListeners(); verify(cb).onSizeReady(eq(width), eq(height)); } @Test(expected = NullPointerException.class) public void testThrowsIfGivenNullView() { new TestViewTarget(null); } @Test public void testDecreasesDimensionsByViewPadding() { view.setLayoutParams(new LayoutParams(100, 100)); view.setPadding(25, 25, 25, 25); shadowView.setIsLaidOut(true); target.getSize(cb); verify(cb).onSizeReady(50, 50); } @Test public void getSize_withValidWidthAndHeight_notLaidOut_doesNotCallSizeReady() { shadowView .setWidth(100) .setHeight(100) .setIsLaidOut(false); target.getSize(cb); verify(cb, never()).onSizeReady(anyInt(), anyInt()); } @Test public void getSize_withLayoutParams_notLaidOut_doesCallSizeReady() { shadowView .setLayoutParams(new LayoutParams(10, 10)) .setWidth(100) .setHeight(100) .setIsLaidOut(false); target.getSize(cb); verify(cb, times(1)).onSizeReady(anyInt(), anyInt()); } @Test public void getSize_withLayoutParams_zeroWidthHeight_notLaidOut_doesNotCallSizeReady() { shadowView .setLayoutParams(new LayoutParams(0, 0)) .setWidth(100) .setHeight(100) .setIsLaidOut(false); target.getSize(cb); verify(cb, never()).onSizeReady(anyInt(), anyInt()); } @Test public void getSize_withValidWidthAndHeight_preV19_layoutRequested_doesNotCallSizeReady() { Util.setSdkVersionInt(18); shadowView .setWidth(100) .setHeight(100) .requestLayout(); target.getSize(cb); verify(cb, never()).onSizeReady(anyInt(), anyInt()); } @Test public void getSize_withWidthAndHeightEqualToPadding_doesNotCallSizeReady() { shadowView .setWidth(100) .setHeight(100) .setIsLaidOut(true); view.setPadding(50, 50, 50, 50); target.getSize(cb); verify(cb, never()).onSizeReady(anyInt(), anyInt()); } @Implements(ViewTreeObserver.class) public static class PreDrawShadowViewTreeObserver { private CopyOnWriteArrayList<OnPreDrawListener> preDrawListeners = new CopyOnWriteArrayList<>(); private boolean isAlive = true; @SuppressWarnings("unused") @Implementation public void addOnPreDrawListener(OnPreDrawListener listener) { checkIsAlive(); preDrawListeners.add(listener); } @SuppressWarnings("unused") @Implementation public void removeOnPreDrawListener(OnPreDrawListener listener) { checkIsAlive(); preDrawListeners.remove(listener); } @Implementation public boolean isAlive() { return isAlive; } private void checkIsAlive() { if (!isAlive()) { throw new IllegalStateException("ViewTreeObserver is not alive!"); } } public void setIsAlive(boolean isAlive) { this.isAlive = isAlive; } public void fireOnPreDrawListeners() { for (OnPreDrawListener listener : preDrawListeners) { listener.onPreDraw(); } } public List<OnPreDrawListener> getPreDrawListeners() { return preDrawListeners; } } @Implements(View.class) public static class SizedShadowView extends ShadowView { private int width; private int height; private LayoutParams layoutParams; private boolean isLaidOut; private boolean isLayoutRequested; public SizedShadowView setWidth(int width) { this.width = width; return this; } public SizedShadowView setHeight(int height) { this.height = height; return this; } public SizedShadowView setLayoutParams(LayoutParams layoutParams) { this.layoutParams = layoutParams; return this; } public SizedShadowView setIsLaidOut(boolean isLaidOut) { this.isLaidOut = isLaidOut; return this; } @Implementation public void requestLayout() { isLayoutRequested = true; } @Implementation public int getWidth() { return width; } @Implementation public int getHeight() { return height; } @Implementation public boolean isLaidOut() { return isLaidOut; } @Implementation public boolean isLayoutRequested() { return isLayoutRequested; } @Implementation public LayoutParams getLayoutParams() { return layoutParams; } } private static class TestViewTarget extends ViewTarget<View, Object> { public TestViewTarget(View view) { super(view); } @Override public void onLoadStarted(Drawable placeholder) { } @Override public void onLoadFailed(Drawable errorDrawable) { } @Override public void onResourceReady(Object resource, Transition<? super Object> transition) { } @Override public void onLoadCleared(Drawable placeholder) { } } }