// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.ui;
import static org.junit.Assert.assertEquals;
import static org.mockito.AdditionalMatchers.not;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import android.graphics.Rect;
import android.util.Size;
import android.view.View;
import android.view.WindowInsets;
import androidx.core.graphics.Insets;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.WindowInsetsCompat.Type.InsetsType;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.ui.InsetsRectProviderTest.ShadowWindowInsetsUtils;
import org.chromium.ui.util.WindowInsetsUtils;
import java.util.List;
/**
* Unit test for {@link InsetsRectProvider}. Since most of the calculations were done in {@link
* WindowInsetsUtils}, this test is mostly used to test if rects are up-to-date for observation when
* certain window insets has an update.
*/
@RunWith(BaseRobolectricTestRunner.class)
@Config(sdk = 30, shadows = ShadowWindowInsetsUtils.class)
public class InsetsRectProviderTest {
private static final int WINDOW_WIDTH = 600;
private static final int WINDOW_HEIGHT = 800;
private static final Size INSETS_FRAME_SIZE = new Size(WINDOW_WIDTH, WINDOW_HEIGHT);
private InsetsRectProvider mInsetsRectProvider;
@Mock private View mView;
@Mock private InsetObserver mInsetObserver;
@Before
public void setup() {
MockitoAnnotations.openMocks(this);
}
@After
public void tearDown() {
ShadowWindowInsetsUtils.reset();
}
@Test
public void testInitialization() {
// Assume a top insets
int type = WindowInsetsCompat.Type.captionBar();
Insets insets = Insets.of(0, 10, 0, 0);
List<Rect> blockingRects =
List.of(new Rect(0, 0, 10, 10), new Rect(WINDOW_WIDTH - 20, 0, WINDOW_WIDTH, 10));
Rect availableArea = new Rect(10, 0, WINDOW_WIDTH - 20, 10);
WindowInsetsCompat windowInsets =
buildTestWindowInsets(
type, insets, availableArea, INSETS_FRAME_SIZE, blockingRects);
mInsetsRectProvider = new InsetsRectProvider(mInsetObserver, type, windowInsets);
assertSuppliedValues(insets, availableArea, blockingRects);
}
@Test
public void testInitializationEmpty() {
int type = WindowInsetsCompat.Type.captionBar();
mInsetsRectProvider = new InsetsRectProvider(mInsetObserver, type, null);
assertSuppliedValues(Insets.NONE, new Rect(), List.of());
}
@Test
public void testObservation() {
// Assume inset is at the bottom for this test.
int type = WindowInsetsCompat.Type.navigationBars();
Insets insets = Insets.of(0, 0, 0, 10);
List<Rect> blockingRects =
List.of(
new Rect(0, WINDOW_HEIGHT - 10, 10, WINDOW_HEIGHT),
new Rect(
WINDOW_WIDTH - 20,
WINDOW_HEIGHT - 10,
WINDOW_WIDTH,
WINDOW_HEIGHT));
Rect availableArea = new Rect(10, WINDOW_HEIGHT - 10, WINDOW_WIDTH - 20, WINDOW_HEIGHT);
// Initialize with empty window insets.
WindowInsetsCompat emptyWindowInsets = new WindowInsetsCompat.Builder().build();
mInsetsRectProvider = new InsetsRectProvider(mInsetObserver, type, emptyWindowInsets);
assertSuppliedValues(Insets.NONE, new Rect(), List.of());
// Attach an observer and supply a new window insets.
CallbackHelper observer = new CallbackHelper();
mInsetsRectProvider.addObserver(rect -> observer.notifyCalled());
WindowInsetsCompat windowInsets =
buildTestWindowInsets(
type, insets, availableArea, INSETS_FRAME_SIZE, blockingRects);
mInsetsRectProvider.onApplyWindowInsets(mView, windowInsets);
assertEquals("Observer not called.", 1, observer.getCallCount());
assertSuppliedValues(insets, availableArea, blockingRects);
}
@Test
public void testInsetRemoved() {
// Assume inset is at the top for this test.
int type = WindowInsetsCompat.Type.statusBars();
Insets insets = Insets.of(0, 10, 0, 0);
List<Rect> blockingRects =
List.of(
new Rect(0, WINDOW_HEIGHT - 10, 10, WINDOW_HEIGHT),
new Rect(
WINDOW_WIDTH - 20,
WINDOW_HEIGHT - 10,
WINDOW_WIDTH,
WINDOW_HEIGHT));
Rect availableArea = new Rect(10, WINDOW_HEIGHT - 10, WINDOW_WIDTH - 20, WINDOW_HEIGHT);
// Initialize with valid insets.
WindowInsetsCompat windowInsets =
buildTestWindowInsets(
type, insets, availableArea, INSETS_FRAME_SIZE, blockingRects);
mInsetsRectProvider = new InsetsRectProvider(mInsetObserver, type, windowInsets);
assertSuppliedValues(insets, availableArea, blockingRects);
// Attach an observer and supply a new window insets.
CallbackHelper observer = new CallbackHelper();
mInsetsRectProvider.addObserver(rect -> observer.notifyCalled());
// Create an insets with a different type so it removes the exists insets.
WindowInsetsCompat newWindowInsets =
buildTestWindowInsets(
WindowInsetsCompat.Type.systemBars(),
Insets.NONE,
new Rect(),
INSETS_FRAME_SIZE,
List.of());
mInsetsRectProvider.onApplyWindowInsets(mView, newWindowInsets);
assertEquals("Observer not called.", 1, observer.getCallCount());
assertSuppliedValues(Insets.NONE, new Rect(), List.of());
}
@Test
public void testAppliedInsetsNotConsumed_EmptyFrame() {
// Assume a caption bar top insets.
int type = WindowInsetsCompat.Type.captionBar();
Insets insets = Insets.of(0, 10, 0, 0);
// Insets frame / bounding rects will be empty on a device that does not support the
// corresponding OS APIs.
// Initialize with empty window insets.
WindowInsetsCompat emptyWindowInsets = new WindowInsetsCompat.Builder().build();
mInsetsRectProvider = new InsetsRectProvider(mInsetObserver, type, emptyWindowInsets);
// Attach an observer to verify that input insets are not processed.
CallbackHelper observer = new CallbackHelper();
mInsetsRectProvider.addObserver(rect -> observer.notifyCalled());
WindowInsetsCompat newWindowInsets =
buildTestWindowInsets(type, insets, new Rect(), new Size(0, 0), List.of());
var appliedInsets = mInsetsRectProvider.onApplyWindowInsets(mView, newWindowInsets);
assertEquals("Input should not be consumed.", appliedInsets, newWindowInsets);
assertEquals("Observer should not be called.", 0, observer.getCallCount());
assertSuppliedValues(Insets.NONE, new Rect(), List.of());
}
@Test
public void testAppliedInsetsConsumed_SameAsCachedInsets() {
// Assume a caption bar top insets.
int type = WindowInsetsCompat.Type.captionBar();
Insets insets = Insets.of(0, 10, 0, 0);
// Initialize with empty window insets.
WindowInsetsCompat emptyWindowInsets = new WindowInsetsCompat.Builder().build();
mInsetsRectProvider = new InsetsRectProvider(mInsetObserver, type, emptyWindowInsets);
// Attach an observer to verify that new insets are processed once with duplicate back to
// back updates. Also verify that the insets are consumed in both cases.
CallbackHelper observer = new CallbackHelper();
mInsetsRectProvider.addObserver(rect -> observer.notifyCalled());
WindowInsetsCompat newWindowInsets =
buildTestWindowInsets(type, insets, new Rect(), INSETS_FRAME_SIZE, List.of());
var appliedInsets = mInsetsRectProvider.onApplyWindowInsets(mView, newWindowInsets);
assertEquals(
"Input insets should be consumed.", Insets.NONE, appliedInsets.getInsets(type));
appliedInsets = mInsetsRectProvider.onApplyWindowInsets(mView, newWindowInsets);
assertEquals(
"Input insets should be consumed.", Insets.NONE, appliedInsets.getInsets(type));
assertEquals("Observer should be called once.", 1, observer.getCallCount());
assertSuppliedValues(insets, new Rect(), List.of());
}
private WindowInsetsCompat buildTestWindowInsets(
@InsetsType int type,
Insets insets,
Rect availableArea,
Size frameSize,
List<Rect> blockingRects) {
// WindowInsetsCompat.Builder does not work in robolectric (always yield an empty Inset).
WindowInsetsCompat windowInsetsCompat = Mockito.mock(WindowInsetsCompat.class);
doReturn(insets).when(windowInsetsCompat).getInsets(eq(type));
doReturn(Insets.NONE).when(windowInsetsCompat).getInsets(not(eq(type)));
ShadowWindowInsetsUtils.sWidestUnoccludedRect = availableArea;
ShadowWindowInsetsUtils.sFrame = frameSize;
ShadowWindowInsetsUtils.sTestRects = blockingRects != null ? blockingRects : List.of();
return windowInsetsCompat;
}
private void assertSuppliedValues(Insets insets, Rect availableArea, List<Rect> blockingRects) {
assertEquals(
"Supplied #getBoundingRects is different.",
blockingRects,
mInsetsRectProvider.getBoundingRects());
assertEquals(
"Supplied #getWidestUnoccludedRect is different.",
availableArea,
mInsetsRectProvider.getWidestUnoccludedRect());
assertEquals(
"Supplied #getCachedInset is different.",
insets,
mInsetsRectProvider.getCachedInset());
}
/** Helper class to get the results using test values. */
@Implements(WindowInsetsUtils.class)
public static class ShadowWindowInsetsUtils {
static Rect sWidestUnoccludedRect;
static Size sFrame = new Size(0, 0);
static List<Rect> sTestRects = List.of();
private static void reset() {
sWidestUnoccludedRect = null;
sFrame = new Size(0, 0);
sTestRects = List.of();
}
@Implementation
protected static Rect getWidestUnoccludedRect(Rect regionRect, List<Rect> blockRects) {
return sWidestUnoccludedRect != null ? sWidestUnoccludedRect : new Rect();
}
@Implementation
protected static Size getFrameFromInsets(WindowInsets windowInsets) {
return sFrame;
}
@Implementation
protected static List<Rect> getBoundingRectsFromInsets(
WindowInsets windowInsets, @InsetsType int insetType) {
return sTestRects;
}
}
}