chromium/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/answer/RichAnswerTextTest.java

// 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.chrome.browser.omnibox.suggestions.answer;

import android.app.Activity;
import android.content.Context;
import android.text.SpannableStringBuilder;
import android.text.style.TextAppearanceSpan;

import androidx.test.filters.SmallTest;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;

import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.chrome.R;
import org.chromium.components.omnibox.AnswerDataProto.AnswerData;
import org.chromium.components.omnibox.AnswerDataProto.FormattedString;
import org.chromium.components.omnibox.AnswerDataProto.FormattedString.ColorType;
import org.chromium.components.omnibox.AnswerDataProto.FormattedString.FormattedStringFragment;
import org.chromium.components.omnibox.AnswerTypeProto.AnswerType;
import org.chromium.components.omnibox.OmniboxFeatureList;
import org.chromium.components.omnibox.OmniboxFeatures;
import org.chromium.components.omnibox.RichAnswerTemplateProto.RichAnswerTemplate;
import org.chromium.components.omnibox.RichAnswerTemplateProto.SuggestionEnhancement;
import org.chromium.components.omnibox.RichAnswerTemplateProto.SuggestionEnhancements;

/** Tests for {@link RichAnswerText}. */
@RunWith(BaseRobolectricTestRunner.class)
public class RichAnswerTextTest {
    private Context mContext;
    private TextAppearanceSpan mGreenText;
    private TextAppearanceSpan mRedText;
    private TextAppearanceSpan mPrimaryText;
    private TextAppearanceSpan mMediumText;
    private TextAppearanceSpan mHeadlineText;

    @Before
    public void setUp() {
        mContext = Robolectric.buildActivity(Activity.class).setup().get();
        mContext.setTheme(R.style.Theme_BrowserUI_DayNight);
        mGreenText =
                new TextAppearanceSpan(
                        mContext,
                        org.chromium.chrome.browser.omnibox.test.R.style
                                .TextAppearance_OmniboxAnswerDescriptionPositive);
        mRedText =
                new TextAppearanceSpan(
                        mContext,
                        org.chromium.chrome.browser.omnibox.test.R.style
                                .TextAppearance_OmniboxAnswerDescriptionNegative);
        mPrimaryText =
                new TextAppearanceSpan(
                        mContext,
                        org.chromium.chrome.browser.omnibox.R.style
                                .TextAppearance_TextLarge_Primary);
        mMediumText =
                new TextAppearanceSpan(
                        mContext,
                        org.chromium.chrome.browser.omnibox.R.style
                                .TextAppearance_TextMedium_Secondary);
        mHeadlineText =
                new TextAppearanceSpan(
                        mContext,
                        org.chromium.chrome.browser.omnibox.R.style
                                .TextAppearance_Headline2Thick_Primary);
    }

    @Test
    @SmallTest
    public void testDictionaryAnswer() {
        FormattedString headline =
                FormattedString.newBuilder()
                        .addFragments(
                                FormattedStringFragment.newBuilder()
                                        .setStartIndex(0)
                                        .setText("define adroit • /əˈdroit/"))
                        .build();
        FormattedString subhead =
                FormattedString.newBuilder()
                        .addFragments(
                                FormattedStringFragment.newBuilder()
                                        .setStartIndex(0)
                                        .setText("clever or skillful in using the hands or mind."))
                        .build();

        RichAnswerTemplate richAnswerTemplate =
                RichAnswerTemplate.newBuilder()
                        .addAnswers(
                                0,
                                AnswerData.newBuilder().setHeadline(headline).setSubhead(subhead))
                        .build();

        AnswerType answerType = AnswerType.ANSWER_TYPE_DICTIONARY;
        AnswerText[] texts =
                RichAnswerText.from(mContext, richAnswerTemplate, answerType, false, false);
        Assert.assertEquals(texts[0].getMaxLines(), 1);
        Assert.assertEquals(texts[1].getMaxLines(), 3);
        Assert.assertEquals(texts[0].getAccessibilityDescription(), "define adroit • /əˈdroit/");
        Assert.assertEquals(
                texts[1].getAccessibilityDescription(),
                "clever or skillful in using the hands or mind.");

        SpannableStringBuilder primaryText = texts[0].getText();
        SpannableStringBuilder secondaryText = texts[1].getText();

        Assert.assertEquals(primaryText.toString(), "define adroit • /əˈdroit/");
        TextAppearanceSpan[] textAppearanceSpans =
                primaryText.getSpans(0, primaryText.length(), TextAppearanceSpan.class);
        Assert.assertEquals(textAppearanceSpans.length, 1);
        Assert.assertEquals(textAppearanceSpans[0].getTextSize(), mPrimaryText.getTextSize());

        Assert.assertEquals(
                secondaryText.toString(), "clever or skillful in using the hands or mind.");
        textAppearanceSpans =
                secondaryText.getSpans(0, secondaryText.length(), TextAppearanceSpan.class);
        Assert.assertEquals(textAppearanceSpans.length, 1);
        Assert.assertEquals(textAppearanceSpans[0].getTextSize(), mMediumText.getTextSize());
    }

