1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 @file:Suppress("TooManyFunctions")
7 package org.mozilla.fenix.ui.robots
9 import android.os.Build
10 import android.widget.TextView
11 import androidx.core.content.pm.PackageInfoCompat
12 import androidx.test.espresso.Espresso
13 import androidx.test.espresso.Espresso.onView
14 import androidx.test.espresso.ViewAssertion
15 import androidx.test.espresso.action.ViewActions.click
16 import androidx.test.espresso.assertion.ViewAssertions.matches
17 import androidx.test.espresso.matcher.ViewMatchers
18 import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
19 import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
20 import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
21 import androidx.test.espresso.matcher.ViewMatchers.withId
22 import androidx.test.espresso.matcher.ViewMatchers.withText
23 import androidx.test.platform.app.InstrumentationRegistry
24 import androidx.test.uiautomator.UiScrollable
25 import androidx.test.uiautomator.UiSelector
26 import mozilla.components.support.utils.ext.getPackageInfoCompat
27 import org.hamcrest.CoreMatchers.allOf
28 import org.hamcrest.CoreMatchers.containsString
29 import org.mozilla.fenix.BuildConfig
30 import org.mozilla.fenix.R
31 import org.mozilla.fenix.helpers.Constants.LISTS_MAXSWIPES
32 import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
33 import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
34 import org.mozilla.fenix.helpers.TestHelper
35 import org.mozilla.fenix.helpers.TestHelper.appName
36 import org.mozilla.fenix.helpers.TestHelper.packageName
37 import org.mozilla.fenix.settings.SupportUtils
38 import java.text.SimpleDateFormat
39 import java.time.LocalDateTime
40 import java.time.format.DateTimeFormatterBuilder
41 import java.time.temporal.ChronoField
42 import java.util.Calendar
46 * Implementation of Robot Pattern for the settings search sub menu.
48 class SettingsSubMenuAboutRobot {
49 fun verifyAboutFirefoxPreviewInfo() {
51 verifyProductCompany()
52 verifyCurrentTimestamp()
56 fun verifyVersionNumber() {
57 val context = InstrumentationRegistry.getInstrumentation().targetContext
59 val packageInfo = context.packageManager.getPackageInfoCompat(context.packageName, 0)
60 val versionCode = PackageInfoCompat.getLongVersionCode(packageInfo).toString()
61 val buildNVersion = "${packageInfo.versionName} (Build #$versionCode)\n"
63 org.mozilla.geckoview.BuildConfig.MOZ_APP_VERSION + "-" + org.mozilla.geckoview.BuildConfig.MOZ_APP_BUILDID
64 val asVersion = mozilla.components.Build.applicationServicesVersion
66 onView(withId(R.id.about_text))
67 .check(matches(withText(containsString(buildNVersion))))
68 .check(matches(withText(containsString(geckoVersion))))
69 .check(matches(withText(containsString(asVersion))))
72 fun verifyProductCompany() {
73 onView(withId(R.id.about_content))
74 .check(matches(withText(containsString("$appName is produced by Mozilla."))))
77 fun verifyCurrentTimestamp() {
78 onView(withId(R.id.build_date))
79 // Currently UI tests run against debug builds, which display a hard-coded string 'debug build'
80 // instead of the date. See https://github.com/mozilla-mobile/fenix/pull/10812#issuecomment-633746833
81 .check(matches(withText(containsString("debug build"))))
82 // This assertion should be valid for non-debug build types.
83 // .check(BuildDateAssertion.isDisplayedDateAccurate())
86 fun verifyAboutToolbar() =
89 withId(R.id.navigationToolbar),
90 hasDescendant(withText("About $appName")),
92 ).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
94 fun verifyWhatIsNewInFirefoxLink() {
95 aboutMenuList.scrollToEnd(LISTS_MAXSWIPES)
97 val firefox = TestHelper.appContext.getString(R.string.firefox)
98 onView(withText("What’s new in $firefox"))
99 .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
102 fun verifySupport() {
103 aboutMenuList.scrollToEnd(LISTS_MAXSWIPES)
105 onView(withText("Support"))
106 .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
109 TestHelper.verifyUrl(
110 "support.mozilla.org",
111 "org.mozilla.fenix.debug:id/mozac_browser_toolbar_url_view",
112 R.id.mozac_browser_toolbar_url_view,
116 fun verifyCrashesLink() {
120 }.openAboutFirefoxPreview {}
122 aboutMenuList.scrollToEnd(LISTS_MAXSWIPES)
124 onView(withText("Crashes"))
125 .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
128 assertUIObjectExists(itemContainingText("No crash reports have been submitted."))
135 fun verifyPrivacyNoticeLink() {
136 aboutMenuList.scrollToEnd(LISTS_MAXSWIPES)
138 onView(withText("Privacy notice"))
139 .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
142 TestHelper.verifyUrl(
144 "org.mozilla.fenix.debug:id/mozac_browser_toolbar_url_view",
145 R.id.mozac_browser_toolbar_url_view,
149 fun verifyKnowYourRightsLink() {
150 aboutMenuList.scrollToEnd(LISTS_MAXSWIPES)
152 onView(withText("Know your rights"))
153 .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
156 TestHelper.verifyUrl(
157 SupportUtils.SumoTopic.YOUR_RIGHTS.topicStr,
158 "org.mozilla.fenix.debug:id/mozac_browser_toolbar_url_view",
159 R.id.mozac_browser_toolbar_url_view,
163 fun verifyLicensingInformationLink() {
164 aboutMenuList.scrollToEnd(LISTS_MAXSWIPES)
166 onView(withText("Licensing information"))
167 .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
170 TestHelper.verifyUrl(
172 "org.mozilla.fenix.debug:id/mozac_browser_toolbar_url_view",
173 R.id.mozac_browser_toolbar_url_view,
177 fun verifyLibrariesUsedLink() {
178 aboutMenuList.scrollToEnd(LISTS_MAXSWIPES)
180 onView(withText("Libraries that we use"))
181 .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
184 onView(withId(R.id.navigationToolbar)).check(matches(hasDescendant(withText(containsString("$appName | OSS Libraries")))))
188 fun verifyTheLinksList() {
190 verifyWhatIsNewInFirefoxLink()
191 navigateBackToAboutPage()
194 navigateBackToAboutPage()
195 verifyPrivacyNoticeLink()
196 navigateBackToAboutPage()
197 verifyKnowYourRightsLink()
198 navigateBackToAboutPage()
199 verifyLicensingInformationLink()
200 navigateBackToAboutPage()
201 verifyLibrariesUsedLink()
205 fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
206 goBackButton().perform(click())
208 SettingsRobot().interact()
209 return SettingsRobot.Transition()
214 private fun navigateBackToAboutPage() {
218 }.openAboutFirefoxPreview {
222 private val aboutMenuList = UiScrollable(UiSelector().resourceId("$packageName:id/about_layout"))
224 private fun goBackButton() =
225 onView(withContentDescription("Navigate up"))
227 class BuildDateAssertion {
228 // When the app is built on firebase, there are times where the BuildDate is off by a few seconds or a few minutes.
229 // To compensate for that slight discrepancy, this assertion was added to see if the Build Date shown
230 // is within a reasonable amount of time from when the app was built.
232 // this pattern represents the following date format: "Monday 12/30 @ 6:49 PM"
233 private const val DATE_PATTERN = "EEEE M/d @ h:m a"
236 private const val NUM_OF_HOURS = 1
238 fun isDisplayedDateAccurate(): ViewAssertion {
239 return ViewAssertion { view, noViewFoundException ->
240 if (noViewFoundException != null) throw noViewFoundException
242 val textFromView = (view as TextView).text
243 ?: throw AssertionError("This view is not of type TextView")
245 verifyDateIsWithinRange(textFromView.toString(), NUM_OF_HOURS)
249 private fun verifyDateIsWithinRange(dateText: String, hours: Int) {
250 // This assertion checks whether has defined a range of tim
251 if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1) {
252 val simpleDateFormat = SimpleDateFormat(DATE_PATTERN)
253 val date = simpleDateFormat.parse(dateText)
254 if (date == null || !date.isWithinRangeOf(hours)) {
255 throw AssertionError("The build date is not within Range.")
258 val textviewDate = getLocalDateTimeFromString(dateText)
259 val buildConfigDate = getLocalDateTimeFromString(BuildConfig.BUILD_DATE)
261 if (!buildConfigDate.isEqual(textviewDate) &&
262 !textviewDate.isWithinRangeOf(hours, buildConfigDate)
264 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.")
269 private fun Date.isWithinRangeOf(hours: Int): Boolean {
270 // To determine the date range, the maxDate is retrieved by adding the variable hours to the calendar.
271 // 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
272 // This will result in the maxDate being equal to the original Date + hours, and minDate being equal to original Date - hours
274 val calendar = Calendar.getInstance()
275 val currentYear = calendar.get(Calendar.YEAR)
277 calendar.set(Calendar.YEAR, currentYear)
278 val updatedDate = calendar.time
280 calendar.add(Calendar.HOUR_OF_DAY, hours)
281 val maxDate = calendar.time
283 Calendar.HOUR_OF_DAY,
285 ) // Gets the minDate by subtracting from maxDate
286 val minDate = calendar.time
287 return updatedDate.after(minDate) && updatedDate.before(maxDate)
290 private fun LocalDateTime.isWithinRangeOf(
292 baselineDate: LocalDateTime,
294 val upperBound = baselineDate.plusHours(hours.toLong())
295 val lowerBound = baselineDate.minusHours(hours.toLong())
296 val currentDate = this
297 return currentDate.isAfter(lowerBound) && currentDate.isBefore(upperBound)
300 private fun getLocalDateTimeFromString(buildDate: String): LocalDateTime {
301 val dateFormatter = DateTimeFormatterBuilder().appendPattern(DATE_PATTERN)
302 .parseDefaulting(ChronoField.YEAR, LocalDateTime.now().year.toLong())
304 return LocalDateTime.parse(buildDate, dateFormatter)