diff --git a/app/src/androidTest/java/org/mozilla/fenix/helpers/TestHelper.kt b/app/src/androidTest/java/org/mozilla/fenix/helpers/TestHelper.kt index d82f47731..074a5fd99 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/helpers/TestHelper.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/TestHelper.kt @@ -55,4 +55,11 @@ object TestHelper { false -> "com.android.packageinstaller" } } + + fun waitUntilObjectIsFound(resourceName: String) { + mDevice.waitNotNull( + Until.findObjects(By.res(resourceName)), + TestAssetHelper.waitingTime + ) + } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAboutTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAboutTest.kt index 24bf8c668..26368bd0a 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAboutTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAboutTest.kt @@ -8,7 +8,6 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import okhttp3.mockwebserver.MockWebServer import org.junit.Rule -import org.junit.Ignore import org.junit.Before import org.junit.After import org.junit.Test @@ -71,7 +70,6 @@ class SettingsAboutTest { } @Test - @Ignore("Temp disable flakey test - see: https://github.com/mozilla-mobile/fenix/issues/7388") fun verifyAboutFirefoxPreview() { homeScreen { }.openThreeDotMenu { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAboutRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAboutRobot.kt index 4a55098cc..4e63e637a 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAboutRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAboutRobot.kt @@ -6,8 +6,12 @@ package org.mozilla.fenix.ui.robots +import android.os.Build +import android.widget.TextView import androidx.core.content.pm.PackageInfoCompat +import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.ViewAssertion import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import org.hamcrest.CoreMatchers @@ -20,8 +24,15 @@ import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import androidx.test.espresso.matcher.ViewMatchers.hasDescendant import androidx.test.espresso.matcher.ViewMatchers.Visibility import org.hamcrest.CoreMatchers.containsString +import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.R import org.mozilla.fenix.helpers.TestHelper +import java.text.SimpleDateFormat +import java.time.LocalDateTime +import java.time.format.DateTimeFormatterBuilder +import java.time.temporal.ChronoField +import java.util.Date +import java.util.Calendar /** * Implementation of Robot Pattern for the settings search sub menu. @@ -79,32 +90,101 @@ private fun assertProductCompany() { private fun assertCurrentTimestamp() { onView(withId(R.id.build_date)) - .check(matches(withText(org.mozilla.fenix.BuildConfig.BUILD_DATE))) + .check(BuildDateAssertion.isDisplayedDateAccurate()) } private fun assertWhatIsNewInFirefoxPreview() { onView(withText("What’s new in Firefox Preview")) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + .perform(click()) + + // This is used to wait for the webpage to fully load + TestHelper.waitUntilObjectIsFound("org.mozilla.fenix.debug:id/mozac_browser_toolbar_title_view") + + onView(withId(R.id.mozac_browser_toolbar_title_view)).check( + matches( + withText( + containsString("What's new in Firefox Preview") + ) + ) + ) + Espresso.pressBack() } private fun assertSupport() { onView(withText("Support")) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + .perform(click()) + + // This is used to wait for the webpage to fully load + TestHelper.waitUntilObjectIsFound("org.mozilla.fenix.debug:id/mozac_browser_toolbar_title_view") + + onView(withId(R.id.mozac_browser_toolbar_title_view)).check( + matches( + withText( + containsString("Firefox Preview | Mozilla Support") + ) + + ) + ) + Espresso.pressBack() } private fun assertPrivacyNotice() { onView(withText("Privacy notice")) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + .perform(click()) + + // This is used to wait for the webpage to fully load + TestHelper.waitUntilObjectIsFound("org.mozilla.fenix.debug:id/mozac_browser_toolbar_title_view") + + onView(withId(R.id.mozac_browser_toolbar_title_view)).check( + matches( + withText( + containsString("Firefox Privacy Notice") + ) + + ) + ) + Espresso.pressBack() } private fun assertKnowYourRights() { + TestHelper.scrollToElementByText("Know your rights") onView(withText("Know your rights")) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + .perform(click()) + + // This is used to wait for the webpage to fully load + TestHelper.waitUntilObjectIsFound("org.mozilla.fenix.debug:id/mozac_browser_toolbar_title_view") + + onView(withId(R.id.mozac_browser_toolbar_title_view)).check( + matches( + withText( + containsString("Firefox Preview - Your Rights | How to | Mozilla Support") + ) + ) + ) + Espresso.pressBack() } private fun assertLicensingInformation() { + TestHelper.scrollToElementByText("Libraries that we use") onView(withText("Licensing information")) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + .perform(click()) + + // This is used to wait for the webpage to fully load + TestHelper.waitUntilObjectIsFound("org.mozilla.fenix.debug:id/mozac_browser_toolbar_title_view") + + onView(withId(R.id.mozac_browser_toolbar_title_view)).check( + matches( + withText( + containsString("Licenses") + ) + ) + ) + Espresso.pressBack() } private fun assertLibrariesUsed() { @@ -114,7 +194,87 @@ private fun assertLibrariesUsed() { .perform(click()) onView(withId(R.id.action_bar)).check(matches(hasDescendant(withText(containsString("Firefox Preview | OSS Libraries"))))) + Espresso.pressBack() } private fun goBackButton() = onView(CoreMatchers.allOf(withContentDescription("Navigate up"))) + +class BuildDateAssertion { + // When the app is built on firebase, there are times where the BuildDate is off by a few seconds or a few minutes. + // To compensate for that slight discrepancy, this assertion was added to see if the Build Date shown + // is within a reasonable amount of time from when the app was built. + companion object { + // this pattern represents the following date format: "Monday 12/30 @ 6:49 PM" + private const val DATE_PATTERN = "EEEE M/d @ h:m a" + // + private const val NUM_OF_HOURS = 1 + + fun isDisplayedDateAccurate(): ViewAssertion { + return ViewAssertion { view, noViewFoundException -> + if (noViewFoundException != null) throw noViewFoundException + + val textFromView = (view as TextView).text + ?: throw AssertionError("This view is not of type TextView") + + verifyDateIsWithinRange(textFromView.toString(), NUM_OF_HOURS) + } + } + + private fun verifyDateIsWithinRange(dateText: String, hours: Int) { + // This assertion checks whether has defined a range of tim + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1) { + val simpleDateFormat = SimpleDateFormat(DATE_PATTERN) + val date = simpleDateFormat.parse(dateText) + if (!date.isWithinRangeOf(hours)) throw AssertionError("The build date is not within Range.") + } else { + val textviewDate = getLocalDateTimeFromString(dateText) + val buildConfigDate = getLocalDateTimeFromString(BuildConfig.BUILD_DATE) + + if (!buildConfigDate.isEqual(textviewDate) && + !textviewDate.isWithinRangeOf(hours, buildConfigDate) + ) { + throw AssertionError("$textviewDate is not equal to the date within the build config: $buildConfigDate, and are not within a reasonable amount of time from each other.") + } + } + } + + private fun Date.isWithinRangeOf(hours: Int): Boolean { + // To determine the date range, the maxDate is retrieved by adding the variable hours to the calendar. + // Since the calendar will represent the maxDate at this time, to retrieve the minDate the variable hours is multipled by negative 2 and added to the calendar + // This will result in the maxDate being equal to the original Date + hours, and minDate being equal to original Date - hours + + val calendar = Calendar.getInstance() + val currentYear = calendar.get(Calendar.YEAR) + calendar.time = this + calendar.set(Calendar.YEAR, currentYear) + val updatedDate = calendar.time + + calendar.add(Calendar.HOUR_OF_DAY, hours) + val maxDate = calendar.time + calendar.add( + Calendar.HOUR_OF_DAY, + hours * -2 + ) // Gets the minDate by subtracting from maxDate + val minDate = calendar.time + return updatedDate.after(minDate) && updatedDate.before(maxDate) + } + + private fun LocalDateTime.isWithinRangeOf( + hours: Int, + baselineDate: LocalDateTime + ): Boolean { + val upperBound = baselineDate.plusHours(hours.toLong()) + val lowerBound = baselineDate.minusHours(hours.toLong()) + val currentDate = this + return currentDate.isAfter(lowerBound) && currentDate.isBefore(upperBound) + } + + private fun getLocalDateTimeFromString(buildDate: String): LocalDateTime { + val dateFormatter = DateTimeFormatterBuilder().appendPattern(DATE_PATTERN) + .parseDefaulting(ChronoField.YEAR, LocalDateTime.now().year.toLong()) + .toFormatter() + return LocalDateTime.parse(buildDate, dateFormatter) + } + } +}