    @Test
    @SmallTest
    public void testFinanceAnswer() {
        FormattedString headline =
                FormattedString.newBuilder()
                        .addFragments(
                                FormattedStringFragment.newBuilder()
                                        .setStartIndex(0)
                                        .setText("goog stock GOOG(NASDAQ), 3:22 PM EDT"))
                        .build();
        FormattedString positiveSubhead =
                FormattedString.newBuilder()
                        .addFragments(
                                FormattedStringFragment.newBuilder()
                                        .setStartIndex(0)
                                        .setText("100.00"))
                        .addFragments(
                                FormattedStringFragment.newBuilder()
                                        .setStartIndex(0)
                                        .setText("+1.00")
                                        .setColor(ColorType.COLOR_ON_SURFACE_POSITIVE))
                        .build();

        RichAnswerTemplate richAnswerTemplate =
                RichAnswerTemplate.newBuilder()
                        .addAnswers(
                                0,
                                AnswerData.newBuilder()
                                        .setHeadline(headline)
                                        .setSubhead(positiveSubhead))
                        .build();

        AnswerType answerType = AnswerType.ANSWER_TYPE_FINANCE;
        AnswerText[] texts =
                RichAnswerText.from(mContext, richAnswerTemplate, answerType, false, false);
        // A11y descriptions are reverse of visual ordering.
        Assert.assertEquals(
                texts[0].getAccessibilityDescription(), "goog stock GOOG(NASDAQ), 3:22 PM EDT");
        Assert.assertEquals(texts[1].getAccessibilityDescription(), "100.00 +1.00");
        SpannableStringBuilder primaryText = texts[0].getText();
        SpannableStringBuilder secondaryText = texts[1].getText();

        Assert.assertEquals(primaryText.toString(), "100.00 +1.00");
        TextAppearanceSpan[] textAppearanceSpans =
                primaryText.getSpans(0, primaryText.length(), TextAppearanceSpan.class);
        Assert.assertEquals(textAppearanceSpans.length, 2);
        Assert.assertEquals(texts[0].getMaxLines(), 1);
        Assert.assertEquals(texts[1].getMaxLines(), 1);
        Assert.assertEquals(textAppearanceSpans[0].getTextSize(), mPrimaryText.getTextSize());
        Assert.assertEquals(textAppearanceSpans[1].getTextSize(), mGreenText.getTextSize());
        Assert.assertEquals(textAppearanceSpans[1].getTextColor(), mGreenText.getTextColor());

        Assert.assertEquals(secondaryText.toString(), "goog stock GOOG(NASDAQ), 3:22 PM EDT");
        textAppearanceSpans =
                secondaryText.getSpans(0, secondaryText.length(), TextAppearanceSpan.class);
        Assert.assertEquals(textAppearanceSpans.length, 1);
        Assert.assertEquals(textAppearanceSpans[0].getTextSize(), mMediumText.getTextSize());

        FormattedString negativeSubhead =
                FormattedString.newBuilder()
                        .addFragments(
                                FormattedStringFragment.newBuilder()
                                        .setStartIndex(0)
                                        .setText("100.00"))
                        .addFragments(
                                FormattedStringFragment.newBuilder()
                                        .setStartIndex(0)
                                        .setText("-1.00")
                                        .setColor(ColorType.COLOR_ON_SURFACE_NEGATIVE))
                        .build();

        RichAnswerTemplate negativeRichAnswerTemplate =
                RichAnswerTemplate.newBuilder()
                        .addAnswers(
                                0,
                                AnswerData.newBuilder()
                                        .setHeadline(headline)
                                        .setSubhead(negativeSubhead))
                        .build();
        texts = RichAnswerText.from(mContext, negativeRichAnswerTemplate, answerType, false, false);
        primaryText = texts[0].getText();

        Assert.assertEquals(primaryText.toString(), "100.00 -1.00");
        textAppearanceSpans =
                primaryText.getSpans(0, primaryText.length(), TextAppearanceSpan.class);
        Assert.assertEquals(textAppearanceSpans.length, 2);
        Assert.assertEquals(textAppearanceSpans[0].getTextSize(), mPrimaryText.getTextSize());
        Assert.assertEquals(textAppearanceSpans[1].getTextSize(), mRedText.getTextSize());
        Assert.assertEquals(textAppearanceSpans[1].getTextColor(), mRedText.getTextColor());
    }

