Merge tag 'v96.3.0' into upstream-sync
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@ -0,0 +1,9 @@
|
||||
package org.mozilla.fenix.customannotations
|
||||
|
||||
/**
|
||||
* A custom annotation to mark the smoke tests corresponding to the ones in TestRail:
|
||||
* https://testrail.stage.mozaws.net/index.php?/suites/view/3192
|
||||
*/
|
||||
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class SmokeTest
|
@ -0,0 +1,40 @@
|
||||
package org.mozilla.fenix.helpers
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.mozilla.fenix.ext.settings
|
||||
|
||||
class FeatureSettingsHelper {
|
||||
private val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
private val settings = context.settings()
|
||||
|
||||
// saving default values of feature flags
|
||||
private var isPocketEnabled: Boolean = settings.showPocketRecommendationsFeature
|
||||
private var isJumpBackInCFREnabled: Boolean = settings.shouldShowJumpBackInCFR
|
||||
private var isRecentTabsFeatureEnabled: Boolean = settings.showRecentTabsFeature
|
||||
|
||||
fun setPocketEnabled(enabled: Boolean) {
|
||||
settings.showPocketRecommendationsFeature = enabled
|
||||
}
|
||||
|
||||
fun setJumpBackCFREnabled(enabled: Boolean) {
|
||||
settings.shouldShowJumpBackInCFR = enabled
|
||||
}
|
||||
|
||||
fun setRecentTabsFeatureEnabled(enabled: Boolean) {
|
||||
settings.showRecentTabsFeature = enabled
|
||||
}
|
||||
|
||||
fun setStrictETPEnabled() {
|
||||
settings.setStrictETP()
|
||||
}
|
||||
|
||||
// Important:
|
||||
// Use this after each test if you have modified these feature settings
|
||||
// to make sure the app goes back to the default state
|
||||
fun resetAllFeatureFlags() {
|
||||
settings.showPocketRecommendationsFeature = isPocketEnabled
|
||||
settings.shouldShowJumpBackInCFR = isJumpBackInCFREnabled
|
||||
settings.showRecentTabsFeature = isRecentTabsFeatureEnabled
|
||||
}
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.ui
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
|
||||
import org.mozilla.fenix.helpers.HomeActivityTestRule
|
||||
import org.mozilla.fenix.helpers.TestAssetHelper
|
||||
import org.mozilla.fenix.ui.robots.navigationToolbar
|
||||
|
||||
/**
|
||||
* Tests for verifying basic functionality of history
|
||||
*
|
||||
*/
|
||||
|
||||
class ShareButtonTest {
|
||||
/* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping.
|
||||
|
||||
private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
|
||||
private lateinit var mockWebServer: MockWebServer
|
||||
|
||||
@get:Rule
|
||||
val activityTestRule = HomeActivityTestRule()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
mockWebServer = MockWebServer().apply {
|
||||
dispatcher = AndroidAssetDispatcher()
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
mockWebServer.shutdown()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ShareButtonAppearanceTest() {
|
||||
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
|
||||
|
||||
// - Visit a URL, wait until it's loaded
|
||||
navigationToolbar {
|
||||
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
|
||||
mDevice.waitForIdle()
|
||||
}
|
||||
|
||||
// From the 3-dot menu next to the Select share menu
|
||||
navigationToolbar {
|
||||
}.openThreeDotMenu {
|
||||
clickShareButton()
|
||||
verifyShareScrim()
|
||||
verifySendToDeviceTitle()
|
||||
verifyShareALinkTitle()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
package org.mozilla.fenix.ui.robots
|
||||
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.Visibility
|
||||
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import org.hamcrest.CoreMatchers.allOf
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.helpers.click
|
||||
|
||||
/**
|
||||
* Implementation of Robot Pattern for the settings Homepage sub menu.
|
||||
*/
|
||||
class SettingsSubMenuHomepageRobot {
|
||||
|
||||
fun verifyHomePageView() {
|
||||
assertMostVisitedTopSitesButton()
|
||||
assertJumpBackInButton()
|
||||
assertRecentBookmarksButton()
|
||||
assertRecentSearchesButton()
|
||||
assertPocketButton()
|
||||
assertOpeningScreenHeading()
|
||||
assertHomepageButton()
|
||||
assertLastTabButton()
|
||||
assertHomepageAfterFourHoursButton()
|
||||
}
|
||||
|
||||
fun clickJumpBackInButton() = jumpBackInButton().click()
|
||||
fun clickRecentBookmarksButton() = recentBookmarksButton().click()
|
||||
fun clickStartOnHomepageButton() = homepageButton().click()
|
||||
fun clickStartOnLastTabButton() = lastTabButton().click()
|
||||
|
||||
class Transition {
|
||||
|
||||
fun goBack(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
|
||||
goBackButton().click()
|
||||
|
||||
HomeScreenRobot().interact()
|
||||
return HomeScreenRobot.Transition()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun mostVisitedTopSitesButton() =
|
||||
onView(allOf(withText(R.string.top_sites_toggle_top_recent_sites_3)))
|
||||
|
||||
private fun jumpBackInButton() =
|
||||
onView(allOf(withText(R.string.customize_toggle_jump_back_in)))
|
||||
|
||||
private fun recentBookmarksButton() =
|
||||
onView(allOf(withText(R.string.customize_toggle_recent_bookmarks)))
|
||||
|
||||
private fun recentSearchesButton() =
|
||||
onView(allOf(withText(R.string.customize_toggle_recently_visited)))
|
||||
|
||||
private fun pocketButton() =
|
||||
onView(allOf(withText(R.string.customize_toggle_pocket)))
|
||||
|
||||
private fun openingScreenHeading() = onView(withText(R.string.preferences_opening_screen))
|
||||
|
||||
private fun homepageButton() =
|
||||
onView(
|
||||
allOf(
|
||||
withId(R.id.title),
|
||||
withText(R.string.opening_screen_homepage),
|
||||
hasSibling(withId(R.id.radio_button))
|
||||
)
|
||||
)
|
||||
|
||||
private fun lastTabButton() =
|
||||
onView(
|
||||
allOf(
|
||||
withId(R.id.title),
|
||||
withText(R.string.opening_screen_last_tab),
|
||||
hasSibling(withId(R.id.radio_button))
|
||||
)
|
||||
)
|
||||
|
||||
private fun homepageAfterFourHoursButton() =
|
||||
onView(
|
||||
allOf(
|
||||
withId(R.id.title),
|
||||
withText(R.string.opening_screen_after_four_hours_of_inactivity),
|
||||
hasSibling(withId(R.id.radio_button))
|
||||
)
|
||||
)
|
||||
|
||||
private fun goBackButton() = onView(allOf(withContentDescription(R.string.action_bar_up_description)))
|
||||
|
||||
private fun assertMostVisitedTopSitesButton() =
|
||||
mostVisitedTopSitesButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
private fun assertJumpBackInButton() =
|
||||
jumpBackInButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
private fun assertRecentBookmarksButton() =
|
||||
recentBookmarksButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
private fun assertRecentSearchesButton() =
|
||||
recentSearchesButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
private fun assertPocketButton() =
|
||||
pocketButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
private fun assertOpeningScreenHeading() =
|
||||
openingScreenHeading().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
private fun assertHomepageButton() =
|
||||
homepageButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
private fun assertLastTabButton() =
|
||||
lastTabButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
private fun assertHomepageAfterFourHoursButton() =
|
||||
homepageAfterFourHoursButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
@ -0,0 +1,91 @@
|
||||
package org.mozilla.fenix.ui.robots
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.intent.Intents
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withResourceName
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.uiautomator.By
|
||||
import androidx.test.uiautomator.UiSelector
|
||||
import androidx.test.uiautomator.Until
|
||||
import org.hamcrest.Matchers.allOf
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.helpers.ext.waitNotNull
|
||||
|
||||
class ShareOverlayRobot {
|
||||
|
||||
// This function verifies the share layout when more than one tab is shared - a list of tabs is shown
|
||||
fun verifyShareTabsOverlay(vararg tabsTitles: String) {
|
||||
onView(withId(R.id.shared_site_list))
|
||||
.check(matches(isDisplayed()))
|
||||
for (tabs in tabsTitles) {
|
||||
onView(withText(tabs))
|
||||
.check(
|
||||
matches(
|
||||
allOf(
|
||||
hasSibling(withId(R.id.share_tab_favicon)),
|
||||
hasSibling(withId(R.id.share_tab_url))
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// This function verifies the share layout when a single tab is shared - no tab info shown
|
||||
fun verifyShareTabLayout() = assertShareTabLayout()
|
||||
|
||||
// this verifies the Android sharing layout - not customized for sharing tabs
|
||||
fun verifyAndroidShareLayout() {
|
||||
mDevice.waitNotNull(Until.findObject(By.res("android:id/resolver_list")))
|
||||
}
|
||||
|
||||
fun selectAppToShareWith(appName: String) =
|
||||
mDevice.findObject(UiSelector().text(appName)).clickAndWaitForNewWindow()
|
||||
|
||||
fun verifySendToDeviceTitle() = assertSendToDeviceTitle()
|
||||
|
||||
fun verifyShareALinkTitle() = assertShareALinkTitle()
|
||||
|
||||
fun verifySharedTabsIntent(text: String, subject: String) {
|
||||
Intents.intended(
|
||||
allOf(
|
||||
IntentMatchers.hasExtra(Intent.EXTRA_TEXT, text),
|
||||
IntentMatchers.hasExtra(Intent.EXTRA_SUBJECT, subject)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
class Transition
|
||||
}
|
||||
|
||||
private fun shareTabsLayout() = onView(withResourceName("shareWrapper"))
|
||||
|
||||
private fun assertShareTabLayout() =
|
||||
shareTabsLayout().check(matches(isDisplayed()))
|
||||
|
||||
private fun sendToDeviceTitle() =
|
||||
onView(
|
||||
allOf(
|
||||
withText("SEND TO DEVICE"),
|
||||
withResourceName("accountHeaderText")
|
||||
)
|
||||
)
|
||||
|
||||
private fun assertSendToDeviceTitle() = sendToDeviceTitle()
|
||||
.check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
|
||||
|
||||
private fun shareALinkTitle() =
|
||||
onView(
|
||||
allOf(
|
||||
withText("ALL ACTIONS"),
|
||||
withResourceName("apps_link_header")
|
||||
)
|
||||
)
|
||||
|
||||
private fun assertShareALinkTitle() = shareALinkTitle()
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
@ -0,0 +1,22 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.components
|
||||
|
||||
import mozilla.components.lib.state.Middleware
|
||||
import mozilla.components.lib.state.Store
|
||||
import org.mozilla.fenix.components.appstate.AppAction
|
||||
import org.mozilla.fenix.components.appstate.AppState
|
||||
import org.mozilla.fenix.components.appstate.AppStoreReducer
|
||||
|
||||
/**
|
||||
* A [Store] that holds the [AppState] for the app and reduces [AppAction]s
|
||||
* dispatched to the store.
|
||||
*
|
||||
* This store is not persisted to disk and is scoped to the life-cycle of the application.
|
||||
*/
|
||||
class AppStore(
|
||||
initialState: AppState = AppState(),
|
||||
middlewares: List<Middleware<AppState, AppAction>> = emptyList()
|
||||
) : Store<AppState, AppAction>(initialState, AppStoreReducer::reduce, middlewares)
|
@ -1,29 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.components
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import org.mozilla.gecko.search.SearchWidgetProvider
|
||||
|
||||
/**
|
||||
* Handles the creation of the pinning search widget dialog.
|
||||
*/
|
||||
object SearchWidgetCreator {
|
||||
|
||||
/**
|
||||
* Attempts to display a prompt requesting the user pin the search widget
|
||||
* Returns true if the prompt is displayed successfully, and false otherwise.
|
||||
*/
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
fun createSearchWidget(context: Context): Boolean {
|
||||
val appWidgetManager: AppWidgetManager = context.getSystemService(AppWidgetManager::class.java)
|
||||
val myProvider = ComponentName(context, SearchWidgetProvider::class.java)
|
||||
return appWidgetManager.requestPinAppWidget(myProvider, null, null)
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.components.appstate
|
||||
|
||||
import mozilla.components.lib.state.Action
|
||||
import org.mozilla.fenix.components.AppStore
|
||||
|
||||
/**
|
||||
* [Action] implementation related to [AppStore].
|
||||
*/
|
||||
sealed class AppAction : Action {
|
||||
data class UpdateInactiveExpanded(val expanded: Boolean) : AppAction()
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.components.appstate
|
||||
|
||||
import mozilla.components.lib.state.State
|
||||
|
||||
/**
|
||||
* Value type that represents the state of the tabs tray.
|
||||
*
|
||||
* @property inactiveTabsExpanded A flag to know if the Inactive Tabs section of the Tabs Tray
|
||||
* should be expanded when the tray is opened.
|
||||
*/
|
||||
data class AppState(
|
||||
val inactiveTabsExpanded: Boolean = false
|
||||
) : State
|
@ -0,0 +1,17 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.components.appstate
|
||||
|
||||
import org.mozilla.fenix.components.AppStore
|
||||
|
||||
/**
|
||||
* Reducer for [AppStore].
|
||||
*/
|
||||
internal object AppStoreReducer {
|
||||
fun reduce(state: AppState, action: AppAction): AppState = when (action) {
|
||||
is AppAction.UpdateInactiveExpanded ->
|
||||
state.copy(inactiveTabsExpanded = action.expanded)
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.compose
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import mozilla.components.browser.icons.IconRequest
|
||||
import mozilla.components.browser.icons.compose.Loader
|
||||
import mozilla.components.browser.icons.compose.Placeholder
|
||||
import mozilla.components.browser.icons.compose.WithIcon
|
||||
import mozilla.components.ui.colors.PhotonColors
|
||||
import org.mozilla.fenix.components.components
|
||||
|
||||
/**
|
||||
* Load and display the favicon of a particular website.
|
||||
*
|
||||
* @param url Website [URL] for which the favicon will be shown.
|
||||
* @param size [Dp] height and width of the image to be loaded.
|
||||
* @param isPrivate Whether or not a private request (like in private browsing) should be used to
|
||||
* download the icon (if needed).
|
||||
*/
|
||||
@Composable
|
||||
fun Favicon(
|
||||
url: String,
|
||||
size: Dp,
|
||||
isPrivate: Boolean = false
|
||||
) {
|
||||
components.core.icons.Loader(
|
||||
url = url,
|
||||
isPrivate = isPrivate,
|
||||
size = size.toIconRequestSize()
|
||||
) {
|
||||
Placeholder {
|
||||
Box(
|
||||
modifier = Modifier.background(
|
||||
color = when (isSystemInDarkTheme()) {
|
||||
true -> PhotonColors.DarkGrey30
|
||||
false -> PhotonColors.LightGrey30
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
WithIcon { icon ->
|
||||
Image(
|
||||
painter = icon.painter,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(size)
|
||||
.clip(RoundedCornerShape(2.dp)),
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Dp.toIconRequestSize() = when {
|
||||
value <= dimensionResource(IconRequest.Size.DEFAULT.dimen).value -> IconRequest.Size.DEFAULT
|
||||
value <= dimensionResource(IconRequest.Size.LAUNCHER.dimen).value -> IconRequest.Size.LAUNCHER
|
||||
else -> IconRequest.Size.LAUNCHER_ADAPTIVE
|
||||
}
|