// Copyright 2019 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.chrome.browser.omnibox;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.AdditionalMatchers.not;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import android.graphics.Paint;
import android.text.InputType;
import android.text.Layout;
import android.text.SpannableStringBuilder;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.TypedValue;
import android.view.ContextThemeWrapper;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewStructure;
import android.view.inputmethod.EditorInfo;
import androidx.core.view.inputmethod.EditorInfoCompat;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.annotation.Config;
import org.chromium.base.ContextUtils;
import org.chromium.base.MathUtils;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.omnibox.UrlBar.UrlBarDelegate;
import org.chromium.chrome.browser.omnibox.test.R;
import java.util.Collections;
import java.util.List;
/** Unit tests for the URL bar UI component. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(qualifiers = "w100dp-h50dp")
public class UrlBarUnitTest {
// UrlBar has 4 px of padding on the left and right. Set this to urlbar width + padding so
// getVisibleMeasuredViewportWidth() returns 100. This ensures NUMBER_OF_VISIBLE_CHARACTERS
// is accurate.
private static final int URL_BAR_WIDTH = 100 + 8;
private static final int URL_BAR_HEIGHT = 50;
private static final float FONT_HEIGHT_NOMINAL = 100f;
private static final float FONT_HEIGHT_ACTUAL_TALL = 120f;
private static final float FONT_HEIGHT_ACTUAL_SHORT = 80f;
private static final float LINE_HEIGHT_REGULAR_FACTOR = UrlBar.LINE_HEIGHT_FACTOR;
private static final float LINE_HEIGHT_ELEGANT_FACTOR = 1.6f;
// Screen width is set to 100px, with a default density of 1px per dp, and we estimate 5dp per
// char, so there will be 20 visible characters.
private static final int NUMBER_OF_VISIBLE_CHARACTERS = 20;
// Separately declare a constant same as UrlBar.MIN_LENGTH_FOR_TRUNCATION so that one of these
// tests will fail if it's accidentally changed.
private static final int MIN_LENGTH_FOR_TRUNCATION = 100;
private UrlBar mUrlBar;
private Paint.FontMetrics mFontMetrics = new Paint.FontMetrics();
public @Rule MockitoRule mockitoRule = MockitoJUnit.rule();
private @Mock UrlBarDelegate mUrlBarDelegate;
private @Mock ViewStructure mViewStructure;
private @Mock Layout mLayout;
private @Mock TextPaint mPaint;
private final String mShortPath = "/aaaa";
private final String mLongPath =
"/" + TextUtils.join("", Collections.nCopies(MIN_LENGTH_FOR_TRUNCATION, "a"));
private final String mShortDomain = "www.a.com";
private final String mLongDomain =
"www."
+ TextUtils.join("", Collections.nCopies(MIN_LENGTH_FOR_TRUNCATION, "a"))
+ ".com";
@Before
public void setUp() {
var ctx =
new ContextThemeWrapper(
ContextUtils.getApplicationContext(), R.style.Theme_BrowserUI_DayNight);
mUrlBar = spy(new UrlBarApi26(ctx, null));
mUrlBar.setDelegate(mUrlBarDelegate);
lenient().doReturn(1).when(mLayout).getLineCount();
lenient()
.doAnswer(invocation -> (int) invocation.getArguments()[0] * 5f)
.when(mLayout)
.getPrimaryHorizontal(anyInt());
lenient()
.doAnswer(invocation -> (int) ((float) invocation.getArguments()[6] / 5))
.when(mPaint)
.getOffsetForAdvance(
any(CharSequence.class),
anyInt(),
anyInt(),
anyInt(),
anyInt(),
anyBoolean(),
anyFloat());
lenient().doReturn(mFontMetrics).when(mPaint).getFontMetrics();
lenient().doReturn(mPaint).when(mUrlBar).getPaint();
}
/** Force reset text layout. */
private void resetTextLayout() {
mUrlBar.nullLayouts();
assertNull(mUrlBar.getLayout());
}
/**
* Simulate measure() and layout() pass on the view. Ensures view size and text layout are
* resolved.
*/
private void measureAndLayoutUrlBarForSize(int width, int height) {
// Measure and layout the Url bar.
mUrlBar.setLayoutParams(new LayoutParams(width, height));
mUrlBar.measure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
mUrlBar.layout(0, 0, width, height);
// Confirmation check: new layout should be available.
assertNotNull(mUrlBar.getLayout());
assertFalse(mUrlBar.isLayoutRequested());
}
/** Resize the UrlBar to its default size for testing. */
private void measureAndLayoutUrlBar() {
measureAndLayoutUrlBarForSize(URL_BAR_WIDTH, URL_BAR_HEIGHT);
}
@Test
public void testAutofillStructureReceivesFullURL() {
mUrlBar.setTextForAutofillServices("https://www.google.com");
mUrlBar.setText("www.google.com");
mUrlBar.onProvideAutofillStructure(mViewStructure, 0);
ArgumentCaptor<SpannableStringBuilder> haveUrl =
ArgumentCaptor.forClass(SpannableStringBuilder.class);
verify(mViewStructure).setText(haveUrl.capture());
assertEquals("https://www.google.com", haveUrl.getValue().toString());
}
@Test
public void onCreateInputConnection_ensureNoAutocorrect() {
var info = new EditorInfo();
mUrlBar.onCreateInputConnection(info);
assertEquals(
EditorInfo.TYPE_TEXT_VARIATION_URI,
info.inputType & EditorInfo.TYPE_TEXT_VARIATION_URI);
assertEquals(0, info.inputType & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT);
}
@Test
public void onCreateInputConnection_disallowKeyboardLearningPassedToIme() {
doReturn(true).when(mUrlBarDelegate).allowKeyboardLearning();
var info = new EditorInfo();
mUrlBar.onCreateInputConnection(info);
assertEquals(0, info.imeOptions & EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING);
}
@Test
public void onCreateInputConnection_allowKeyboardLearningPassedToIme() {
doReturn(false).when(mUrlBarDelegate).allowKeyboardLearning();
var info = new EditorInfo();
mUrlBar.onCreateInputConnection(info);
assertEquals(
EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING,
info.imeOptions & EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING);
}
@Test
public void onCreateInputConnection_setDefaultsWhenDelegateNotPresent() {
mUrlBar.setDelegate(null);
var info = new EditorInfo();
mUrlBar.onCreateInputConnection(info);
assertEquals(
EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING,
info.imeOptions & EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING);
assertEquals(
EditorInfo.TYPE_TEXT_VARIATION_URI,
info.inputType & EditorInfo.TYPE_TEXT_VARIATION_URI);
assertEquals(0, info.inputType & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT);
}
@Test
public void urlBar_editorSetupPermitsWordSelection() {
// This test verifies whether the Omnibox is set up so that it permits word selection. See:
// https://cs.android.com/search?q=function:Editor.needsToSelectAllToSelectWordOrParagraph
int klass = mUrlBar.getInputType() & InputType.TYPE_MASK_CLASS;
int variation = mUrlBar.getInputType() & InputType.TYPE_MASK_VARIATION;
int flags = mUrlBar.getInputType() & InputType.TYPE_MASK_FLAGS;
assertEquals(InputType.TYPE_CLASS_TEXT, klass);
assertEquals(InputType.TYPE_TEXT_VARIATION_NORMAL, variation);
assertEquals(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS, flags);
}
@Test
public void testTruncation_LongUrl() {
doReturn(mLayout).when(mUrlBar).getLayout();
measureAndLayoutUrlBar();
String url = mShortDomain + mLongPath;
mUrlBar.setTextWithTruncation(url, UrlBar.ScrollType.SCROLL_TO_TLD, mShortDomain.length());
String text = mUrlBar.getText().toString();
assertEquals(url.substring(0, NUMBER_OF_VISIBLE_CHARACTERS), text);
}
@Test
public void testTruncation_ShortUrl() {
// Test with a url one character shorter than the minimum length for truncation so that this
// test fails when the UrlBar.MIN_LENGTH_FOR_TRUCATION_V2 is changed to something smaller.
String url = mShortDomain + mLongPath;
url = url.substring(0, 99);
mUrlBar.setTextWithTruncation(url, UrlBar.ScrollType.SCROLL_TO_TLD, mShortDomain.length());
String text = mUrlBar.getText().toString();
assertEquals(url, text);
}
@Test
public void testTruncation_LongTld_ScrollToTld() {
doReturn(mLayout).when(mUrlBar).getLayout();
measureAndLayoutUrlBar();
String url = mLongDomain + mShortPath;
mUrlBar.setTextWithTruncation(url, UrlBar.ScrollType.SCROLL_TO_TLD, mLongDomain.length());
String text = mUrlBar.getText().toString();
assertEquals(mLongDomain, text);
}
@Test
public void testTruncation_LongTld_ScrollToBeginning() {
doReturn(mLayout).when(mUrlBar).getLayout();
measureAndLayoutUrlBar();
String url = mShortDomain + mLongPath;
mUrlBar.setTextWithTruncation(url, UrlBar.ScrollType.SCROLL_TO_BEGINNING, 0);
String text = mUrlBar.getText().toString();
assertEquals(url.substring(0, NUMBER_OF_VISIBLE_CHARACTERS), text);
}
@Test
public void testTruncation_NoTruncationForWrapContent() {
measureAndLayoutUrlBar();
LayoutParams previousLayoutParams = mUrlBar.getLayoutParams();
LayoutParams params =
new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
mUrlBar.setLayoutParams(params);
mUrlBar.setTextWithTruncation(mLongDomain, UrlBar.ScrollType.SCROLL_TO_BEGINNING, 0);
String text = mUrlBar.getText().toString();
assertEquals(mLongDomain, text);
mUrlBar.setLayoutParams(previousLayoutParams);
}
@Test
public void performClick_emitsTouchEvents() {
mUrlBar.performClick();
verify(mUrlBarDelegate).onFocusByTouch();
// No subsequent events.
mUrlBar.performClick();
verifyNoMoreInteractions(mUrlBarDelegate);
// Simulate focus lost, then applied programmatically.
// This will reset the internal state, and then enable alternative event.
mUrlBar.onFocusChanged(false, 0, null);
mUrlBar.onFocusChanged(true, 0, null);
mUrlBar.performClick();
verify(mUrlBarDelegate).onTouchAfterFocus();
// No subsequent events.
mUrlBar.performClick();
verifyNoMoreInteractions(mUrlBarDelegate);
}
@Test
public void onTouchEvent_touchDownIsIgnored() {
mUrlBar.onFocusChanged(true, View.FOCUS_DOWN, null);
mUrlBar.onTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0));
verify(mUrlBarDelegate, never()).onTouchAfterFocus();
}
@Test
public void onTouchEvent_touchUpEmitsTouchEvents() {
mUrlBar.onTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0, 0, 0));
verify(mUrlBarDelegate).onFocusByTouch();
// No subsequent events.
mUrlBar.onTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0, 0, 0));
verifyNoMoreInteractions(mUrlBarDelegate);
// Simulate focus lost, then applied programmatically.
// This will reset the internal state, and then enable alternative event.
mUrlBar.onFocusChanged(false, 0, null);
mUrlBar.onFocusChanged(true, 0, null);
mUrlBar.onTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0, 0, 0));
verify(mUrlBarDelegate).onTouchAfterFocus();
// No subsequent events.
mUrlBar.onTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0, 0, 0));
verifyNoMoreInteractions(mUrlBarDelegate);
}
@Test
public void performClick_emittedOnlyOnce() {
mUrlBar.performClick();
verify(mUrlBarDelegate).onFocusByTouch();
clearInvocations(mUrlBarDelegate);
mUrlBar.performClick();
verifyNoMoreInteractions(mUrlBarDelegate);
// Simluate focus lost. This should re-set recorded state and permit the UrlBar to emit
// focus events once more.
mUrlBar.onFocusChanged(false, 0, null);
mUrlBar.performClick();
verify(mUrlBarDelegate).onFocusByTouch();
}
@Test
public void performClick_safeWithNoDelegate() {
mUrlBar.setDelegate(null);
mUrlBar.performClick();
}
@Test
public void testTruncation_NoTruncationWhileFocused() {
mUrlBar.onFocusChanged(true, 0, null);
mUrlBar.setTextWithTruncation(mLongDomain, UrlBar.ScrollType.SCROLL_TO_BEGINNING, 0);
String text = mUrlBar.getText().toString();
assertEquals(mLongDomain, text);
mUrlBar.onFocusChanged(false, 0, null);
}
@Test
public void
scrollToBeginning_fallBackToDefaultWhenLayoutUnavailable_ltrLayout_noText_ltrHint() {
// Explicitly invalidate text layouts. This could happen for a number of reasons.
// This is also the implicit default value until text is measured, but don't rely on this.
doReturn(View.LAYOUT_DIRECTION_LTR).when(mUrlBar).getLayoutDirection();
mUrlBar.setHint("hint text");
resetTextLayout();
// As long as layouts are not available, no action should be taken.
// This is typically the case when the text view or content is manipulated in some way and
// has not yet completed the full measure/layout cycle.
mUrlBar.scrollDisplayText(UrlBar.ScrollType.SCROLL_TO_BEGINNING);
verify(mUrlBar, never()).scrollTo(anyInt(), anyInt());
assertTrue(mUrlBar.hasPendingDisplayTextScrollForTesting());
clearInvocations(mUrlBar);
// LTR layout always scrolls to 0, because that's the natural origin of LTR text.
measureAndLayoutUrlBar();
verify(mUrlBar).scrollTo(0, 0);
assertFalse(mUrlBar.hasPendingDisplayTextScrollForTesting());
clearInvocations(mUrlBar);
// Simulate request to update scroll type with no changes of scroll type, text, or view
// size. This should avoid recalculations and simply re-set the scroll position.
mUrlBar.scrollDisplayText(UrlBar.ScrollType.SCROLL_TO_BEGINNING);
verify(mUrlBar, never()).scrollToTLD();
verify(mUrlBar, never()).scrollToBeginning();
verify(mUrlBar).scrollTo(0, 0);
}
@Test
public void
scrollToBeginning_fallBackToDefaultWhenLayoutUnavailable_rtlLayout_noText_ltrHint() {
// Explicitly invalidate text layouts. This could happen for a number of reasons.
// This is also the implicit default value until text is measured, but don't rely on this.
doReturn(View.LAYOUT_DIRECTION_RTL).when(mUrlBar).getLayoutDirection();
mUrlBar.setHint("hint text");
resetTextLayout();
// As long as layouts are not available, no action should be taken.
// This is typically the case when the text view or content is manipulated in some way and
// has not yet completed the full measure/layout cycle.
mUrlBar.scrollDisplayText(UrlBar.ScrollType.SCROLL_TO_BEGINNING);
verify(mUrlBar, never()).scrollTo(anyInt(), anyInt());
assertTrue(mUrlBar.hasPendingDisplayTextScrollForTesting());
clearInvocations(mUrlBar);
// RTL layouts should scroll to 0 too, because that's the natural origin of LTR text.
measureAndLayoutUrlBar();
verify(mUrlBar).scrollTo(0, 0);
assertFalse(mUrlBar.hasPendingDisplayTextScrollForTesting());
clearInvocations(mUrlBar);
// Simulate request to update scroll type with no changes of scroll type, text, or view
// size. This should avoid recalculations and simply re-set the scroll position.
mUrlBar.scrollDisplayText(UrlBar.ScrollType.SCROLL_TO_BEGINNING);
verify(mUrlBar, never()).scrollToTLD();
verify(mUrlBar, never()).scrollToBeginning();
verify(mUrlBar).scrollTo(0, 0);
}
@Test
public void
scrollToBeginning_fallBackToDefaultWhenLayoutUnavailable_ltrLayout_noText_rtlHint() {
// Explicitly invalidate text layouts. This could happen for a number of reasons.
// This is also the implicit default value until text is measured, but don't rely on this.
doReturn(View.LAYOUT_DIRECTION_LTR).when(mUrlBar).getLayoutDirection();
mUrlBar.setHint("טקסט רמז");
resetTextLayout();
// As long as layouts are not available, no action should be taken.
// This is typically the case when the text view or content is manipulated in some way and
// has not yet completed the full measure/layout cycle.
mUrlBar.scrollDisplayText(UrlBar.ScrollType.SCROLL_TO_BEGINNING);
verify(mUrlBar, never()).scrollTo(anyInt(), anyInt());
assertTrue(mUrlBar.hasPendingDisplayTextScrollForTesting());
clearInvocations(mUrlBar);
// LTR layout always scrolls to 0, even if the hint text is RTL. View hierarchy dictates the
// layout direction.
measureAndLayoutUrlBar();
verify(mUrlBar).scrollTo(0, 0);
assertFalse(mUrlBar.hasPendingDisplayTextScrollForTesting());
clearInvocations(mUrlBar);
// Simulate request to update scroll type with no changes of scroll type, text, or view
// size. This should avoid recalculations and simply re-set the scroll position.
mUrlBar.scrollDisplayText(UrlBar.ScrollType.SCROLL_TO_BEGINNING);
verify(mUrlBar, never()).scrollToTLD();
verify(mUrlBar, never()).scrollToBeginning();
verify(mUrlBar).scrollTo(0, 0);
}
@Test
public void
scrollToBeginning_fallBackToDefaultWhenLayoutUnavailable_rtlLayout_noText_rtlHint() {
// Explicitly invalidate text layouts. This could happen for a number of reasons.
// This is also the implicit default value until text is measured, but don't rely on this.
doReturn(View.LAYOUT_DIRECTION_RTL).when(mUrlBar).getLayoutDirection();
mUrlBar.setHint("טקסט רמז");
resetTextLayout();
// As long as layouts are not available, no action should be taken.
// This is typically the case when the text view or content is manipulated in some way and
// has not yet completed the full measure/layout cycle.
mUrlBar.scrollDisplayText(UrlBar.ScrollType.SCROLL_TO_BEGINNING);
verify(mUrlBar, never()).scrollTo(anyInt(), anyInt());
assertTrue(mUrlBar.hasPendingDisplayTextScrollForTesting());
clearInvocations(mUrlBar);
// RTL layout should position RTL text at an appropriate offset relative to view end.
measureAndLayoutUrlBar();
verify(mUrlBar).scrollTo(not(eq(0)), eq(0));
assertFalse(mUrlBar.hasPendingDisplayTextScrollForTesting());
clearInvocations(mUrlBar);
// Simulate request to update scroll type with no changes of scroll type, text, or view
// size. This should avoid recalculations and simply re-set the scroll position.
mUrlBar.scrollDisplayText(UrlBar.ScrollType.SCROLL_TO_BEGINNING);
verify(mUrlBar, never()).scrollToTLD();
verify(mUrlBar, never()).scrollToBeginning();
verify(mUrlBar).scrollTo(not(eq(0)), eq(0));
}
@Test
public void layout_noScrollWithNoSizeChanges() {
// Initialize the URL bar. Verify test conditions.
mUrlBar.setText(mShortDomain);
mUrlBar.scrollDisplayText(UrlBar.ScrollType.SCROLL_TO_BEGINNING);
measureAndLayoutUrlBar();
assertFalse(mUrlBar.hasPendingDisplayTextScrollForTesting());
clearInvocations(mUrlBar);
// Simulate layout re-entry.
// We know the url bar has no pending scroll request, and we apply the same size.
measureAndLayoutUrlBar();
verify(mUrlBar, never()).scrollDisplayText(anyInt());
}
@Test
public void layout_noScrollWhenHeightChanges() {
// Initialize the URL bar. Verify test conditions.
mUrlBar.setText(mShortDomain);
mUrlBar.scrollDisplayText(UrlBar.ScrollType.SCROLL_TO_BEGINNING);
measureAndLayoutUrlBar();
assertFalse(mUrlBar.hasPendingDisplayTextScrollForTesting());
clearInvocations(mUrlBar);
// Simulate layout re-entry.
// We change the height of the view which should not affect scroll position.
measureAndLayoutUrlBarForSize(URL_BAR_WIDTH, URL_BAR_HEIGHT + 1);
verify(mUrlBar, never()).scrollDisplayText(anyInt());
}
@Test
public void layout_updateScrollWhenWidthChanges() {
// Initialize the URL bar. Verify test conditions.
mUrlBar.setText(mShortDomain);
mUrlBar.scrollDisplayText(UrlBar.ScrollType.SCROLL_TO_BEGINNING);
measureAndLayoutUrlBar();
assertFalse(mUrlBar.hasPendingDisplayTextScrollForTesting());
clearInvocations(mUrlBar);
// Simulate layout re-entry.
// We change the width, which may impact scroll position.
measureAndLayoutUrlBarForSize(URL_BAR_WIDTH + 1, URL_BAR_HEIGHT);
verify(mUrlBar).scrollDisplayText(anyInt());
}
@Test
@EnableFeatures(ChromeFeatureList.ANDROID_NO_VISIBLE_HINT_FOR_DIFFERENT_TLD)
public void scrollToTLD_sameTLD_calculateVisibleHint() {
doReturn(mLayout).when(mUrlBar).getLayout();
doReturn(mPaint).when(mLayout).getPaint();
measureAndLayoutUrlBar();
// Url needs to be long enough to fill the entire url bar.
String url =
mShortDomain
+ "/"
+ TextUtils.join(
"", Collections.nCopies(NUMBER_OF_VISIBLE_CHARACTERS, "a"));
mUrlBar.setText(url);
mUrlBar.setScrollState(UrlBar.ScrollType.SCROLL_TO_TLD, mShortDomain.length());
verify(mUrlBar, times(0)).calculateVisibleHint();
// Keep domain the same, but change the path.
String url2 =
mShortDomain
+ "/"
+ TextUtils.join(
"", Collections.nCopies(NUMBER_OF_VISIBLE_CHARACTERS, "b"));
mUrlBar.setText(url2);
mUrlBar.setScrollState(UrlBar.ScrollType.SCROLL_TO_TLD, mShortDomain.length());
verify(mUrlBar, times(1)).calculateVisibleHint();
String visibleHint = mUrlBar.getVisibleTextPrefixHint().toString();
assertEquals(url2.substring(0, NUMBER_OF_VISIBLE_CHARACTERS + 2), visibleHint);
}
@Test
@EnableFeatures(ChromeFeatureList.ANDROID_NO_VISIBLE_HINT_FOR_DIFFERENT_TLD)
public void scrollToTLD_differentTLD_noVisibleHintCalculation() {
doReturn(mLayout).when(mUrlBar).getLayout();
doReturn(mPaint).when(mLayout).getPaint();
measureAndLayoutUrlBar();
// Url needs to be long enough to fill the entire url bar.
String url =
"www.a.com/"
+ TextUtils.join(
"", Collections.nCopies(NUMBER_OF_VISIBLE_CHARACTERS, "a"));
mUrlBar.setText(url);
mUrlBar.setScrollState(UrlBar.ScrollType.SCROLL_TO_TLD, mShortDomain.length());
verify(mUrlBar, times(0)).calculateVisibleHint();
// Change the domain, but keep the path the same.
String url2 =
"www.b.com/"
+ TextUtils.join(
"", Collections.nCopies(NUMBER_OF_VISIBLE_CHARACTERS, "a"));
mUrlBar.setText(url2);
mUrlBar.setScrollState(UrlBar.ScrollType.SCROLL_TO_TLD, mShortDomain.length());
verify(mUrlBar, times(0)).calculateVisibleHint();
assertNull(mUrlBar.getVisibleTextPrefixHint());
}
@Test
public void keyEvents_nonEnterActionDownKeyHandling() {
var keysToCheck =
List.of(
KeyEvent.KEYCODE_A,
KeyEvent.KEYCODE_TAB,
KeyEvent.KEYCODE_DPAD_UP,
KeyEvent.KEYCODE_DPAD_DOWN);
var listener = mock(View.OnKeyListener.class);
mUrlBar.setKeyDownListener(listener);
for (int keyCode : keysToCheck) {
var event = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);
// Pre-IME Key Down, consumed: do not pass to IME.
doReturn(true).when(listener).onKey(any(), anyInt(), any());
assertTrue(mUrlBar.onKeyPreIme(keyCode, event));
verify(listener).onKey(mUrlBar, keyCode, event);
verify(mUrlBar, never()).super_onKeyPreIme(anyInt(), any());
clearInvocations(listener, mUrlBar);
// Pre-IME Key Down, not consumed: pass to IME.
doReturn(false).when(listener).onKey(any(), anyInt(), any());
doReturn(false).when(mUrlBar).super_onKeyPreIme(anyInt(), any());
assertFalse(mUrlBar.onKeyPreIme(keyCode, event));
verify(listener).onKey(mUrlBar, keyCode, event);
verify(mUrlBar).super_onKeyPreIme(keyCode, event);
clearInvocations(listener, mUrlBar);
// Pre-IME Key Down, not consumed: return IME result.
doReturn(true).when(mUrlBar).super_onKeyPreIme(anyInt(), any());
assertTrue(mUrlBar.onKeyPreIme(keyCode, event));
verify(mUrlBar).super_onKeyPreIme(keyCode, event);
clearInvocations(listener, mUrlBar);
// Post-IME Key Down: never passed to the listener.
doReturn(false).when(mUrlBar).super_onKeyDown(anyInt(), any());
assertFalse(mUrlBar.onKeyDown(keyCode, event));
verifyNoMoreInteractions(listener);
clearInvocations(listener, mUrlBar);
// Post-IME Key Down: return IME result.
doReturn(true).when(mUrlBar).super_onKeyDown(anyInt(), any());
assertTrue(mUrlBar.onKeyDown(keyCode, event));
verifyNoMoreInteractions(listener);
clearInvocations(listener, mUrlBar);
}
}
@Test
public void keyEvents_enterActionDownKeyHandling() {
var keysToCheck = List.of(KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_NUMPAD_ENTER);
var listener = mock(View.OnKeyListener.class);
mUrlBar.setKeyDownListener(listener);
for (int keyCode : keysToCheck) {
var event = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);
// Pre-IME Key Down: passed only to IME.
doReturn(false).when(mUrlBar).super_onKeyPreIme(anyInt(), any());
assertFalse(mUrlBar.onKeyPreIme(keyCode, event));
verify(listener, never()).onKey(any(), anyInt(), any());
verify(mUrlBar).super_onKeyPreIme(keyCode, event);
clearInvocations(listener, mUrlBar);
// Post-IME Key Down: consumed keys not passed to View.
doReturn(true).when(listener).onKey(any(), anyInt(), any());
assertTrue(mUrlBar.onKeyDown(keyCode, event));
verify(listener).onKey(mUrlBar, keyCode, event);
verify(mUrlBar, never()).super_onKeyDown(anyInt(), any());
verifyNoMoreInteractions(listener);
clearInvocations(listener, mUrlBar);
// Post-IME Key Down: not consumed keys passed to View.
doReturn(false).when(listener).onKey(any(), anyInt(), any());
doReturn(true).when(mUrlBar).super_onKeyPreIme(anyInt(), any());
assertTrue(mUrlBar.onKeyDown(keyCode, event));
verify(listener).onKey(mUrlBar, keyCode, event);
verify(mUrlBar).super_onKeyDown(keyCode, event);
verifyNoMoreInteractions(listener);
clearInvocations(listener, mUrlBar);
}
}
@Test
public void keyEvents_actionUpKeysBypassListenerCompletely() {
var keysToCheck =
List.of(
KeyEvent.KEYCODE_A,
KeyEvent.KEYCODE_TAB,
KeyEvent.KEYCODE_ENTER,
KeyEvent.KEYCODE_NUMPAD_ENTER,
KeyEvent.KEYCODE_DPAD_UP,
KeyEvent.KEYCODE_DPAD_DOWN);
var listener = mock(View.OnKeyListener.class);
mUrlBar.setKeyDownListener(listener);
for (int keyCode : keysToCheck) {
var event = new KeyEvent(KeyEvent.ACTION_UP, keyCode);
// Pre-IME, not consumed by IME.
doReturn(false).when(mUrlBar).super_onKeyPreIme(anyInt(), any());
assertFalse(mUrlBar.onKeyPreIme(keyCode, event));
verify(mUrlBar).super_onKeyPreIme(keyCode, event);
verifyNoMoreInteractions(listener);
clearInvocations(mUrlBar);
// Pre-IME, consumed by IME.
doReturn(true).when(mUrlBar).super_onKeyPreIme(anyInt(), any());
assertTrue(mUrlBar.onKeyPreIme(keyCode, event));
verify(mUrlBar).super_onKeyPreIme(keyCode, event);
verifyNoMoreInteractions(listener);
clearInvocations(mUrlBar);
// Post-IME.
assertFalse(mUrlBar.onKeyUp(keyCode, event));
verifyNoMoreInteractions(listener);
clearInvocations(mUrlBar);
}
}
@Test
public void horizontalFadingEdge_followsScrollWhenNotFocused() {
// By default we show up unfocused.
mUrlBar.setScrollX(0);
assertTrue(mUrlBar.isHorizontalFadingEdgeEnabled());
assertEquals(0.f, mUrlBar.getRightFadingEdgeStrength(), MathUtils.EPSILON);
assertEquals(0.f, mUrlBar.getLeftFadingEdgeStrength(), MathUtils.EPSILON);
// Scroll the view to the left. This should present fading edge now.
mUrlBar.setScrollX(100);
assertTrue(mUrlBar.isHorizontalFadingEdgeEnabled());
assertEquals(0.f, mUrlBar.getRightFadingEdgeStrength(), MathUtils.EPSILON);
assertEquals(1.f, mUrlBar.getLeftFadingEdgeStrength(), MathUtils.EPSILON);
// Scroll back to initial position. Observe no fading.
mUrlBar.setScrollX(0);
assertTrue(mUrlBar.isHorizontalFadingEdgeEnabled());
assertEquals(0.f, mUrlBar.getRightFadingEdgeStrength(), MathUtils.EPSILON);
assertEquals(0.f, mUrlBar.getLeftFadingEdgeStrength(), MathUtils.EPSILON);
}
@Test
public void horizontalFadingEdge_noFadeInWhenFocused() {
measureAndLayoutUrlBar();
mUrlBar.setScrollX(100);
mUrlBar.onFocusChanged(true, View.LAYOUT_DIRECTION_LTR, null);
assertFalse(mUrlBar.isHorizontalFadingEdgeEnabled());
// NOTE: defocusing should restore fading edge.
mUrlBar.onFocusChanged(false, View.LAYOUT_DIRECTION_LTR, null);
assertTrue(mUrlBar.isHorizontalFadingEdgeEnabled());
}
/**
* Simulate specific font metrics.
*
* @param useElegantText whether Android can increase the line height by up to 60% to show text
* @param fontActualHeight the desired actual difference between top and the bottom pixel ever
* drawn by the font
*/
private void applyFontMetrics(boolean useElegantText, float fontActualHeight) {
mUrlBar.setTextSize(TypedValue.COMPLEX_UNIT_PX, FONT_HEIGHT_NOMINAL);
float lineHeightScaleFactor =
useElegantText ? LINE_HEIGHT_ELEGANT_FACTOR : LINE_HEIGHT_REGULAR_FACTOR;
doReturn((int) (FONT_HEIGHT_NOMINAL * lineHeightScaleFactor)).when(mUrlBar).getLineHeight();
// Respect the font height, but simulate that it's shifted 10px up.
mFontMetrics.top = -10;
mFontMetrics.bottom = fontActualHeight - 10;
assertEquals(FONT_HEIGHT_NOMINAL, mUrlBar.getTextSize(), MathUtils.EPSILON);
assertEquals(fontActualHeight, mUrlBar.getMaxHeightOfFont(), MathUtils.EPSILON);
}
/**
* Compute the expected font height given the Url bar constraints.
*
* @param useElegantText whether Android can increase the line height by up to 60% to show text
* @param fontActualHeight the desired actual difference between top and the bottom pixel ever
* drawn by the font
* @param urlBarHeight the usable area of the UrlBar that will accommodate the text
*/
private float computeExpectedFontHeight(
boolean useElegantText, float fontActualHeight, int urlBarHeight) {
float lineHeightScaleFactor =
useElegantText ? LINE_HEIGHT_ELEGANT_FACTOR : LINE_HEIGHT_REGULAR_FACTOR;
return FONT_HEIGHT_NOMINAL * (urlBarHeight / (fontActualHeight * lineHeightScaleFactor));
}
@Test
public void enforceMaxTextHeight_shrinkTallFontToFit_noElegantText_noPadding() {
doReturn(false).when(mPaint).isElegantTextHeight();
measureAndLayoutUrlBar();
applyFontMetrics(false, FONT_HEIGHT_ACTUAL_TALL);
mUrlBar.setPaddingRelative(0, 0, 0, 0);
mUrlBar.enforceMaxTextHeight();
assertEquals(
computeExpectedFontHeight(false, FONT_HEIGHT_ACTUAL_TALL, URL_BAR_HEIGHT),
mUrlBar.getTextSize(),
MathUtils.EPSILON);
}
@Test
public void enforceMaxTextHeight_shrinkTallFontToFit_noElegantText_withPadding() {
doReturn(false).when(mPaint).isElegantTextHeight();
measureAndLayoutUrlBar();
applyFontMetrics(false, FONT_HEIGHT_ACTUAL_TALL);
mUrlBar.setPaddingRelative(0, 5, 0, 15);
mUrlBar.enforceMaxTextHeight();
assertEquals(
computeExpectedFontHeight(false, FONT_HEIGHT_ACTUAL_TALL, URL_BAR_HEIGHT - 20),
mUrlBar.getTextSize(),
MathUtils.EPSILON);
}
@Test
public void enforceMaxTextHeight_shrinkShortFontToFit_noElegantText_noPadding() {
doReturn(false).when(mPaint).isElegantTextHeight();
applyFontMetrics(false, FONT_HEIGHT_ACTUAL_SHORT);
measureAndLayoutUrlBar();
mUrlBar.setPaddingRelative(0, 0, 0, 0);
mUrlBar.enforceMaxTextHeight();
assertEquals(
computeExpectedFontHeight(false, FONT_HEIGHT_ACTUAL_SHORT, URL_BAR_HEIGHT),
mUrlBar.getTextSize(),
MathUtils.EPSILON);
}
@Test
public void enforceMaxTextHeight_shrinkShortFontToFit_noElegantText_withPadding() {
doReturn(false).when(mPaint).isElegantTextHeight();
applyFontMetrics(false, FONT_HEIGHT_ACTUAL_SHORT);
measureAndLayoutUrlBar();
mUrlBar.setPaddingRelative(0, 5, 0, 15);
mUrlBar.enforceMaxTextHeight();
assertEquals(
computeExpectedFontHeight(false, FONT_HEIGHT_ACTUAL_SHORT, URL_BAR_HEIGHT - 20),
mUrlBar.getTextSize(),
MathUtils.EPSILON);
}
@Test
public void enforceMaxTextHeight_shrinkTallFontToFit_withElegantText_noPadding() {
doReturn(true).when(mPaint).isElegantTextHeight();
measureAndLayoutUrlBar();
applyFontMetrics(true, FONT_HEIGHT_ACTUAL_TALL);
mUrlBar.setPaddingRelative(0, 0, 0, 0);
mUrlBar.enforceMaxTextHeight();
assertEquals(
computeExpectedFontHeight(true, FONT_HEIGHT_ACTUAL_TALL, URL_BAR_HEIGHT),
mUrlBar.getTextSize(),
MathUtils.EPSILON);
}
@Test
public void enforceMaxTextHeight_shrinkTallFontToFit_withElegantText_withPadding() {
doReturn(true).when(mPaint).isElegantTextHeight();
measureAndLayoutUrlBar();
applyFontMetrics(true, FONT_HEIGHT_ACTUAL_TALL);
mUrlBar.setPaddingRelative(0, 5, 0, 15);
mUrlBar.enforceMaxTextHeight();
assertEquals(
computeExpectedFontHeight(true, FONT_HEIGHT_ACTUAL_TALL, URL_BAR_HEIGHT - 20),
mUrlBar.getTextSize(),
MathUtils.EPSILON);
}
@Test
public void enforceMaxTextHeight_shrinkShortFontToFit_withElegantText_noPadding() {
doReturn(true).when(mPaint).isElegantTextHeight();
measureAndLayoutUrlBar();
applyFontMetrics(true, FONT_HEIGHT_ACTUAL_SHORT);
mUrlBar.setPaddingRelative(0, 0, 0, 0);
mUrlBar.enforceMaxTextHeight();
assertEquals(
computeExpectedFontHeight(true, FONT_HEIGHT_ACTUAL_SHORT, URL_BAR_HEIGHT),
mUrlBar.getTextSize(),
MathUtils.EPSILON);
}
@Test
public void enforceMaxTextHeight_shrinkShortFontToFit_withElegantText_withPadding() {
doReturn(true).when(mPaint).isElegantTextHeight();
measureAndLayoutUrlBar();
applyFontMetrics(true, FONT_HEIGHT_ACTUAL_SHORT);
mUrlBar.setPaddingRelative(0, 5, 0, 15);
mUrlBar.enforceMaxTextHeight();
assertEquals(
computeExpectedFontHeight(true, FONT_HEIGHT_ACTUAL_SHORT, URL_BAR_HEIGHT - 20),
mUrlBar.getTextSize(),
MathUtils.EPSILON);
}
@Test
public void enforceMaxTextHeight_growToFitCurrentlyDisabled() {
doReturn(false).when(mPaint).isElegantTextHeight();
measureAndLayoutUrlBarForSize(URL_BAR_WIDTH, 50);
mUrlBar.setPaddingRelative(0, 0, 0, 0);
mUrlBar.setTextSize(TypedValue.COMPLEX_UNIT_PX, 40);
mFontMetrics.top = 0;
mFontMetrics.bottom = 40;
mUrlBar.enforceMaxTextHeight();
assertEquals(40, mUrlBar.getTextSize(), MathUtils.EPSILON);
}
@Test
public void layout_adjustFontSizeWithFixedHeight() {
mUrlBar.setLayoutParams(new LayoutParams(123, 123));
mUrlBar.layout(0, 0, 123, 123);
verify(mUrlBar).post(mUrlBar.mEnforceMaxTextHeight);
}
@Test
public void layout_fixedFontSizeWithWrappingHeight() {
mUrlBar.setLayoutParams(new LayoutParams(123, LayoutParams.WRAP_CONTENT));
mUrlBar.layout(0, 0, 123, 123);
verify(mUrlBar, never()).post(mUrlBar.mEnforceMaxTextHeight);
}
}