    @Test
    @SmallTest
    public void testFinanceAnswer_withColorReversal() {
        FormattedString headline =
                FormattedString.newBuilder()
                        .addFragments(
                                FormattedStringFragment.newBuilder()
                                        .setStartIndex(0)
                                        .setText("goog stock GOOG(NASDAQ), 3:22 PM EDT"))
                        .build();
        FormattedString positiveSubhead =
                FormattedString.newBuilder()
                        .addFragments(
                                FormattedStringFragment.newBuilder()
                                        .setStartIndex(0)
                                        .setText("100.00"))
                        .addFragments(
                                FormattedStringFragment.newBuilder()
                                        .setStartIndex(0)
                                        .setText("+1.00")
                                        .setColor(ColorType.COLOR_ON_SURFACE_POSITIVE))
                        .build();

        RichAnswerTemplate richAnswerTemplate =
                RichAnswerTemplate.newBuilder()
                        .addAnswers(
                                0,
                                AnswerData.newBuilder()
                                        .setHeadline(headline)
                                        .setSubhead(positiveSubhead))
                        .build();

        AnswerType answerType = AnswerType.ANSWER_TYPE_FINANCE;
        AnswerText[] texts =
                RichAnswerText.from(mContext, richAnswerTemplate, answerType, true, false);
        SpannableStringBuilder primaryText = texts[0].getText();

        Assert.assertEquals(primaryText.toString(), "100.00 +1.00");
        TextAppearanceSpan[] textAppearanceSpans =
                primaryText.getSpans(0, primaryText.length(), TextAppearanceSpan.class);
        Assert.assertEquals(textAppearanceSpans.length, 2);
        Assert.assertEquals(textAppearanceSpans[1].getTextSize(), mRedText.getTextSize());
        Assert.assertEquals(textAppearanceSpans[1].getTextColor(), mRedText.getTextColor());

        FormattedString negativeSubhead =
                FormattedString.newBuilder()
                        .addFragments(
                                FormattedStringFragment.newBuilder()
                                        .setStartIndex(0)
                                        .setText("100.00"))
                        .addFragments(
                                FormattedStringFragment.newBuilder()
                                        .setStartIndex(0)
                                        .setText("-1.00")
                                        .setColor(ColorType.COLOR_ON_SURFACE_NEGATIVE))
                        .build();

        RichAnswerTemplate negativeRichAnswerTemplate =
                RichAnswerTemplate.newBuilder()
                        .addAnswers(
                                0,
                                AnswerData.newBuilder()
                                        .setHeadline(headline)
                                        .setSubhead(negativeSubhead))
                        .build();

        texts = RichAnswerText.from(mContext, negativeRichAnswerTemplate, answerType, true, false);
        primaryText = texts[0].getText();

        Assert.assertEquals(primaryText.toString(), "100.00 -1.00");
        textAppearanceSpans =
                primaryText.getSpans(0, primaryText.length(), TextAppearanceSpan.class);
        Assert.assertEquals(textAppearanceSpans.length, 2);
        Assert.assertEquals(textAppearanceSpans[1].getTextSize(), mGreenText.getTextSize());
        Assert.assertEquals(textAppearanceSpans[1].getTextColor(), mGreenText.getTextColor());
    }

    @Test
    @SmallTest
    public void testWeatherAnswer() {
        FormattedString headline =
                FormattedString.newBuilder()
                        .addFragments(
                                FormattedStringFragment.newBuilder()
                                        .setStartIndex(0)
                                        .setText("redmond weather"))
                        .build();
        FormattedString subhead =
                FormattedString.newBuilder()
                        .addFragments(
                                FormattedStringFragment.newBuilder()
                                        .setStartIndex(0)
                                        .setText("64•F Thu - Redmond, WA"))
                        .build();

        RichAnswerTemplate richAnswerTemplate =
                RichAnswerTemplate.newBuilder()
                        .addAnswers(
                                0,
                                AnswerData.newBuilder().setHeadline(headline).setSubhead(subhead))
                        .build();

        AnswerType answerType = AnswerType.ANSWER_TYPE_WEATHER;
        AnswerText[] texts =
                RichAnswerText.from(mContext, richAnswerTemplate, answerType, false, false);
        Assert.assertEquals(texts[0].getMaxLines(), 1);
        Assert.assertEquals(texts[1].getMaxLines(), 1);

        SpannableStringBuilder primaryText = texts[0].getText();
        SpannableStringBuilder secondaryText = texts[1].getText();

        Assert.assertEquals(primaryText.toString(), "64•F Thu - Redmond, WA");
        TextAppearanceSpan[] textAppearanceSpans =
                primaryText.getSpans(0, primaryText.length(), TextAppearanceSpan.class);
        Assert.assertEquals(textAppearanceSpans.length, 1);
        Assert.assertEquals(textAppearanceSpans[0].getTextSize(), mPrimaryText.getTextSize());

        Assert.assertEquals(secondaryText.toString(), "redmond weather");
        textAppearanceSpans =
                secondaryText.getSpans(0, secondaryText.length(), TextAppearanceSpan.class);
        Assert.assertEquals(textAppearanceSpans.length, 1);
        Assert.assertEquals(textAppearanceSpans[0].getTextSize(), mMediumText.getTextSize());
    }

    @Test
    @SmallTest
    public void testTranslationAnswer() {
        FormattedString headline =
                FormattedString.newBuilder()
                        .addFragments(
                                FormattedStringFragment.newBuilder()
                                        .setStartIndex(0)
                                        .setText("nuit (French)"))
                        .build();
        FormattedString subhead =
                FormattedString.newBuilder()
                        .addFragments(
                                FormattedStringFragment.newBuilder()
                                        .setStartIndex(0)
                                        .setText("night in French"))
                        .build();

        RichAnswerTemplate richAnswerTemplate =
                RichAnswerTemplate.newBuilder()
                        .addAnswers(
                                0,
                                AnswerData.newBuilder().setHeadline(headline).setSubhead(subhead))
                        .build();

        AnswerType answerType = AnswerType.ANSWER_TYPE_TRANSLATION;
        AnswerText[] texts =
                RichAnswerText.from(mContext, richAnswerTemplate, answerType, false, false);
        Assert.assertEquals(texts[0].getMaxLines(), 3);
        Assert.assertEquals(texts[1].getMaxLines(), 1);
    }

    @Test
    @SmallTest
    public void testCurrencyAnswer() {
        FormattedString headline =
                FormattedString.newBuilder()
                        .addFragments(
                                FormattedStringFragment.newBuilder()
                                        .setStartIndex(0)
                                        .setText("1 usd to jpy"))
                        .build();
        FormattedString subhead =
                FormattedString.newBuilder()
                        .addFragments(
                                FormattedStringFragment.newBuilder()
                                        .setStartIndex(0)
                                        .setText("1 United States Dollar = 156.23 Japanese Yen"))
                        .build();
        RichAnswerTemplate richAnswerTemplate =
                RichAnswerTemplate.newBuilder()
                        .addAnswers(
                                0,
                                AnswerData.newBuilder().setHeadline(headline).setSubhead(subhead))
                        .build();

        AnswerType answerType = AnswerType.ANSWER_TYPE_CURRENCY;
        AnswerText[] texts =
                RichAnswerText.from(mContext, richAnswerTemplate, answerType, false, false);
        SpannableStringBuilder primaryText = texts[0].getText();
        SpannableStringBuilder secondaryText = texts[1].getText();

        Assert.assertEquals(primaryText.toString(), "156.23 Japanese Yen");
        TextAppearanceSpan[] textAppearanceSpans =
                primaryText.getSpans(0, primaryText.length(), TextAppearanceSpan.class);
        Assert.assertEquals(textAppearanceSpans.length, 1);
        Assert.assertEquals(textAppearanceSpans[0].getTextSize(), mPrimaryText.getTextSize());

        Assert.assertEquals(secondaryText.toString(), "1 usd to jpy");
    }

    @Test
    @SmallTest
    public void testNoFragments() {
        FormattedString headline = FormattedString.newBuilder().setText("redmond weather").build();
        FormattedString subhead =
                FormattedString.newBuilder().setText("64•F Thu - Redmond, WA").build();

        RichAnswerTemplate richAnswerTemplate =
                RichAnswerTemplate.newBuilder()
                        .addAnswers(
                                0,
                                AnswerData.newBuilder().setHeadline(headline).setSubhead(subhead))
                        .build();

        AnswerType answerType = AnswerType.ANSWER_TYPE_WEATHER;
        AnswerText[] texts =
                RichAnswerText.from(mContext, richAnswerTemplate, answerType, false, false);
        SpannableStringBuilder primaryText = texts[0].getText();
        SpannableStringBuilder secondaryText = texts[1].getText();

        Assert.assertEquals(primaryText.toString(), "64•F Thu - Redmond, WA");
        TextAppearanceSpan[] textAppearanceSpans =
                primaryText.getSpans(0, primaryText.length(), TextAppearanceSpan.class);
        Assert.assertEquals(textAppearanceSpans.length, 1);
        Assert.assertEquals(textAppearanceSpans[0].getTextSize(), mPrimaryText.getTextSize());

        Assert.assertEquals(secondaryText.toString(), "redmond weather");
        textAppearanceSpans =
                secondaryText.getSpans(0, secondaryText.length(), TextAppearanceSpan.class);
        Assert.assertEquals(textAppearanceSpans.length, 1);
        Assert.assertEquals(textAppearanceSpans[0].getTextSize(), mMediumText.getTextSize());
    }

    @Test
    @SmallTest
    @EnableFeatures(OmniboxFeatureList.OMNIBOX_ANSWER_ACTIONS)
    public void testRichAnswerCard() {
        OmniboxFeatures.sAnswerActionsShowRichCard.setForTesting(true);
        // The backend sends the lines in Answer > query order for some answer types (dictionary,
        // sports, weather, finance, knowledge graph). These should not have their order reversed.
        FormattedString headline =
                FormattedString.newBuilder().setText("64•F Thu - Redmond, WA").build();
        FormattedString subhead = FormattedString.newBuilder().setText("redmond weather").build();

        RichAnswerTemplate richAnswerTemplate =
                RichAnswerTemplate.newBuilder()
                        .setEnhancements(
                                SuggestionEnhancements.newBuilder()
                                        .addEnhancements(
                                                SuggestionEnhancement.newBuilder()
                                                        .setDisplayText("7 day forecast"))
                                        .build())
                        .addAnswers(
                                0,
                                AnswerData.newBuilder().setHeadline(headline).setSubhead(subhead))
                        .build();

        AnswerType answerType = AnswerType.ANSWER_TYPE_WEATHER;
        AnswerText[] texts =
                RichAnswerText.from(mContext, richAnswerTemplate, answerType, false, true);
        SpannableStringBuilder primaryText = texts[0].getText();
        SpannableStringBuilder secondaryText = texts[1].getText();

        Assert.assertEquals(primaryText.toString(), "64•F Thu - Redmond, WA");
        TextAppearanceSpan[] textAppearanceSpans =
                primaryText.getSpans(0, primaryText.length(), TextAppearanceSpan.class);
        Assert.assertEquals(textAppearanceSpans.length, 1);
        Assert.assertEquals(textAppearanceSpans[0].getTextSize(), mHeadlineText.getTextSize());

        Assert.assertEquals(secondaryText.toString(), "redmond weather");
        textAppearanceSpans =
                secondaryText.getSpans(0, secondaryText.length(), TextAppearanceSpan.class);
        Assert.assertEquals(textAppearanceSpans.length, 1);
        Assert.assertEquals(textAppearanceSpans[0].getTextSize(), mPrimaryText.getTextSize());
    }
}